数位dp总结 之 从入门到模板(stO)
#轉載自https://blog.csdn.net/wust_zzwh/article/details/52100392
基礎篇
數位dp是一種計數用的dp,一般就是要統計一個區間[le,ri]內滿足一些條件數的個數。所謂數位dp,字面意思就是在數位上進行dp咯。數位還算是比較好聽的名字,數位的含義:一個數有個位、十位、百位、千位......數的每一位就是數位啦!之所以要引入數位的概念完全就是為了dp。數位dp的實質就是換一種暴力枚舉的方式,使得新的枚舉方式滿足dp的性質,然后記憶化就可以了。
兩種不同的枚舉:對于一個求區間[le,ri]滿足條件數的個數,最簡單的暴力如下:
for(int i=le;i<=ri;i++)if(right(i)) ans++;然而這樣枚舉不方便記憶化,或者說根本無狀態可言。
新的枚舉:控制上界枚舉,從最高位開始往下枚舉,例如:ri=213,那么我們從百位開始枚舉:百位可能的情況有0,1,2(覺得這里枚舉0有問題的繼續看)
然后每一位枚舉都不能讓枚舉的這個數超過上界213(下界就是0或者1,這個次要),當百位枚舉了1,那么十位枚舉就是從0到9,因為百位1已經比上界2小了,后面數位枚舉什么都不可能超過上界。所以問題就在于:當高位枚舉剛好達到上界是,那么緊接著的一位枚舉就有上界限制了。具體的這里如果百位枚舉了2,那么十位的枚舉情況就是0到1,如果前兩位枚舉了21,最后一位之是0到3(這一點正好對于代碼模板里的一個變量limit 專門用來判斷枚舉范圍)。最后一個問題:最高位枚舉0:百位枚舉0,相當于此時我枚舉的這個數最多是兩位數,如果十位繼續枚舉0,那么我枚舉的就是以為數咯,因為我們要枚舉的是小于等于ri的所以數,當然不能少了位數比ri小的咯!(這樣枚舉是為了無遺漏的枚舉,不過可能會帶來一個問題,就是前導零的問題,模板里用lead變量表示,不過這個不是每個題目都是會有影響的,可能前導零不會影響我們計數,具體要看題目)
由于這種新的枚舉只控制了上界所以我們的Main函數總是這樣:
int main() {long long le,ri;while(~scanf("%lld%lld",&le,&ri))printf("%lld\n",solve(ri)-solve(le-1)); }w_w 是吧!統計[1,ri]數量和[1,le-1],然后相減就是區間[le,ri]的數量了,這里我寫的下界是1,其實0也行,反正相減后就沒了,注意題目中le的范圍都是大于等于1的(不然le=0,再減一就G_G了)
在講例題之前先講個基本的動態模板(先看后面的例題也行):dp思想,枚舉到當前位置pos,狀態為state(這個就是根據題目來的,可能很多,畢竟dp千變萬化)的數量(既然是計數,dp值顯然是保存滿足條件數的個數)
typedef long long ll; int a[20]; ll dp[20][state];//不同題目狀態不同 ll dfs(int pos,/*state變量*/,bool lead/*前導零*/,bool limit/*數位上界變量*/)//不是每個題都要判斷前導零 {//遞歸邊界,既然是按位枚舉,最低位是0,那么pos==-1說明這個數我枚舉完了if(pos==-1) return 1;/*這里一般返回1,表示你枚舉的這個數是合法的,那么這里就需要你在枚舉時必須每一位都要滿足題目條件,也就是說當前枚舉到pos位,一定要保證前面已經枚舉的數位是合法的。不過具體題目不同或者寫法不同的話不一定要返回1 *///第二個就是記憶化(在此前可能不同題目還能有一些剪枝)if(!limit && !lead && dp[pos][state]!=-1) return dp[pos][state];/*常規寫法都是在沒有限制的條件記憶化,這里與下面記錄狀態是對應,具體為什么是有條件的記憶化后面會講*/int up=limit?a[pos]:9;//根據limit判斷枚舉的上界up;這個的例子前面用213講過了ll ans=0;//開始計數for(int i=0;i<=up;i++)//枚舉,然后把不同情況的個數加到ans就可以了{if() ...else if()...ans+=dfs(pos-1,/*狀態轉移*/,lead && i==0,limit && i==a[pos]) //最后兩個變量傳參都是這樣寫的/*這里還算比較靈活,不過做幾個題就覺得這里也是套路了大概就是說,我當前數位枚舉的數是i,然后根據題目的約束條件分類討論去計算不同情況下的個數,還有要根據state變量來保證i的合法性,比如題目要求數位上不能有62連續出現,那么就是state就是要保存前一位pre,然后分類,前一位如果是6那么這意味就不能是2,這里一定要保存枚舉的這個數是合法*/}//計算完,記錄狀態if(!limit && !lead) dp[pos][state]=ans;/*這里對應上面的記憶化,在一定條件下時記錄,保證一致性,當然如果約束條件不需要考慮lead,這里就是lead就完全不用考慮了*/return ans; } ll solve(ll x) {int pos=0;while(x)//把數位都分解出來{a[pos++]=x%10;//個人老是喜歡編號為[0,pos),看不慣的就按自己習慣來,反正注意數位邊界就行x/=10;}return dfs(pos-1/*從最高位開始枚舉*/,/*一系列狀態 */,true,true);//剛開始最高位都是有限制并且有前導零的,顯然比最高位還要高的一位視為0嘛 } int main() {ll le,ri;while(~scanf("%lld%lld",&le,&ri)){//初始化dp數組為-1,這里還有更加優美的優化,后面講printf("%lld\n",solve(ri)-solve(le-1));} }相信讀者還對這個有不少疑問,筆者認為有必要講一下記憶化為什么是if(!limit)才行,大致就是說有無limit會出現狀態沖突,舉例:
約束:數位上不能出現連續的兩個1(11、112、211都是不合法的)
假設就是[1,210]這個區間的個數
狀態:dp[pos][pre]:當前枚舉到pos位,前面一位枚舉的是pre(更加前面的位已經合法了),的個數(我的pos從0開始)
先看錯誤的方法計數,就是不判limit就是直接記憶化
那么假設我們第一次枚舉了百位是0,顯然后面的枚舉limit=false,也就是數位上0到9的枚舉,然后當我十位枚舉了1,此時考慮dp[0][1],就是枚舉到個位,前一位是1的個數,顯然dp[0][1]=9;(個位只有是1的時候是不滿足的),這個狀態記錄下來,繼續dfs,一直到百位枚舉了2,十位枚舉了1,顯然此時遞歸到了pos=0,pre=1的層,而dp[0][1]的狀態已經有了即dp[pos][pre]!=-1;此時程序直接return dp[0][1]了,然而顯然是錯的,因為此時是有limit的個位只能枚舉0,根本沒有9個數,這就是狀態沖突了。有lead的時候可能出現沖突,這只是兩個最基本的不同的題目可能還要加限制,反正宗旨都是讓dp狀態唯一
對于這個錯誤說兩點:一是limit為true的數并不多,一個個枚舉不會很浪費時間,所以我們記錄下! limit的狀態解決了不少子問題重疊。第二,有人可能想到把dp狀態改一下dp[pos][state][limit]就是分別記錄不同limit下的個數,這種方法一般是對的,關于這個具體會講,下面有題bzoj3209會用到這個。
數位的部分就是這些,然后就是難點,dp部分,dp大牛的藝術,弱雞只能看看+...+
既然從高位往低位枚舉,那么狀態一般都是與前面已經枚舉的數位有關并且通常是根據約束條件當前枚舉的這一位能使得狀態完整(比如一個狀態涉及到連續k位,那么就保存前k-1的狀態,當前枚舉的第k個是個恰好湊成成一個完整的狀態,不過像那種狀態是數位的和就直接保存前綴和為狀態了),不過必然有一位最簡單的一個狀態dp[pos]當前枚舉到了pos位。dp部分就要開始講例題了,不過會介紹幾種常用防tle的優化。
實戰篇
例一:HDU 2089?不要62 入門題。就是數位上不能有4也不能有連續的62,沒有4的話在枚舉的時候判斷一下,不枚舉4就可以保證狀態合法了,所以這個約束沒有記憶化的必要,而對于62的話,涉及到兩位,當前一位是6或者不是6這兩種不同情況我計數是不相同的,所以要用狀態來記錄不同的方案數。 dp[pos][sta]表示當前第pos位,前一位是否是6的狀態,這里sta只需要去0和1兩種狀態就可以了,不是6的情況可視為同種,不會影響計數。 #include<iostream> #include<cstdio> #include<cstring> #include<string> using namespace std; typedef long long ll; int a[20]; int dp[20][2]; int dfs(int pos,int pre,int sta,bool limit) {if(pos==-1) return 1;if(!limit && dp[pos][sta]!=-1) return dp[pos][sta];int up=limit ? a[pos] : 9;int tmp=0;for(int i=0;i<=up;i++){if(pre==6 && i==2)continue;if(i==4) continue;//都是保證枚舉合法性tmp+=dfs(pos-1,i,i==6,limit && i==a[pos]);}if(!limit) dp[pos][sta]=tmp;return tmp; } int solve(int x) {int pos=0;while(x){a[pos++]=x%10;x/=10;}return dfs(pos-1,-1,0,true); } int main() {int le,ri;//memset(dp,-1,sizeof dp);可優化while(~scanf("%d%d",&le,&ri) && le+ri){memset(dp,-1,sizeof dp);printf("%d\n",solve(ri)-solve(le-1));}return 0; }入門就不多講了,開始講常用優化吧!
第一:memset(dp,-1,sizeof dp);放在多組數據外面。
這一點是一個數位特點,使用的條件是:約束條件是每個數自身的屬性,而與輸入無關。 具體的:上一題不要62和4,這個約束對每一個數都是確定的,就是說任意一個數滿不滿足這個約束都是確定,比如444這個數,它不滿足約束條件,不管你輸入的區間是多少你都無法改變這個數不滿足約束這個事實,這就是數自身的屬性(我們每組數據只是在區間計數而已,只能說你輸入的區間不包含444的話,我們就不把它統計在內,而無法改變任何事實)。 由此,我們保存的狀態就可以一直用(注意還有要limit,不同區間是會影響數位在有限制條件下的上限的) 這點優化就不給具體題目了,這個還有進一步的擴展。不過說幾個我遇到的簡單的約束: 1.求數位和是10的倍數的個數,這里簡化為數位sum%10這個狀態,即dp[pos][sum]這里10 是與多組無關的,所以可以memset優化,不過注意如果題目的模是輸入的話那就不能這樣了。 2.求二進制1的數量與0的數量相等的個數,這個也是數自身的屬性。 3.。。。。。 還是做題積累吧。搞懂思想! 下面介紹的方法就是要行memset優化,把不滿足前提的通過修改,然后優化。 介紹之前,先說一種較為笨拙的修改,那就是增加狀態,前面講limit的地方說增加一維dp[pos][state][limit],能把不同情況下狀態分別記錄(不過這個不能memset放外面)。基于這個思想,我們考慮:約束為數位是p的倍數的個數,其中p數輸入的,這和上面sum%10類似,但是dp[pos][sum]顯然已經不行了,每次p可能都不一樣,為了強行把memset提到外面加狀態dp[pos][sum][p],對于每個不同p分別保存對應的狀態。這里前提就比較簡單了,你dp數組必須合法,p太大就G_G了。所以對于與輸入有關的約束都可以強行增加狀態(這并不代表能ac,如果題目數據少的話就隨便你亂搞了)第二:相減。
例題:HDU 4734 題目給了個f(x)的定義:F(x) = An?* 2n-1?+ An-1?* 2n-2?+ ... + A2?* 2 + A1?* 1,Ai是十進制數位,然后給出a,b求區間[0,b]內滿足f(i)<=f(a)的i的個數。 常規想:這個f(x)計算就和數位計算是一樣的,就是加了權值,所以dp[pos][sum],這狀態是基本的。a是題目給定的,f(a)是變化的不過f(a)最大好像是4600的樣子。如果要memset優化就要加一維存f(a)的不同取值,那就是dp[10][4600][4600],這顯然不合法。 這個時候就要用減法了,dp[pos][sum],sum不是存當前枚舉的數的前綴和(加權的),而是枚舉到當前pos位,后面還需要湊sum的權值和的個數, 也就是說初始的是時候sum是f(a),枚舉一位就減去這一位在計算f(i)的權值,那么最后枚舉完所有位 sum>=0時就是滿足的,后面的位數湊足sum位就可以了。 仔細想想這個狀態是與f(a)無關的(新手似乎很難理解),一個狀態只有在sum>=0時才滿足,如果我們按常規的思想求f(i)的話,那么最后sum>=f(a)才是滿足的條件。 #include<cstdio> #include<cstring> #include<iostream> #include<string>using namespace std; const int N=1e4+5; int dp[12][N]; int f(int x) {if(x==0) return 0;int ans=f(x/10);return ans*2+(x%10); } int all; int a[12]; int dfs(int pos,int sum,bool limit) {if(pos==-1) {return sum<=all;}if(sum>all) return 0;if(!limit && dp[pos][all-sum]!=-1) return dp[pos][all-sum];int up=limit ? a[pos] : 9;int ans=0;for(int i=0;i<=up;i++){ans+=dfs(pos-1,sum+i*(1<<pos),limit && i==a[pos]);}if(!limit) dp[pos][all-sum]=ans;return ans; } int solve(int x) {int pos=0;while(x){a[pos++]=x%10;x/=10;}return dfs(pos-1,0,true); } int main() {int a,ri;int T_T;int kase=1;scanf("%d",&T_T);memset(dp,-1,sizeof dp);while(T_T--){scanf("%d%d",&a,&ri);all=f(a);printf("Case #%d: %d\n",kase++,solve(ri));}return 0; }減法的藝術!!!
例題 POJ 3252 這題的約束就是一個數的二進制中0的數量要不能少于1的數量,通過上一題,這題狀態就很簡單了,dp[pos][num],到當前數位pos,0的數量減去1的數量不少于num的方案數,一個簡單的問題,中間某個pos位上num可能為負數(這不一定是非法的,因為我還沒枚舉完嘛,只要最終的num>=0才能判合法,中途某個pos就不一定了),這里比較好處理,Hash嘛,最小就-32吧(好像),直接加上32,把32當0用。這題主要是要想講一下lead的用法,顯然我要統計0的數量,前導零是有影響的。至于!lead&&!limit才能dp,都是類似的,自己慢慢體會吧。 #pragma comment(linker, "/STACK:10240000,10240000") #include<iostream> #include<cstdio> #include<cstring> #include<string> #include<queue> #include<set> #include<vector> #include<map> #include<stack> #include<cmath> #include<algorithm> using namespace std; const double R=0.5772156649015328606065120900; const int N=1e5+5; const int mod=1e9+7; const int INF=0x3f3f3f3f; const double eps=1e-8; const double pi=acos(-1.0); typedef long long ll; int dp[35][66]; int a[66]; int dfs(int pos,int sta,bool lead,bool limit) {if(pos==-1)return sta>=32;if(!limit && !lead && dp[pos][sta]!=-1) return dp[pos][sta];int up=limit?a[pos]:1;int ans=0;for(int i=0;i<=up;i++){if(lead && i==0) ans+=dfs(pos-1,sta,lead,limit && i==a[pos]);//有前導零就不統計在內else ans+=dfs(pos-1,sta+(i==0?1:-1),lead && i==0,limit && i==a[pos]);}if(!limit && !lead ) dp[pos][sta]=ans;return ans; } int solve(int x) {int pos=0;while(x){a[pos++]=x&1;x>>=1;}return dfs(pos-1,32,true,true); } int main() {memset(dp,-1,sizeof dp);int a,b;while(~scanf("%d%d",&a,&b)){printf("%d\n",solve(b)-solve(a-1));}return 0; }
然后就是一些需要自己yy的題:
HDU 3709?這題就是要枚舉中軸,然后數位dp UVA 1305?這題我二分然后數位dp搞(好像不是正解,我水過的) Hbzoj 1799?這題講一下: (偷偷告訴你,這個oj是單組測試,然后memset什么的都是浮云了) 約束:一個數是它自己數位和的倍數,直接dp根本找不到狀態,枚舉數位和,因為總就162,然后問題就變成了一個數%mod=0,mod是枚舉的,想想狀態:dp[pos][sum][val],當前pos位上數位和是sum,val就是在算這個數%mod,(從高位算 ?*10+i),因為我們枚舉的數要保證數位和等于mod,還要保證這個數是mod的倍數,很自然就能找到這些狀態,顯然對于每一個mod,val不能保證狀態唯一,這是你要是想加一維dp[pos][sum][val][mod],記錄每一個mod的狀態(這里sum可以用減法,然而val不行,就只能加一維),那你就想太多了,這樣是會超時的(因為狀態太多,記憶化效果不好)。這里直接對每一個mod,memset一次就能ac。下面的代碼還把limit的當做了狀態,因為每次都要初始化,所以能這樣,memset在多組外面是不能這樣的,不過奇葩的,這代碼,如果不把limit當狀態,還是在!limit 條件下記錄dp,提交一發,時間竟然更短了,可能是每次memset的關系!!! #include<cstdio> #include<cstring> #include<iostream> #include<string>using namespace std;typedef long long ll;ll dp[20][163][163][2]; int a[20]; ll dfs(int pos,int sum,int val,int mod,bool limit) {if(sum-9*pos-9>0) return 0;//最壞的情況,這一位及后面的全部為9都不能達到0那就直接GG,這個剪枝不會影響acif(pos==-1) return sum==0 && val==0;if(dp[pos][sum][val][limit]!=-1) return dp[pos][sum][val][limit];int up=limit?a[pos]:9;ll ans=0;for(int i=0;i<=up;i++){if(sum-i<0) break;ans+=dfs(pos-1,sum-i,(val*10+i)%mod,mod,limit && i==a[pos]);}dp[pos][sum][val][limit]=ans;return ans; } ll solve(ll x) {int pos=0;while(x){a[pos++]=x%10;x/=10;}ll ans=0;for(int i=1;i<=pos*9;i++)//上限就是每一位都是9{memset(dp,-1,sizeof dp);ll tmp=dfs(pos-1,i,0,i,true);ans+=tmp;}return ans; } int main() { // cout<<18*9<<endl;ll le,ri; // memset(dp,-1,sizeof dp);while(~scanf("%lld%lld",&le,&ri))printf("%lld\n",solve(ri)-solve(le-1));return 0; } /* 1 1000000000000000000 */基本講的差不多了。前段時間學了點新東西!!
新的領域--計數轉求和
HDU 4507 這題麻煩就是要求數的平方和。 我們先考慮求和的問題,一個區間,數位dp能在一些約束下計數,現在要這些數的和。其實組合數學搞搞就可以了:如 現在枚舉的某一位pos,我統計了這一位枚舉i的滿足條件的個數cnt,其實只要算i對總和的貢獻就可以了,對于一個數而言第pos位是i,那么對求和貢獻就是i*10^pos,就是十進制的權值,然后有cnt個數都滿足第pos位是i,最后sum=cnt*i*10^pos.原理就是這樣平方和可以看做(a*10^pos+b)^2,a是你當前pos位要枚舉的,b其實是個子問題,就是pos之后的位的貢獻值,把這個平方展開就可以了! #pragma comment(linker, "/STACK:10240000,10240000") #include<iostream> #include<cstdio> #include<cstring> #include<string> #include<queue> #include<set> #include<vector> #include<map> #include<stack> #include<cmath> #include<algorithm> using namespace std; const double R=0.5772156649015328606065120900; const int N=1e5+5; const int mod=1e9+7; const int INF=0x3f3f3f3f; const double eps=1e-8; const double pi=acos(-1.0); typedef long long ll; ll fact[20]; void init() {fact[0]=1;for(int i=1;i<20;i++)fact[i]=fact[i-1]*10%mod; } struct node {ll cnt,sum,sqr;node(ll cnt=-1,ll sum=0,ll sqr=0):cnt(cnt),sum(sum),sqr(sqr){} }dp[20][7][7]; int a[20]; ll fac(ll x) {return x*x%mod; } ll dfs(int pos,ll num,ll val,ll&cnt,ll&sum,bool limit) {if(pos==-1) {if(num==0 || val==0)return 0;cnt=1;return 0;}if(!limit && dp[pos][num][val].cnt!=-1) {cnt=dp[pos][num][val].cnt;sum=dp[pos][num][val].sum;return dp[pos][num][val].sqr;}int up=limit?a[pos]:9;ll sq=0;for(int i=0;i<=up;i++)if(i!=7){ll cn=0,su=0;ll tmp=dfs(pos-1,(num+i)%7,(val*10+i)%7,cn,su,limit && i==a[pos]);ll tm=i*fact[pos]%mod;tmp=(tmp+fac(tm)*cn%mod+(tm*su%mod)*2%mod)%mod;//計數之后要更新sum,sqrsum=(sum+su+(i*fact[pos]%mod)*cn%mod)%mod;cnt=(cnt+cn)%mod;sq=(sq+tmp)%mod;}if(!limit) dp[pos][num][val]=node(cnt,sum,sq);return sq; } ll solve(ll x) {int pos=0;while(x){a[pos++]=x%10;x/=10;}ll t1=0,t2=0;return dfs(pos-1,0,0,t1,t2,true); } bool judge(ll x) {int sum=0;int pos=0;if(x%7==0) return false;while(x){if(x%10==7) return false;sum+=x%10;x/=10;}sum%=7;return sum!=0; } int main() {init();for(int i=0;i<20;i++)for(int j=0;j<7;j++)for(int k=0;k<7;k++)//memset{dp[i][j][k].cnt=-1;dp[i][j][k].sum=0;dp[i][j][k].sqr=0;}int T_T;scanf("%d",&T_T);while(T_T--){ll le,ri;scanf("%I64d%I64d",&le,&ri);ll ans=solve(ri)-solve(le-1);ans=(ans%mod+mod)%mod;printf("%I64d\n",ans);}return 0; }做題去~~
總結
以上是生活随笔為你收集整理的数位dp总结 之 从入门到模板(stO)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 尿检板怎么看
- 下一篇: Popular Cows POJ - 2