引言
视频封面:
今天我们来讲今年一月考的汕头零模的填空压轴题。这是一道小清新的脑筋急转弯题,难度不大,又能让人更好地体会像“正难则反”这样的数学思想。但如何构造出符合题意的数列呢?于是我开始写代码搜索这样的数列。这时,我忽然想到:为什么不vibe coding一个网页,让同学们更清楚地观赏这道好题的风景呢?
题干:
将 到 排成数列 ,已知 ,且前 项和 总能被下一项 整除(即 )。求
本文 52pojie: www.52pojie.cn/thread-2106…
本文 博客园: www.cnblogs.com/hans77/arti…
本文 juejin: juejin.cn/post/763659…
作者:hans774882968以及hans774882968以及hans774882968以及hans77
题解
首先,我们有一个朴素的想法:求出数列的前几项,看看有什么规律
, 。37是素数,所以 ,但 ,所以
。38的质因数分解为 ,并且1已经被占用,所以
我们在这卡住了!因为我们接下来需要分别假设 和 ,后续求出的 的分支只会越来越多。
指望这样找到规律似乎很难!
怎么办呢?
正着推导很困难,我们就直接看最后一项!这就是正难则反的数学思想
根据题意, 必须能被 整除(令 )。 不是固定的,但 是定值,所以
19、37都是素数,所以 。但之前已经推出 ,所以
总结
题干中值得被反复观察、把玩的信息:
- 数列 是 到 的排列。排列意味着什么呢?它意味着这个数列任取两项,这两项都不相等。比如说,我们知道
a1=37,a2=1,那么a3到a37都不会是37,也不会是1了 - 总能被下一项 整除
- 观察开头:令
- 观察结尾:令
扩展
我们来回顾下题干:
将 到 排成数列 ,已知 ,且前 项和 总能被下一项 整除。求
下面我们考虑怎么对这题进行扩展。首先,37是素数,不难想到:数列的项数n应为奇素数,设为p。
于是得到修改后的题干:
将 到 排成数列 ,已知 ,且前 项和 总能被下一项 整除。求
接下来回顾之前的解题过程,我们能够确定的是 , ,以及 。于是我们不难想到:限制 为素数,必然能让 固定为
这表明“ 也为素数”是 的充分条件。但它是必要条件吗?
注:p为素数+ 为正整数等价于p为奇素数。
我们不妨写代码来寻找符合条件的数列
回到最开始说的朴素的想法:
假设数列的前 项(a[1~idx])均已求出。枚举所有还没用过的数u,只要符合题意,就令 为u。于是,假设被增强为:数列的前 项均已求出——这是一个递归的过程
a[1~n]都求出后,就得到了一个符合条件的数列。这样的数列可能很多
这个非常朴素的想法,就是所谓的dfs(深度优先搜索)。下面我们简单看下这个朴素的想法,在代码中是怎么体现的。
int P;
__int64 current_sum; // 记录当前前缀和 S[idx]
vector<int> perm; // 当前构造的序列
vector<bool> used; // 标记数字是否已使用
__int64 solution_count = 0; // 合法排列计数
void dfs(int idx) {
if (idx == P) {
solution_count++;
// 输出答案
return;
}
for (int cand = 1; cand <= P; ++cand) {
if (used[cand]) continue;
if (current_sum % cand != 0) continue;
used[cand] = true;
perm[idx] = cand;
current_sum += cand;
dfs(idx + 1);
current_sum -= cand;
used[cand] = false;
}
}
完整代码:
#include <iostream>
#include <vector>
#include <cmath>
#include <chrono>
// Copyright (c) 2026 hans7
using namespace std;
bool isPrime(int n) {
if (n < 2) return false;
for (int i = 2; i <= static_cast<int>(sqrt(n)); ++i) {
if (n % i == 0) return false;
}
return true;
}
int P;
__int64 current_sum; // 记录当前前缀和 S[idx]
vector<int> perm; // 当前构造的序列
vector<bool> used; // 标记数字是否已使用
__int64 solution_count = 0; // 合法排列计数
void dfs(int idx) {
if (idx == P) {
solution_count++;
cout << "【解 " << solution_count << "】: ";
for (int i = 0; i < P; ++i) {
cout << perm[i] << (i == P - 1 ? "" : ", ");
}
cout << "\n";
return;
}
for (int cand = 1; cand <= P; ++cand) {
if (used[cand]) continue;
if (current_sum % cand != 0) continue;
used[cand] = true;
perm[idx] = cand;
current_sum += cand;
dfs(idx + 1);
current_sum -= cand;
used[cand] = false;
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout << "请输入素数 p: " << flush;
if (!(cin >> P)) return 0;
if (!isPrime(P)) {
cerr << "❌ 错误: " << P << " 不是素数。\n";
return 114514;
}
if ((P + 1) % 2 != 0) {
cerr << "❌ 错误: (p+1)/2 不是整数(p 必须为奇素数)。\n";
return 114514;
}
if (P > 23) {
cout << "⚠️ 警告: P > 23 时搜索树极深。DFS "
"剪枝可大幅加速,但仍可能耗时较长。\n";
}
perm.assign(P, 0);
used.assign(P + 1, false);
perm[0] = P;
used[P] = true;
current_sum = P;
auto start = chrono::high_resolution_clock::now();
cout << "🔍 开始搜索合法排列...\n";
dfs(1);
auto end = chrono::high_resolution_clock::now();
chrono::duration<double> elapsed = end - start;
cout << "\n✅ 搜索完成。\n";
cout << "📊 共找到 " << solution_count << " 个合法排列。\n";
cout << "⏱️ 耗时: " << elapsed.count() << " 秒\n";
return 0;
}
dfs剪枝优化
考虑dfs中可行的cand需要满足的条件:
1 <= cand <= P && used[cand] = falsecand是current_sum的因数
考虑从“2”入手进行优化。我们预处理出1到 的每个数的所有因数,并把大于P的去掉,形成一张因数表vector<vector<int>> factors,然后dfs枚举的时候可以直接遍历这张表,不再需要遍历1到P
| 原版代码 | 剪枝版代码 | |
|---|---|---|
p=19 | 0.023874 秒 | 0.010446 秒 |
p=23 | 0.873968 秒 | 0.342937 秒 |
p=29 | 138.214 秒 | 45.8159 秒 |
优化后的完整代码:
#include <iostream>
#include <vector>
#include <cmath>
#include <chrono>
#include <algorithm>
// Copyright (c) 2026 hans7
using namespace std;
bool isPrime(int n) {
if (n < 2) return false;
for (int i = 2; i <= static_cast<int>(sqrt(n)); ++i) {
if (n % i == 0) return false;
}
return true;
}
vector<vector<int>> factors;
void precompute_factors(int max_sum, int P) {
factors.resize(max_sum + 1);
for (int s = 1; s <= max_sum; ++s) {
for (int d = 1; d * d <= s; ++d) {
if (s % d == 0) {
if (d <= P) factors[s].push_back(d);
int other = s / d;
if (other != d && other <= P) factors[s].push_back(other);
}
}
// 升序排序在此没必要,但排序后调试更方便
sort(factors[s].begin(), factors[s].end());
}
}
int P;
__int64 current_sum; // 记录当前前缀和 S[idx]
vector<int> perm; // 当前构造的序列
vector<bool> used; // 标记数字是否已使用
__int64 solution_count = 0; // 合法排列计数
void dfs(int idx) {
if (idx == P) {
solution_count++;
cout << "【解 " << solution_count << "】: ";
for (int i = 0; i < P; ++i) {
cout << perm[i] << (i == P - 1 ? "" : ", ");
}
cout << "\n";
return;
}
for (int cand : factors[current_sum]) {
if (used[cand]) continue;
used[cand] = true;
perm[idx] = cand;
current_sum += cand;
dfs(idx + 1);
current_sum -= cand;
used[cand] = false;
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout << "请输入素数 p: " << flush;
if (!(cin >> P)) return 0;
if (!isPrime(P)) {
cerr << "❌ 错误: " << P << " 不是素数。\n";
return 114514;
}
if ((P + 1) % 2 != 0) {
cerr << "❌ 错误: (p+1)/2 不是整数(p 必须为奇素数)。\n";
return 114514;
}
if (P > 23) {
cout << "⚠️ 警告: P > 23 时搜索树极深。DFS "
"剪枝可大幅加速,但仍可能耗时较长。\n";
}
const int max_sum = P * (P + 1) / 2;
cout << "📦 预处理因数表 [1, " << max_sum << "] ...\n";
auto pre_start = chrono::high_resolution_clock::now();
precompute_factors(max_sum, P);
auto pre_end = chrono::high_resolution_clock::now();
chrono::duration<double> pre_elapsed = pre_end - pre_start;
cout << "✅ 预处理完成,耗时: " << pre_elapsed.count() << " 秒\n\n";
perm.assign(P, 0);
used.assign(P + 1, false);
perm[0] = P;
used[P] = true;
current_sum = P;
auto start = chrono::high_resolution_clock::now();
cout << "🔍 开始搜索合法排列...\n";
dfs(1);
auto end = chrono::high_resolution_clock::now();
chrono::duration<double> elapsed = end - start;
cout << "\n✅ 搜索完成。\n";
cout << "📊 共找到 " << solution_count << " 个合法排列。\n";
cout << "⏱️ 搜索耗时: " << elapsed.count() << " 秒\n";
cout << "📈 总耗时(含预处理): " << (pre_elapsed + elapsed).count() << " 秒\n";
return 0;
}
推广问题的结论
“ 也为素数”是 的充分不必要条件。但“p为奇素数”是 的必要不充分条件
比如 的两个排列中,出现了末项不是 的情况
11, 1, 2, 7, 3, 8, 4, 9, 5, 10, 6
11, 1, 4, 8, 6, 10, 5, 9, 2, 7, 3
的6个排列的末项恰好都是
19, 1, 2, 11, 3, 12, 4, 13, 5, 14, 6, 15, 7, 16, 8, 17, 9, 18, 10
19, 1, 2, 11, 3, 4, 5, 15, 12, 6, 13, 7, 14, 16, 8, 17, 9, 18, 10
19, 1, 4, 12, 3, 13, 2, 18, 8, 16, 6, 17, 7, 9, 5, 14, 11, 15, 10
19, 1, 4, 12, 6, 2, 11, 5, 15, 3, 13, 7, 14, 16, 8, 17, 9, 18, 10
19, 1, 4, 12, 6, 14, 8, 16, 5, 17, 2, 13, 9, 18, 3, 7, 11, 15, 10
19, 1, 4, 6, 15, 9, 18, 8, 5, 17, 3, 7, 16, 2, 13, 11, 14, 12, 10
26汕头零模T14-可视化网页
还能继续提问:
- 能发现一些比较有规律的数列吗?
- 除了 ,末项还可以是哪些数?
- 每种可能的末项的占比如何(这里称为“末项分布”)?
为了更加清晰地展示对这些问题的回答,我vibe coding了一个网页。
网页传送门,示意图:
开发网页的提示词
大佬,请完整复述这题的题干。然后我们不难容易知道这题a37=19。可以写dfs代码求出所有可能的解。请写一个html文件。网页有一个输入框,输入框支持输入一个素数p(表示a1=p,以及数列共有p项)然后要判定(p+1)/2也是素数。两个都满足,就能确定头和尾。就跑dfs算法,在网页展示10组满足条件的数列。这时出现一个“继续生成”按钮,每点击一次就再生成10组数据。这里“继续生成”的能力可以靠js的yield实现。
技术栈:Tailwind CSS、React。
大佬,下面这个函数报错:Uncaught SyntaxError: /Inline Babel script: Unexpected reserved word 'yield'. (40:10)
38 | // 终止条件:找到了长度为 p 的序列
39 | if (depth === p) {
> 40 | yield[...path];
| ^
41 | return;
42 | }
// solveSequence 所有代码
谢谢大佬!网页能够运行,接下来请你帮我做以下修改:
- “验证条件”按钮改成“验证条件并搜索”,点击后会验证条件。如果不通过,行为和目前一致。如果通过,就预先生成前10个数列。
- 对于p=37,第一次点击“继续生成10组”按钮后,我看到有10组数据出来,但是按钮一直都是“搜索中”,无法继续点击。请帮我查找原因并修复这个bug
- 复制按钮改为总是显示
- 复制按钮的左边新增一个“验证”按钮,点击后弹出一个对话框,对话框显示每一个 和 ,以表明它确实符合题意
请在我给你的代码的基础上修改:
谢谢大佬!接下来请帮我在“已找到XX组解”的上方新增一个组件。这个组件有一个输入框,可以输入一个逗号分隔的数组。首先做输入校验,
比如验证数列是否是1到p的排列。然后验证数列是否符合题意。这里可以考虑复用前面开发过的“验证”的对话框的组件,
但是这个组件不需要弹出对话框。
请在我给你的代码的基础上修改:
谢谢大佬!接下来请帮我在validationMsg的上方新增一行文本,展示1到200的正整数中,满足p和(p+1)/2都是素数的所有p。这次你只需要输出需要改动的代码
谢谢大佬,接下来请帮我在“已找到X组解”的下方新增一行文本,展示目前搜索到的每种末项,及它们各出现了几次。
1. 请在我给你的代码的基础上修改,给出修改后的完整代码
2. 代码要符合React最佳实践
谢谢大佬,接下来请帮我在“继续生成10组”按钮的左边加一个“搜索所有数列”按钮,点击后继续跑dfs,直到dfs运行完毕,并把所有数列都展示在页面上。
请在我给你的代码的基础上修改:
// 完整代码
UI 升级:变成 quantum-rose 主题
大佬,你是一名专家前端工程师,精通前端工程化。我有一个html文件,技术栈是Tailwind CSS、React,目前它的UI比较朴素。希望你帮我把这个网页的颜色主题变为 quantum-rose (来自 tweakcn 网站)
要求:符合最小改动原则,只改变颜色主题,其他都不改变。
quantum-rose 主题的 CSS 代码:
完整HTML代码:
吸收点JS知识
为了理解前面给出的提示词,除了要知道这题可以用dfs求出所有解,还要知道在JS中如何优雅地拿到dfs给出的前若干个解:Generator函数和yield语句(ES6推出)
下面我们再对比下“继续生成10组”和“搜索所有数列”的代码:
“继续生成10组”按钮:
const generateBatch = (count) => {
let gen = generatorRef.current;
if (!gen) {
const p = parseInt(inputP);
gen = solveSequence(p);
generatorRef.current = gen;
}
const newSolutions = [];
let done = false;
for (let i = 0; i < count; i++) {
const result = gen.next();
if (result.done) {
done = true;
break;
}
newSolutions.push(result.value);
}
if (newSolutions.length > 0) {
setSolutions(prev => [...prev, ...newSolutions]);
}
if (done) {
setIsFinished(true);
}
};
“搜索所有数列”按钮:
const searchAll = async () => {
if (isSearchingAll) return;
setIsSearchingAll(true);
let gen = generatorRef.current;
if (!gen) {
const p = parseInt(inputP);
gen = solveSequence(p);
generatorRef.current = gen;
}
const BATCH_SIZE = 50;
while (true) {
const batch = [];
let done = false;
for (let i = 0; i < BATCH_SIZE; i++) {
const result = gen.next();
if (result.done) {
done = true;
break;
}
batch.push(result.value);
}
if (batch.length > 0) {
setSolutions(prev => [...prev, ...batch]);
}
if (done) {
setIsFinished(true);
break;
}
// 让出主线程, UI 才能更新,用户才能看到每次增加 50 个解的效果
await new Promise(resolve => setTimeout(resolve, 0));
}
setIsSearchingAll(false);
};
不难发现,两段代码的前面部分是类似的,都调用了solveSequence方法。solveSequence方法基本就是把dfs封装了一下,但它是一个Generator函数,所以这句话并不会真的执行dfs,只是想要拿到一个gen对象。只有在后续gen对象调用next方法时,才会真正执行dfs。
“搜索所有数列”的循环虽然有两层,但内层循环和“继续生成10组”单层循环的也类似。最大的区别是,“搜索所有数列”有一句await new Promise。这句话很重要。只有添加了这句话,才能让出主线程,UI才能更新,用户才能看到每次增加50个解的效果。如果没有这句话,在求出所有数列前,UI会完全卡住,用户体验会很糟糕。