【算法】數位 dp

時隔多日,我終於再次開始寫博客了!!數組

上午聽了數位 dp,感受沒聽懂,因而在網上進行一番愉 ♂ 快 ♀ 的學習後,寫篇博來加深一下印象~~學習

前置的沒用的知識

數位

不一樣計數單位,按照必定順序排列,它們所佔位置叫作數位。spa

在整數中的數位是從右往左,逐漸變大:第一位是個位,第二位是十位,第三位是百位,第四位是千位,第五位是萬位,第六位是十萬位,第七位是百萬位,第八位是千萬位,以此類推。code

同一個數字,因爲所在數位不一樣,計數單位不一樣,所表示數值也就不一樣。遞歸

對於每個數都應當有一個名稱,以天然數來講,天然數是無限多的,若是每個天然數都用一個獨立的名稱來讀出它,這是很是不方便的,也是不可能作到的。get

爲了解決這個問題,人們創造出一種計數制度,就是如今咱們使用的十進制計數法。博客

                                        ———— 360 百科數學

感謝 360 百科朋友的友情茲瓷table

顧名思義,數位 dp 就是在數位上的 dp。class

主要應用

常常用來解決計數類的、範圍很大的題目,

固然一般會有一些條件。

特色

很板很板,幾乎跟揹包 dp 同樣一個板子走天下,

用句話來誤導人一下:記個板子而後不用理解都行……

正題

先放個例題

不含前導零且相鄰兩個數字之差至少爲 \(2\) 的正整數被稱爲 \(\text{windy}\) 數。

windy 想知道,在 \(a\)\(b\) 之間,包括 \(a\)\(b\) ,總共有多少個 \(\text{windy}\) 數?

範圍:\(a,b\in [1,2\times10^{9}]\)

我相信,若是沒有那個巨大的範圍的話,正常人都能作得出來 = =

但這個範圍就神奇……滅掉了一羣正常人……

總不能從 \(1\) 一直枚舉到 \(10^{9}\) 吧……

這時候就是 超級飛俠 數位 dp 出現的時候了!!

分析

咱們能夠想一下:\(2\times10^{9}\) 一共就只有 \(9\) 位,毫無疑問的是,若是針對這個數的每一位開一個數組,最多就只開 \(9\) 個。

而後,再想一下:

  • 若是在每一位枚舉 \(0\)\(9\),那時間複雜度確定不會炸掉。

  • 若是規定了一個數的最高位,那麼剩下的位數有多少種組合,而後再規定剩下的數的最高位,而後再……以後這個數就被完全地拆分了~~

結論

咱們能夠用萬能 yuechi ———— 大法師直接打過去!!

而後就 T 了~~

因此用記搜啊~~~(本蒟蒻太弱了,不會遞推)

代碼實現

設一個 \(f[N][1]\) 來記錄狀態,其中的 \(f[i][j]\) 表示位數最高位爲 \(i\),該位數的上一個數爲 \(j\)

每搜一個數位,枚舉這個數位的數字(從 \(0\)\(9\)),而後判斷這個數合不合法(好比這個例題就是判斷是否與上個數之差不小於 \(2\))。

  • 若是合法,就繼續搜下一個數位;

  • 若是不合法,就剪枝。

判斷邊界:

判斷最高位時看看有沒有到最大值,而後一位一位的推。

舉個栗子:枚舉 \(7456\) 的時候最高位先枚舉 \([0,6]\),等以 \([0,6]\) 開頭的數都被判斷完了,就使 Qd (判斷的變量)變成一,而後一一排查千位不能超過 \(7\),百位不能超過 \(4\),十位不能超過 \(5\),個位不能超過 \(6\)等等……

這時候大約輪廓就出來了:(以這個題爲例)

變量名 意義
Ws 位數
pre 前一個數
Qd1 判斷是否有前導零
Qd2 判斷是否到了邊界
top 枚舉的數不能超過 top
ans 記錄答案
int dp(int Ws, int pre, bool Qd1, bool Qd2) {
	if (!Ws) return 1; // 若是位數減到了 0,說明以前的數都是合法的,返回 1 。
	if (!Qd1 && !Qd2 && f[Ws][pre] != -1) return f[Ws][pre]; // 若是沒有前導零而且未到達邊界,且記憶數組 f 不爲空,就返回 f 。
	int top = Qd2 ? a[Ws] : 9, ans = 0; // 判斷上限
	for (int i = 0; i <= top; ++i) { // 枚舉數
		if (abs(pre - i) < 2 && !Qd1) continue; // 不合法,剪枝
		ans += (i || !Qd1) ? dp(Ws - 1, i, 0, Qd2 && i == a[Ws]) : dp(Ws - 1, i, 1, Qd2 && i == a[Ws]); // 判斷是否有前導零,如有,則使 Qd2 變成 1 ,若是到達邊界,則使下一個數的 Qd2 變成 1 。
	}
	if (!Qd1 && !Qd2) f[Ws][pre] = ans; // 記憶 ans 。
	return ans;
}

我相信到如今仍是有一些疑問的,好比,爲何要先判斷 Qd1 和 Qd2 後才能進行記憶化,才能返回 f 值。

解釋一下

咱們能夠想象,\(f[i][j]\) 數組記錄的是第 \(i\) 位數,前一個數爲 \(j\) 的數的方案數,

那麼若是這個數組已經在還未到達邊界,還未有前導零的時候已經被更新過了,這時候遞歸到邊界, \(f[i][j]\) 記錄的是後面的數從 \([0,9]\) 的方案數,但邊界不必定能取到 \([0,9]\),因此不判邊界和前導零顯然是不對的。

再放個例題

給定兩個正整數 \(a\)\(b\),求在 \([a,b]\) 中的全部整數中,每一個數碼各出現了多少次。

範圍:\(a,b\in[1,10^{12}]\)

當你看到這個數據範圍的時候,毫無疑問這絕對又是個數位 dp 題,

分析

與上一題同樣,但條件變成計數了(每一個數碼有多少個)。

代碼實現

設一個 \(f[N][20]\) 來記錄狀態,其中的 \(f[i][j]\) 表示位數最高位爲 \(i\),該數碼在這個數中出現了幾回爲 \(j\)

而後跟上一題同樣。

判斷邊界:

跟上一題同樣,並且仍是要判斷前導零(畢竟不能把前導零計入 \(0\) 的計數中)。

這時候代碼就出來了:(以這個題爲例)

變量名 意義
Ws 位數
Qd1 判斷是否有前導零
Qd2 判斷是否到了邊界
shu 數碼
sum 記錄該數碼出現多少次
top 枚舉的數不能超過 top
ans 記錄答案
int dp(int Ws, bool Qd1, bool Qd2, int sum) {
	if (!Ws) return sum;
	if (!Qd1 && !Qd2 && f[Ws][sum] != -1) return f[Ws][sum];
	int top = Qd2 ? a[Ws] : 9, ans = 0;
	for (int i = 0; i <= top; ++i)
		ans += dp(Ws - 1, !(i || !Qd1), Qd2 && i == a[Ws], sum + ((i == shu) && (i || !Qd1)));
	// 這裏解釋一下,當 i 爲該數碼時,判斷該數是否爲前導零。
	if (!Qd1 && !Qd2) f[Ws][sum] = ans;
	return ans;
}

如今應該能看出來了,數位 dp 什麼的,就是個板子(對於記搜來講)~~

例題

  1. 洛谷 P2657 [SCOI2009] windy 數(上面第一道例題)

  2. 洛谷 P2602 [ZJOI2010]數字計數(上面第二道例題)

  3. 洛谷 P4999 煩人的數學做業

剩下的本身搜~~

相關文章
相關標籤/搜索