數位DP學習筆記
阿新 • • 發佈:2021-08-13
數位DP解決形如“區間[l,r]中,符合……形式的數的個數”的問題——所謂“形式”往往與數字本身每一位相關。通過大量狀態的加持,這一演算法能快速準確搜出答案。
,不TLE就見鬼了。
數位dp似乎是通過加狀態實現的記憶化搜尋。
一、 題目和暴力
數位dp的題目一般是這樣的:求出區間 \([l,r]\) 中含有/不含數字 \(x\) 的數的個數。由此引申出來的還有 “從左到右各位不降” 或者 “相鄰兩位差為2” 等等。區間長度一般比較大,一般是 \(1 \leq l \leq r \leq 2^{31}-1\) 或者 \(1 \leq l \leq r \leq 2^{63}-1\) 。
顯然的思路是轉化為類似於字首和的東西,這樣就把問題轉化為對於 \([1,n]\) 求了。
暴力1: \(O(n)\) 掃一遍區間,對於每個數 \(O(lgn)\) 掃一遍看看符不符合。時間複雜度 \(O(nlgn)\)
暴力2: dfs每一位放什麼數,時間複雜度\(O(n)\),少了個 lg 的原因是進入下一層的時候可以保證合法,但是沒有本質區別。
對於暴力2,有一個小問題:怎麼保證合法呢?加狀態——前導零、前一位、前兩位……
二、 優化
對於每一位,往下搜尋時會出現大量重複狀態。比如,第一位填了 1 ,第二位要考慮一遍 0~9 ;第一位填 2 的時候,第二位還是要考慮 0~9 。既然如此,為什麼不記憶化呢?
再考慮有哪些狀態顯然不行:如果區間是 \([1,3567]\) ,第一位顯然不能填大於 3 的數。那第二位呢?如果第一位填了 3 ,那第二位不能填大於 5 的數,但如果第一位是 2 ,那第二位就沒有限制了。
一個方法出現了,記錄下每一位的上限 \(a_i\) , 在dfs時記錄狀態 \(lim\) 並轉移。如果上一位填到上限,下一位就不能填超。
三、 程式碼實現和注意事項
注意事項: 記憶化陣列開多大?每增加一個狀態是不是就要加一維?求得是滿足的還是不滿足的?
#include<stdio.h> #include<string.h> typedef long long ll; int a[20];ll f[20][10]; ll dfs(int len,bool lim,int pre){ if(!len) return 1; if(!lim&&f[len][pre]!=-1) return f[len][pre]; int up=lim?a[len]:9;ll ans=0; for(int i=0;i<=up;++i){ if(pre==4&&i==9) continue; ans+=dfs(len-1,lim&&i==up,i); } if(!lim) f[len][pre]=ans; return ans; } inline ll calc(ll x){ memset(a,0,sizeof a); memset(f,-1,sizeof f); int len=0; while(x) a[++len]=x%10,x/=10; return dfs(len,1,0); } int main(){ int t;scanf("%d",&t); while(t--){ ll n;scanf("%lld",&n); printf("%lld\n",(ll)n-calc(n)+1); } return 0; }
#include<stdio.h>
#include<string.h>
int a[11],f[11][11];
int dfs(int len,bool lim,int pre){
if(!len) return 1;
if(!lim&&f[len][pre]!=-1) return f[len][pre];
int ans=0,up=lim?a[len]:9;
for(int i=pre;i<=up;++i)
ans+=dfs(len-1,lim&&i==up,i);
if(!lim) f[len][pre]=ans;
return ans;
}
inline int calc(int x){
memset(a,0,sizeof a);
memset(f,-1,sizeof f);
int len=0;
while(x) a[++len]=x%10,x/=10;
return dfs(len,1,0);
}
int main(){
int a,b;
while(scanf("%d%d",&a,&b)==2)
printf("%d\n",calc(b)-calc(a-1));
return 0;
}
#include<stdio.h>
#include<string.h>
#include<math.h>
int a[11],f[11][11][2];
int dfs(int len,bool lim,int pre,bool zero){
if(!len) return 1;
if(!lim&&f[len][pre][zero]!=-1) return f[len][pre][zero];
int ans=0,up=lim?a[len]:9;
for(int i=0;i<=up;++i){
if(abs(i-pre)<2&&!zero) continue;
ans+=dfs(len-1,lim&&i==up,i,zero&&!i);
}
if(!lim) f[len][pre][zero]=ans;
return ans;
}
inline int calc(int x){
memset(a,0,sizeof a);
memset(f,-1,sizeof f);
int len=0;
while(x) a[++len]=x%10,x/=10;
return dfs(len,1,0,1);
}
int main(){
int a,b;
scanf("%d%d",&a,&b);
printf("%d\n",calc(b)-calc(a-1));
return 0;
}