12.10 CW 模拟赛 T1. 数位
算法
挂个 \(\rm{pdf}\)
题目下载
容易想到的是暴力的 \(\rm{dp}\) , 这里不加阐述
发现瓶颈在找符合要求的倍数串, 那么我们怎么去优化呢
首先我们需要考虑类似前缀的方法, 只有这样理论复杂度才能达到要求
令 \(pre_i \gets pre_{i + 1} + 10^{n - i} \times S_i\) , 那么区间 \([j, i]\) 为倍数串当且仅当 \((pre_j - pre_{i + 1}) \div 10^{n - i} \equiv 0 \pmod D\)
考虑观察 \(\rm{Subtask}\) , 发现当 \(\gcd (D, 10) = 1\) 时, 只需要满足 \(pre_j \equiv pre_{i + 1} \pmod D\) 即可
这个怎么处理呢?
开桶记录一下桶中前缀和即可
那么考虑 \(\gcd (D, 10) \neq 1\) 时怎么去做, 一般的, 令 \(D = 2^a5^b D^{\prime}\) , 考虑转化成上面那种情况
一般的, 只要满足
转化成同时满足
容易发现令 \(\omega = \min(a, b) \leq 20\) , 那么显然的, 对于每一个数, 超过 \(20\) 位之后, 一定有
这里其实使用了一个思路的转化, 相当于将前缀和拆开来看, 其实就是一个带 \(10\) 的次幂的 \(S_i\) 之和
具体怎么实现?
对于每一个 \(f_i\) 的更新, 我们需要知道满足倍增串的 \(f_j\) 的前缀和
先枚举从 \(i\) 为结尾的 \(\omega\) 位数字, 判断是否满足 \(2^a, 5^b\) 的限制条件, 若满足, 再调用桶中的前缀和并更新
时间复杂度 \(\mathcal{O} (T \omega n)\) , 其中 \(\omega \leq 20\)
实现
框架
首先读入, 拆分 \(D\) 求得 \(D^{\prime}\) , 特别的, 对于 \(\rm{Subtask \ 2}\) , \(D = D^{\prime}\)
然后我们预处理出 \(pre_i \pmod {D^{\prime}}\)
然后我们只需要在递推的时候维护 \(f_{i, 0} + f_{i, 1}\) 的桶前缀和, \(f_{i, 1}\) 的前缀和即可
代码
#include <bits/stdc++.h>
const int MAXN = 1e5 + 20;
const int MAXVAL = 1e6 + 41;
const int MOD = 1e9 + 7;
int T;
int D; std::string S; int n;
int w = 20;
long long PreSum = 1; // f[i][1] 的前缀和
long long PreBuk[MAXVAL], PreBuk1[MAXVAL]; // f[i][0] + f[i][1] 的桶前缀和
long long f[MAXN][2];
int Dmod, lsy;
int Hash[MAXN];
int init_Max = 0;
#define getchar() getchar_unlocked()
inline std::string read()
{
std::string str="";
char ch = getchar();
//处理空格、换行或回车
while(ch==' ' || ch=='\n' || ch=='\r')
ch=getchar();
//读入
while(ch!=' ' && ch!='\n' && ch!='\r')
{
str+=ch;
ch=getchar();
}
return str;
}
void print(int x) {
if (x > 9)
print(x / 10);
putchar(x % 10 + '0');
}
class Sol_Class
{
private:
public:
/*初始化字符串 hash*/
inline void init()
{
init_Max = 0;
Dmod = D, lsy = 1;
int cnt1 = 0, cnt2 = 0;
while (!(Dmod & 1)) Dmod >>= 1, lsy <<= 1, ++cnt1; while (!(Dmod % 5)) Dmod /= 5, lsy *= 5, cnt2++;
w = cnt1 > cnt2 ? cnt1 : cnt2;
Hash[n + 1] = 0;
for (int i = n, pow10 = 1; i >= 1; i--, pow10 = pow10 * 10 % Dmod)
Hash[i] = (Hash[i + 1] + (S[i] - '0') * pow10) % Dmod, init_Max = std::max(init_Max, Hash[i]);
}
inline void solve()
{
PreSum = 1;
f[0][1] = 1, f[0][0] = 0;
PreBuk1[Hash[1]] = PreBuk[Hash[1]] = !w;
#pragma GCC unroll 8
for (int i = 1; i <= n; ++i)
{
f[i][0] = PreSum;
bool flag = true;
for (int j = 0, pow10 = 1, num = 0; j < i && j < w; ++j, pow10 = pow10 * 10 % D) {
num = (num + pow10 * (S[i - j] - '0')) % D;
if (!num) {
(f[i][1] += f[i - j - 1][0] + f[i - j - 1][1]) %= MOD;
f[i][0] = (f[i][0] + MOD - f[i - j - 1][1]) % MOD; // 撤销
}
flag = !(num % lsy);
}
/*处理 20 位以外的部分*/
if (flag) {
f[i][1] = (f[i][1] + PreBuk[Hash[i + 1]]) % MOD;
f[i][0] = (f[i][0] + MOD - PreBuk1[Hash[i + 1]]) % MOD;
}
PreSum = (PreSum + f[i][1]) % MOD;
if (i >= w) {
PreBuk1[Hash[i - w + 1]] = (PreBuk1[Hash[i - w + 1]] + f[i - w][1]) % MOD;
PreBuk[Hash[i - w + 1]] = (PreBuk[Hash[i - w + 1]] + f[i - w][1] + f[i - w][0]) % MOD;
}
}
print((f[n][1] + f[n][0]) % MOD), putchar('\n');
#pragma GCC unroll 8
for (int i = 1; i <= n; ++i)
Hash[i] = f[i][0] = f[i][1] = 0;
#pragma GCC unroll 8
for (int i = 0; i <= init_Max; ++i)
PreBuk1[i] = PreBuk[i] = 0;
}
} Sol;
signed main()
{
scanf("%d", &T);
while (T--) {
S = read(); scanf("%d", &D);
n = S.size(); S = ' ' + S;
Sol.init();
Sol.solve();
}
return 0;
}
总结
常用的前缀转换, 注意转移到模数意义下之后可以忽视爆 \(\rm{long \ long}\)
分治思想无敌, 我说的
新 \(\rm{trick}\) : 对于一个需要消掉的数字(本题中为 \(10\) 的次幂) , 我们考虑先将其提出, 再分解处理