簡介
動態規划遵循一套固定的流程:遞歸的暴力解法( 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];
}
}