E. Divisible Substring
题目:
给了一个长度为 n 的数字串,和一个质数 p ,询问有多少子串对应的数字满足是 p 的倍数,输出答案, 若有前导零也算作合法数字。
数据范围:
1 ≤ N ≤ 2 ∗
2 ≤ P ≤ 10000
思路:
结论:假设 x1x2x3x4x5 ∗ % p = m,x1x2 ∗
% p = m。则 x3x4x5 ∗
% p = 0, 即 x3x4x5 % p = 0(若p是质数,p != 2且p != 5)
证明:x1x2x3x4x5 ∗ % p = ((x1x2 *
)%p + (x3x4x5 *
)%p)%p;
因为x1x2 * % p = m,那么x3x4x5 *
% p = 0。证毕。
所以我们可以把 s[] 所有的前缀数 *%p (这里的n是字符串长度,i 是数字在字符串对应的下标)的值全部用一个数组 cnt[] 记录,这样同样模式相同的任意两个都可以找到一个满足条件的子序列。
除此之外,对于 p = 2 和 p = 5 时需要特判,直接根据 s[i] 是否能被 2 或 5 整除,如果成立,则以s[i] 为结尾的满足条件的子串的个数为 i+1(以 s[i] 为结尾的长度大于 1 的子串和子串 s[i] 本身,因为凡是 2 或 5 的倍数的数最后一位一定是 2 或 5 的倍数)
初始化 cnt[0] = 1。因为如果出现前缀数为 0 时,也满足条件。
Code:
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
#define int long long
//快速幂
int qmi(int a, int k, int p)
{
int res = 1;
while (k)
{
if (k & 1)res = res * a % p;
a = a * a % p;
k >>= 1;
}
return res;
}
void solve()
{
int n, p;
string s;
cin >> n >> p >> s;
int sum = 0, ans = 0;
vector<int>cnt(p, 0);
if (p == 2 || p == 5) //特判
{
for (int i = n - 1; i >= 0; i--)
ans = ans + ((s[i] - '0') % p == 0) * (i + 1); //逆序遍历字符串,如果s[i]是p的倍数,那么以s[i]为最后一个字符的子字符串都成立
}
else
{
cnt[0] = 1;
for (int i = 0; i < n; i++)
{
sum = (sum * 10 + s[i] - '0') % p; //sum记录前缀数,即x0x1……xi
int now = qmi(10, n - i - 1, p) * sum % p; //now记录x0x1……xi ∗ 10^(n-i-1)%p
ans += cnt[now]; //此时的cnt[now]为目前找到的值为now的子序列个数,每次加的是此时所找到的值为now的子串与之前累计得出的cnt[now]个子串两两配对形成新的子串的个数,最终计算的就是两两配对后总计的。
cnt[now]++; //更新找到的值为now的子序列个数
}
cout << ans << endl;
}
signed main()
{
int t = 1;
//cin >> t;
while (t--)
{
solve();
}
return 0;
}
除了上述写法外,还可以根据结论得出另一种方法:
根据上面的结论也可以说是:当 mod p != 0(p != 2 && p != 5),那么长为 n 的主串,对于 子串S[l……r] ,当且仅当 S[l……n] ≡ S[r……n] (mod p) 成立时,该子串满足要求,即S[l……r] % p == 0。
所以我们可以不用快速幂,直接找有哪些结尾是 s[n-1](最后一个字符,这是下标从0算的,也就是上面说的n)的子串对 p 取模相同。
Code:
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
#define int long long
void solve()
{
int n, p;
string s;
cin >> n >> p >> s;
int res = 0, ans = 0;
vector<int>cnt(p, 0);
if (p == 2 || p == 5) //特判
{
for (int i = n - 1; i >= 0; i--)
ans = ans + ((s[i] - '0') % p == 0) * (i + 1); //逆序遍历字符串,如果s[i]是p的倍数,那么以s[i]为最后一个字符的子字符串都成立
}
else
{
int k = 1;
cnt[0] = 1;
for (int i = n - 1; i >= 0; i--) //倒序循环,保证子串的最后一个是a[n-1]
{
res = (res + (s[i] - '0') * k) % p; //res为子串s[i~n-1]表示的数对p取模
ans += cnt[res]++; //每出现一对res同的子串[l,n]与[r,n],就有一个区间[l,r]满足
k = k * 10 % p;
}
}
cout << ans << endl;
}
signed main()
{
int t = 1;
//cin >> t;
while (t--)
{
solve();
}
return 0;
}
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
吐槽:这题一开始的结论证明我瞅了半天差不多看懂了,然后看代码实现的时候懵了好久,可疑惑为什么只处理出来前缀数就可以对所有满足达到两两配对的情况记数。关键在于每次循环ans都加,举例模拟下就发现最终res相同的组,ans总共加上的就是这些组中两两任意配对后的。呼,这种实现方式还是第一次见,真奇妙啊,有种不管你咋想这代码,反正这样算加的结果就是对的赶脚QAQ。
上面是之前自己第一次看时想的乱七八糟的,写题时又遇到相同的需要计算两两配对的对数的题时,经过学长的指点,我终于懂了orz!所以我来改博客了(我之前的理解是单纯用样例模拟得出结果对就感觉这代码好奇妙,这是不严谨的,还是要懂原理和细节)。ans每次加,算的是在又找到新的一个序列时,该序列与之前所累计的子串进行两两匹配,匹配的对数就是此时的子串个数。最终得到的ans就是算上所有子序列任意两两匹配的总个数。