數位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\)的形參可能會隨需求有增加)
[留坑]