動態規劃及相關算法題總結

1. 概念

動態規劃和分治法類似,把原問題劃分成若干個子問題,不同的是,分治法,子問題間相互獨立,動態規劃,子問題不獨立。

在這裏插入圖片描述

動態規劃解題一般分爲三個步驟:

  1. 表示狀態

分析問題的狀態時,不要分析整體,只分析最後一個階段即可!因爲動態規劃問題都是劃分爲多個階段的,各個階段的狀態表示都是一樣,而我們的最終答案在就是在最後一個階段。每一種狀態都是使用數組的位數來表示的。一般我們會先使用數組的第一維來表示階段,然後再根據需要通過增加數組維數,來添加其他狀態。

  1. 找出狀態轉移方程

狀態轉移指的是,當前階段的狀態如何通過之前計算過的階段狀態得到。

  1. 初始化邊界

初始化邊角狀態,以及第一個階段的狀態。

整個動態規劃,最難的就是定義狀態。一旦狀態定義出來,表明你已經抽象出了子問題,可以肢解原來的大問題了。

2. 相關算法題

2.1 斐波那契數列

斐波那契數列指的是這樣一個數列:1、1、2、3、5、8、13、21、34、…… 其規律很明顯,從第3個數開始,每個數都等於它前兩個數的和。
遞歸解法

//遞歸方式
    public static int fibonacci(int i){
        if(i < 2) return i;
        return fibonacci(i - 1) + fibonacci(i - 2);
    }

使用遞歸的方式,會有好很多的重複計算
在這裏插入圖片描述
使用動態規劃的方式,來避免重複計算

解題思路

定義一個一維數組dp,dp[ i ] 代表斐波那契數列第i個數字,轉移方程爲dp[ i ] = dp[ i - 1 ] + dp[ i - 2 ],初始化dp[ 0 ] = 1,dp[ 1 ] = 1,之後,從2開始循環,直到計算出要求的第n個數字結束。

// 動態規劃
    public static int fibonacci2(int i){
        //定義一個一維數組dp,dp[i]表示斐波那契數列第i個數字
        int[] dp = new int[i + 1];
        dp[0] = 0;
        dp[1] = 1;
        for(int j = 2; j < i + 1; j++){
            dp[j] = dp[j - 1] + dp[j - 2];
        }
        return dp[i];
    }

與之類似的還有:跳臺階問題:每次只能跳一個或者兩個臺階,跳到n層臺階上有幾種方法 。

2.2 矩陣連乘問題

給定n個矩陣{A1,A2,…,An},其中Ai與Ai+1是可乘的,i=1,2…,n-1。如何確定計算矩陣連乘積的計算次序,使得依此次序計算矩陣連乘積需要的數乘次數最少。

例如,給定三個連乘矩陣{A1,A2,A3}的維數分別是10100,1005和550,採用(A1A2)A3,乘法次數爲101005+10550=7500次,而採用A1(A2A3),乘法次數爲100550+10100*50=75000次乘法,顯然,最好的次序是(A1A2)A3,乘法次數爲7500次。

矩陣乘法
矩陣A 和矩陣B,只有當A的列數和B的行數相等的時候纔可以相乘,假如A(m x n),B(n x k),A x B 得到一個m 行 k列的矩陣,具體乘法是拿A的第一行與B的第一列中各個元素的乘積的和爲結果矩陣的第一行第一列的數,然後拿A的第一行和B的第二列的各個元素的乘積作爲第一行第二列的數…。

解題思路

定義一個二維數組m,m[ i ] [ j ] 表示第i個矩陣到第j個矩陣連乘的最小次數,則最優值就是m[ 1 ] [ n ]。假設A1A2…An的一個最優加括號把乘積在Ak和Ak+1間分開,則前綴子鏈A1…Ak的加括號方式必定爲A1…Ak的一個最優加括號,後綴子鏈同理。
所以m[ i ] [ j ] = min( m[ i ] [ k ] + m [ k + 1 ] + p(i - 1) * p(k) *p(j) ) ( p(i - 1) 、p(k)、p(j) 分別是第i個矩陣的行數,第k個矩陣的列數,第j個矩陣的列數),如果i = j,m[ i ] [ j ] = 0

一開始並不知道k的確切位置,需要遍歷所有位置以保證找到合適的k來分割乘積。

對於一組矩陣:A1(30x35),A2(35x15),A3(15x5),A4(5x10),A5(10x20),A6(20x25) 個數N爲6

那麼p數組保存它們的行數和列數:p={30,35,15,5,10,20,25}共有N+1即7個元素

p[0],p[1]代表第一個矩陣的行數和列數,p[1],p[2]代表第二個矩陣的行數和列數…p[5],p[6]代表第六個矩陣的行數和列數

在這裏插入圖片描述
在這裏插入圖片描述
輔助表m: m[i][j]代表從矩陣Ai,Ai+1,Ai+2…直到矩陣Aj最小的相乘次數,比如A[2][5]代表A2A3A4A5最小的相乘次數,即最優的乘積代價。我們看上圖,從矩陣A2到A5有三種斷鏈方式:A2{A3A4A5}、{A2A3}{A4A5}、{A2A3A4}A5,這三種斷鏈方式會影響最終矩陣相乘的計算次數,我們分別算出來,然後選一個最小的,就是m[2][5]的值,同時保留斷開的位置k在s數組中。

代碼

public class MartixChain {
    /**
     *
     * @param p p爲矩陣鏈,p[0],p[1]代表第一個矩陣的行數和列數,p[1],p[2]代表第二個矩陣的行數和列數
     * @return 返回最優值
     */
    public static int martixChain(int[] p){

        //計算矩陣的個數
        int n = p.length - 1;
        //m[i][j],表示第i個矩陣到第j個矩陣連乘的最小相乘次數
        int[][] m = new int[n + 1][n + 1];
        //s[i][j]=k,表示,第i個矩陣到第j個矩陣的連乘,從第k個矩陣分割,相乘次數最小
        int[][] s = new int[n + 1][n + 1];
        //初始化二維數組,將對角線位置上的值設爲0,本身就是0,可以上略
        for(int i = 0; i < n + 1; i++){
           m[i][i] = 0;
        }

        for(int r = 2; r < n + 1; r++){//表示r個矩陣相乘
            // i表示從第i個矩陣開始,r個矩陣連乘。n-r,表示r個矩陣相乘,最左邊的矩陣是第n-r+1個,
            // 如果超過這個,最後一組r個矩陣就不是r個了,也就是i的左邊界
            for(int i = 1; i <= n - r + 1; i++){
                //j 表示從第i個矩陣開始,長度爲r的矩陣鏈的最後一個矩陣
                int j = i + r - 1;
                //先以i進行劃分
                m[i][j] = m[i + 1][j] + p[i - 1] * p[i] *p[j];
                //記錄劃分位置
                s[i][j] = i;

                //尋找使矩陣相乘次數最小的劃分點
                for(int k = i + 1; k < j; k++){
                    int t = m[i][k] + m[k + 1][j] + p[i - 1] * p[k] * p[j];
                    if(t < m[i][j]){
                        m[i][j] = t;
                        s[i][j] = k;
                    }
                }
            }
        }
        return m[1][n];
    }
    
    public static void main(String[] args) {
        int[] p = new int[]{5, 7, 4, 3, 5};
        System.out.println(martixChain(p)); // 264
    }
}

2.3 劍指 Offer 42. 連續子數組的最大和

輸入一個整型數組,數組裏有正數也有負數。數組中的一個或連續多個整數組成一個子數組。求所有子數組的和的最大值。

要求時間複雜度爲O(n)。

示例1:

輸入: nums = [-2,1,-3,4,-1,2,1,-5,4]
輸出: 6
解釋: 連續子數組 [4,-1,2,1] 的和最大,爲 6。

解題思路

使用動態規劃算法,定義一個數組dp,dp[i]代表以nums[i]結尾的連續子數組的最大和。如果dp[i - 1] >= 0,dp[i]=dp[i-1]+nums[i],如果
dp[i - 1] < 0,dp[i] = nums[i]。dp[0] = nums[0]

public static int maxSubArray(int[] nums){
        if(nums == null || nums.length == 0) return 0;
        //dp[i]表示以nums[i]結尾的連續子數組的最大和
        int[] dp = new int[nums.length];
        dp[0] = nums[0];
        int res = nums[0];
        for(int i = 1; i < nums.length; i++){
            if(dp[i - 1] < 0){
                dp[i] = nums[i];
            }else {
                dp[i] = dp[i - 1] + nums[i];
            }
            res = Math.max(res, dp[i]);
        }
        return res;
    }

這道題,也可以不使用數組,只使用一個變量來記錄以nums[i]結尾的連續子數組的最大和

public static int maxSubArray2(int[] nums){
    if(nums == null || nums.length == 0) return -1;
    int max = nums[0];
    int res = nums[0];
    for (int i = 1; i < nums.length; i++) {
        if(max < 0){
            max = nums[i];
        }else{
            max = max + nums[i];
        }
        res = Math.max(max, res);
    }
    return res;
}

2.4 01揹包問題

有n個物品,它們有各自的體積和價值,現有給定容量的揹包,如何讓揹包裏裝入的物品具有最大的價值總和?
eg:number=4,capacity=8

i(物品編號) 1 2 3 4
w(體積) 2 3 4 5
v(價值) 3 4 5 6

解題思路

  1. 表示狀態
    V[i ,j]表示當前揹包容量 j,前 i 個物品最佳組合獲得的最大的價值
  2. 找出狀態轉移方程
    揹包容量不足以裝物品i(w(i) 大於揹包容量)則裝入前i個物品得到的最大價值和裝入前i - 1個物品的最大價值是相同的,即V[i ,w] = V[i - 1,w]
    揹包容量可以裝入物品i,可以裝也可以不裝,所以V[i,j] = max { V[i - 1 ,j - w(i)] + v(i) , V[i - 1,j] }

爲什麼揹包容量足夠的情況下,還需要 V(i,j)=max{V(i-1,j),V(i-1,j-w(i))+v(i)}?

V(i-1,j-w(i))+v(i)表示裝了第i個物品後背包中的最大價值,所以當前揹包容量 j 中,必定有w(i)個容量給了第i個揹包。
因此只剩餘j-w(i)個容量用來裝除了第i件物品的其他所有物品。
V(i-1,j-w(i))是前i-1個物品裝入容量爲j-w(i)的揹包中最大價值。
注意,這裏有一個問題。前i-1個物品裝入容量爲j-w(i)的揹包中最大價值+物品i的價值。可能不如將,前i-1個物品裝入容量爲j的揹包中得到的價值大。也就是說,可能出現 V(i-1,j) > (V(i-1,j-w(i))+v(i))
比如說,將第i個物品放入揹包,可能會導致前面更有價值的物品放不進揹包。因此,還不如不把第i個物品放進去,把空間讓出來,從而能讓前i-1個物品中更有價值的物品能夠放入揹包。從而讓V(i,j)取得最大的值。
所以我們需要 max{V(i-1,j),V(i-1,j-w(i))+v(i)},來作爲把前i個物品裝入容量爲j的揹包中的最優解。

根據上面的例子,重量分別爲2,3,4,5 ,價值分別爲3,4,5,6,商品編號是1,2,3,4
在這裏插入圖片描述
表中橫座標代表容量j,縱座標代表商品編號i,表中數字代表在揹包容量爲j時,前i個物品最佳組合獲得的最大價值dp[ i ][ j ]。物品最小的重量爲2,所以容量爲0、1時,表格中都爲0,並且縱座標爲0的那一列,也全爲0,因爲0個商品,價值爲0。當容量爲2時,1號商品,重量爲2,可以放,也可以不放,因爲1號商品前面沒有商品了,所以前1個物品,獲得的最大價值爲3,不管容量爲幾都爲3,看2號商品,揹包容量爲2時,沒法放2好商品,所以dp[ 2 ][ 2 ] = dp[ 1 ][ 2 ],當揹包容量爲3時, 2號商品可以放,也可以不放,主要看價值,dp[ 1 ][ 3 ] = 2,
而3號商品的價值爲3,所以應該放如揹包,這樣,揹包容量爲3時,就不放1號商品了,dp[ 2 ][ 3 ] = max { dp[ 1 ] [ 3 -2] + v(2) ,dp [ 1 ][ 3 ] } = 3。

只要上面的圖理解了,以及dp[ i ] [ j ] 代表的含義理解了,01揹包問題就很簡單了。

代碼實現

public class ZeroOnePack {
    /**
     *
     * @param n 物品數量
     * @param v 揹包容量
     * @param weights 物品的重量
     * @param values  物品的價值
     * @return  返回容量爲v的揹包所能獲得的最大價值
     */
    public static void zeroOnePack(int n, int v, int[] weights, int[] values){
        //初始化動態規劃數組,dp[i][j]代表揹包容量爲j時,前i個物品最佳組合所能獲得的最大價值
        int[][] dp = new int[n + 1][v + 1];
        //爲了便於理解,將dp[i][0]和dp[0][j]均置爲0,從1開始計算
        for(int i = 1; i < n + 1; i++){
            for(int j = 1; j < v + 1; j++){
                //如果第i件物品的重量大於揹包容量j,則不裝入揹包
                //由於weight和value數組下標都是從0開始,故注意第i個物品的重量爲weight[i-1],價值爲value[i-1]
                if(weights[i - 1] > j){
                     dp[i][j] = dp[i - 1][j];
                 }else {
                     dp[i][j] = Math.max(dp[i - 1][j - weights[i - 1]] + values[i - 1],dp[i - 1][j]);
                 }
            }
        }
        //獲得的最大商品重量
        int maxValue = dp[n][v];
        System.out.println("獲得的最大商品價值爲" + maxValue);
       
        //通過回溯法算出哪些商品被裝到揹包裏了。從dp[n][v]開始,如果dp[n][v]=dp[n-1][v]
        //說明,第n件商品沒有被裝到揹包裏,如果不相等,說明,被裝到揹包裏了,之後,減去
        //被裝入商品的重量,再判斷容量爲當前容量時物品是否裝入揹包了(重複上述過程)。
        
        int j = v;
        for(int i = n; i > 0; i--){
            //如果dp[i][j] > dp[i - 1][j],說明,第i件商品是放入揹包的
            if(dp[i][j] > dp[i - 1][j]){
                System.out.print(i + " ");
                j = j - weights[i - 1];
            }

            if(j == 0){
                break;
            }
        }
    }

}

2.5 面試題66. 構建乘積數組

給定一個數組 A[0,1,…,n-1],請構建一個數組 B[0,1,…,n-1],其中 B 中的元素 B[i]=A[0]×A[1]×…×A[i-1]×A[i+1]×…×A[n-1]。不能使用除法。
示例:

輸入: [1,2,3,4,5]
輸出: [120,60,40,30,24]

提示:

所有元素乘積之和不會溢出 32 位整數
a.length <= 100000

解題思路

題目中求的是每一個元素左邊的元素和右邊的元素的乘積,不包括當前元素,可以分別求出每一個元素左邊元素的乘積和右邊元素的乘積,並且計算的時候,後一個左邊元素的乘積是前一個數左邊元素的乘積乘以前一個元素,比如3的左邊元素的乘積,是2的左邊元素的乘積乘以2,同理,每一個數的右邊元素的乘積,是這個數後邊的元素的乘積乘以後邊的元素。

代碼

class Solution {
    public int[] constructArr(int[] a) {
        //用動態規劃的思想解題,先算出每一個元素從第一個元素開始到當前元素之前的一個元素的
        //乘積,再算出從數組末尾開始,每一個元素到當前元素的前一個元素的乘積,將前面算出的
        //結果一一相乘就是答案
        int n = a.length;
        if(n == 0) return new int[0];
        int[] left = new int[n];
        //第一個數的左邊的數的乘積是1
        left[0] = 1;
        //算出每一元素左邊的乘積(不包括自己)
        for(int i = 1; i < n; i++){
            left[i] = left[i - 1] * a[i - 1];
        }
        //算出每一個元素右邊的乘積(不包括自己),可以定義一個temp變量代替數組,temp每一個數的右邊元素的乘積
        //最後一個數的右邊元素的乘積是1
        int temp = 1;
        int[] res = new int[n];
        res[n - 1] = left[n - 1];
        for(int i = a.length - 2; i >= 0; i--){
            temp = temp * a[i + 1];
            res[i] = left[i] * temp;
        }
        return res;
    }
}

如有不足之處,歡迎指正,謝謝!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章