1. 程式人生 > >數位DP 淺談(hihocoder 1033:交錯和)

數位DP 淺談(hihocoder 1033:交錯和)

數位DP是一種比較特殊的DP方法,之所以瞭解到是為了嘗試解決hihocoder上一道交錯和的題目,更詳細的資訊請參考:文章《淺談數位類統計問題》和講義《初探數位DP

事實上在ACM中,我們經常遇到如下類問題:

求整數區間[L,R]中滿足條件Q的整數的個數(或它們的和、積等)

對於這類問題,乍一看是數論問題,但是卻發現條件Q一般不是那麼明顯,找不到簡單的規律;嘗試逐個列舉,顯然時間超出。

仔細考慮,顯然[L,R]中滿足條件的個數,等於[0,R]減去[0,L-1]滿足條件的個數,這樣我們第一步把問題變成了求f(n)的問題,其中f(n)表示區間[0,n]內滿足條件Q的個數。

而如何高效率求f(n), 這個時候我們往往需要用數位DP來進行解決,本質上來說數位DP是一種記憶化搜尋的方法(當然也有很多題目比較簡單,可以直接DP)。 事實上它基於一個很顯然的事實:[0,n]中任意的數,一定滿足從高位往地位看,必然出現某一位嚴格小於n的對應位,而之前的數位卻一直相等,因此我們只需要列舉數字的位數和最高位數字即可。

因此我們需要一個二維陣列dp[len][digit] (開篇的問題“交錯和”中涉及到計算和,所以是個三維陣列,但前兩維仍然是長度和最高位數字,因此本質相同), 表示長度為len位(可以有前導0),且最高位為digit的且滿足條件Q的數字的個數。而如何求dp[i][j]? 注意到當最高位固定為j時,我們只需要根據需要列舉下一位,即第i-1位,此時只需要從0列舉到9(假設問題都是10進位制)即可。最後我們再根據這些dp的結果,求出n即可(詳細的可以參照上面的講義《初探數位DP》)。

 但是上面的方法還是稍微複雜,因為需要先求出dp陣列,然後還要列舉n的各位情況。但是我們通過對程式碼的修改,可以將其合併在一起。事實上我們需要三個東西:

  1. DP[len][digit]陣列,用來記錄中間資料,定義為上面的定義
  2. solve(n) 用來求出[0,n]中滿足條件Q的整數的個數(或者和等),主要分兩步:第一步求出n的長度l並將n的各個數位存在全域性陣列bits中,第二步呼叫下面的dfs(l+1, 0, true)用來求結果。
  3. dfs(len, digit, end_flag)結合bits和dp兩個陣列,求出結果,並有可能將結果存入對應dp陣列中以免多次計算同一種情況。這個函式前兩個引數很容易理解,關鍵是最後一個end_flag, 事實上由於我們把求dp陣列和求[0,n]中滿足Q的整數個數這兩個步驟合在了一起,因此dfs必須區分這兩種情況,而區分的方法就是用end_flag做標記:
    • 當該標記為true的時候,表示前面列舉的高位和n的高位完全相同,因此這一步列舉下一位時,為了使數字大小不超過n,必須只能列舉到bits[低位],並且計算的結果不能存入dp陣列
    • 反之,如果為false,表示之前列舉的高位已經小於n對應高位了,那麼這一步在列舉下一位時,可以從0列舉到9,且計算結果可以存入dp陣列。

因此我們有下面的模板(這裡只考慮求個數,求和的情況要複雜一些請參看交錯和的程式碼):

int dp[len][digit]

int dfs(int len, int dig, bool end_flag){
	int ILLEGAL, LEGAL;
	//超過邊界值 
	if(len <= 0)
		return ILLEGAL;
	//返回已有的DP結果,即記憶化搜尋 
	if(!end_flag && dp[len][dig] != -1)
		return dp[len][dig];	
	//長度只有一位,就不需要列舉下一位了,直接討論返回即可 
	if(len == 1){
		return (Q? LEGAL : ILLEGAL); 
	}
	//開始列舉下一位的數字 
	int end = end_flag? bits[len-2] : 9; 
	int ans = 0;
	rep(j,0,end + 1){
		ans += dfs(len-1, j,end_flag && j == end);
	}
	if(!end_flag) dp[len][dig] = ans;
	return ans;
}

int solve(int n){
	if(n <= 0)
		return 0;
	//求出n的位數,並將其各個位存入陣列bits中
	int l = 0;
	rep(i,0,21) bits[i] = 0;
	while(n){
		bits[l++]= n%10;
		n /= 10; 
	}
	//呼叫dfs求出最後結果,注意到第一個引數l +1是指我們將n前補一個高位0,然後開始查詢,這樣避免了列舉原來n的最高位
	return dfs(l+1,0,true);
}

注意到上面程式碼中涉及到end_flag的狀態轉換。上面就是數位DP的基本模板,大部分題目都可以直接在上面的基礎修改。

最後是交錯和的問題解,注意到由於交錯和和前面的位數有關,因此dfs多了兩個引數begin_zero和sum,但其餘的和上面模板沒有本質區別。

#include<iostream>
#include<cstring>
using namespace std;

#define ll long long int
#define rep(a,b,c) for(int a = b ;  a < c; a++)

const int mod = 1000000007;

struct node{
	ll s,n;
};

node dp[21][20][400];

int bits[21];
ll base[21];

//len數位長度, dig是首個數字, begin_zero表示之前是否已經開始變號, end_flag表示下一位列舉時是否列舉到bit[len-2],否則就列舉到9, sum是要求的數字和 
node dfs(int len, int dig, bool begin_zero, bool end_flag, int sum){
	node t;
	t.s = 0, t.n = 0;
	//超過邊界值 
	if(len <= 0 || len >= 20 || dig < 0 || dig > 9 || sum < -200 || sum >= 200)
		return t;
	//返回已有的DP結果,即記憶化搜尋 
	if(!end_flag && dp[len][dig + (begin_zero?0:10)][sum+200].n != -1)
		return dp[len][dig + (begin_zero?0:10)][sum+200];	
	//長度只有一位,就不需要列舉下一位了,直接討論返回即可 
	if(len == 1){
		if(dig != sum)
			return t;
		t.n = 1, t.s = sum;
		return t; 
	}
	//開始列舉下一位的數字 
	int end = end_flag? bits[len-2] : 9; 
	int newsum = dig - sum;
	node tmp;
	rep(j,0,end + 1){
		if(!begin_zero){
			tmp = dfs(len-1, j, j!=0, end_flag&& (j == end), sum);
		}
		else{
			tmp = dfs(len-1, j, true, end_flag&& (j == end), newsum);
		}
		//將tmp的值累加到t上
		t.n += tmp.n;
		t.s = ((t.s + tmp.s)%mod + ((tmp.n * dig )%mod * base[len-1])%mod)%mod; 
	}
	if(!end_flag) dp[len][dig + (begin_zero?0:10)][sum+200] = t;
	return t;
}

int solve(ll n, int s){
	if(n <= 0)
		return 0;
	int l = 0;
	rep(i,0,21) bits[i] = 0;
	while(n){
		bits[l++]= n%10;
		n /= 10; 
	}
	return dfs(l+1,0,false,true,s).s;
}

int main(){
	ll l, r, s;
	node t;
	t.n = -1;
	t.s = 0;
	rep(i,0,21) rep(j,0,20) rep(k,0,400) dp[i][j][k] = t;
	base[0] = 1;
	rep(i,1,21) base[i] = (base[i-1]*10)%mod;
	cin >> l >> r >> s;
	cout <<(solve(r,s) - solve(l-1,s) + mod )%mod << endl;
	return 0;
}