青创八临时小测

Done IOI Start at: 2026-3-25 19:30 0.7 hour(s) Host: 26

格机格机格机格机~~~

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 整除。

  • 原理101(mod9)10 \equiv 1 \pmod 9,所以 10n1n1(mod9)10^n \equiv 1^n \equiv 1 \pmod 9
  • 推广:对于 10k110^k - 1(即 kk 个 9 组成的数),同样的逻辑适用: 10k1(mod10k1)10^k \equiv 1 \pmod{10^k - 1} 这意味着,如果我们把一个大数 NN 按照 kk 位一组 进行分割,每一组其实就是 10k10^k 的不同幂次的系数。 例如,k=3k=3 时,1031=99910^3-1 = 999: $123456789 = 123 \cdot (10^3)^2 + 456 \cdot (10^3)^1 + 789 \cdot (10^3)^0$ 在模 999999 的意义下,这等价于: $123 \cdot (1)^2 + 456 \cdot (1)^1 + 789 \cdot (1) = 123 + 456 + 789$

结论:一个数 NN 能被 10k110^k - 1 整除,当且仅当它kk 位分段求和后的结果也能被 10k110^k - 1 整除。


2. 迭代缩减策略 (Iterative Reduction)

由于原数 NN 可能非常大(10510^5 位),分段求和一次后的结果虽然变小了,但可能仍然超过 kk 位(无法直接判断)。

  • 代码逻辑:使用 while (n.size() > k)
  • 只要当前的字符串长度还超过 kk,就继续进行“分段求和”操作。
  • 这类似于我们把 8888 求和得 1616,再把 1616 求和得 77,直到剩下一位数。

3. 高精度模拟 (High-Precision Arithmetic)

由于 NN 达到 10510^5 位,kk 达到 100100,普通的数据类型无法处理。

  • 分段提取n.substr(max(0, i - k), ...) 实现了从右往左每 kk 位截取一段。
  • 字符串加法add(string a, string b) 函数实现了标准的高精度加法(带进位处理)。
  • 性能考量:虽然是高精度,但每次迭代字符串长度都会显著缩短(从 LL 变成 L/kL/k 左右的位数),因此这种迭代收敛极快,总复杂度在 O(TN)O(T \cdot |N|) 级别。

4. 临界值判断 (The "99...9" Trap)

在不断的迭代求和后,如果原数 NN10k110^k - 1 的倍数,最终会剩下什么?

  • N=18,k=1N=18, k=1 为例:1+8=91+8=9。最终剩下 99
  • N=999,k=3N=999, k=3 为例:本身就是 3 位,循环不进入,剩下 999999
  • 代码实现if (n == s)。这里的 s 就是预先构造的 kk 个 9。
  • 思考:为什么不判断 n == "0"?因为题目保证 NN 是正整数且不含前导零,在分段求和的过程中,结果永远不可能是 00,最小的倍数就是 10k110^k-1 本身。

5. 总结思维模型

  1. 数论转换:将“大数整除 10k110^k-1”转化为“分段(kk位一段)求和”的等价问题。
  2. 高精度处理:利用字符串模拟大数的分段截取与累加。
  3. 递归/迭代思想:不断缩小问题规模,直到结果落入可以直接比对的范围(kk 位以内)。
  4. 特殊情况:识别出 kk 个 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)

首先,通过小规模数据的手动模拟,寻找幸存者编号 f(n)f(n) 的规律:

  • n=1n=1: [1] \to 1
  • n=2n=2: [1], 2 \to 1
  • n=3n=3: [1], 2, 3 \to 剩余 [1, 3] \to 1敬酒, 3留, 1退 \to 3
  • n=4n=4: [1], 2, 3, 4 \to 剩余 [1, 3] \to 1留, 3退 \to 1
  • n=5n=5: [1], 2, 3, 4, 5 \to 剩余 [1, 3, 5] \to 5敬酒, 1退, 3留 \to 3
  • n=6n=6: [1], 2, 3, 4, 5, 6 \to 剩余 [1, 3, 5] \to 5敬酒, 1退, 3敬酒, 5留 \to 5
  • n=7n=7: [1], 2, 3, 4, 5, 6, 7 \to 剩余 [1, 3, 5, 7] \to 7
  • n=8n=8: [1], 2, 3, 4, 5, 6, 7, 8 \to 1

观察结果序列1,1,3,1,3,5,7,11, 1, 3, 1, 3, 5, 7, 1 \dots

2. 发现关键性质:2 的幂 (The Power of 2 Property)

从序列中可以清晰看到,每当人数 nn 恰好是 22 的整数次幂(20=1,21=2,22=4,23=82^0=1, 2^1=2, 2^2=4, 2^3=8)时,最后剩下的必然是 1 号

逻辑推理: 在一个循环中,如果总人数是偶数,第一轮过后会剩下一半人,且起始位置不变。如果总人数一直是偶数(即 2k2^k),经过 kk 轮“隔一个删一个”的操作,起始的 1 号会因为始终处于“跳过(敬酒者)”的位置而留到最后。

3. 状态转换与化归 (Reduction)

nn 不是 22 的幂时,我们可以通过“杀掉”一部分人,让剩余的人数变成 22 的幂。

假设 n=2k+Ln = 2^k + L(其中 2k2^k 是小于等于 nn 的最大功率,而 LL 是多出来的人数)。

  • 规则是:每敬一次酒,就有一个人退出。
  • 为了让圈子里只剩下 2k2^k 个人,必须有 LL 个人退出。
  • 既然每敬一次酒退出一人,那么在 LL 次敬酒 发生后,剩下的圈子大小就是 2k2^k
  • 关键点:在第 LL 个人退出后,下一个轮到敬酒(被跳过)的那个人,就相当于新的 2k2^k 规模圆圈中的“1号”,根据性质,他就是最终的赢家。

4. 数学表达式推导 (Mathematical Formula)

  • 每退出一个人,光标在圆圈上移动 2 个位置(跳过一个,删除一个)。
  • 退出 LL 个人后,光标总共移动了 2L2L 个位置。
  • 此时,光标停留在了第 2L+12L+1 个人的位置上。
  • 所以,最终赢家的编号就是 2L+12L + 1

L=n2kL = n - 2^k 代入公式:

f(n)=2(n2k)+1f(n) = 2(n - 2^k) + 1

5. 代码逻辑实现 (Algorithm Implementation)

根据上述公式,代码的执行逻辑非常清晰:

  1. 寻找 2k2^k
    while (m * 2 <= n) m *= 2; // 找到不大于 n 的最大 2 的幂
    
    例如 n=10n=10,代码会找到 m=8m=8。此时 L=108=2L = 10 - 8 = 2
  2. 计算结果
    cout << 2 * (n - m) + 1 << '\n';
    
    代入公式:2(108)+1=52 * (10 - 8) + 1 = 5。输出 5。
  3. 效率分析: 由于 nn 最大为 101810^{18}while 循环最多执行 log2(1018)60\log_2(10^{18}) \approx 60 次。对于 T=105T=10^5 的数据量,总复杂度为 O(Tlogn)O(T \log n),在 1 秒时限内绰绰有余。

总结思维模型

模拟寻找规律 \to 锁定 2n2^n 特殊状态 \to 利用“距离 2n2^n 的偏移量 LL”进行状态化归 \to 导出 2L+12L+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)推导出递推公式,最终利用矩阵快速幂O(logN)O(\log N) 的时间内完成计算。

以下是该代码的解题思维链:


1. 状态定义与初步 DP (State Definition)

首先,设第 ii 块梯田的颜色为 cic_i,我们需要统计长度为 NN 的合法序列总数。根据题目约束:

  • ci=1c_i = 1(红):不能接在 1 后面,只能接在 2 或 3 后面。
  • ci=2c_i = 2(蓝):可以接在 1, 2, 3 任何颜色后面。
  • ci=3c_i = 3(金):不能接在 3 后面,只能接在 1 或 2 后面。

定义 Ri,Bi,GiR_i, B_i, G_i 分别为长度为 ii 且最后一块是红、蓝、金的方案数:

  • Ri=Bi1+Gi1R_i = B_{i-1} + G_{i-1}
  • Bi=Ri1+Bi1+Gi1B_i = R_{i-1} + B_{i-1} + G_{i-1}
  • Gi=Ri1+Bi1G_i = R_{i-1} + B_{i-1}

总方案数 Si=Ri+Bi+GiS_i = R_i + B_i + G_i

2. 利用对称性简化状态 (Symmetry Reduction)

观察发现,红色和金色的约束是对称的(都不能自交,且对蓝色的影响一致)。因此在每一层中,必然有 Ri=GiR_i = G_i。 令 Xi=Ri=GiX_i = R_i = G_i(代表红或金),Yi=BiY_i = B_i(代表蓝)。 新的递推关系:

  • Xi=Yi1+Xi1X_i = Y_{i-1} + X_{i-1}
  • Yi=2Xi1+Yi1Y_i = 2X_{i-1} + Y_{i-1}

代入总数公式 Si=2Xi+YiS_i = 2X_i + Y_i

3. 推导二阶递推公式 (Finding the Recurrence)

为了进一步简化,我们尝试找出 SiS_i 的直接递推公式:

  • $S_i = 2(Y_{i-1} + X_{i-1}) + (2X_{i-1} + Y_{i-1}) = 4X_{i-1} + 3Y_{i-1}$
  • 同样写出 Si1=2Xi1+Yi1S_{i-1} = 2X_{i-1} + Y_{i-1}
  • 我们可以通过线性组合发现规律:Si=2Si1+Si2S_i = 2S_{i-1} + S_{i-2}

验证规律:

  • N=1:S1=3N=1: S_1 = 3 (1, 2, 3)
  • N=2:S2=2×3+1=7N=2: S_2 = 2 \times 3 + 1 = 7 (题目样例 1)
  • N=3:S3=2×7+3=17N=3: S_3 = 2 \times 7 + 3 = 17
  • N=10:S10=8119N=10: S_{10} = 8119 (题目样例 2)

这个数列在数学上被称为 Pell 数列 的一种变体。

4. 构造转移矩阵 (Matrix Construction)

由于 NN 高达 101810^{18},必须使用矩阵快速幂。 我们要构造一个矩阵 AA,使得:

$$\begin{pmatrix} S_i \\ S_{i+1} \end{pmatrix} = A \begin{pmatrix} S_{i-1} \\ S_i \end{pmatrix}$$

根据 Si+1=Si×2+Si1×1S_{i+1} = S_i \times 2 + S_{i-1} \times 1,矩阵 AA 应该是:

A=(0112)A = \begin{pmatrix} 0 & 1 \\ 1 & 2 \end{pmatrix}

代码实现中的矩阵: 代码中定义了 a[3][3] = {{0, 0, 0}, {1, 0, 1}, {0, 1, 2}}。 虽然多开了一维且排列不同,但其核心逻辑是一致的(即第一行不用,后两行执行了 Si=Si1S_i = S_{i-1}Si+1=Si1+2SiS_{i+1} = S_{i-1} + 2S_i 的逻辑)。 初始状态向量 ans = {0, 1, 3}

  • ans[1] 存放的是 S0=1S_0 = 1(这里将 N=0N=0 定义为 1 是一种技巧)。
  • ans[2] 存放的是 S1=3S_1 = 3

5. 矩阵快速幂 (Binary Exponentiation)

  • mul1 函数:实现“向量 ×\times 矩阵”,用于将当前答案向量进行一次状态转换。
  • mul2 函数:实现“矩阵 ×\times 矩阵”,用于矩阵的倍增自乘(A,A2,A4A, A^2, A^4 \dots)。
  • 主逻辑:对 NN 进行二进制拆分。如果 NN 的某一位为 1,则让结果向量乘上对应的矩阵幂次。

总结思维模型

  1. 转化:将相邻颜色约束转化为状态转移方程(DP)。
  2. 降维:利用红/金颜色的对称性,将三个变量压缩为两个。
  3. 发现规律:通过代换发现其本质是 Si=2Si1+Si2S_i = 2S_{i-1} + S_{i-2} 的线性递推。
  4. 加速:针对超大规模 NN,利用矩阵快速幂将 O(N)O(N) 复杂度降低至 O(logN)O(\log N)

这道题是典型的“计数类 DP \to 线性递推 \to 矩阵加速”的综合应用题。

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