- 下面是一段对该代码如何统计小于给定整数 $R$ 的“蛇数” (Snake numbers) 的中文解释。然后通过以下方式来得到区间 $[L, R]$ 中的“蛇数”个数:
$$ \text{countIn}[L, R] = \text{solve}(R+1) \;-\; \text{solve}(L). $$
这里,“蛇数”被定义为:一个正整数(至少两位)最左侧(最高位)的数字比它剩下的所有数字都要大。
1. 整体思路
solve(R)
返回所有“小于”用字符串R
表示的整数的蛇数的个数。- 为了数出区间 $[L, R]$ 内的蛇数,代码执行:
$$ \text{solve}(R+1) \;-\; \text{solve}(L). $$ solve(R+1)
统计所有小于 $R+1$ 的蛇数,即所有 $\le R$ 的蛇数。solve(L)
统计所有小于 $L$ 的蛇数。- 两者的差即得到区间 $[L, R]$ 里的蛇数总数。
为什么要加 “$+1$”?
因为 solve(X)
计算的是 小于 $X$ 的蛇数数量,所以要得到 “$\le R$” 的蛇数,就需要统计 “$< R+1$” 的蛇数。
2. solve(string R)
的代码流程
给定一个十进制的字符串 R
(表示一个 $$$\ge 0$$$ 的整数):
i64 solve(string R){
i64 n = R.size();
i64 ans = 0;
// 1) 统计所有位数 < n 的蛇数
for(i64 k=2; k<n; k++){
for(i64 t=1; t<=9; t++){
i64 cnt = 1;
rep(i,k-1) cnt *= t; // cnt = t^(k-1)
ans += cnt;
}
}
// 2) 对于 n 位数,先统计 leading digit < R[0] 的那些
for(i64 t=1; t < R[0]-'0'; t++){
i64 cnt = 1;
rep(i,n-1) cnt *= t; // cnt = t^(n-1)
ans += cnt;
}
// 3) 再处理 leading digit == R[0] 的情况
i64 t = R[0] - '0';
for(i64 j=1; j<n; j++){
if(R[j] >= R[0]){
i64 cnt = 1;
for(i64 i=j; i<n; i++){
cnt *= t;
}
ans += cnt;
break;
} else {
i64 cnt = (R[j] - '0');
for(i64 i=j+1; i<n; i++){
cnt *= t;
}
ans += cnt;
}
}
return ans;
}
让我们分段分析:
2.1. 统计位数少于 $n$ 的蛇数
for(i64 k=2; k<n; k++){
for(i64 t=1; t<=9; t++){
i64 cnt = 1;
rep(i,k-1) cnt *= t; // t^(k-1)
ans += cnt;
}
}
- 如果一个数有 $k$ 位,且最高位是 $t \in \{1..9\}$,剩下的 $(k-1)$ 位都必须 < $t$,也就是从 $\{0,1,\dots,t-1\}$ 中选取。
- 因此对于固定的最高位 $t$,有 $t^{k-1}$ 种选法。
- 把 $t=1$ 到 $9$ 的情况都加起来就是$$ \sum_{t=1}^9 t^{k-1}。$$
- 这个循环就是把 2 位到 $n-1$ 位 所有蛇数都加进来。执行完后,
ans
等于 “所有位数 < $n$ 的蛇数个数”。
2.2. 统计与 R
同位数、但最高位比 R[0]
小的蛇数
for(i64 t=1; t < R[0]-'0'; t++){
i64 cnt = 1;
rep(i,n-1) cnt *= t; // t^(n-1)
ans += cnt;
}
- 如果有 $n$ 位数的最高位是 $t$,且 $t < R[0]$,那么这些数在数值上肯定比以
R[0]
为首的R
小。 - 对于这类最高位 $t$(从 1 到
R[0]-1
),蛇数的个数是 $t^{n-1}$。 - 这里就把它们一次性加进去。
2.3. 处理最高位与 R[0]
相同的情况(部分前缀)
i64 t = R[0] - '0';
for(i64 j=1; j<n; j++){
if(R[j] >= R[0]){
i64 cnt = 1;
for(i64 i=j; i<n; i++){
cnt *= t;
}
ans += cnt;
break;
} else {
i64 cnt = (R[j] - '0');
for(i64 i=j+1; i<n; i++){
cnt *= t;
}
ans += cnt;
}
}
- 现在的情况是,首位定成
t = R[0] - '0'
,那么需要从高到低比较后面各位。 - 在“蛇数”条件下,后续各位必须在$$ \{0,\dots,t-1\} $$以内。
- 这段代码逐位遍历
R[j]
: - 若
R[j] >= R[0]
,则表示如果我们要保持和R
的前 $j-1$ 位一样,但在第 $j$ 位选择比R[j]
小,后续位可任意取 $\{0..t-1\}$,从而推算出这一批次的所有可能之和。然后break
退出循环。 - 否则 (
R[j] < R[0]
),它就计算 “第 $j$ 位可以从 $\{0..(R[j]-‘0’)\}$ 选择” 的种数,再乘以后面各位可选的范围 $\{0..t-1\}$,累加到ans
。
最后把累积的 ans
返回,即 “小于字符串 R
表示的整数”的蛇数总数。
3. 区间 $[L, R]$ 的答案
在 testcase()
函数中:
void testcase(){
i64 L, R; cin >> L >> R;
i64 ans = solve(to_string(R+1));
ans -= solve(to_string(L));
cout << ans << endl;
}
solve(to_string(R+1))
= 统计 小于 $R+1$ 的蛇数数量 = 统计 $\le R$ 的蛇数数量。solve(to_string(L))
= 统计 小于 $L$ 的蛇数数量。- 它们的差就得到在 $[L, R]$ 区间内(含两端)的蛇数总数。
4. 示例:$\;L=93,\; R=2500$
- 代码会计算:
$$ \text{ans} = \text{solve}(“2501”) \;-\; \text{solve}(“93”). $$ solve("2501")
:- 先数所有 2 位、3 位的蛇数(因为 “2501” 有 4 位)。
- 再数首位 <
2
(只有1
) 的 4 位蛇数。 - 最后处理首位 =
2
的情况,逐位与 “2501” 比较。 solve("93")
:- “93” 有 2 位,先数首位 <
9
的 2 位蛇数。 - 接着把首位 =
9
、第二位 $\le 3$ 的情况计入。
两者相减,即得到 $[93, 2500]$ 中的蛇数个数。
5. 总结
尽管看上去像是手写了“一部分一部分”的逻辑,但它实际上做了组合前缀的统计过程:
- 先把位数更少的蛇数全部算完。
- 再算与目标数
R
同位数但最高位更小的情况。 - 最后通过“依次比较后续数字”来考虑最高位相同的情况。
最终,为了得到区间 $[L, R]$ 的个数,按照常规做法:
$$
\text{#Snake in }[L, R]
\;=\;
\text{solve}(R+1)
\;-\;
\text{solve}(L).
$$
这样就无需遍历 ([L,R]) 之间的所有数,能够高效处理非常大的输入。