atcoder abc158 E - Divisible Substring
先看題面
題目的意思是,給定字符串和一個質數P,求字符串當中的子串被P所整除的個數。
寫一下心路歷程,一開始看到題目直接莽dp(最近dp上頭)
意義是以第i個字符爲結尾的所有字符串所對應的數字被P除,餘數爲j的個數,上面的公式是由i層遍歷j,去找到數值k將其相加,事實上,我們可以反一下,我們不遍歷j,去遍歷餘數,也就是根據公式
k爲餘數,這樣子遍歷一遍i-1層的同時,就完成了i層的賦值。
看了看數據範圍,dp數組空間可以優化成只用2N,因爲每一次都只用前一層,只存兩層就夠了,所以空間複雜度能接受,算了一下時間複雜度,大概。。。不行,但是仍然要試試看 (當時覺得這個dp能優化) 此時時間複雜度爲o(NP)
於是寫出了令人窒息的dp
#include<bits/stdc++.h>
using namespace std;
char s[100000*2+5];
long long dp[2][10000+5];
int n,P;
int main(){
cin>>n>>P;
long long sum=0;
scanf("%s",s);
for(int i=0;i<n;i++){
int t=s[i]-'0';
memset(dp[i%2],0,P*sizeof (long long));
for(int j=0;j<P;j++)
dp[i%2][(j*10+t)%P]+=dp[(i+1)%2][j];
dp[i%2][t%P]++;
sum+=dp[i%2][0];
}
printf("%lld\n",sum);
}
上交之後有幾個大案例超時。。。。雖然也在意料之中。。。你問我第一個公式有什麼用?可能這種形式的dp有大佬能優化吧。。。還是先留着吧,沒準日後有用。。。。
事後查看大佬的代碼,我人蒙了。。。
#include <iostream>
using namespace std;
long N,P,ans,cur,t=1,cnt[10010],i;string S;
int main()
{
cin>>N>>P>>S;
if(P==2||P==5){
for(i=0;i<N;i++)if((S[i]-'0')%P==0)ans+=i+1;
}else{
cnt[0]=1;
for(i=N-1;i>=0;i--){
(cur+=(S[i]-'0')*t)%=P;
ans+=cnt[cur]++;
t=t*10%P;
}
}
cout<<ans;
}
着實nb的代碼。我看不懂,於是研究了一上午,終於看懂了。
首先,大佬很巧妙地運用了%的性質
首先要確定一點,就是對於一個字符串abcdef...
,如果abcdef..%p==ef..%p
,那麼我們可以認爲abcd0..%p==0
,怎麼證明呢,上一行等式,移項,同加p,同%p,可得(abcde..%p-e..%p+p)%p=p%p=0
,再運用%的性質,可得(abcde..-e..)%p=0
但是我們想要知道的是abcd能不能被p整除,而不是abcd0%p,對於任意的字符串相減之後,我們只能知道abcd…*10^k次能不能被p整除,要通過某一種方法轉換,設中間的字符串所對應的數字爲M,現在我們已知(M*10^k)%p==0
,運用%的乘法性質展開,得((M%p)*(10^k%p))%p==0
,括號內部左邊就是我們想知道的,我們想知道他會不會等於0。如果等式成立,那麼只有三種可能
M%p=0
10^k%p=0
(M%p)*(10^k%p)
是p的倍數,且不等於0
但是因爲p是質數只能被分解爲1*p,且(M%p)和(10^k%p)都小於p,那麼不存在一個數=p,所以必然不能滿足第三點。
第一種可能即爲我們所希望的,我們只要再考慮一下第二種情況,並將其排除第二種情況成立的時候排除,剩下的就是符合我們第一種可能的所有情況。考慮第二種可能,%左邊的數分解質因數必然分解爲2^k*5^k
,也就是說質因子只有k個2和5,若想要第二種可能不成立,則p只要不等於2或5即可,換句話說,我們只要特判p=2或5的情況,除此以外,其他情況下(M*10^k)%p==0
和M%p==0
的答案一樣。
那麼方法就很明顯了,在p!=2或5的時候,找到滿足abcdef..%p==ef..%p
的情況,那麼此中間字符串一定能被p整除。所以可以從最右邊開始記錄餘數,cnt[i]的意義是以i爲餘數的字符串所對應的數字個數,每次計算餘數結束之後,就給答案加上cnt[i],再進行cnt[i]++,因爲之前出現過的位置必然也能和當前位置組合成能夠被整除的字符串。有一個要注意的地方就是,cnt[0]必須要賦初值爲1,因爲如果餘數爲0,說明本身就能被整除,無需其他位置也能被整除。
剛纔考慮p!=2或5的一般情況,現在考慮p==2或5的特殊情況,我們都知道p爲2或5的時候,p的倍數有一個有趣的性質,p的倍數的最後一位必然是2或5!也就是說,如果遇到p=2或5,只要遍歷字符串,遇到2或5的時候,給答案每次都加上當前下標+1即可。舉個例子,125,p=5時,答案是3,p=2時答案是。
在此深深感受到了出題人的nb,所有條件正好全用上,我一開始真沒想到質數的特殊,在整個解法中質數貫穿全部。
關於爲什麼不能從頭開始遍歷
一開始我也很疑惑,但其實看了推導過程就會發現,這是由公式決定的,舉個例子,字符串abcdef
,已知如果從頭開始遍歷,公式會變爲判斷(abcd-a000)%p
是否爲0,括號內部左邊易求,右邊不易求需要對每個值進行*10^k之後取模,會超時。
如果從尾部開始遍歷,判斷(abcd-bcd)%p
是否爲0,左右都易求。所以只能從尾部開始遍歷。
以下是我寫的代碼,大同小異,幾乎一樣。
#include<bits/stdc++.h>
using namespace std;
char s[100000*2+5];
int ys[10000+5];
int n,P;
int main(){
cin>>n>>P;
long long sum=0;
scanf("%s",s);
int t=1,v=0;
if(P==2||P==5){
for(int i=0;i<n;i++)
if((s[i]-'0')%P==0)
sum+=i+1;
}
else{
ys[0]=1;
for(int i=n-1;i>=0;i--){
v+=t*(s[i]-'0');
v%=P;
sum+=ys[v];
ys[v]++;
t*=10;
t%=P;
}
}
printf("%lld\n",sum);
}
總結
上面方法完美用上了質數的性質,但是如果題目不給質數的條件,這個方法就不能適用,但是我一開始TLE的代碼,卻可以解決,使用了動態規劃。當然dp優化那麼多,本人才疏學淺還不能優化他,也許將來有一天可以,所以先放着。