時隔多日,我終於再次開始寫博客了!!數組
上午聽了數位 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 什麼的,就是個板子(對於記搜來講)~~
洛谷 P2657 [SCOI2009] windy 數(上面第一道例題)
洛谷 P2602 [ZJOI2010]數字計數(上面第二道例題)
剩下的本身搜~~