數位DP--P2657--Windy數 java實現
什麼是數位DP
數位 DP 問題往往都是這樣的題型,給定一個閉區間 [L,R],讓你求這個區間中滿足 某種條件 的數的總數。
(來自 OI-WIKI )
按照一般的方法,我們會遍歷區間[L,R],對每個數字進行判斷,是否符合題目要求.
但是這類題目的區間範圍往往都比較大,單純地遍歷每一個數字,會超時.
這種情況下,使用數位DP的方式,進行求解.
下面根據
進行具體分析
這類演算法,基本都是套模板.所以也按照這道題目,將模板寫出來.
題目背景
windy 定義了一種 windy 數。
題目描述
不含前導零且相鄰兩個數字之差至少為 2 的正整數被稱為 windy 數。windy 想知道,在 a 和 b
輸入格式
輸入只有一行兩個整數,分別表示 a 和 b。
輸出格式
輸出一行一個整數表示答案。
對於全部的測試點,保證
\[1 \leq a \leq b \leq 2 \cdot 10^9 \]總體思路
1.問題轉化
對於區間中的問題,我們將它簡化成只有一個邊界的問題.類似於計算區間和.
對於在區間[L,R]上,計算滿足某項條件的數字的個數result.
假設我們有函式F(x),表示在[0,x]區間上,滿足條件的數字的個數.
那麼result = F(R)-F(L-1);
2.具體求出F(x)
對於給定的輸入M,在區間範圍[0,M]中,我們按照每一位數字進行列舉(區別於遍歷每一個數字),記錄所有的情況---相鄰數字的差至少為2.將結果記錄到陣列 A[]中.
-
如何描述一類數字的狀態:
我們使用當前列舉的數字的i位,當前位的前一位具體是數字j來來記錄此種情況下的滿足題目條件的情況有多少種.
A[i][j] = result;
舉例:
A[3][4] 記錄的是當前位置在第三位,並且前一位(第二位)數字是4的情況.
-
如何寫轉移方程
考慮一般情況.
舉例:
對於數字(範圍)12345.
\[A[3][1] = \sum_{i=0}^{9}A[4][i]\\ 當且僅當 |i-前一位的數字| \leq 2 的時候 \]表示當前位置是第三位,前一個位置是1的數,能夠組成滿足題目要求的情況的總和
等於
當前位置是第四位,前一個位置是0~9,能夠組成滿足題目要求的情況的總和.
另外,對於記錄狀態的重複使用:
對於11???,可以使用陣列A[2][1]表示.
對於10???,可以使用陣列A[2][1]表示.
它們都表示當前位置在第二位,前面一位是1的狀態.所以只要計算過一次後,後面再次到這個狀態,就能夠直接使用了.
-
特殊情況
-
對於前面數字位已經在邊界的數字,需要單獨計算,不能夠使用之前表示的狀態.
仍舊以12345舉例.
對於狀態A[3][2],表示當前位置是3,前面位置的狀態是2的情況,
表示的數字是12???.
(10???,11??? 因為第二為取得了0或者1,所以後面的數字仍舊能夠取0~9,所以不算邊界)
這種情況下,就不能使用跟之前類似的公式:
\[A[3][1] = \sum_{i=0}^{9}A[4][i]\\ 當且僅當 |i-前一位的數字| \leq 2 的時候 \]因為第三位只能夠取0~3三位, 如果取4~9,最終的數字會超出範圍.
所以,這部分單獨處理.
因為只有前面全部都取到了數字的邊界,才會出現這種情況,所以增加對於狀態的記錄,記作limit
-
引入"前導零"狀態,表示當前位置之前的所有數字全部是零.
因為我們在列舉數字每一位的時候,會碰到例如這樣的情況:
00123.實際上代表數字123.但是我們不能因為第二位和第三位之間相差一,就否定這種情況.
所以增加 "前導零" 狀態, 記作lead .表示當前位置之前的所有位置,全部都是0.這種狀態下,當前位置的數字可以沒有顧及地從0取值到9.
具體程式碼
可以再對照著註釋部分,理解程式碼.
import java.io.BufferedReader; import java.io.InputStreamReader; import java.util.Arrays; import java.util.StringTokenizer; public class Main { static long[][] dp; static int[] positionNum; public static void main(String[] args) throws Exception { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); StringTokenizer st = new StringTokenizer(br.readLine()); int a = Integer.parseInt(st.nextToken()); int b = Integer.parseInt(st.nextToken()); int aa = Math.min(a,b); int bb = Math.max(a,b); //因為問的是閉區間,所以對於[L,R],需要的結果是F(R)和F(L-1). long aResult = getCnt(aa-1); long bResult = getCnt(bb); System.out.println(bResult-aResult); } private static long getCnt(int num){ positionNum = new int[11];//這裡記錄數字地每一位,到陣列positionNum中 int index = 0; while (num>0){ positionNum[index++] = num%10; num /= 10; } //因為給定數字的範圍最大不超過2億,所以數字位數不超過9位,這裡第一維度使用了11 //因為第二維代表數字的取值範圍,所以最多就是0~9,一共10位. dp = new long[11][10]; //將dp陣列初始化為-1.因為後續需要記憶化搜尋,通過初始化的-1,來表示,某個狀態沒有被記錄到 for(int i = 0;i<dp.length;i++){ Arrays.fill(dp[i],-1); } return dfs(index-1,0,true,true); } //通過dfs,進行動態規劃過程. //position:當前數字是第幾位,對應於A[i][j]中的i //preNum:當前位置的前一位是哪個數字,對應於A[i][j]中的j //isLimit:當前位置之前的所有數字位置,是否填入了它們能夠達到的最大值 //lead:前導零.當前位置之前所有位置,是否都為零. private static long dfs(int position, int preNum, boolean isLimit,boolean lead) { //如果搜尋到了-1,說明每一位都被搜尋到了,dfs可以在這裡結束. if(position < 0){ return 1; } //用於記錄結果 long result = 0; //記憶化的步驟. //1.如果前面位置填寫的數字,沒有達到最大值 //2.dp[position][preNum]這個狀態之前有過記錄 //3.前面數字全部都不是零 //這種狀態說明之前dfs時,已經計算過,這裡直接使用. if(!isLimit && dp[position][preNum]!=-1 && !lead){ return dp[position][preNum]; } //如果position位置之前的所有數字都為它們位置的最大值, //那麼當前位置最大隻能夠是讀取的數字的當前位置的值 //否則,這一位能夠填寫0~9,因為這種情況下不管怎麼填寫,數字都不會超過給定範圍. int maxNum = isLimit?positionNum[position]:9; int abs; for(int i = 0;i<=maxNum;i++){ abs = Math.abs(i-preNum); //條件:相鄰兩個數字之間的差值必須不小於2,並且是沒有前導零的情況 //如果有前導零,那麼就不需要這個條件. if(abs<2 && !lead){ continue; } //下面進行的都是下一位的dfs.分類了不同情況,不同情況下,dfs的第三第四個引數不同 //當前位置填寫了最大的數字,並且前面的數字也填寫成為了當前位置最大的數字 if(i == maxNum && isLimit){ result += dfs(position-1,i,true,false); } //當前位置取值不是0或者沒有前導零 //1.如果沒有前導零,無論當前位取值如何,該狀態表示的數字都不會是當前位置之前全部是零的情況 //2.如果當前位置取值非零,那麼之後一定不是前導零的狀態了 else if(i != 0 || !lead){ result+= dfs(position-1,i,false,false); } //和上面的狀態相反,這裡就表示了,是前導零的狀態. else{ result += dfs(position-1,i,false,true); } } //這裡針對一般的情況,進行了結果記錄,以便後續使用 //一般情況的條件: //1.各個位置不是最大取值 //2.不包含前導零狀態 if(!isLimit && !lead){ dp[position][preNum] = result; } return result; } }
-