动态规划

什么是动态规划(Dynamic Programming)?

动态规划是一个强大且通用的方法, 它被用来解决某些优化问题

动态规划(Dynamic Programming)

VS 递归(Recursion)

  • 二者都是通过解决子问题的方式来解决主问题
  • 递归是更通用的方法
  • 如果一个问题可以通过动态规划来解决, 那么往往动态规划的效率更高
  • 动态规划往往会消耗更多内存空间
  • 空间 VS 时间

什么时候使用动态规划?

  • 问题可以通过递归分解为多个子问题
  • 递归时会进行重复运算
  • 问题含有最优子结构:
    原问题的最优解包含子问题的最优解

如何使用动态规划?

  • 考虑递归的解决方案
  • 发现子问题被计算过多次
  • 确认子问题是否为最优子结构
  • 尝试保存子问题的解, 防止重复计算

斐波那契数列(Fibonacci Numbers)

int Fabonacci(int n) {
    if(n == 0 || n == 1) return 1;
    return F(n-1) + F(n-2);
}

F(6)

F(5)

F(4)

F(4)

F(3)

F(3)

F(3)

F(2)

F(2)

F(1)

F(1)

F(1)

F(0)

F(1)

F(0)

F(1)

F(0)

F(1)

F(0)

F(2)

F(1)

F(1)

F(0)

F(2)

F(2)

为了避免重复计算, 需要额外的存储空间

int Fabonacci(int n) {
    if(n <= 1) return 1;
    int[] result = new int[n + 1];
    result[0] = 1;
    result[1] = 1;
    for(int i = 2; i < n + 1; i ++) {
        result[i] = result[i-1] + result[i-2];
    }
    return result[n];
}

我们利用数组来存储临时结果.

这个问题的最优子结构是什么?

路径个数(Unique Paths)

路径个数(Unique Paths)

int uniquePaths(int m, int n) {
      if(m==1 || n==1) return 1;
      return uniquePaths(m-1, n) + uniquePaths(m, n-1);
}

哪些地方是重复计算?

这个问题的最优子结构是什么?

路径个数(Unique Paths)

最优子结构

a(i,j) = a(i-1,j) + a(i, j-1)

路径个数(Unique Paths)

public int uniquePaths(int m, int n) {  
    int[][] a = new int[m][n];  
    for (int i = 0; i < m; i++) {  
        a[i][0] = 1;  
    }  
    for (int i = 0; i < n; i++) {  
        a[0][i] = 1;  
    }  
    for (int i = 1; i < m; i++) {  
        for (int j = 1; j < n; j++) {  
            a[i][j] = a[i-1][j] + a[i][j-1];  
        }  
    }  
    return a[m-1][n-1];  
}  

最小路径和

1 3 4 2
3 5 2 3
2 1 2 3
2 2 4 2

最优子结构?

最小路径和

1 3 4 2
3 5 2 3
2 1 2 3
2 2 4 2

PathSum(m,n) = MIN(PathSum(m,n-1),PathSum(m-1,n)) + matrix(m,n)

最小路径和

public int minPathSum(int[][] grid) {
    if(grid == null || grid.length==0)
        return 0;
    int m = grid.length;
    int n = grid[0].length;
    int[][] dp = new int[m][n];
    dp[0][0] = grid[0][0];    
    for(int i=1; i<n; i++){
        dp[0][i] = dp[0][i-1] + grid[0][i];
    }
    for(int j=1; j<m; j++){
        dp[j][0] = dp[j-1][0] + grid[j][0];
    }
    for(int i=1; i<m; i++){
        for(int j=1; j<n; j++){
            if(dp[i-1][j] > dp[i][j-1]){
                dp[i][j] = dp[i][j-1] + grid[i][j];
            }else{
                dp[i][j] = dp[i-1][j] + grid[i][j];
            }
        }
    }
    return dp[m-1][n-1];
}

最小路径和

如何改进?

我们可以用更少的空间去解决问题同时不影响时间复杂度

最小路径和 II

public static int minPathSum(int[][] grid) {
    if(grid == null || grid.length==0)
        return 0;
    int m = grid.length;
    int n = grid[0].length;
     int[] newline = new int[n];
     int[] oldline = new int[n];
     oldline[0] = grid[0][0];    
     for(int i=1; i<n; i++){
    	 oldline[i] = oldline[i-1] + grid[0][i];
     }
     for(int i=1; i<m; i++){
    	 newline[0] = grid[i][0] + oldline[0];
         for(int j=1; j<n; j++){
             if(oldline[j] > newline[j-1]){
                 newline[j] = newline[j-1] + grid[i][j];
             }else{
                 newline[j] = oldline[j] + grid[i][j];
             }
         }
         oldline = newline;
     }
     return newline[n-1];
}

最小路径和 II

我们可以只用一个数组

0-1背包 (0-1 Knapsack)

有一个称重量为w磅的背包, 还有一些物品重量分别为w1, w2, ... wn. 每个物品的价值分别为s1,s2,...,sn. 请尝试选择物品使背包可以放下并且使的价值最大.

这个问题的最优子结构是什么?

0-1背包 (0-1 Knapsack)

w[i][j]: 表示前i个物品中, 在容量为j时的最大价值

我们应该比较哪两种情况?

w[i][j]: 表示前i个物品中, 在容量为j时的最大价值

当我们遍历i和j时, 我们应该尝试:

  1. 新的物品i是否可以加入到容量j中;
  2. 如果可以加入, 看结果是否可以更好, 即价值是否可以更大

 

0 1 2 3 4 5 6 7
0 0 0 0 0 0 0 0 0
1 0 3 3 3 3 3 3 3
2 0 3 3 8 11 11 11 11
3 0 3 3 8 11 11 11 12
4 0 3 3 8 11 11 11 12

w[i][j]: 表示前i个物品中, 在容量为j时的最大价值

例子: weights{1,3,4,5} values{3,8,4,7}

public int knapsack(int capacity, int[] weights, int[] values) {
	int length = weights.length;
	if (capacity == 0 || length == 0)
		return 0;
	int[][] w = new int[length + 1][capacity + 1];
	for (int i = 1; i <= length; i++) {
		int index = i - 1;
		for (int j = 1; j <= capacity; j++) {
			if (j < weights[index]) {
				w[i][j] = w[i - 1][j];
			} else if (w[i - 1][j - weights[index]] + values[index] > w[i - 1][j]) {
				w[i][j] = w[i - 1][j - weights[index]] + values[index];
			} else {
				w[i][j] = w[i - 1][j];
			}
		}
	}
	return w[length][capacity];
}

硬币找零(Coin Change)

这个问题用DFS或DFS都会超时(TLE)

这个问题和背包问题很相似

硬币种类和数量分别是问题的两个维度

我们应该如何更新结果呢?

public int coinChange(int[] coins, int amount) {
    Arrays.sort(coins);
    int length = coins.length;
    int[][] dp = new int[length][amount + 1];
    for (int j = 0; j <= amount; j ++) {
        if (j % coins[0] == 0) {
            dp[0][j] = j / coins[0];
        } else {
            dp[0][j] = -1;
        }
    }
    for (int i = 1; i < length; i ++) {
        for (int j = 0; j <= amount; j ++) {
            if (j < coins[i]) {
                dp[i][j] = dp[i-1][j];
            } else {
                int temp = Integer.MAX_VALUE;
                for (int k = 0; k <= j / coins[i]; k ++) {
                    int remaining = j - coins[i] * k;
                    if (dp[i-1][remaining] != -1 && dp[i-1][remaining] + k < temp) {
                        temp = dp[i-1][remaining] + k;
                    } 
                }
                dp[i][j] = temp < Integer.MAX_VALUE ? temp : -1;
            }
        }
    }
    return dp[length - 1][amount];
}

最长递增子序列(Longest Increasing Subsequence)

3, 1, 4, 5, 7, 6, 8, 2

1, 4, 5, 6, 8 (或 1, 4, 5, 7, 8)

最长递增子序列(Longest Increasing Subsequence)

这个问题的最优子结构是什么呢?

最长递增子序列(Longest Increasing Subsequence)

我们为不同的长度i, 存储lis[i]作为当前长度的最长递增子序列?

我们用 sequence[i]来存储当前长度i的最长递增子序列

最长递增子序列(Longest Increasing Subsequence)

public int longestIncreasingSubsequence(int[] nums) {
    if(nums.length == 0){
        return 0;
    }
    int[] lis = new int[nums.length];
    int max = 0;
    for (int i = 0; i < nums.length; i++){
        int localMax = 0;
        for (int j = 0; j < i; j++){
            if (lis[j] > localMax && nums[j] <= nums[i]){
                localMax = lis[j];
            }
        }
        lis[i] = localMax + 1;
        max = Math.max(max, lis[i]);
    }
    return max;
}

时间复杂度是多少?

空间复杂度是多少?

还可以再优化吗?

耐心排序

1, 3, 5, 2, 8, 4, 7, 6, 0, 9, 10

1 -> 0
1,3 -> 1,2
1,3,5 -> 1,3,4
1,3,5,8 -> 1,3,5,7 -> 1,3,5,6
1,3,5,6,9
1,3,5,6,9,10
public int longestIncreasingSubsequence(int[] nums) {
    if(nums.length == 0){
        return 0;
    }
    int len = 0;
    int[] tails = new int[nums.length];
    tails[0] = nums[0];
    for(int i = 1; i < nums.length; i++){
        if(nums[i] < tails[0]){
            tails[0] = nums[i];
        } else if (nums[i] >= tails[len]){
            tails[++len] = nums[i];
        } else {
            tails[binarySearch(tails, 0, len, nums[i])] = nums[i];
        }
    }
    return len + 1;
}
private int binarySearch(int[] tails, int min, int max, int target){
    while(min < max){
        int mid = min + (max - min) / 2;
        if(tails[mid] == target){
            return mid;
        }
        else if(tails[mid] < target){
            min = mid + 1;
        }
        else max = mid;
    }
    return min;
}

子长公共子序列(Longest Common Sequence)

例如: abcfbc abfcab

返回 4 (abcb)

最长递增子序列(Longest Increasing Subsequence)

这个问题的最优子结构是什么?

maxCommon(i,j): 表示String A(0,i) 和 String B(0,j)的最长公共子序列

最后需要得到的是 maxCommon(stringA.length, stringB.length)

最长递增子序列(Longest Increasing Subsequence)

maxCommon(i,j)和maxCommon(i-1,j-1)之间的关系是什么?

If(A[i-1] = B[j-1]) ?

If(A[i-1] != B[j-1])?

最长递增子序列(Longest Increasing Subsequence)

maxCommon(i,j)和maxCommon(i-1,j-1)之间的关系是什么?

If(A[i-1] = B[j-1]) ?

If(A[i-1] != B[j-1])?

maxCommon(i,j) = maxCommon(i-1,j-1) + 1

maxCommon(i,j) = max(maxCommon(i-1,j), maxCommon(i,j-1))

最长递增子序列(Longest Increasing Subsequence)

public static int longestCommonString(String a, String b) {
	int m = a.length();
	int n = b.length();
	int[][] maxCommon = new int[m+1][n+1];
	//for(int i = 0; i <= m; i ++) {
	//	maxCommon[i][0] = 0;
	//}
	//for(int j = 0; j <= n; j ++) {
	//	maxCommon[0][j] = 0;
	//}
	for(int i = 1; i <= m; i ++) {
		for(int j = 1; j <= n; j ++) {
		  if(a.charAt(i-1) == b.charAt(j-1)) {
		    maxCommon[i][j] = maxCommon[i-1][j-1] + 1;
		  }
		  else {
		    maxCommon[i][j] = Math.max(maxCommon[i][j-1], maxCommon[i-1][j]);
		  }
		}
	}
	return maxCommon[m][n];
}

矩阵乘法(Matrix Multiplication)

Matrix A m*n

Matrix B n*p

C = A * B 需要 m*n*p 次计算

A 100 * 10, B 10 * 100, C 100 * 5

D = A * B * C

如果计算(AB)C, 需要100*10*100 + 100*100*5 = 150000次计算

如果计算A(BC), need 100*10*5 + 10*100*5 = 10000次计算

给定A1,A2,.....,An共n个矩阵, 找出总的计算次数最少的方式

input: 一个数组P共有n+1个数字p0, p1...pn

其中A1 = p0*p1, An = Pn-1*Pn

矩阵乘法(Matrix Multiplication)

这个问题的最优子结构是什么?

对于矩阵乘法A1*...*An, 如果我们在Ak出分开:

计算会变为(A1*...Ak)(Ak+1*...An)

T(1,n) = T(1,k) + T(k+1,n) + p0*pk*pn

所以如果T(1,n)是最好的, 那么T(1,k)和T(k+1,n)必须各自是最好的.

矩阵乘法(Matrix Multiplication)

我们如何得到T(1,k)?

我们需要从链式长度为1的乘法一直到链式长度为n的乘法, 不断地更新得到结果

矩阵乘法(Matrix Multiplication)

public static int MatrixChain(int[] p)  
{  
    int n = p.length;
    n --;
    int[][] m = new int[n][n];
    for(int i = 0; i < n; i++)  
        m[i][i] = 0; 
    for(int r = 2; r <= n; r++)  
    { 
        for(int i = 0; i < n - r + 1; i ++)  
        { 
            int j = i + r - 1; 
            m[i][j] = m[i + 1][j]  + p[i] * p[i+1] * p[j + 1];  
            for(int k = i + 1; k < j; k++)  
            {  
                int t = m[i][k] + m[k + 1][j] + p[i] * p[k+1] * p[j+1];  
                if( t < m[i][j])  
                    m[i][j] = t;  
            }  
        }  
    }
    return m[0][n-1];
}

链的长度

链的开始

作业

作业 (可选的)