[筆記]數位dp例題及詳解(更新中)

Sinktank發表於2024-04-06

數位dp的定義引自洛穀日報#84

求出在給定區間\([L,R]\)內,符合條件\(f(i)\)的數\(i\)的個數。條件\(f(i)\)一般與數的大小無關,而與數的組成有關。
由於是按位dp,數的大小對複雜度的影響很小

由於數位dp狀態的上下文資訊比較多,所以一般用記憶化搜尋實現,而非遞推。

P4999 煩人的數學作業

題意簡述\(T\)次詢問,每次給定\(L,R\),輸出\([L,R]\)中所有數的數位和之和。
\(1\leq L\leq R\leq 10^{18},1\leq T\leq 20\)

我們發現範圍很大,如果模擬會超時。所以引入數位dp的做法,數位dp一般會利用字首和的思想,把\([L,R]\)轉化為\([1,R]-[1,L-1]\),那麼怎麼計算\([1,x]\)呢?

\(f[i][j]\)表示從最高位開始填了\(i\)位,數位和為\(j\)的答案。

思考如何轉移:因為我們從最高位開始填,那麼顯然每一位都有限制。拿\(520\)舉例:

  • \(1\)位如果填\(0\sim 4\),那麼後面可以隨便填沒有限制。
  • \(1\)位如果填\(5\),那麼第\(2\)位就要受限,如果填\(0\sim 1\)就和第一條一樣,填\(2\)就是第二條,這樣迴圈下去直到填完……

所以\(dfs\)的引數應有三個。

  • \(pos\),表示當前正在填哪一位,從最高位\(len\)(原數的位數)開始往前填,根節點\(pos=len\),即正在填最高位。\(pos=0\)為結束條件。
  • \(limit\)bool型別,表示當前這一位有沒有限制。
  • \(sum\),表示從最高位填到\(pos+1\)數位和是多少,用作遞迴結束的返回值。

但是我們發現這樣就是一個普通的模擬,把所有數都試了一遍。所以需要記憶化,如果\(f[pos][sum]\)已經計算過了,直接返回即可(需要注意\(limit=false\)時才能用)。

為什麼\(limit=false\)才需要記憶化,是因為\(limit=true\)的情況只有\(i=a[pos]\)(見程式碼),所以在遞迴樹上只是一條鏈,沒有記憶的必要。

至於時間複雜度,那就是狀態數量,換句話說就是\(f\)陣列的大小,因為呼叫是\(f[pos][sum]\),所以大小是\(len*10len\)\(log_{10}r\),總\(O(T\ log_{10}r)\)

思考:傳遞的引數中如果有陣列等,為了節省空間,把它定義成全域性陣列,然後利用回溯傳遞狀態更好(這裡的\(pos,sum\)就可以這樣子最佳化掉,但是本身遞迴層數就不多,所以影響幾乎沒有)。

點選檢視程式碼
#include<bits/stdc++.h>
#define int long long
#define mod 1000000007
using namespace std;
int f[25][250],a[25],t,l,r;
int dfs(int pos,bool limit,int sum){
	if(pos==0) return sum;
	if(!limit&&f[pos][sum]) return f[pos][sum];
	int rig=limit?a[pos]:9;
	int ans=0;
	for(int i=0;i<=rig;i++){
		ans=(ans+dfs(pos-1,limit&&i==rig,sum+i))%mod;
		//依次列舉這一位填什麼
		//如果這一位沒有限制,那麼填前一位也一定沒有限制。
		//如果這一位有限制,那麼只有這一位填的數為a[pos]時才有限制(具體上面有說明)
	}
	if(!limit) f[pos][sum]=ans;
	return ans;
}
int solve(int x){//把x的值存入a陣列
	int len=0;
	while(x){
		a[++len]=x%10;
		x/=10;
	}
	return dfs(len,1,0);
}
signed main(){
	cin>>t;
	while(t--){
		cin>>l>>r;
		cout<<(solve(r)-solve(l-1)+mod)%mod<<endl;
	}
	return 0;
}

P2602 [ZJOI2010] 數字計數

與剛才那道很像哦,只不過詢問的是\(0\sim 9\)每個數字出現多少次。一個求和,一個計數。

似乎數位dp一般思路就是先寫一個暴搜,然後再思考怎麼記憶化。一開始想到兩種暴搜思路:

  • 將狀態表示成\(sta[10]\),為了省空間不再透過引數傳遞,而是開一個全域性,透過回溯實現。傳遞的引數有\(pos\)\(limit\),含義和上面的一樣。具體過程也差不多,就是把所有狀態列舉一遍。\(pos=0\)時結束,把當前的\(sta\)加入到\(ans\)陣列中。最後輸出\(ans\)
  • 不用陣列表示狀態,開三個引數\(pos,limit,cnt\),其中\(cnt\)表示數字\(x\)的出現次數(與上面的\(sum\)類似,不過一個是求和,一個是計數),\(pos=0\)結束,返回\(cnt\)。主函式中呼叫\(10\)\(dfs\)\(x\)分別取\(0\sim 9\)。呼叫一次輸出一個。

(注:這兩種思路都需要處理前導\(0\)的情況,所以上面只是簡單的思路,\(dfs\)的形參可能會隨需求有增加)

[留坑]

相關文章