1. 程式人生 > 其它 >動態規劃 - 歸類總結

動態規劃 - 歸類總結

動態規劃(DP)思想

動態規劃 Dynamic programming => DP

動態規劃:普通遞迴 + dp陣列記錄,達到空間換時間

動態規劃一般都不會很高效,因為dp[]記錄了途中每個狀態最優解

儘量找巧妙的方法,來做到比dp效果更好的解法

DP三大性質:

  • 最優子結構
  • 子問題重疊
  • 無後效性(算好的dp[]不會再改)

DP四步走:

  1. 拆分出子問題
  2. 子問題的遞推公式(狀態轉移方程)
  3. 確定 DP 陣列的計算順序、並初始化
  4. 空間優化(可選)

動態規劃vs貪心演算法

貪心演算法是一種特殊的動態規劃演算法

對於一個動態規劃問題,問題的最優解往往包含重複的子問題的最優解,動態規劃就是為了消除重複的子問題

而貪心演算法由於每一次都貪心選取一個子問題,所以不會重複計運算元問題的最優解

DP問題大致分類

  • 【斐波拉切】(跳臺階系列)
  • 【遞推型】(醜數、剪繩子、圓圈最後數)
  • 【劃分型】間斷序列-最值(打家劫舍、股票類、不連續子序列)
  • 【二維座標型】細分為:1)棋盤dfs回溯問題(flag試錯) 2)棋盤dp[][]遞推問題
  • 【區間型】連續序列-最值(拿石子、連續子序列)=》二維dp[][],雙指標
  • 【揹包型】目標值(揹包(sum<=k)、和為K的序列(未必連續)、零錢兌換)
  • 【樹型】(樹狀遞迴、dp;經常劃到樹而不是動態規劃)

前三類是一維dp[],緊接後面兩類是二維dp[][],揹包型可能1維、2維、多維

斐波拉切型

1. 斐波那契數列

大家都知道斐波那契數列,現在要求輸入一個整數n,請你輸出斐波那契數列的第n項(從0開始,第0項為0,第1項是1)。n≤39

public class Solution {
    public int Fibonacci(int n) {
        int[] fi=new int[40];//設定陣列記錄中間結果,不然重複計算太多   //根據題目,放心設定陣列大小	 
        fi[0]=0;fi[1]=1;
        for(int i=2;i<=n;i++){
            fi[i]=fi[i-1]+fi[i-2];
        }
        return fi[n];
    }
}
//動態規劃,時間複雜度O(N),空間複雜度O(N)
//如果用遞迴,時間複雜度O(1.618^N)【上網查的,略小於2^N】,空間複雜度O(1)【不包括系統棧空間】

2. 跳臺階

一隻青蛙一次可以跳上1級臺階,也可以跳上2級。求該青蛙跳上一個n級的臺階總共有多少種跳法(先後次序不同算不同的結果)。

1)斐波拉切-O(N)動態規劃

public class Solution {
    public int JumpFloor(int target) {
        int frog[]=new int[100];
        frog[1]=1;frog[2]=2;
        for (int i=3;i<=target;i++){
            frog[i]=frog[i-1]+frog[i-2];
        }
        return frog[target];
    }
}
//原理同:斐波那契數列
//【動態規劃】時間O(N),空間O(N)
//如果只要最後的結果,那麼可以撤銷陣列,使用a/b/c三個變數儲存即可。空間複雜度減為O(1)

2)空間O(1)的方法

public class Solution {
    public int jumpFloor(int target) {
        if(target<=2)return target;
        int lastOne = 2;  //現在位置上一個,相當於fi[i-1]
        int lastTwo = 1;  //相當於fi[i-2]
        int res = 0;
        for(int i=3; i<=target; ++i){
            res = lastOne + lastTwo;
            lastTwo = lastOne;
            lastOne = res;
        }
        return res;
    }
}
//這種方法的空間複雜度為:O(1)
//時間複雜度雖然也為O(N),但是比上一種動態規劃的方法耗時,因為迴圈裡面操作較多
//相當於時間換空間,花費時間在不斷倒騰地方

3. 跳臺階擴充套件問題

一隻青蛙一次可以跳上1級臺階,也可以跳上2級……它也可以跳上n級。求該青蛙跳上一個n級的臺階總共有多少種跳法。
1)找出公式

public class Solution {
    public int JumpFloorII(int target) {
        int way=1;for(int i=1;i<target;i++)way*=2;return way;
    }
}
//【找出數學公式】2的n-1次方:類似向n個點之間的n-1個空畫橫線
// 其實不難找,在找遞推公式時,前幾項一寫就知道了
// 時間  O(N)
// 空間  O(1)

2)(動態規劃)硬算

public class Solution {
    public int jumpFloorII(int target) {
        int[] array =new int[100];
        array[1] = 1;
        for(int i=2; i<=target; ++i){
            int sum = 0;
            for(int j=1; j<=i-1; ++j)sum+=array[j];
            array[i] = sum +1;  //之前所有路徑,再加上直接全部的1個跳法
        }
        return array[target];
    }
}
//時間  O(N^2)
//空間  O(N)

4. 矩形覆蓋

我們可以用21的小矩形橫著或者豎著去覆蓋更大的矩形。請問用n個21的小矩形無重疊地覆蓋一個2n的大矩形,總共有多少種方法?
比如n=3時,2
3的矩形塊有3種覆蓋方法:

public class Solution {
    public int rectCover(int target) {
        int fi[] = new int[100];
        for(int i= 0; i<=2; ++i)fi[i]=i;
        for(int i=3; i<=target; ++i)fi[i]=fi[i-1]+fi[i-2];
        return fi[target];
    }
}
//(除了初始少許不一樣,後面是斐波拉切)
// 找遞推關係:分解情況==》最右邊只可能為豎或橫兩種情況,這兩種情況無交集,分別佔用1個塊塊和2個塊塊

遞推型

1. 醜數

把只包含質因子2、3和5的數稱作醜數(Ugly Number)。例如6、8都是醜數,但14不是,因為它包含質因子7。 習慣上我們把1當做是第一個醜數。求按從小到大的順序的第N個醜數。

import java.lang.Math;

public class Solution {
    public int GetUglyNumber_Solution(int index) {
        int ugly [] = new int [2000];
        ugly[1] = 1;//第一個醜數是1  //ugly[]陣列:從1開始,而不是0,增加可讀性
        int t2 = 1;
        int t3 = 1;
        int t5 = 1;//標記2/3/5這三個賽道中(非獨立),潛在候選者的位置   //ugly[]下標   //t2t3t5跑得比i要慢
        for(int i=2; i<=index; ++i){
            ugly[i] = Math.min(Math.min(2*ugly[t2],3*ugly[t3]),5*ugly[t5]);//Java裡面的min()太low了,只能兩個數
            if(ugly[i] == 2*ugly[t2]) ++t2;//t2沿著主幹線ugly[]走到下一個:因為這個被選中了,選下一個為候選
            if(ugly[i] == 3*ugly[t3]) ++t3;
            if(ugly[i] == 5*ugly[t5]) ++t5;//為什麼要搞三個類似語句?因為:這三個可能中一個、也可能中兩個、或三個全中(三個因子都含有)
        }
        return ugly[index];
    }
}
//時間 O(N)   空間 O(N)

2. 剪繩子

給你一根長度為n的繩子,請把繩子剪成整數長的m段(m、n都是整數,n>1並且m>1),每段繩子的長度記為k[0],k[1],...,k[m]。請問k[0]xk[1]x...xk[m]可能的最大乘積是多少?例如,當繩子的長度是8時,我們把它剪成長度分別為2、3、3的三段,此時得到的最大乘積是18。(2 <= n <= 60)

方法一:數學函式求導法

【總體思路】建構函式求導,得到:m=n/e (小數的情況下),也就是說盡量拆成一堆:2、3(最接近e的整數)

數學函式求導法:針對性規律
result= f(m) = (n/m) ^m,設n為定值,m為自變數,f(m)為乘積結果。
max{ f(m) }= max{ ln f(m) },取對數。
求 ln f(m)= m*( ln n- ln m )最值點的m值,求導並令f(m)'=0,得到m=n/e.
e=2.718,然後因為取整數,所以是拆成一堆2、3;
具體看下:4>>>2x2;5>>>2x3;6>>>3x3 符合分析的結果。

public class Solution {
    public int cutRope(int target) {
        if(target == 2)return 1;//因為題目要求最少拆成2份
        if(target == 3)return 2;
        int res = 1;
        while(target > 4 ){//target剩<=4時,三種情況:4=>2*2=4; 3=>3; 2=>2; (-=3 不存在1)
            target -= 3;
            res *= 3;
        }
        return res * target;//三種情況合併處理
    }
}//時間O(N),空間O(1)

方法二:動態規劃

【總體思路】dp[] 存一步步的最優 + 找到 遞推公式

public class Solution {
    int[] dp = new int[60];
    public int cutRope(int target) {
        if(target == 2) return 1;
        if(target == 3) return 2;//這裡的策略不同,要單獨拎出來
        dp[2] = 2;
        dp[3] = 3;//在target>=4的前提下,dp[]陣列2~3對應的值(不必強制分兩段)
        for(int i=4; i<=target; ++i){
            int max = Integer.MIN_VALUE;
            for(int j=2; j<=i-1; ++j){//果然,dp的本質是窮舉
                if(max < dp[j]*(i-j)) max = dp[j]*(i-j);//動態規劃重點是找到=>【最優子結構的遞推公式】
            }//另一種遞推:將上一行的(i-j)換成dp[i-j]
            dp[i] = max;
        }
        return dp[target];
    }
}//時間O(N^2)  空間O(N)

3. 孩子們的遊戲(圓圈中最後剩下的數)

每年六一兒童節,牛客都會準備一些小禮物去看望孤兒院的小朋友,今年亦是如此。HF作為牛客的資深元老,自然也準備了一些小遊戲。其中,有個遊戲是這樣的:首先,讓小朋友們圍成一個大圈。然後,他隨機指定一個數m,讓編號為0的小朋友開始報數。每次喊到m-1的那個小朋友要出列唱首歌,然後可以在禮品箱中任意的挑選禮物,並且不再回到圈中,從他的下一個小朋友開始,繼續0...m-1報數....這樣下去....直到剩下最後一個小朋友,可以不用表演,並且拿到牛客名貴的“名偵探柯南”典藏版(名額有限哦!!_)。請你試著想下,哪個小朋友會得到這份禮品呢?(注:小朋友的編號是從0到n-1;報數0到m-1)

如果沒有小朋友,請返回-1

方法一:樸素模擬法 O(m*n)

public class Solution {
    public int LastRemaining_Solution(int n, int m) {
        if(n<=0 || m<=0)return -1;
        ListNode head = new ListNode(0);
        ListNode p = head;
        for(int i=1; i<=n-1; ++i){
            ListNode node = new ListNode(i);//串成鏈
            p.next = node;
            p = p.next;
        }
        p.next = head;//p回到開頭位置的前一個,形成閉環
                      //還有一個作用是,讓p指向head前一個開始,可以使每輪迴圈一樣套路
        for(int i=1; i<=n-1; ++i){
            for(int j=1; j<=m-1; ++j){
                p=p.next;
            }
            p.next = p.next.next;//java會自動回收,所以不管那個被刪除的節點
        }
        return p.val;//剩下的最後一個
    }
}//這種思路是:模擬完整的遊戲執行機制,不跳步
//時間O(m*n) 空間O(n)

方法二:數學歸納法 O(n) [分析是難點]

public class Solution {
    public int LastRemaining_Solution(int n, int m) {//時光倒流遞推法:為什麼要逆向?因為逆向是由少到多不會有空位;而正向會有空位,必須模擬、不能跳步
        if(n<=0)return -1;
        int res = 0; //f(1,m)=0
        for(int i=2; i<=n; i++){//i就是小朋友數量,i=2是遊戲最後一輪,但是我解法的第一輪 //迴圈從i=2到i=n,小朋友越來越多,此解法是倒推(時光倒流)
            res = (res + m) % i;  //相鄰項關係:左邊res是f(n,m) 右邊res是f(n-1,m)
        }
        return res;
    }
}//時間O(n) 空間O(1)

數學歸納法分析:

數學歸納法:
f(n,m)表示:【相對參考系:從0位置開始,最終到達f(n,m)位置】//例如:從0開始,最終到達f(5,3)=3的位置
f(1,m)=0;//首項
f(n,m)=[(m%n) + f(n-1,m)]%n;//公式化簡為:f(n,m)=[m + f(n-1,m)]%n //相鄰項關係【重點,推導如下】:
例如:
f(5,3)=[f(4,3)+ 3%5 ] %5=f(4,3)+3 什麼意思?
f(5,3)從0開始,0-1-2,刪除2節點,然後來到3==》這時的情況就類似f(4,3)。但還有一點不一樣,就是標準的f(4,3)從0開始,而這裡從3開始
f(4,3)根據定義,必須從0開始(所有的f(i,m)的定義需要一致),而不是從3。所以必須進行【對齊操作】:
於是f(5,3)的參靠系裡:先走3步,然後以3為起點,走f(4,3)步 ==》f(5,3)=3+ f(4,3) [這裡是簡化,再考慮%n的細節優化下就ok了]
有了遞推公式,用遞迴法or迭代法,求解都幾乎同理
迭代法:f(1,m)=0 f(2,m)=[m + f(1,m)]%n ... 一直算到f(n,m)

劃分型

1. 買賣股票的最佳時機 III

給定一個數組,它的第 i 個元素是一支給定的股票在第 i 天的價格。

設計一個演算法來計算你所能獲取的最大利潤。你最多可以完成 兩筆 交易。

注意:你不能同時參與多筆交易(你必須在再次購買前出售掉之前的股票)。

class Solution {
    public int maxProfit(int[] prices) {
    	//本題的狀態比較少,只有一維、而且只有4個狀態
    	//這4個變數都是當前狀態下的總profit:
        int buy1 = prices[0];
        int buy2 = prices[0];
        int sell1 = 0;
        int sell2 = 0;
        for(int i=0; i<=prices.length-1; ++i){
        	//下面4行都是狀態轉移方程:
            buy1 = Math.min(buy1, prices[i]);
            sell1 = Math.max(sell1, prices[i]-buy1);
            buy2 = Math.min(buy2, prices[i]-sell1);
            sell2 = Math.max(sell2, prices[i]-buy2);
        }
        return sell2;
    }
}//時間O(N)  空間O(1)

類似題目1
題目:只有一次買賣機會
解法:上面方法留下buy1和sell1即可

類似題目2
題目:k次買賣次數
解法:(難)上面方法加陣列,加層迴圈

2. 買賣股票的最佳時機含手續費

每筆交易你只需要為支付一次手續費。

輸入:prices = [1, 3, 2, 8, 4, 9], fee = 2
輸出:8

class Solution {
    public int maxProfit(int[] prices, int fee) {
        int profit =0;
        int buy = prices[0] + fee;
        for(int i = 0; i<=prices.length-1; ++i){
            if(buy > prices[i]+ fee)buy = prices[i]+ fee;//更新最低價
            if(prices[i] > buy){
                profit += (prices[i] - buy);
                buy = prices[i];//這裡是關鍵:當我們賣出一支股票時,我們就立即獲得了以相同價格並且免除手續費買入一支股票的權利
            }
        }
        return profit;
    }
}//時間O(N)  空間O(1)

類似題目
題目:不限制買賣次數
解法:每步都操作,所有 "上坡" 都收下

3. 打家劫舍(不相鄰最大子序列和)

給定一個代表每個房屋存放金額的非負整數陣列,在不偷相鄰兩屋情況下 ,一夜之內能夠偷竊到的最高金額。

關鍵點:

狀態轉移方程:dp[i] = dp[i-1] > dp[i-2]+nums[i-1] ? dp[i-1] : dp[i-2]+nums[i-1];

注意分析 dp[i]只需要和dp[i-1]、dp[i-2]的關係

class Solution {
    public int rob(int[] nums) {
        int len = nums.length;
        int dp[] = new int[len + 1];//多一個
        dp[0] = 0;
        dp[1] = nums[0];
        for(int i = 2; i<=len; ++i){
            dp[i] = dp[i-1] > dp[i-2]+nums[i-1] ? dp[i-1] : dp[i-2]+nums[i-1];
        }
        return dp[len];
    }
}//時間O(N) 空間O(N)

空間優化:

class Solution {
    public int rob(int[] nums) {
        int len = nums.length;
        int two = 0;
        int one = nums[0];
        int zero = nums[0];
        for(int i = 2; i<=len; ++i){
            zero = one > two+nums[i-1] ? one : two+nums[i-1];//狀態轉移方程
            two = one;
            one = zero;
        }
        return zero;
    }
}//時間O(N) 空間O(1)
//雖然這個時間也是O(N), 但是比上面的方法耗時(時間換空間)

4. 最長遞增子序列

方法一:兩重迴圈完全dp

描述:
一維陣列dp[i]用於儲存0-i序列的最大res,初始化為1
i、j 兩重迴圈,當 nums[j] < nums[i] 時:
dp[i] = Math.max(dp[i], dp[j] + 1);
時間O(N^2) 空間O(N)

方法二:設定輔助陣列
時間O(N*logN) 空間O(N)

import java.util.ArrayList;

class Solution {
    public int lengthOfLIS(int[] nums) {
        ArrayList<Integer> min = new ArrayList<Integer>();//這個tails未必是子序列的值,但長度一定是對的
        min.add(nums[0]);
        for(int num:nums){
            int left = 0;
            int right = min.size()-1;
            if(num > min.get(right))
                min.add(num);
            else{
                while(left < right){
                    int mid = (left+right)/2;
                    if(num <= min.get(mid)) right = mid;//這裡while裡面的寫法挺多的,只要保證num與min[mid]相等時區間向左即可
                    else left = mid+1;
                }
                min.set(right,num);//left==right //每一輪必將最新數字插入佇列
            }
        }
        return min.size();
    }
}

程式碼簡短些的方法:

public class Solution {
    public int LIS(int[] arr) {
        int[] min = new int[arr.length];
        int count = 0;
        for(int num:arr){
            int left = 0;
            int right = count;//這裡right包含min陣列有效值的右邊一個
            while(left<right){
                int mid = (left+right)/2;
                if(num>min[mid])left = mid+1;
                else right = mid;
            }
            if(count == right) ++count;
            min[right]=num;
        }
        return count;
    }
}

深化:要求輸出最優序列(長度相同時,要求每個位置儘量小)

public class Solution {
    public int[] LIS(int[] arr) {
        int n = arr.length;
        int[] min = new int[n];
        int[] index = new int[n];//為每個arr元素,記錄(min中的)更新下標
        int count = 0;
        for(int i=0; i<=n-1; ++i){
            int left = 0;
            int right = count;
            while(left<right){
                int mid = (left+right)/2;
                if(arr[i]>min[mid])left = mid+1;
                else right = mid;
            }
            if(count == right) ++count;
            min[right]=arr[i];
            index[i] = right;//記錄(min[]中的)更新下標right
        }
        int[] res = new int[count];//
        for(int i= n-1,k = count-1; i>=0; --i){//
            if(k == index[i])res[k--]=arr[i];//一定要從右往左,才能拿到最新的
        }
        return res;
    }
}

二維dp[][]遞推型

1. 不同路徑

方法一:動態規劃

關鍵點:

1)狀態轉移方程:path[ i ][ j ] = path[ i-1 ][ j ] + path[ i ][ j-1 ];

2)初始化:第一行第一列初始化為1(因為都是隻有一種方法)

class Solution {
    public int uniquePaths(int m, int n) {
        int[][] path = new int [m][n];
        //由於最上的邊、最左的邊,初始化為1 (因為都是隻有一種方法到達)
        for(int i=0; i<=m-1; ++i)path[i][0]=1;
        for(int j=0; j<=n-1; ++j)path[0][j]=1;
        for(int i=1; i<=m-1; ++i){
            for(int j=1; j<=n-1; ++j){
                path[i][j]=path[i-1][j]+path[i][j-1];
            }
        }
        return path[m-1][n-1];
    }
}

時間 O(mn)、空間 O(mn)

方法二:組合數學
M x N 網格,從左上到右下
分析:一共(M-1)+(N-1)步,其中向右(M-1)步,所以是:

時間 O(n) 、空間O(1)

2. 最小路徑和

給定一個包含非負整數的 m x n 網格 grid ,請找出一條從左上角到右下角的路徑,使得路徑上的數字總和為最小。

public class Solution {
    public int minPathSum(int[][] matrix) {
        int row = matrix.length;
        int col = matrix[0].length;
        int[][]sum = new int[row][col];
        int s = 0;
        for(int i=0; i<=row-1; ++i){
            s += matrix[i][0];
            sum[i][0] = s;
        }
        s = 0;
        for(int j=0; j<=col-1; ++j){
            s += matrix[0][j];
            sum[0][j] = s;
        }
        for(int i=1; i<=row-1; ++i){
            for(int j=1; j<=col-1; ++j){
                sum[i][j] = sum[i-1][j]<sum[i][j-1] ? sum[i-1][j]+matrix[i][j] : sum[i][j-1]+matrix[i][j];
            }
        }
        return sum[row-1][col-1];
    }
}//時間O(mn), 空間O(mn)

優化 Tips:

1)可以直接修改題目提供的grid陣列,這樣空間就是O(1)了。

2)如果使用sum[row+1][col+1]陣列,虛擬的邊上為0,就可以統一步驟、不用初始化的兩個迴圈了。

類似題目
題目:三角形最小路徑和
解法:
設定一個最小和的輔助dp陣列
第1步:左邊界、右邊界只有一個"父";先把邊計算好
第2步:裡面的點來自兩個"父"的min
第3步:取最下面一層的最小值
(空間優化:只保留計算的上一行即可)

3. 最大正方形

二維棋盤上,所有值為 0或1。求全為1的最大正方形。

public class Solution {
    public int maximalSquare(char[][] matrix) {//動態規劃O(N^2)  //暴力法O(N^4)
        int maxSide = 0;
        if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
            return maxSide;
        }
        int rows = matrix.length;
        int columns = matrix[0].length;
        int[][] dp = new int[rows][columns];
        for (int i = 0; i < rows; ++i) {
            for (int j = 0; j < columns; ++j) {
                if (matrix[i][j] == '1') {//右下角為非空
                    if (i == 0 || j == 0) {
                        dp[i][j] = 1;
                    } else {
                        dp[i][j] = Math.min(Math.min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]) + 1;//這句是核心 //左、上、左上 3者之中最小的+1
                    }
                    maxSide = Math.max(maxSide, dp[i][j]);
                }
            }
        }
        int maxSquare = maxSide * maxSide;
        return maxSquare;
    }
}

二維dfs回溯型(flag試錯)

1. 矩陣中的路徑

請設計一個函式,用來判斷在一個矩陣中是否存在一條包含某字串所有字元的路徑。路徑可以從矩陣中的任意一個格子開始,每一步可以在矩陣中向左,向右,向上,向下移動一個格子。如果一條路徑經過了矩陣中的某一個格子,則該路徑不能再進入該格子。例如:

矩陣中包含一條字串"bcced"的路徑,但是矩陣中不包含"abcb"路徑,因為字串的第一個字元b佔據了矩陣中的第一行第二個格子之後,路徑不能再次進入該格子。

【總體思路】(dfs+剪枝) x 多個起點

public class Solution {
    public boolean hasPath (char[][] matrix, String word) {
        boolean flag[][] = new boolean[matrix.length][matrix[0].length];//flag[][]陣列,初始化為false,表示未經過的點
        //一次初始化,之後共用 ==>是因為每次試探後,都會復原flag陣列
        for(int i = 0; i<= matrix.length-1; ++i){
            for(int j=0; j<=matrix[0].length-1; ++j){//每行每列的全部格子作為起點,開始嘗試
                if(dfs(matrix, word, i, j, 0, flag)==true)return true;//如果找到一個,則完成任務+停止嘗試,立即返回true
            }
        }
        return false;//全部失敗,返回false  //單個嘗試的失敗不會有任何返回
    }
    public boolean dfs(char[][] matrix, String word, int i, int j, int count, boolean flag[][]){
        if(0<=i && i<= matrix.length-1 && 0<=j && j<=matrix[0].length-1){//統一攔截==>【剪枝】
            if(matrix[i][j] == word.charAt(count) && flag[i][j]==false){//匹配++
                ++count;//也可以在後面都用count+1
                if(count == word.length())return true;//完整匹配,則主動停止  //全文僅此一處、是true的源頭
                flag[i][j] = true;//【嘗試改flag】(與下文還原flag對應)
                //下面遞迴結構類似4叉樹的遞迴:
                if(dfs(matrix, word, i+1, j, count, flag)
                || dfs(matrix, word, i-1, j, count, flag)
                || dfs(matrix, word, i, j+1, count, flag)
                || dfs(matrix, word, i, j-1, count, flag)
                )return true;//這個return true是帶有if的、起到傳遞true的作用,它不是源頭
                flag[i][j] = false;//【還原flag】//注意,平時傳值都不需要"還原"(如count),而這裡需要。
            }                                   //說明flag[][]陣列,傳的是指標(而不是提供副本),遞迴分支是共用一個的
        }
        return false;
    }
}//時間:O(rows*cols*3^word)//3是因為不能回頭、減少一種路
//空間:1)flag空間 O(rows*cols)  2)棧空間O(word)
//如果有類似線性匹配的KMP模式串的優化,會快一些

2. N皇后問題

牛客N皇后
兩種方法都是基於動態規劃(回溯):

方法一:二維陣列+4個方向探索+起點只從第一行開始+設定二維flag表示是否可能新落子

方法二:一位陣列(i、A[i]分別表示行、列)+單向探索

回溯的本質是" 科學地 窮舉 "

本題時間複雜度O( N! )

3. 機器人的運動範圍

地上有一個m行和n列的方格。一個機器人從座標0,0的格子開始移動,每一次只能向左,右,上,下四個方向移動一格,但是不能進入行座標和列座標的數位之和大於k的格子。 例如,當k為18時,機器人能夠進入方格(35,37),因為3+5+3+7 = 18。但是,它不能進入方格(35,38),因為3+5+3+8 = 19。請問該機器人能夠達到多少個格子?

【總體思路】dfs+剪枝

public class Solution {//相鄰方格移動  //由於各個位相加(非線性)造成跳步困難,所以用相鄰探索的方法
    int count = 0;
    public int movingCount(int threshold, int rows, int cols) {
        boolean flag[][] = new boolean[rows][cols];//標記方格有沒有來過,這裡的flag不可逆
        dfs(threshold, rows, cols, 0, 0, flag);//從(0,0)開始探索。。
        return count;
    }
    public void dfs(int threshold, int rows, int cols, int i, int j, boolean flag[][]){
        if(0<=i && i<=rows-1 && 0<=j && j<=cols-1){
            if(flag[i][j] == false && i/10 + i%10 + j/10 + j%10 <= threshold){//i、j屬於[0,99]
                ++count;
                flag[i][j] = true;//不用還原
                //如果一個塊塊不符合,它周圍就不用再試了
                dfs(threshold, rows, cols, i+1, j, flag);
                dfs(threshold, rows, cols, i-1, j, flag);
                dfs(threshold, rows, cols, i, j+1, flag);
                dfs(threshold, rows, cols, i, j-1, flag);
            }
        }
    }
}//時間O(rows*cols)  空間O(rows*cols)
//不能用兩層for迴圈來做是因為:本題需要相連的空間,所以必須bfs、dfs二選一
//感覺修改一下就是迷宮找路

4. 海島數量

給出二維矩陣,其中1是海島、0是海。求一共有多少個海島(1的連線區域)?

public class Solution {
    static int count = 0;//黑區域數量(海島數量)
    public static void main(String[] args) {
        int[][] bitmap = {{1, 0, 0}, {0, 0, 1}, {0, 0, 1}};
        System.out.println("connectedComponts(bitmap) = " + connectedComponts(bitmap));
    }
    public static int connectedComponts (int[][] bitmap) {
        //boolean black = false;
        int rows = bitmap.length;
        int cols = bitmap[0].length;
        boolean flag[][] = new boolean[rows][cols];
        for(int i=0; i<=rows-1; ++i){
            for(int j=0; j<=cols-1; ++j){
                dfs(i,j,rows,cols,flag,bitmap, false);//每次由1進去一次,就使總的黑區域+1
            }
        }
        return count;
    }
    public static void dfs(int i, int j, int rows, int cols, boolean[][] flag, int[][] bitmap, boolean black){
        if(0<=i && i<=rows-1 && 0<=j && j<=cols-1){//在區域內
            if(flag[i][j] == false && bitmap[i][j]==1){
                if(black == false){//這裡是整個的核心!!!
                    count++;
                }
                flag[i][j] = true;//走過
                dfs(i+1,j,rows,cols,flag,bitmap, true);
                dfs(i-1,j,rows,cols,flag,bitmap, true);
                dfs(i,j+1,rows,cols,flag,bitmap, true);
                dfs(i,j-1,rows,cols,flag,bitmap, true);
            }
        }
    }
}//時間複雜度是O(cols*rows),因為每個陸地最多走一遍,就會被flag標記,後面遇到直接跳出。

區間型

1. 最長迴文子串

給定字串A以及它的長度n,請返回最長迴文子串的長度。
輸入:"abc1234321ab",12
返回值:7

方法一:暴力求解(窮舉)
i、j兩重迴圈表示開始結束的座標,然後O(N)判斷是否對稱
時間O(N^3)
空間O(1)

方法二:動態規劃
dp [ i ] [ j ]儲存:i開始、j結束的串
P(i,j) <-- P(i+1,j−1)
時間O(N^2)
空間O(N^2)

方法三:中心擴充套件法
從每個點開始,
同時向左右擴充套件、判斷是否相等
時間O(N) x O(N) = O(N^2)
空間O(1)

public class Solution {
    public int getLongestPalindrome(String A, int n) {
        int maxLen = 0;
        for(int i=0; i<=n-2; ++i){//i==n-1略過
            int len1 = expand(A, i, i);//1)單中心
            int len2 = expand(A, i, i+1);//2)雙中心
            int len = len1 > len2 ? len1 : len2;
            if(len > maxLen) maxLen = len;
        }
        return maxLen;
    }
    public int expand(String A, int left, int right){
        while(0<=left && right<=A.length()-1 && A.charAt(left)==A.charAt(right)){
            --left;
            ++right;
        }
        return right-left-1;//right-left+1-2
    }
}//時間O(N^2) 空間O(1)

方法四:Manacher 演算法
時間O(N)
空間O(N)
演算法較為複雜,後面再記一下

2. 石子游戲

A和B用幾堆石子在做遊戲。偶數堆石子排成一行,每堆都有正整數顆石子。
遊戲以誰手中的石子最多來決出勝負。石子的總數是奇數,所以沒有平局
A和B輪流進行,A先開始。 每回合,玩家從行的開始或結束處取走整堆石頭。 這種情況一直持續到沒有更多的石子堆為止,此時手中石子最多的玩家獲勝。
假設A和B都發揮出最佳水平,當A贏得比賽時返回 true ,當B贏得比賽時返回 false 。

方法一:動態規劃

三角形階梯dp陣列,i,j分別是連續石子堆的左右邊界,dp[i][j]表示領先石子數
初始化底層n個為石子堆數量,然後每次在邊界增加一堆石子(左/右)
dp[i][j] = Math.max(piles[i] - dp[i + 1][j], piles[j] - dp[i][j - 1]);
res= dp[0][len-1]
時間O(N^2)
空間O(N^2)可優化為O(N)

方法二:直接 return true

==> 如果兩人都是最佳策略,先手必勝

3. 連續子陣列的最大和

輸入一個整型陣列,數組裡有正數也有負數。陣列中的一個或連續多個整陣列成一個子陣列。求所有子陣列的和的最大值。要求時間複雜度為 O(n).

//這是實用演算法課上講過的方法
public class Solution {
    public int FindGreatestSumOfSubArray(int[] array) {
        if(array.length ==0)return 0;
        int max = Integer.MIN_VALUE;//全域性最大值   //(這裡將max=array[0]也可)
        int currentSum = 0;//鄰近最大值:小於0時候熔斷,最小為0
        for(int i =0; i<=array.length-1; ++i){
            currentSum += array[i];
            if(currentSum > max) max=currentSum;
            if(currentSum<0)currentSum=0;
        }
        return max;
    }
}
//時間 O(N)  空間 O(1)

4. 最大矩形(連續二維陣列)

此類問題是上面連續子陣列最大和的二維升級版,
核心思路是:轉換壓縮到一維問題,也就是將一豎排都壓縮到一個值,用兩重迴圈來遍歷,分別確定上下界start/end

public class Main {
    public int MaxMatrix(int[][]matrix) {
        int rows = matrix.length;
        int cols = matrix[0].length;
        int res = Integer.MIN_VALUE;
        for(int begin = 0; begin<=rows-1; ++begin){//上邊界
            int[] line = new int [cols];//每一列之和 //修改上邊界之後,要清空重新開始
            for(int end = begin; end<= rows-1; ++end){//下邊界
                //計算列元素和
                for(int j=0; j<=cols-1; ++j){
                    line[j] += matrix[end][j];//下邊界每向下一行,就計算更新下line[]陣列的值
                                            //上邊界固定,下邊界依次計算=>避免重複計算
                }
                res = Math.max(res,line[0]);
                int sum = 0;
                for(int j=0; j<=cols-1; ++j){
                    sum += line[j];
                    res = Math.max(res,sum);//取最大
                    if(sum<0)sum=0;//小於零,置零
                }
            }
        }
        return res;
    }
}//這一題的思路是:由一維的連續子陣列最大和,然後擴充套件到上下界的遍歷,則可計算二維問題。
//時間複雜度O(N^3)  //應該是最優解了

5. 合併區間

方法:按照區間左端點排序,然後向右遍歷
不斷更新合併後區間的右端點,
如果遇到合併後右端點 小於 下一個的左端點,就重新開始一個區間;
時間複雜度O(N*logN):先排序、後線性遍歷

6. 射箭問題

方法:按照區間右端點排序,然後向右遍歷
如果遇到右端點 小於 下一個的左端點,就換下一個右端點重開;
時間複雜度O(N*logN):先排序、後線性遍歷

public class Solution {
    // 貪心策略:先將左右區間按照右端點排序,然後從小到大掃描,
    // 如果遇到右端點小於左端點,就換成此區間的右端點(並且count++),然後繼續掃描
    public int findMinArrowShots (int[][] targets) {//每個區間代表靶子的上下界,儘量多重射穿靶子
        if(targets == null || targets.length==0) return 0;
        Arrays.sort(targets, new MyComparator());
        int res = 1;
        int lastEnd = targets[0][1];
        for(int i=1; i<targets.length; ++i){
            if(targets[i][0] > lastEnd){
                res++;
                lastEnd = targets[i][1];
            }
        }
        return res;
    }
	//特殊的比較方法,需要自己寫:
    //實現Comparator介面類,重寫compare方法。
    class MyComparator implements Comparator<int[]>{
        public int compare(int[] X, int[] Y){
            return X[1] - Y[1];
        }
    }
}

揹包型

1. 分割等和子集【0-1揹包問題】

==》問題等價於:不連續子集和為 k(本題k=sum/2)

和【0-1揹包問題】不同的是:揹包要 <= k,這裡是==k

class Solution {
    public boolean canPartition(int[] nums) {
        int sum = 0;
        for(int num : nums)sum += num;
        if((sum & 1) == 1)return false;//位運算,看二進位制最後一位是不是1
        int target = sum/2;
        boolean[] dp = new boolean[1+target];
        dp[0] = true;
        for(int i=0; i<=nums.length-1; ++i){//nums[i]
            boolean[] mem = new boolean[1+target];//用於記錄;空間優化方法是從後往前
            for(int k=0; k<=target; ++k){//dp[k]
                if(dp[k]==true) mem[k]=true;
            }
            for(int k=0; k<=target; ++k){
                if(mem[k]==true && k + nums[i]<=target){
                    dp[k + nums[i]]=true;
                }
            }
        }
        return dp[target];
    }
}

空間優化:

class Solution {
    public boolean canPartition(int[] nums) {
        int sum = 0;
        for(int num : nums)sum += num;
        if((sum & 1) == 1)return false;
        int target = sum/2;
        boolean[] dp = new boolean[1+target];
        dp[0] = true;
        for(int i=0; i<=nums.length-1; ++i){
            for(int k=target; k>=0; --k){//dp[k]從後往前
                if(k-nums[i]>=0 && dp[k-nums[i]]==true)dp[k]=true;//優化
            }
        }
        return dp[target];
    }
}//時間 O(len * target)
//空間 O(target)

2. 零錢兌換【完全揹包問題】

  • 【0/1揹包問題】:每個元素最多選取一次
  • 【完全揹包問題】:每個元素可以重複選擇
  • 【分類揹包問題】:有多個揹包,分別裝不同東西,需要多重遍歷
class Solution {
    public int coinChange(int[] coins, int amount) {
        int[] dp = new int[amount + 1];//i和dp[i]分別為總金額、硬幣數量
        Arrays.fill(dp, Integer.MAX_VALUE-1);//
        dp[0] = 0;//
        for(int i=1; i<=amount; ++i){
            for(int j=0; j<=coins.length-1; ++j){
                if(i-coins[j]>=0 && dp[i-coins[j]]+1 < dp[i]){
                    dp[i] = dp[i-coins[j]]+1;
                }
            }
        }
        if(dp[amount]<=amount) return dp[amount];//這裡判斷含有等於
        else return -1;//未找到方法,則dp[i]裡面還是初始值
    }
}//時間O(amount*coin) 空間O(amount)

3. 一和零【分類揹包問題】

給你一個二進位制字串陣列 strs 和兩個整數 m 和 n 。
請你找出並返回 strs 的最大子集的大小,該子集中 最多 有 m 個 0 和 n 個 1 。
如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。

class Solution {
    public int findMaxForm(String[] strs, int m, int n) {
        int[][] dp = new int[m+1][n+1];//第一行第一列自動初始化為0
        for(String str:strs){//
            int zeros = 0;
            int ones = 0;
            for(char c:str.toCharArray()){//連續用兩個高階for迴圈
                if(c=='1')++ones;
                else ++zeros;
            }
            for(int i=m; i>=0; --i){//揹包內容在外層,dp在內層
                for(int j=n; j>=0; --j){//揹包經典的回退--
                    if(i-zeros>=0 && j-ones>=0 && dp[i][j] < dp[i-zeros][j-ones] + 1){//要求:1.無陣列溢位 2.更優才更新
                        dp[i][j] = dp[i-zeros][j-ones] + 1;
                    }
                }
            }                    
        }
        return dp[m][n];
    }
}
//時間複雜度 O(S*M*N), 其中S為strs[]元素個數
//空間複雜度 O(M*N)

本文先簡要說明了動態規劃的特點,然後給出問題的大致分類;
後面按照題目型別給出高頻演算法題的題解,以及詳細的註釋分析;
希望能對大家關於動態規劃方面有一個overview的認識,並幫助大家刷題備考!