「一本通 1.3 练习 3」质数方阵

GitHub跳转原题关系图返回列表

枚举列0与主对角线两条素数链锁定每行的首位与第i位,行3用副对角线差值桶过滤,行0由列和反推,最后查素数表。

OJ: roj

题目 ID: 1446

难度:提高

标签:搜索素数构造剪枝

创建: 2026-09-30 11:00

更新: 2026-10-07 11:01

形式化题目

给定整数 S(位和)与 d(左上角数字)。在 5×5 数字方阵中:

  • 每一行从左到右、每一列从上到下、主对角线从左上到右下、副对角线从左下到右上(都是"从左到右")读出的 5 位数字串都必须是素数;
  • 这些素数每个的数位和都等于 S;
  • 左上角格子等于 d,行/列/对角线首位不能为 0。

输出所有满足条件的方阵(每个方阵 5 行、每行 5 个数字),两组方案之间空一行,方案按 25 个数字组成的 25 位数升序排列;无解输出 NONE。

正解

思路

素数表是唯一原料。 五位素数不到 10^5 个,先筛出"位和恰好为 S"的那部分(记个数为 P,首位非 0 自动满足)。例如 S=11 时 P=143,S=23 时 P=757——中段位和的素数最多,是最坏情况。

朴素做法及其瓶颈。 最直接的想法是逐行 DFS:每行从素数表里选一个,用"每一列已放的前缀必须能延伸成某个素数"剪枝。但这个剪枝强度取决于位和:当 S=23 时,任取两个数字开头的两位前缀几乎都能延伸成某个素数(剩余三位有近千种补法),第一层剪枝几乎失效,757 × 757 的第二层组合就会失控。位和类约束在中段最弱,这是按行搜索的天然短板。

换成固定三条"素数链"。 注意方阵里有三条互相只共享左上角一格的完整素数链:第 0 行、第 0 列、主对角线,它们的首位都是左上角 d。把第 0 列(记 c0)和主对角线(记 dg)固定后,第 i 行就被钉死两位——第 0 位是 c0[i]、第 i 位是 dg[i]:

行\列 0 1 2 3 4
0 ● □ □ □ □
1 ░ ◆ · · ·
2 ░ · ◆ · ·
3 ░ · · ◆ ·
4 ░ · · · ◆

(● 左上角;□ 第 0 行;░ 第 0 列;◆ 主对角线;· 12 个自由格)

于是第 i 行根本不用逐位搜索:候选就是素数桶 [首位=c0[i], 第 i 位=dg[i]] 里的素数,平均只有 P/90 ≈ 8 个;又因为行本身是素数,该行 3 个自由格的和自动等于 S - c0[i] - dg[i],无需再检查。

枚举顺序:先 (c0, dg),r0 留到最后。 若把三条链都当独立枚举对象,是 F³ 对组合(F 为首位是 d 的素数个数)。但第 0 行的 4 个非首位数字恰好是列 1…4 的"列顶",一旦行 1…4 选定,列 j 的自由格和就确定了,由列和式 r0[j] + dg[j] + 自由格和 = S 直接反推出 r0[j]——第 0 行一个循环都不用枚举,组合数降为 F² ≈ 90² = 8×10³。

副对角线的方向与 Δ 过滤。 副对角线"从左到右"读是指从左下 (4,0) 读到右上 (0,4),方向极易弄反。用 11 2 这组真实数据的唯一解验证:

读法 数字串 判定
从左下 (4,0) 到右上 (0,4)(正确) 11171 素数 ✔
从右上 (0,4) 到左下 (4,0)(错误) 17111 17111 = 71 × 241 ✘

副对角线的位和式 c0[4] + r3[1] + dg[2] + r1[3] + r0[4] = S 与列 4 的位和式 r0[4] + r1[4] + r2[4] + r3[4] + dg[4] = S 都含未知的 r0[4],联立消元得

r3[1] - r3[4] = dg[4] + r1[4] + r2[4] - c0[4] - dg[2] - r1[3] (记作 Δ)

右边在 r1、r2 选定后是常数。所以行 3 也不用枚举:把行 3 的素数桶按 第1位 - 第4位 再建一层差值桶,查 Δ 即可,恰好也把副对角线的位和约束一次做掉。

窗口剪枝与终检。 选 r1 × r2 时,列 3 只剩行 4 一位、列 4 只剩行 3 一位自由格,两位都在 [0,9],立刻得到 r1[3]+r2[3] ∈ [S-18-dg[3], S-dg[3]] 这类窗口剪枝;r3 选定后对列 1、列 2 同理。r4 选定后四个列和反推出 r0 的四个数字(须在 [0,9]),最后查一遍素数表:r0、副对角线、列 1…4(行 1…4、列 0、主对角线本来就来自素数表)。全部通过即得到一个解。

不重不漏与输出。 一个合法方阵的 (c0, dg, r1..r4) 分解是唯一的,所以每个解恰好被枚举一次,无需去重。所有行都是 5 位等长串,25 位串的字典序就是 25 位数的大小,排序后用空行分隔输出即可。

整个搜索流程如下:

flowchart TD
    A["筛出位和为 S 的五位素数表"] --> B["枚举第 0 列素数 c0"]
    B --> C["枚举主对角线素数 dg"]
    C --> D["行1: 素数桶 首位=c0[1], 第1位=dg[1]"]
    D --> E["行2: 素数桶 + 列3/列4 窗口剪枝"]
    E --> F["行3: 差值桶查 Δ=r3[1]-r3[4]"]
    F --> G["行4: 素数桶, 四个列和反推行0"]
    G --> H{"行0 与四位数字在 0..9?"}
    H -- 否 --> E
    H -- 是 --> I["查表: 行0/副对角线/列1..4"]
    I -- 全是素数 --> J["记录解"]
    I -- 否 --> E
    J --> K["按 25 位串排序输出"]

代码

Python 版:

python
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-09-30 12:04
# update_at: 2026-09-30 12:04
#
# 质数方阵:行/列/两条对角线都是"位和为 S"的五位素数,左上角给定。
# 核心:枚举列0素数 c0 与主对角线素数 dg(首位=左上角),行1..4 由
# (首位=c0[i], 第i位=dg[i]) 的素数桶确定,行0 由四个列和反推,全程整数位运算。
# 副对角线从左下往右上读(从左到右),它与"列4的和"联立 => 行3满足 r3[1]-r3[4]=Δ,
# 用 (差值)桶 O(1) 过滤;行0/副对角线/列1..4 最后查素数表。

import sys
from collections import defaultdict


def solve() -> None:
    S, corner = map(int, sys.stdin.buffer.read().split())

    def is_prime(n: int) -> bool:
        """试除判断素数(n ≤ 99999,只对位和为 S 的数调用)。"""
        return n > 1 and all(n % d for d in range(2, int(n ** 0.5) + 1))

    # 位和为 S 的全部五位素数:整数集合 pset(查表)+ 逐位元组(取位)
    pset = {n for n in range(10000, 100000) if sum(map(int, str(n))) == S and is_prime(n)}
    firsts = sorted(n for n in pset if n // 10000 == corner)  # 行0/列0/对角线候选
    if not firsts:
        print('NONE')
        return
    tups = [(n // 10000, n // 1000 % 10, n // 100 % 10, n // 10 % 10, n % 10) for n in sorted(pset)]

    # 行 i(1..4) 的素数桶:首位 = 列0第i位,第 i 位 = 主对角线第 i 位
    b1: dict[tuple[int, int], list] = defaultdict(list)              # (d0, d1)
    b2: dict[tuple[int, int], list] = defaultdict(list)              # (d0, d2)
    b3: dict[tuple[int, int], dict] = defaultdict(lambda: defaultdict(list))  # (d0,d3)->d1-d4
    b4: dict[tuple[int, int], list] = defaultdict(list)              # (d0, d4)
    for t in tups:
        b1[(t[0], t[1])].append(t)
        b2[(t[0], t[2])].append(t)
        b3[(t[0], t[3])][t[1] - t[4]].append(t)
        b4[(t[0], t[4])].append(t)

    ans: list[str] = []
    for c0 in firsts:  # 第 0 列(素数)
        c01, c02, c03, c04 = c0 // 1000 % 10, c0 // 100 % 10, c0 // 10 % 10, c0 % 10
        for dg in firsts:  # 主对角线(素数)
            dg1, dg2, dg3, dg4 = dg // 1000 % 10, dg // 100 % 10, dg // 10 % 10, dg % 10
            l1 = b1.get((c01, dg1)); l2 = b2.get((c02, dg2))
            m3 = b3.get((c03, dg3)); l4 = b4.get((c04, dg4))
            if not (l1 and l2 and m3 and l4):
                continue
            for r1 in l1:
                x13, x14 = r1[3], r1[4]
                for r2 in l2:
                    # 列3只剩行4一位、列4只剩行3一位:两位都在 [0,9] 的可行窗口
                    if not S - 18 - dg3 <= x13 + r2[3] <= S - dg3:
                        continue
                    if not S - 18 - dg4 <= x14 + r2[4] <= S - dg4:
                        continue
                    # 副对角线位和 + 列4位和联立 => 行3须满足 r3[1]-r3[4] = Δ
                    for r3 in m3.get(dg4 + x14 + r2[4] - c04 - dg2 - x13, ()):
                        # 列2、列1 同理的可行窗口
                        if not S - 18 - dg2 <= r1[2] + r3[2] <= S - dg2:
                            continue
                        if not S - 18 - dg1 <= r2[1] + r3[1] <= S - dg1:
                            continue
                        for r4 in l4:
                            # 四个列和反推行0的四个数字(列0/主对角线已由 c0,dg 保证)
                            d01 = S - dg1 - r2[1] - r3[1] - r4[1]
                            d02 = S - dg2 - r1[2] - r3[2] - r4[2]
                            d03 = S - dg3 - x13 - r2[3] - r4[3]
                            d04 = S - dg4 - x14 - r2[4] - r3[4]
                            if not (0 <= d01 <= 9 and 0 <= d02 <= 9 and 0 <= d03 <= 9 and 0 <= d04 <= 9):
                                continue
                            r0 = corner * 10000 + d01 * 1000 + d02 * 100 + d03 * 10 + d04
                            # 行0、副对角线、列1..4 都须在素数表(行1..4/列0/主对角线已保证)
                            if (r0 in pset
                                    and c04 * 10000 + r3[1] * 1000 + dg2 * 100 + x13 * 10 + d04 in pset
                                    and d01 * 10000 + r1[1] * 1000 + r2[1] * 100 + r3[1] * 10 + r4[1] in pset
                                    and d02 * 10000 + r1[2] * 1000 + r2[2] * 100 + r3[2] * 10 + r4[2] in pset
                                    and d03 * 10000 + x13 * 1000 + r2[3] * 100 + r3[3] * 10 + r4[3] in pset
                                    and d04 * 10000 + x14 * 1000 + r2[4] * 100 + r3[4] * 10 + r4[4] in pset):
                                ans.append('\n'.join([str(r0)] + [''.join(map(str, t)) for t in (r1, r2, r3, r4)]))

    ans.sort()  # 等长的 25 位串按字典序 = 按 25 位数大小
    print('\n\n'.join(ans) if ans else 'NONE')


if __name__ == "__main__":
    solve()

C++ 版(同一算法):

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-10-05 02:16
 * update_at: 2026-10-05 02:18
 */
// main.cpp:质数方阵。
// 行/列/两条对角线都是“位和为 S”的五位素数,左上角固定为 corner。
// 做法:枚举第 0 列素数 c0 与主对角线素数 dg,二者把第 1..4 行各钉死两位,
// 每行只需在 (首位, 第 i 位) 的素数桶里选;第 0 行最后由四个列和反推;
// 副对角线(左下→右上)与列 4 的位和式联立,把第 3 行变成差值过滤;
// 行 0、副对角线、列 1..4 结尾统一查素数表确认。

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;

typedef long long ll; // 题目数据默认 long long;本题数值都在 5 位以内,实际用 int 足够

const int MAXV = 100000; // 五位数上界,筛法与素数表都开到它

int S;      // 各数位之和
int corner; // 左上角数字

bool composite[MAXV]; // composite[x] = true 表示 x 是合数
bool in_pset[MAXV];   // in_pset[x] = true 表示 x 是位和为 S 的五位素数
vector<int> first_list; // 首位等于 corner 的候选素数(第 0 行 / 第 0 列 / 主对角线)

// 行候选桶,装的是“位和为 S 的五位素数”。
// b1[a][b]:首位为 a、第 1 位为 b,供第 1 行使用;b2/b3/b4 同理对应第 2/3/4 位。
vector<int> b1[10][10];
vector<int> b2[10][10];
vector<int> b3[10][10];
vector<int> b4[10][10];

vector<string> ans; // 每个方案拼成 25 位串(5 行数字用换行连接)

// 取 x 的第 pos 位数字:pos = 0 是万位,pos = 4 是个位
int digit_at(int x, int pos) {
    if (pos == 0) return x / 10000;
    if (pos == 1) return x / 1000 % 10;
    if (pos == 2) return x / 100 % 10;
    if (pos == 3) return x / 10 % 10;
    return x % 10;
}

// 埃氏筛标记合数,并把位和恰好为 S 的五位素数分装进各个桶
void build_tables() {
    composite[0] = true;
    composite[1] = true;
    for (int i = 2; i * i < MAXV; i++) {
        if (composite[i]) continue;
        for (int j = i * i; j < MAXV; j += i) composite[j] = true;
    }

    for (int x = 10000; x < MAXV; x++) {
        if (composite[x]) continue;
        int sum = 0;
        for (int pos = 0; pos < 5; pos++) sum += digit_at(x, pos);
        if (sum != S) continue;

        in_pset[x] = true;
        int d0 = digit_at(x, 0);
        if (d0 == corner) first_list.push_back(x);
        b1[d0][digit_at(x, 1)].push_back(x);
        b2[d0][digit_at(x, 2)].push_back(x);
        b3[d0][digit_at(x, 3)].push_back(x);
        b4[d0][digit_at(x, 4)].push_back(x);
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> S >> corner;

    build_tables();

    for (size_t ci = 0; ci < first_list.size(); ci++) {
        int c0 = first_list[ci]; // 第 0 列(素数),c0 的第 i 位就是第 i 行首位
        int c01 = digit_at(c0, 1);
        int c02 = digit_at(c0, 2);
        int c03 = digit_at(c0, 3);
        int c04 = digit_at(c0, 4);

        for (size_t di = 0; di < first_list.size(); di++) {
            int dg = first_list[di]; // 主对角线(素数),dg 的第 i 位是第 i 行第 i 位
            int dg1 = digit_at(dg, 1);
            int dg2 = digit_at(dg, 2);
            int dg3 = digit_at(dg, 3);
            int dg4 = digit_at(dg, 4);

            vector<int>& L1 = b1[c01][dg1]; // 第 1 行的候选素数
            vector<int>& L2 = b2[c02][dg2]; // 第 2 行的候选素数
            vector<int>& L3 = b3[c03][dg3]; // 第 3 行的候选素数(还要过滤 第1位-第4位)
            vector<int>& L4 = b4[c04][dg4]; // 第 4 行的候选素数

            for (size_t i1 = 0; i1 < L1.size(); i1++) {
                int r1 = L1[i1];
                int x11 = digit_at(r1, 1);
                int x12 = digit_at(r1, 2);
                int x13 = digit_at(r1, 3);
                int x14 = digit_at(r1, 4);

                for (size_t i2 = 0; i2 < L2.size(); i2++) {
                    int r2 = L2[i2];
                    int x21 = digit_at(r2, 1);
                    int x22 = digit_at(r2, 2);
                    int x23 = digit_at(r2, 3);
                    int x24 = digit_at(r2, 4);

                    // 列 3、列 4 只剩第 4、第 3 行各一个自由格,其和与行 0 的数字都在 [0,9]
                    if (x13 + x23 < S - 18 - dg3) continue;
                    if (x13 + x23 > S - dg3) continue;
                    if (x14 + x24 < S - 18 - dg4) continue;
                    if (x14 + x24 > S - dg4) continue;

                    // 副对角线位和 c04+r31+dg2+x13+d04=S 与列 4 位和消去 d04,
                    // 得到第 3 行必须满足:第 1 位 - 第 4 位 = delta
                    int delta = dg4 + x14 + x24 - c04 - dg2 - x13;

                    for (size_t i3 = 0; i3 < L3.size(); i3++) {
                        int r3 = L3[i3];
                        int x31 = digit_at(r3, 1);
                        int x32 = digit_at(r3, 2);
                        int x33 = digit_at(r3, 3);
                        int x34 = digit_at(r3, 4);
                        if (x31 - x34 != delta) continue;

                        // 列 2、列 1 同样只剩一个自由格,做窗口剪枝
                        if (x12 + x32 < S - 18 - dg2) continue;
                        if (x12 + x32 > S - dg2) continue;
                        if (x21 + x31 < S - 18 - dg1) continue;
                        if (x21 + x31 > S - dg1) continue;

                        for (size_t i4 = 0; i4 < L4.size(); i4++) {
                            int r4 = L4[i4];
                            int x41 = digit_at(r4, 1);
                            int x42 = digit_at(r4, 2);
                            int x43 = digit_at(r4, 3);
                            int x44 = digit_at(r4, 4);

                            // 四个列和反推第 0 行的四个数字(第 0 列、主对角线已由 c0、dg 保证)
                            int d01 = S - dg1 - x21 - x31 - x41;
                            int d02 = S - dg2 - x12 - x32 - x42;
                            int d03 = S - dg3 - x13 - x23 - x43;
                            int d04 = S - dg4 - x14 - x24 - x34;
                            if (d01 < 0 || d01 > 9) continue;
                            if (d02 < 0 || d02 > 9) continue;
                            if (d03 < 0 || d03 > 9) continue;
                            if (d04 < 0 || d04 > 9) continue;

                            int r0 = corner * 10000 + d01 * 1000 + d02 * 100 + d03 * 10 + d04;
                            if (!in_pset[r0]) continue;

                            // 副对角线从左下 (4,0) 读到右上 (0,4),即 c04, r31, dg2, r13, d04
                            int anti = c04 * 10000 + x31 * 1000 + dg2 * 100 + x13 * 10 + d04;
                            if (!in_pset[anti]) continue;

                            // 列 1..4 从上往下读
                            int col1 = d01 * 10000 + x11 * 1000 + x21 * 100 + x31 * 10 + x41;
                            int col2 = d02 * 10000 + x12 * 1000 + x22 * 100 + x32 * 10 + x42;
                            int col3 = d03 * 10000 + x13 * 1000 + x23 * 100 + x33 * 10 + x43;
                            int col4 = d04 * 10000 + x14 * 1000 + x24 * 100 + x34 * 10 + x44;
                            if (!in_pset[col1]) continue;
                            if (!in_pset[col2]) continue;
                            if (!in_pset[col3]) continue;
                            if (!in_pset[col4]) continue;

                            string s = to_string(r0);
                            s += "\n" + to_string(r1);
                            s += "\n" + to_string(r2);
                            s += "\n" + to_string(r3);
                            s += "\n" + to_string(r4);
                            ans.push_back(s);
                        }
                    }
                }
            }
        }
    }

    // 所有方案都是等长的 25 位串,按字典序排就是按 25 位数大小排
    sort(ans.begin(), ans.end());

    if (ans.empty()) {
        cout << "NONE\n";
    } else {
        for (size_t i = 0; i < ans.size(); i++) {
            if (i > 0) cout << "\n"; // 两组方案之间空一行
            cout << ans[i] << "\n";
        }
    }

    return 0;
}

复杂度

  • 预处理:筛位和为 S 的五位素数,只对约 6×10³ 个候选试除,O(9×10⁴ + P·√N)。
  • 主搜索:设 F 为首位等于左上角的素数个数。外层 F² 对 (c0, dg),内层是两个约 P/90 的桶相乘,行 3 由差值桶 O(1) 命中、行 4 桶枚举后常数次查表,总代价 O(F² · (P/90)²) 次整数运算;最坏点 S=23 时约 101² × 8.4² ≈ 7×10⁵ 个桶对,远小于朴素逐行 DFS 的组合数。
  • 空间:素数表与桶 O(P)。
  • 实测(Python,本题最慢点 23 3):0.61s,全部 10 组官方数据通过。

图示解析

以测试点 S=11, d=2 的唯一解为例,看三条链如何锁住整张方阵:

text
+---+---+---+---+---+
| 2 | 5 | 1 | 2 | 1 |   行0 = 25121(链 □)
+---+---+---+---+---+
| 1 | 0 | 2 | 7 | 1 |   行1:首位=列0[1]=1,第1位=对角线=0
+---+---+---+---+---+
| 5 | 4 | 1 | 0 | 1 |   行2:首位=5,第2位=1
+---+---+---+---+---+
| 2 | 1 | 6 | 1 | 1 |   行3:首位=2,第3位=1;副对角线格 (3,1)=1
+---+---+---+---+---+
| 1 | 1 | 1 | 1 | 7 |   行4:首位=1,第4位=7
+---+---+---+---+---+
     列0 = 21521(链 ░)  对角线 = 20117(链 ◆)
  • 行 1 的候选桶 [首位=1, 第1位=0]、行 2 的 [首位=5, 第2位=1]……每行平均只有几个素数可选;
  • 副对角线按从左下到右上读出 (4,0)(3,1)(2,2)(1,3)(0,4) = 1,1,1,7,1 → 11171,是素数;若读反成 17111 则是合数,整个解会被漏掉;
  • 列 1…4 与行 0 的数字最后由列和反推、查表确认,任何一格出界(不在 [0,9])就整枝剪掉。

总结

  • 质数方阵是"位和约束 + 多链耦合"的构造搜索题:位和处在中段时按行 DFS 的前缀剪枝失效,必须换更强的固定对象。
  • 关键三步:① 用列 0 + 主对角线两条素数链把每行钉死两位,候选缩成平均 8 个的小桶;② 枚举顺序上把第 0 行留到最后由列和反推,省掉一整层枚举;③ 副对角线(注意从左下往右上读)与列 4 的位和式联立消元,把行 3 变成差值桶查询。
  • 配合"剩余自由格 ≤ 位数 × 9"的窗口剪枝和末端素数表查证,组合数从朴素搜索的指数级压到 F²·(P/90)²,Python 也能轻松通过。