1. 程式人生 > 其它 >數位DP--P2657--Windy數 java實現

數位DP--P2657--Windy數 java實現

什麼是數位DP

數位 DP 問題往往都是這樣的題型,給定一個閉區間 [L,R],讓你求這個區間中滿足 某種條件 的數的總數。

(來自 OI-WIKI )

按照一般的方法,我們會遍歷區間[L,R],對每個數字進行判斷,是否符合題目要求.

但是這類題目的區間範圍往往都比較大,單純地遍歷每一個數字,會超時.

這種情況下,使用數位DP的方式,進行求解.

下面根據

進行具體分析

這類演算法,基本都是套模板.所以也按照這道題目,將模板寫出來.

題目背景

windy 定義了一種 windy 數。

題目描述

不含前導零且相鄰兩個數字之差至少為 2 的正整數被稱為 windy 數。windy 想知道,在 ab

之間,包括 ab ,總共有多少個 windy 數?

輸入格式

輸入只有一行兩個整數,分別表示 ab

輸出格式

輸出一行一個整數表示答案。

對於全部的測試點,保證

\[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的狀態.所以只要計算過一次後,後面再次到這個狀態,就能夠直接使用了.

  • 特殊情況

    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

    2. 引入"前導零"狀態,表示當前位置之前的所有數字全部是零.

      因為我們在列舉數字每一位的時候,會碰到例如這樣的情況:

      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;
        }
    }