數位DP入門詳解+題目推薦


\(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;
}

題目推薦

[HAOI2010]計數

[AHOI2009]同類分布

那么數位DP大概就是這個樣子,很簡單,也很明顯

如果這篇博客對你的學習有些許幫助,不妨點個推薦吧


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM