\(update:2019-9-6\)
博客里某些東西沒有解釋清楚,完善了對應的解釋
在開始之前,我們先來看一道題——題目鏈接
題目要求,相鄰兩位的差大於等於2,那么我們先來構造一個試一試。
比如說\(15246\)這個數,我們先取第一位為\(1\),然后第二位是\(5\),\(5-1=4>2\)所以符合條件,第三位是\(2\),\(5-2=3>2\)符合條件,第四位是\(4\),\(4-2=2\)符合條件,第五位是\(6\),\(6-4=4\)符合條件,所以這個數使符合條件的。
那么問題來了,如果我們一個數一個數的構造,復雜度顯然是有問題的,我們就需要對其進行優化。來看\(15246\)和\(96246\)這兩個數,他們都是符合規則的,而且它們后三位是相同的,那么我們很容易可以聯想到,只要倒數第四位與\(2\)的差符合規則,那么只要后三位是\(246\)就一定是符合規則的,也就是說我們根本不需要去重復判斷。
有沒有想到什么熟悉的東西?沒錯,記憶化!從前往后構造,當后幾位已經被處理過,我們就可以直接使用,而不是重新判斷,大大節省了時間。那么我們就可以用\(f[pos][pre]\)來記錄當前位為第\(pos\)位,上一位為數值為\(pre\),且不含前導零,沒有卡在最大值上時,能夠對答案產生的貢獻。
那么判斷也就很簡單了,只需要枚舉下一位,看和當前位的差是否滿足規則即可。題目又要求不含前導零,也就是除了\(0\)本身以外的任何數不准用\(0\)開頭,那么從第一位開始,我們記錄有前導\(0\)當有一位不為\(0\)之后,把狀態記錄為不含前導零,前導零后的第一個不為零位無任何限制。
那么我們來看一下程序吧
#include<iostream>
#include<cstring>
#include<cstdio>
#include<cctype>
#include<cmath>
#define ll long long
#define gc() getchar()
#define maxn 15
using namespace std;
inline ll read(){
ll a=0;int f=0;char p=gc();
while(!isdigit(p)){f|=p=='-';p=gc();}
while(isdigit(p)){a=(a<<3)+(a<<1)+(p^48);p=gc();}
return f?-a:a;
}
void write(ll a){
if(a>9)write(a/10);
putchar(a%10+'0');
}
int x,y,d[maxn],l;
ll f[maxn][maxn];
ll dfs(int pos,int pre,bool limit,bool lead){ //pos表示從后往前數第pos位,pre記錄前一位我們選擇的數值,limit記錄當前是不是卡着最大值,如果卡着最大值就不能從[0,9]中任意選數,而是在[0,區間右端點當前位的數值]之間選數,lead就是記錄前導零的問題了
if(!pos)return 1; //如果所有位都已經構造完了,說明這是一個合法數值,貢獻加一
if(!limit&&!lead&&~f[pos][pre])return f[pos][pre]; //如果已經處理過特殊要求均相同的情況,直接返回答案,避免重復計算
int up=limit?d[pos]:9;ll ans=0; //up就是當前位選數的右端點
for(int i=0;i<=up;++i){ //枚舉構造
if(abs(i-pre)<2&&!lead)continue; //如果不合法則跳過
ans+=dfs(pos-1,i,limit&&i==d[pos],lead&&!i);
}
if(!limit&&!lead)f[pos][pre]=ans; //這里有多種寫法,其實就是要求你把各種特殊狀態都記錄下來
return ans;
}
ll solve(int k){
l=0;
while(k){ //這里是為了記錄一下當前范圍最大是幾位
d[++l]=k%10;
k/=10;
}
return dfs(l,0,1,1);
}
int main(){memset(f,-1,sizeof f);
x=read();y=read();
write(solve(y)-solve(x-1)); //答案要求是[x,y]之間的windy數,所以減去[0,x)的windy數即可
return 0;
}
下面我們再來看兩道題,一道是ZJOI2010數字計數,另一道是CQOI2016的手機號碼
對於這兩道題,還是跟剛才一樣——先構造。
數字計數這道題就是要求統計每個數出現的次數,那么我們只需要分開統計,每次只統計一個數字,重復十次即可,需要記錄的特殊情況就是前導零還有邊界情況
下面是代碼(就不再附詳細解釋了)
#include<iostream>
#include<cstring>
#include<cstdio>
#include<cctype>
#define ll long long
#define gc() getchar()
#define maxn 15
using namespace std;
inline ll read(){
ll a=0;int f=0;char p=gc();
while(!isdigit(p)){f|=p=='-';p=gc();}
while(isdigit(p)){a=(a<<3)+(a<<1)+(p^48);p=gc();}
return f?-a:a;
}
void write(ll a){
if(a>9)write(a/10);
putchar(a%10+'0');
}
int l,d[maxn];
ll x,y,f[maxn][maxn];
ll dfs(int pos,int s,int x,bool limit,bool lead){
if(!pos)return s;
if(!limit&&!lead&&~f[pos][s])return f[pos][s];
int up=limit?d[pos]:9;ll ans=0;
for(int i=0;i<=up;++i)
ans+=dfs(pos-1,s+(i==0?(!lead&&x==0):i==x),x,limit&&i==d[pos],lead&&!i);
if(!limit&&!lead)f[pos][s]=ans;
return ans;
}
ll solve(ll k,int a){
l=0;
while(k){
d[++l]=k%10;
k/=10;
}
return dfs(l,0,a,1,1);
}
int main(){
x=read();y=read();
for(int i=0;i<=9;++i){
memset(f,-1,sizeof f);
printf("%lld ",solve(y,i)-solve(x-1,i));
}
return 0;
}
那么對於手機號碼這道題,需要記錄的特殊狀態就比較多了,分別是前導零,邊界情況,前兩位的數值以及是否出現8和是否出現4
那么代碼還是大同小異,只是加了幾個判斷而已
#include<algorithm>
#include<iostream>
#include<cstring>
#include<cstdio>
#include<cctype>
#define ll long long
#define gc getchar
#define maxn 15
using namespace std;
inline ll read(){
ll a=0;int f=0;char p=gc();
while(!isdigit(p)){f|=p=='-';p=gc();}
while(isdigit(p)){a=(a<<3)+(a<<1)+(p^48);p=gc();}
return f?-a:a;
}ll l,r;int d[maxn];
ll f[maxn][maxn][maxn][5][5][5][5];
ll dfs(int pos,int pre,int pre2,int limit,int c,int ba,int si){
if(ba&si)return 0;
if(!pos)return c;
if(~f[pos][pre][pre2][limit][c][ba][si])
return f[pos][pre][pre2][limit][c][ba][si];
int up=limit?d[pos]:9,down=pos==11;ll sum=0;
for(int i=down;i<=up;++i)
sum+=dfs(pos-1,i,pre,limit&(i==up),c|(i==pre&&i==pre2),ba|(i==8),si|(i==4));
return f[pos][pre][pre2][limit][c][ba][si]=sum;
}
inline ll solve(ll x){memset(f,-1,sizeof f);
memset(d,0,sizeof d);
if(x<1e10)return 0;
int len=0;
while(x){
d[++len]=x%10;
x/=10;
}
return dfs(len,11,11,1,0,0,0);
}
int main(){
l=read();r=read();
printf("%lld\n",solve(r)-solve(l-1));
return 0;
}
題目推薦
那么數位DP大概就是這個樣子,很簡單,也很明顯
如果這篇博客對你的學習有些許幫助,不妨點個推薦吧