枚举列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 版:
#!/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++ 版(同一算法):
/**
* 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 的唯一解为例,看三条链如何锁住整张方阵:
+---+---+---+---+---+
| 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 也能轻松通过。