吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 18|回复: 0
上一主题 下一主题
收起左侧

[其他原创] 深挖26汕头零模T14:做个网页展示这道好题的风景~

[复制链接]
跳转到指定楼层
楼主
hans7 发表于 2026-5-6 16:44 回帖奖励

引言

本文视频版传送门

视频封面:

今天我们来讲今年一月考的汕头零模的填空压轴题。这是一道小清新的脑筋急转弯题,难度不大,又能让人更好地体会像“正难则反”这样的数学思想。但如何构造出符合题意的数列呢?于是我开始写代码搜索这样的数列。这时,我忽然想到:为什么不vibe coding一个网页,让同学们更清楚地观赏这道好题的风景呢?

题干:

$1$$37$ 排成数列 $\{a_n\}$ ,已知 $a_1 = 37$ ,且前 $n$ 项和 $S_n$ 总能被下一项 $a_{n+1}$ 整除(即 $S_n \pmod{a_{n+1}} = 0$ )。求 $a_{37}$

本文 52pojie:

本文 博客园:

本文 juejin:

作者:hans774882968以及hans774882968以及hans774882968以及hans77

题解

首先,我们有一个朴素的想法:求出数列的前几项,看看有什么规律

$a_{1} = 37$$S_{1} \pmod{a_{2}} = 0$ 。37是素数,所以 $a_{2} = 1\text{ or }37$ ,但 $a_{1}=37$ ,所以 $a_{2}=1$

$S_{2} \pmod{a_{3}} = 38 \pmod{a_{3}} = 0$ 。38的质因数分解为 $2 \times 19$ ,并且1已经被占用,所以 $a_{3}=2\text{ or }19$

我们在这卡住了!因为我们接下来需要分别假设 $a_{3}=2$$a_{3}=19$ ,后续求出的 $a_{4},\  a_{5},\ \dots$ 的分支只会越来越多。

指望这样找到规律似乎很难!

怎么办呢?

正着推导很困难,我们就直接看最后一项!这就是正难则反的数学思想

根据题意, $S_{36}$ 必须能被 $a_{37}$ 整除(令 $n+1=37$ )。 $S_{36}$ 不是固定的,但 $S_{37} = \frac{(1+37) \times 37}{2} = 19 \times 37 = 703$ 是定值,所以

$$ S_{36} = S_{37} - a_{37} = 703 - a_{37} \implies a_{37} \mid (703 - a_{37}) \implies a_{37} \mid (19 \times 37) $$

19、37都是素数,所以 $a_{37}=1\text{ or }19\text{ or }37$ 。但之前已经推出 $a_{1}=37,\ a_{2}=1$ ,所以 $a_{37}=19$

总结

题干中值得被反复观察、把玩的信息:

  1. 数列 $\{a_n\}$$1$$37$排列。排列意味着什么呢?它意味着这个数列任取两项,这两项都不相等。比如说,我们知道a1=37a2=1,那么a3a37都不会是37,也不会是1了
  2. $S_n$ 总能被下一项 $a_{n+1}$ 整除
    1. 观察开头:令 $n=1$
    2. 观察结尾:令 $n=36$

扩展

我们来回顾下题干:

$1$$37$ 排成数列 $\{a_n\}$ ,已知 $a_1 = 37$ ,且前 $n$ 项和 $S_n$ 总能被下一项 $a_{n+1}$ 整除。求 $a_{37}$

下面我们考虑怎么对这题进行扩展。首先,37是素数,不难想到:数列的项数n应为奇素数,设为p。

于是得到修改后的题干:

$1$$p$ 排成数列 $\{a_n\}$ ,已知 $a_1 = p$ ,且前 $n$ 项和 $S_n$ 总能被下一项 $a_{n+1}$ 整除。求 $a_{p}$

接下来回顾之前的解题过程,我们能够确定的是 $a_{2} = 1$$S_{p}=p*\frac{p+1}{2}$ ,以及 $a_{p} \mid \left( p \times \frac{p+1}{2} \right)$ 。于是我们不难想到:限制 $\frac{p+1}{2}$ 为素数,必然能让 $a_{p}$ 固定为 $\frac{p+1}{2}$

这表明“ $\frac{p+1}{2}$ 也为素数”是 $a_{p}=\frac{p+1}{2}$ 的充分条件。但它是必要条件吗?

注:p为素数+ $\frac{p+1}{2}$ 为正整数等价于p为奇素数

我们不妨写代码来寻找符合条件的数列

回到最开始说的朴素的想法:

假设数列的前 $idx$ 项(a[1~idx])均已求出。枚举所有还没用过的数u,只要符合题意,就令 $a[idx+1]$u。于是,假设被增强为:数列的前 $idx+1$ 项均已求出——这是一个递归的过程

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. 1 <= cand <= P && used[cand] = false
  2. candcurrent_sum的因数

考虑从“2”入手进行优化。我们预处理出1到 $P*\frac{P+1}{2}$ 的每个数的所有因数,并把大于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;
}

推广问题的结论

$\frac{p+1}{2}$ 也为素数”是 $a_{p}=\frac{p+1}{2}$ 的充分不必要条件。但“p为奇素数”是 $a_{p}=\frac{p+1}{2}$ 的必要不充分条件

比如 $p=11$ 的两个排列中,出现了末项不是 $\frac{p + 1}{2} = 6$ 的情况

11, 1, 2, 7, 3, 8, 4, 9, 5, 10, 6
11, 1, 4, 8, 6, 10, 5, 9, 2, 7, 3

$p=19$ 的6个排列的末项恰好都是 $\frac{p + 1}{2} = 10$

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-可视化网页

还能继续提问:

  1. 能发现一些比较有规律的数列吗?
  2. 除了 $\frac{p+1}{2}$ ,末项还可以是哪些数?
  3. 每种可能的末项的占比如何(这里称为“末项分布”)?

为了更加清晰地展示对这些问题的回答,我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 所有代码

谢谢大佬!网页能够运行,接下来请你帮我做以下修改:

  1. “验证条件”按钮改成“验证条件并搜索”,点击后会验证条件。如果不通过,行为和目前一致。如果通过,就预先生成前10个数列。
  2. 对于p=37,第一次点击“继续生成10组”按钮后,我看到有10组数据出来,但是按钮一直都是“搜索中”,无法继续点击。请帮我查找原因并修复这个bug
  3. 复制按钮改为总是显示
  4. 复制按钮的左边新增一个“验证”按钮,点击后弹出一个对话框,对话框显示每一个 $S_n$$a_{n+1}$ ,以表明它确实符合题意

请在我给你的代码的基础上修改:

谢谢大佬!接下来请帮我在“已找到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会完全卡住,用户体验会很糟糕。

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-5-6 16:50

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表