\(O(n^2)\) 做法
和大部分人最開始一樣,我也想的是 DP。
設 \(dp_i\) 表示用前面 \(i\) 個字元拆分得到的答案。既然是統計方案數,我們肯定是根據前面的答案累加。考慮在 \([1,i-1]\) 中選擇一個 \(j\),如果 \([j+1,i]\) 的字元組成的數字能夠被 \(m\) 整除,那麼 \(dp_i\) 就可以累加一個 \(dp_j\) 的值,因為如果當前區間滿足條件,就相當於這裡是一個可行的拆分,那麼前面 \(j\) 個字元得到的答案很明顯也都可以成為累加的一部分。
假設 \(flag_{j,i}\) 表示區間 \([j,i]\) 組成的數字是否可以被 \(m\) 整除,\(1\) 表示可以,\(0\) 表示不可以。則有轉移方程:
那麼答案就是在 \(dp_{n}\) 這裡了。
此做法時間複雜度為 \(O(n^2)\),而 \(n \leq 3\times 10^5\),並且無法進行最佳化,所以 DP 只能進行騙分。
\(O(n)\) 做法
考慮運用數學運算進行求解。
設想一下,假如字串的字首組成的數字 \(x\) 能夠被 \(m\) 整除會怎麼樣?如果整個字串組成的數字 \(sum\) 也能夠被 \(m\) 整除,那麼這個字首以後的所有字元組成的數字也必定可以被 \(m\) 整除。即 \(m\mid sum-x\times 10^{num}\),其中 \(num\) 是非字首的字元個數。這個是非常容易想到的一個式子。
那麼這樣的一個式子有什麼用呢?既然字首後面的數字可以被 \(m\) 整除,那麼我們能否按照相同的思路,在這之中進行拆分?假設後面的數字為 \(sum\),在這個數字裡面找一個字首組成數字 \(x\),由上文第一步推斷知道 \(m\mid sum\),如果此時 \(m\mid x\),那麼這個字首後面的數字也可以被 \(m\) 整除,這個思路和上文一模一樣。
所以我們可以得出一個結論,如果整個字串的某個字首組成的數字能被 \(m\) 整除,且整個字串組成的數字能夠被 \(m\) 整除,那麼此時這個字首的最後一個字元的下標處就是一個可以進行拆分的地方。如果在這裡進行拆分,那麼前後的字串也都會被 \(m\) 整除,因此這裡一定會被某一個拆分方式進行拆分。
所以我們可以找到所有被 \(m\) 整除的字首數字,記錄下這樣的字首的個數 \(res\)。然後這個問題就轉化成了對 \(res\) 個可以拆分的地方進行組合。因為這裡面的字首會包括整個字串,所以中間選擇的拆分的地方有 \(res-1\) 個。由於每個地方有選與不選的 \(2\) 種可能,因此計算的答案就是 \(2^{res-1}\) 次方。而求冪我們使用快速冪就可以了。
一定要注意,如果整個字串組成的數字不能被 \(m\) 整除,那麼答案一定是 \(0\),因為找不到任何一個拆分的地方,使得前後兩個數字都能夠被 \(m\) 整除。
程式碼如下:
#include<bits/stdc++.h>
#define int long long//方案取模題,日常開 long long
using namespace std;
const int MAXN=3e5+5;
const int MOD=1e9+7;
int n,m;
char s[MAXN];
int quick_pow(int x)//2^x的快速冪
{
int ans=1,sum=2;
while(x)
{
if(x&1) ans=ans*sum%MOD;
sum=sum*sum%MOD;
x>>=1;
}
return ans;
}
signed main()
{
cin>>n>>m>>(s+1);
int res=0,x=0;
for(int i=1;i<=n;i++) x=(x<<1)+(x<<3)+(s[i]^48),x%=m,res+=(!x);//計算可拆分地方的個數
if(x) puts("0");//特判
else cout<<quick_pow(res-1);//組合
return 0;
}