動態規划


簡介

動態規划遵循一套固定的流程:遞歸的暴力解法( O(2^n) ) -> 帶備忘錄的遞歸解法( O(n) ) -> 非遞歸的動態規划解法( O(n) )。


「自頂向下」:
	是從上向下延伸,
	都是從一個規模較大的原問題比如說 f(20),向下逐漸分解規模,直到 f(1) 和 f(2) 觸底,然后逐層返回答案。


「自底向上」:
	直接從最底下,最簡單,問題規模最小的 f(1) 和 f(2) 開始往上推,直到推到我們想要的答案 f(20),

動態規划解法

1. 將原問題分解為子問題
    f(0),f(1),f(2)...f(n)

2. 確定狀態
    f(n)

3. 確定一些初始狀態(邊界條件)的值
    f(n)=0

4. 確定狀態轉移方程(當前子問題值與前一個子問題值的關系)
	f(n) 是一個狀態 n,這個狀態 n 是由狀態 n - 1 和狀態 n - 2 相加轉移而來,這就是狀態轉移
	動態規划問題最困難的就是寫出狀態轉移方程。

適合使用動規求解的問題

1. 問題具有最優子結構(問題的最優解所包含的子問題的解也是最優的)

2. 求最優解問題

動態規划1:爬樓梯,求共多少爬法(n為正整數)

/**
 * 方法1:遞歸(超時):太多冗余
 */
class Solution1 {
    public static int climbStairs(int n) {
        if(n==1 || n==2)return n;
        return climbStairs(n-1)+climbStairs(n-2);
    }
}


/**
 * 方法2:記憶化遞歸遞歸
 *      把每一步的結果存儲在 memo 數組之中,每當函數再次被調用,我們就直接從 memo 數組返回結果。
 *
 * 時間復雜度:O(n),樹形遞歸的大小可以達到 n。
 * 空間復雜度:O(n),遞歸樹的深度可以達到 n。
 */
class Solution2 {
    public static int climbStairs(int n) {
        int memo[] = new int[n + 1];
        return climb_Stairs(0, n, memo);
    }
    public static int climb_Stairs(int i, int n, int memo[]) {
        if (i > n) {
            return 0;
        }
        if (i == n) {
            return 1;
        }
        if (memo[i] > 0) {
            return memo[i];
        }
        memo[i] = climb_Stairs(i + 1, n, memo) + climb_Stairs(i + 2, n, memo);
        return memo[i];
    }
}

class Solution2 {
    public static int climbStairs(int n) {
        int memo[] = new int[n + 1];
        return climb_Stairs(n, memo);
    }

    public static int climb_Stairs(int n, int memo[]) {
        if (n == 1 || n == 2) return memo[n] = n;
        if (memo[n] > 0) {
            return memo[n];
        }
        return memo[n] = climb_Stairs(n - 1, memo) + climb_Stairs(n - 2, memo);
    }
}


/**
 * 方法3:動態規划
 *
 * 思路(狀態轉移方程):
 *      第n階爬法的數量=第n-1階爬法的數量+第n-2階爬法的數量
 * 
 * 時間復雜度:O(n)
 * 空間復雜度:O(1)
 */
class Solution3 {
    public static int climbStairs(int n) {
        if(n==1 || n==2)return n;
        int xxx =1;
        int xx =2;
        int x = 0;
        int i=3;

        while(i++<=n){
            x=xxx+xx;
            xxx=xx;
            xx=x;
        }
        return x;
    }
}


class aaa {
    public static void main(String[] args) {
        long l = System.currentTimeMillis();
        System.out.println(Solution1.climbStairs(45));
        System.out.println(System.currentTimeMillis()-l);
    }
}

動態規划2:打家劫舍:求可以盜取的最大數(不能盜取相鄰元素)

/**
 * 思路(狀態轉移方程):
 *      n個元素最大可以盜取數值=
 *          n-2個元素最大可以盜取數值+第n個元素數值
 *          與
 *          n-1個元素最大可以盜取數值
 *          取最大值
 *
 * 時間復雜度:O(n)
 * 空間復雜度:O(1)
 */
class Solution {
    public int rob(int[] nums) {
        if (nums.length == 0) return 0;
        if (nums.length == 1) return nums[0];
        if (nums.length == 2) return nums[0] > nums[1] ? nums[0] : nums[1];

        int xxx = nums[0];  //前前一個最大
        int xx = nums[0] > nums[1] ? nums[0] : nums[1]; //前一個最大
        int x = nums[2];    //當前數
        int sum = 0;

        for (int i = 2; i < nums.length; i++) {
            sum = xx > xxx + x ? xx : xxx + x;
            if (i < nums.length - 1) {
                xxx = xx;
                xx = sum;
                x = nums[i + 1];
            }
        }
        return sum;
    }
}

動態規划3:給定一個整數數組 nums ,找到一個具有最大和的連續子數組(子數組最少包含一個元素),返回其最大和

/**
 * 思路:
 *  前一個子數組和是正數,則說明有增益,與當前元素相加(前面的子序列對自己有力,保留前面,團結互助)
 *  前一個數組是負數,則說明無增益,子數組從當前數組開始(前面的子序列對自己不利,丟棄前面,自立門戶)
 *
 * 狀態:dp[i]:表示以 nums[i] 結尾的連續子數組的最大和(如果數組長度為n,則有n種情況)
 * 
 * 狀態轉移方程:dp[i]=max{nums[i],dp[i−1]+nums[i]}
 * 
 * 時間復雜度:O(N)。
 * 空間復雜度:O(1)。
 */
class Solution {
    public int maxSubArray(int[] nums) {
        int max= nums[0];
        int sum= 0;
        for (int i = 0; i <nums.length ; i++) {
            if(sum<0)sum=nums[i];
            else sum+=nums[i];
            max=Math.max(max,sum);
        }
        return max;
    }
}

動態規划4:最少的硬幣個數。如果沒有任何一種硬幣組合能組成總金額,返回 -1

/**
 * 思路:動態規划
 * 
 * 動態轉移方程:金額n的最少硬幣數=(金額n-各種硬幣面值)+1 中所需硬幣數最小的元素
 * 
 * 時間復雜度:O(Sn)    S是金額,n是硬幣面值數
 * 空間復雜度:O(S)
 */
class Solution {
    public int coinChange(int[] coins, int amount) {
        int[] f = new int[amount + 1];
        f[0] = 0;

        for (int i = 1; i <= amount; i++) {
            int cast = Integer.MAX_VALUE;
            for (int j = 0; j < coins.length; j++) {
                if (i - coins[j] >= 0) {
                    if (f[i - coins[j]] != Integer.MAX_VALUE) cast = Math.min(cast, f[i - coins[j]] + 1);
                }
            }
            f[i] = cast;
        }
        return f[amount] == Integer.MAX_VALUE ? -1 : f[amount];
    }
}

動態規划5:三角形最小路徑和(給定一個三角形,找出最小路徑和。每一步只能移動到下一行中相鄰的結點上)

/**
 * 方法1:記憶化遞歸法
 * 
 * 思路:
 *      當前元素的最小值=下一層元素兩個元素的最小值+當前元素
 */
class Solution2 {
    private Integer[][] arr = null;

    public int minimumTotal(List<List<Integer>> triangle) {
        arr = new Integer[triangle.size()][triangle.size()];
        return helper(0, 0, triangle);
    }

    private int helper(int row, int col, List<List<Integer>> triangle) {
        if (row == triangle.size() - 1) return arr[row][col] = triangle.get(row).get(col);
        if (arr[row][col] != null) return arr[row][col];
        int left = helper(row + 1, col, triangle);
        int right = helper(row + 1, col + 1, triangle);
        return arr[row][col] = Math.min(left, right) + triangle.get(row).get(col);
    }
}


/**
 * 思路(狀態轉移方程):自底向上
 *      當前元素的最小值=下一層元素兩個元素的最小值+當前元素
 */
class Solution1 {
    public int minimumTotal(List<List<Integer>> triangle) {
        int[] ints = new int[triangle.size() + 1];
        for (int i = triangle.size() - 1; i <= 0; i--) {
            for (int j = 0; j < triangle.get(i).size(); j++) {
                ints[j] = Math.min(ints[j], ints[j + 1]) + triangle.get(i).get(j);
            }
        }
        return ints[0];
    }
}

動態規划6:給定一個無序的整數數組,找到其中最長上升子序列的長度

/**
 * 示例:
 *      輸入: [10,9,2,5,3,7,101,18]
 *      輸出: 4
 *
 * 思路(狀態轉移方程):
 *      dp[i] 表示以 nums[i] 這個數結尾的最長遞增子序列的長度。
 *  
 *      設計動態規划算法,需要一個 dp 數組。
 *      我們可以假設 dp[0...i-1]dp[0...i−1] 都已經被算出來了,然后通過這些結果算出 dp[i]的最大值。
 *
 * 時間復雜度 O(N^2)
 * 空間復雜度 O(N)
 */
class Solution {
    public int lengthOfLIS(int[] nums) {
        if(nums.length<2)return nums.length;
        int max=0;
        int[] ints = new int[nums.length];
        ints[0]=1;
        for (int i = 1; i < nums.length; i++) {
            ints[i]=1;
            for (int j = 0; j <i ; j++) {
                if(nums[j]<nums[i]){
                    ints[i]=Math.max(ints[i],ints[j]+1);
                }
            }
            max=Math.max(max,ints[i]);
        }
        return max;
    }
}

動態規划7:給定一個包含非負整數的網格,請找出一條從左上角到右下角的路徑,使得路徑上的數字總和為最小(只能向下或者向右移動一步)

/**
 * 思路(狀態轉移方程):
 *      dp[i][j]=min(dp[i-1][j],dp[i][j-1])+dp[i][j]
 * 
 * 時間復雜度 :O(mn)。遍歷整個矩陣恰好一次。
 * 空間復雜度 :O(mn)。額外的一個同大小矩陣。
 * 
 * 注:在原數組上存儲,這樣就不需要額外的存儲空間
 */
class Solution {
    public static int minPathSum(int[][] grid) {
        int row = grid.length;
        int col = grid[0].length;

        int[][] ints = new int[row][col];
        ints[0][0] = grid[0][0];

        for (int i = 1; i < row; i++) {
            ints[i][0] = ints[i - 1][0] + grid[i][0];
        }
        for (int i = 1; i < col; i++) {
            ints[0][i] = ints[0][i - 1] + grid[0][i];
        }

        for (int i = 1; i < row; i++) {
            for (int j = 1; j < col; j++) {
                ints[i][j] = Math.min(ints[i - 1][j], ints[i][j - 1]) + grid[i][j];
            }
        }
        return ints[row - 1][col - 1];
    }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM