1. 程式人生 > 其它 >數位DP學習筆記

數位DP學習筆記

數位DP解決形如“區間[l,r]中,符合……形式的數的個數”的問題——所謂“形式”往往與數字本身每一位相關。通過大量狀態的加持,這一演算法能快速準確搜出答案。

數位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)\)

,不TLE就見鬼了。

暴力2: dfs每一位放什麼數,時間複雜度\(O(n)\),少了個 lg 的原因是進入下一層的時候可以保證合法,但是沒有本質區別。

對於暴力2,有一個小問題:怎麼保證合法呢?加狀態——前導零、前一位、前兩位……

二、 優化

對於每一位,往下搜尋時會出現大量重複狀態。比如,第一位填了 1 ,第二位要考慮一遍 0~9 ;第一位填 2 的時候,第二位還是要考慮 0~9 。既然如此,為什麼不記憶化呢?

再考慮有哪些狀態顯然不行:如果區間是 \([1,3567]\) ,第一位顯然不能填大於 3 的數。那第二位呢?如果第一位填了 3 ,那第二位不能填大於 5 的數,但如果第一位是 2 ,那第二位就沒有限制了。

一個方法出現了,記錄下每一位的上限 \(a_i\) , 在dfs時記錄狀態 \(lim\) 並轉移。如果上一位填到上限,下一位就不能填超

三、 程式碼實現和注意事項

注意事項: 記憶化陣列開多大?每增加一個狀態是不是就要加一維?求得是滿足的還是不滿足的?

U163166 Bomb

#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;
}

T130742 數字遊戲

#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;
}

P2657 [SCOI2009] windy 數

#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;
}

THE END