Loading

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}\) , 考虑转化成上面那种情况

一般的, 只要满足

\[(pre_j - pre_{i + 1}) \div 10^{n - i} \equiv 0 \pmod D \rightarrow \frac{pre_j - pre_{i + 1}}{2^{n - i}5^{n - i}} \equiv 0 \pmod {2^a5^b D^{\prime}} \]

转化成同时满足

\[\begin{cases} \begin{aligned} & \frac{pre_j}{2^{n - i}5^{n - i}} \equiv \frac{pre_{i + 1}}{2^{n - i}5^{n - i}} \pmod {D ^ \prime} \iff pre_j \equiv pre_{i + 1} \pmod {D ^ \prime} \\ & \frac{pre_j}{2^{n - i}5^{n - i}} \equiv \frac{pre_{i + 1}}{2^{n - i}5^{n - i}} \pmod {2^a} \\ & \frac{pre_j}{2^{n - i}5^{n - i}} \equiv \frac{pre_{i + 1}}{2^{n - i}5^{n - i}} \pmod {5^b} \end{aligned} \end{cases}\\ \]

容易发现令 \(\omega = \min(a, b) \leq 20\) , 那么显然的, 对于每一个数, 超过 \(20\) 位之后, 一定有

\[\begin{cases} \begin{aligned} \left(\frac{S_j \times 10^{n - j}}{2^{n - i}5^{n - i}} = S_j \times 10^{i - j} \right)\equiv 0 \pmod {2^a} \\ \left(\frac{S_j \times 10^{n - j}}{2^{n - i}5^{n - i}} = S_j \times 10^{i - j} \right)\equiv 0 \pmod {5^b} \end{aligned} \end{cases}\\ \]

这里其实使用了一个思路的转化, 相当于将前缀和拆开来看, 其实就是一个带 \(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\) 的次幂) , 我们考虑先将其提出, 再分解处理

posted @ 2024-12-11 09:50  Yorg  阅读(4)  评论(0编辑  收藏  举报