青创八临时小测
格机格机格机格机~~~
T1
#include <bits/stdc++.h>
using namespace std;
string add(string a, string b) {
int n = a.size(), m = b.size();
string res = "";
for (int i = n - 1, j = m - 1, pos = 0; i >= 0 || j >= 0 || pos;) {
int sum = pos;
if (i >= 0)
sum += a[i--] - '0';
if (j >= 0)
sum += b[j--] - '0';
res += (sum % 10 + '0');
pos = sum / 10;
}
reverse(res.begin(), res.end());
return res;
}
int main() {
int t;
cin >> t;
while (t--) {
int k;
string n, s = "";
cin >> k >> n;
for (int i = 0; i < k; i++)
s += '9';
while (n.size() > k) {
string sum = "0";
for (int i = n.size(); i > 0; i -= k)
sum = add(sum, n.substr(max(0, i - k), i - max(0, i - k)));
n = sum;
}
if (n == s)
cout << "YES\n";
else
cout << "NO\n";
}
return 0;
}
这是一个经典的数论性质与高精度模拟结合的问题。它的核心逻辑基于“9的整除特征”的广义推广。
1. 发现数学规律:广义的“九除法” (Generalization of Rule of 9s)
我们都知道一个常识:一个数能被 9 整除,当且仅当它的各位数字之和能被 9 整除。
- 原理:,所以 。
- 推广:对于 (即 个 9 组成的数),同样的逻辑适用: 这意味着,如果我们把一个大数 按照 每 位一组 进行分割,每一组其实就是 的不同幂次的系数。 例如, 时,: $123456789 = 123 \cdot (10^3)^2 + 456 \cdot (10^3)^1 + 789 \cdot (10^3)^0$ 在模 的意义下,这等价于: $123 \cdot (1)^2 + 456 \cdot (1)^1 + 789 \cdot (1) = 123 + 456 + 789$
结论:一个数 能被 整除,当且仅当它按 位分段求和后的结果也能被 整除。
2. 迭代缩减策略 (Iterative Reduction)
由于原数 可能非常大( 位),分段求和一次后的结果虽然变小了,但可能仍然超过 位(无法直接判断)。
- 代码逻辑:使用
while (n.size() > k)。 - 只要当前的字符串长度还超过 ,就继续进行“分段求和”操作。
- 这类似于我们把 求和得 ,再把 求和得 ,直到剩下一位数。
3. 高精度模拟 (High-Precision Arithmetic)
由于 达到 位, 达到 ,普通的数据类型无法处理。
- 分段提取:
n.substr(max(0, i - k), ...)实现了从右往左每 位截取一段。 - 字符串加法:
add(string a, string b)函数实现了标准的高精度加法(带进位处理)。 - 性能考量:虽然是高精度,但每次迭代字符串长度都会显著缩短(从 变成 左右的位数),因此这种迭代收敛极快,总复杂度在 级别。
4. 临界值判断 (The "99...9" Trap)
在不断的迭代求和后,如果原数 是 的倍数,最终会剩下什么?
- 以 为例:。最终剩下 。
- 以 为例:本身就是 3 位,循环不进入,剩下 。
- 代码实现:
if (n == s)。这里的s就是预先构造的 个 9。 - 思考:为什么不判断
n == "0"?因为题目保证 是正整数且不含前导零,在分段求和的过程中,结果永远不可能是 ,最小的倍数就是 本身。
5. 总结思维模型
- 数论转换:将“大数整除 ”转化为“分段(位一段)求和”的等价问题。
- 高精度处理:利用字符串模拟大数的分段截取与累加。
- 递归/迭代思想:不断缩小问题规模,直到结果落入可以直接比对的范围( 位以内)。
- 特殊情况:识别出 个 9 是整除情况下的特征值。
这个题目展示了如何通过简单的数论性质(同余)极大地简化看似复杂的高精度除法问题。
T2
#include <bits/stdc++.h>
using namespace std;
#define int long long
signed main() {
int t;
cin >> t;
while (t--) {
int m = 1, n;
cin >> n;
if (n == 1) {
cout << "1\n";
continue;
}
while (m * 2 <= n)
m *= 2;
cout << 2 * (n - m) + 1 << '\n';
}
return 0;
}/*
1 2 3 4 5 6
| | | | |
1 2 3 4 5 6 7 8 9
| | | | | | | |*/
这份 AC 代码背后的核心是 约瑟夫环问题(Josephus Problem)的一个最简特例。
以下是针对该代码的解题思维链条,分为五个逻辑阶段:
1. 观察与模拟 (Observation)
首先,通过小规模数据的手动模拟,寻找幸存者编号 的规律:
- : [1] 1
- : [1], 2 1
- : [1], 2, 3 剩余 [1, 3] 1敬酒, 3留, 1退 3
- : [1], 2, 3, 4 剩余 [1, 3] 1留, 3退 1
- : [1], 2, 3, 4, 5 剩余 [1, 3, 5] 5敬酒, 1退, 3留 3
- : [1], 2, 3, 4, 5, 6 剩余 [1, 3, 5] 5敬酒, 1退, 3敬酒, 5留 5
- : [1], 2, 3, 4, 5, 6, 7 剩余 [1, 3, 5, 7] 7
- : [1], 2, 3, 4, 5, 6, 7, 8 1
观察结果序列:
2. 发现关键性质:2 的幂 (The Power of 2 Property)
从序列中可以清晰看到,每当人数 恰好是 的整数次幂()时,最后剩下的必然是 1 号。
逻辑推理: 在一个循环中,如果总人数是偶数,第一轮过后会剩下一半人,且起始位置不变。如果总人数一直是偶数(即 ),经过 轮“隔一个删一个”的操作,起始的 1 号会因为始终处于“跳过(敬酒者)”的位置而留到最后。
3. 状态转换与化归 (Reduction)
当 不是 的幂时,我们可以通过“杀掉”一部分人,让剩余的人数变成 的幂。
假设 (其中 是小于等于 的最大功率,而 是多出来的人数)。
- 规则是:每敬一次酒,就有一个人退出。
- 为了让圈子里只剩下 个人,必须有 个人退出。
- 既然每敬一次酒退出一人,那么在 次敬酒 发生后,剩下的圈子大小就是 。
- 关键点:在第 个人退出后,下一个轮到敬酒(被跳过)的那个人,就相当于新的 规模圆圈中的“1号”,根据性质,他就是最终的赢家。
4. 数学表达式推导 (Mathematical Formula)
- 每退出一个人,光标在圆圈上移动 2 个位置(跳过一个,删除一个)。
- 退出 个人后,光标总共移动了 个位置。
- 此时,光标停留在了第 个人的位置上。
- 所以,最终赢家的编号就是 。
将 代入公式:
5. 代码逻辑实现 (Algorithm Implementation)
根据上述公式,代码的执行逻辑非常清晰:
- 寻找 :
例如 ,代码会找到 。此时 。while (m * 2 <= n) m *= 2; // 找到不大于 n 的最大 2 的幂 - 计算结果:
代入公式:。输出 5。cout << 2 * (n - m) + 1 << '\n'; - 效率分析:
由于 最大为 ,
while循环最多执行 次。对于 的数据量,总复杂度为 ,在 1 秒时限内绰绰有余。
总结思维模型
模拟寻找规律 锁定 特殊状态 利用“距离 的偏移量 ”进行状态化归 导出 公式。
这种思维方式在处理约瑟夫环、数位 DP 或带有倍增性质的题目时非常典型:即先看特殊点,再看偏移量。
T3
#include <bits/stdc++.h>
using namespace std;
#define in cin
#define out cout
#define int long long
#define ios ios::sync_with_stdio(false), in.tie(nullptr), out.tie(nullptr)
const int Mod = 1e9 + 7;
int a[3][3] = {{0, 0, 0}, {1, 0, 1}, {0, 1, 2}}, ans[3] = {0, 1, 3}, n;
void mul1() {
int b[3] = {0, 0, 0};
for (int i = 0; i < 3; i++)
for (int j = 0; j < 3; j++)
b[i] = (b[i] + ans[j] * a[i][j]) % Mod;
ans[0] = b[0], ans[1] = b[1], ans[2] = b[2];
}
void mul2() {
int b[3][3] = {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}};
for (int i = 0; i < 3; i++)
for (int j = 0; j < 3; j++)
for (int k = 0; k < 3; k++)
b[i][j] = (b[i][j] + a[i][k] * a[k][j]) % Mod;
for (int i = 0; i < 3; i++)
for (int j = 0; j < 3; j++)
a[i][j] = b[i][j];
}
signed main() {
ios;
in >> n;
while (n) {
if (n & 1)
mul1();
mul2(), n >>= 1;
}
out << ans[1];
return 0;
}
这是一个带约束条件的计数问题,通过从动态规划(DP)推导出递推公式,最终利用矩阵快速幂在 的时间内完成计算。
以下是该代码的解题思维链:
1. 状态定义与初步 DP (State Definition)
首先,设第 块梯田的颜色为 ,我们需要统计长度为 的合法序列总数。根据题目约束:
- (红):不能接在 1 后面,只能接在 2 或 3 后面。
- (蓝):可以接在 1, 2, 3 任何颜色后面。
- (金):不能接在 3 后面,只能接在 1 或 2 后面。
定义 分别为长度为 且最后一块是红、蓝、金的方案数:
总方案数 。
2. 利用对称性简化状态 (Symmetry Reduction)
观察发现,红色和金色的约束是对称的(都不能自交,且对蓝色的影响一致)。因此在每一层中,必然有 。 令 (代表红或金),(代表蓝)。 新的递推关系:
代入总数公式 。
3. 推导二阶递推公式 (Finding the Recurrence)
为了进一步简化,我们尝试找出 的直接递推公式:
- $S_i = 2(Y_{i-1} + X_{i-1}) + (2X_{i-1} + Y_{i-1}) = 4X_{i-1} + 3Y_{i-1}$
- 同样写出
- 我们可以通过线性组合发现规律:。
验证规律:
- (1, 2, 3)
- (题目样例 1)
- (题目样例 2)
这个数列在数学上被称为 Pell 数列 的一种变体。
4. 构造转移矩阵 (Matrix Construction)
由于 高达 ,必须使用矩阵快速幂。 我们要构造一个矩阵 ,使得:
$$\begin{pmatrix} S_i \\ S_{i+1} \end{pmatrix} = A \begin{pmatrix} S_{i-1} \\ S_i \end{pmatrix}$$根据 ,矩阵 应该是:
代码实现中的矩阵:
代码中定义了 a[3][3] = {{0, 0, 0}, {1, 0, 1}, {0, 1, 2}}。
虽然多开了一维且排列不同,但其核心逻辑是一致的(即第一行不用,后两行执行了 和 的逻辑)。
初始状态向量 ans = {0, 1, 3}:
ans[1]存放的是 (这里将 定义为 1 是一种技巧)。ans[2]存放的是 。
5. 矩阵快速幂 (Binary Exponentiation)
- mul1 函数:实现“向量 矩阵”,用于将当前答案向量进行一次状态转换。
- mul2 函数:实现“矩阵 矩阵”,用于矩阵的倍增自乘()。
- 主逻辑:对 进行二进制拆分。如果 的某一位为 1,则让结果向量乘上对应的矩阵幂次。
总结思维模型
- 转化:将相邻颜色约束转化为状态转移方程(DP)。
- 降维:利用红/金颜色的对称性,将三个变量压缩为两个。
- 发现规律:通过代换发现其本质是 的线性递推。
- 加速:针对超大规模 ,利用矩阵快速幂将 复杂度降低至 。
这道题是典型的“计数类 DP 线性递推 矩阵加速”的综合应用题。
- Status
- Done
- Rule
- IOI
- Problem
- 3
- Start at
- 2026-3-25 19:30
- End at
- 2026-3-25 20:09
- Duration
- 0.7 hour(s)
- Host
- Partic.
- 26