一起來學數位dp

基本的數位dp-windy數

首先讓我們來看一下數位DP這玩意到底是啥,對於一些剛開始練的萌新(我)來說會比較好。

數位DP一般他對問題的求解都是與數字上的位數有關,比如統計數字出現個數,有多少個數字之類的,這類問題一般能用數位dp做的,都是可以從小數字推到大數字的。比如我們從第一位往後可以一層一層,逐漸推出我們需要的答案。

先來一道簡單點的例題

windy定義了一種windy數。不含前導零且相鄰兩個數字之差至少爲2的正整數被稱爲windy數。 windy想知道,
在A和B之間,包括A和B,總共有多少個windy數?

f(x)f(x)表示x前面的數 即 t[1,x)t∈[1,x) 中windy數的個數(注意這是開區間

那麼顯然如果要求 [l,r][l,r]中windy數的個數就是:

F(l,r)=fr+1f(l)F(l,r) = f(r+1)-f(l)

數位dp開始,預處理位數爲ii最高位爲jj的windy數個數 dp[i][j]dp[i][j]

轉移:

dp[i][j]=dp[i1][k]dp[i][j]=dp[i-1][k]:其中kk是非負整數k[0,9]k∈[0,9]kj>=2|k-j|>=2

初始值:

dp[1][i]=1dp[1][i]=1 | 其中ii爲非負整數 i[0,9]i∈[0,9]

求f(x)怎麼求呢?

爲了方便處理先對數xx進行按10進制位拆分到a[]a[]數組,然後我們分三種情況進行討論(代碼中有詳細註釋)

  1. 顯然位數比x要小的數字都是合法的都在[1,x)[1,x)區間內,直接統計就行
  2. 位數和x一樣最高位的數字比x小的數字都是合法的都在[1,x)[1,x)區間內直接統計就行
  3. 位數和x一樣,最高位又和x一樣我們從左到右掃一遍x各個位子上的數字大小然後枚舉合法的該位子上的數[0,9][0,9]判斷是否合法就行。

其實每個數位dp基本上都是這個思路,只不過相鄰位數之間要滿足的條件不同,有幾個易錯點需要注意

  • 討論最高位時一定不要將0加進去,如上面的1,2兩種情況,一個數的最高位只能從1-9。第三種情況最高位確定了,討論中間每一位時才能加上0,
  • 注意我們求得是開區間[1,x)[1,x)的滿足條件的數的總數。
#define inf 0x3f3f3f3f
#define MAX 1000005
#define ll long long
#define vec vector<ll>
#define P pair<int,int>

ll dp[15][10];//dp[i][j]:位數爲i,最高位爲j時最多有多少windy數

//統計[1,x)區間內的windy數
ll cal(ll x) {
	ll a[11], len = 0;
	while (x > 0) {
		a[++len] = x % 10;
		x /= 10;
	}
	ll sum = 0;
	//1:統計位數比x少的
	for (int i = 1; i < len; i++)
		for (int j = 1; j < 10; j++)//注意這裏從1開始,最高位不能爲0!
			sum += dp[i][j];
	//2:統計位數和x一樣,但是首位比x小的,同樣最高位不能爲0
	for (int i = 1; i < a[len]; i++)
		sum += dp[len][i];
	//3:統計位數與x一樣,首位也一樣的,那麼之前的每一位加上限制a[i]
	for (int i = len - 1; i >= 1; i--) {
		for (int j = 0; j < a[i]; j++)  //注意該位的限制,由於不是最高位,可以從0開始
			if (abs(j - a[i + 1]) >= 2)//該位和上一位滿足要求
				sum += dp[i][j];
		if (abs(a[i + 1] - a[i]) < 2)//傳入數字連續兩位不滿足了
			break;
	}
	return sum;
}

int main() {
	memset(dp, 0, sizeof(dp));
	for (int i = 0; i < 10; i++)dp[1][i] = 1;//預處理第一位

	for (int i = 2; i < 11; i++) //遍歷每2-10每個位數,1預處理了
		for (int j = 0; j < 10; j++) //遍歷這一位的每種可能數字
			for (int k = 0; k < 10; k++) //遍歷上一位的每種可能數字
				if (abs(k - j) >= 2) //相鄰數字之差大於2
					dp[i][j] += dp[i - 1][k];
	ll a, b;
	while (cin >> a >> b) {
		if (a > b)swap(a, b);
		cout << cal(b + 1) - cal(a) << endl;
	}
}

文章主要參考自洛谷的兩位大佬的題解

https://www.luogu.com.cn/problem/solution/P2657
https://www.luogu.com.cn/blog/mak2333/swdp001

略微複雜的數位dp-數字計數

題目鏈接

這道題相比於windy數,難度會稍微大一點,但也是數位dp的應用,數位dp其實就是兩個部分。初始化、求解

初始化\textbf{初始化}

f[i][j][k]f[i][j][k]表示第ii位,最高位爲jjkk有多少個。

這裏用到了前綴和的思想。比如f[2][1][1]f[2][1][1]就表示[0,19][0,19]有多少個11

在求解的時候,我們就用solve(b+1)solve(b + 1)算出小於b+1b+1的數對應的每位數出現的vectorvector,然後再求出solve(a)solve(a)

首先我們可以得到一個粗略的方程:

f[i][j][k]=l=09f[i1][l][k]+f[i][j][k]=\sum_{l=0}^9f[i-1][l][k]+一個特殊的數字

這一個很玄學的數是指kk在這一位上出現的次數。

那麼在這一位上出現了多少次呢?

j=kj=k的時候,比如最高位爲1,四位數字1xxx,求1出現的次數,那麼f[4][1][1]f[4][1][1]還要加上10310^3,即1在最高位出現的次數,

求解\textbf{求解}

這個求解又被一些大佬稱爲“拆數”,可以遞歸和非遞歸實現,我比較喜歡非遞歸

求解過程一般有以下幾步:

  • 首先把原數拆了,存入數組;
  • 統計位數比原數少,不含前導零的數字
  • 統計位數與原數一樣,但最高位較小的數,同樣不能有前導零
  • 位數和x一樣,最高位又和x一樣我們從左到右掃一遍x各個位子上的數字大小然後枚舉合法的該位子上的數,值得注意的是,比如我們有個四位數ABCDABCD,第二位BB,第三位CC,第四位DD還給數A帶來了B100+C10+DB*100+C*10+D的增加量,同樣BBC10+DC*10+D的增加量。
#define inf 0x3f3f3f3f
#define MAX 1000005
#define ll long long
#define vec vector<ll>
#define P pair<int,int>

//dp[i][j][k]:i位數,最高位爲j,小於他的數字中k出現的次數
ll dp[20][10][10];

vec cnt(ll x) {
	ll a[20], len = 0;
	vec v(10);
	while (x > 0) {
		a[++len] = x % 10, x /= 10;
	}
	//位數比x少,忽略前導0
	for (int i = 1; i < len; i++)
		for (int j = 1; j < 10; j++)
			for (int k = 0; k < 10; k++)
				v[k] += dp[i][j][k];
	//首位比x小,忽略前導0
	for (int i = 1; i < a[len]; i++)
		for (int k = 0; k < 10; k++)
			v[k] += dp[len][i][k];
	//首位一樣,按位退回,每位設限
	for (int i = len - 1; i >= 1; i--) {
		for (int j = 0; j < a[i]; j++) {
			for (int k = 0; k < 10; k++) {
				v[k] += dp[i][j][k];
			}
		}
		//6153 6後面的每一位分別爲他帶來了 1*100+5*10+3的包含6的個數(以6爲千位)
		for (int j = len; j > i; j--) {
			v[a[j]] += a[i] * pow(10, i - 1);
		}
	}
	return v;
}

int main() {
	memset(dp, 0, sizeof(dp));
	for (int i = 0; i < 10; i++) dp[1][i][i] = 1;//只有該位爲i時,該值才爲1
	for (int i = 2; i < 20; i++) //位數
		for (int j = 0; j < 10; j++) {//這一位
			for (int k = 0; k < 10; k++) {//上一位
				for (int n = 0; n < 10; n++) //元素
					dp[i][j][n] += dp[i - 1][k][n];
			}
			dp[i][j][j] += pow(10, i - 1);//加上這個特殊值
		}
	ll a, b;
	while (cin >> a >> b) {
		vec v1 = cnt(b + 1);
		vec v2 = cnt(a);
		for (int i = 0; i < 10; i++)
			cout << v1[i] - v2[i] << ' ';
		cout << endl;
	}
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章