动态规划

动态规划

动态规划简介

每一个动态规划都是从一个网格开始的。

动态规划主要解决的问题是:求最值
       主要的核心思想是:穷举

动态规划特点
    1.重叠子问题
    2.状态转移方程
    3.最优子结构
    
解题的思路:
    明确状态
    明确选择
    明确dp数组/函数的定义

  1. 线性dp
    1. 杨辉三角 leetcode 118题
    2. 杨辉三角II leetcode 119题
    3. 打家劫舍 leetcode 198题
    4. 打家劫舍II leetcode 213题
  2. 背包dp
    1. 零钱兑换 leetcode 322题
    2. 零钱兑换II
    3. 分割等和子集 leetcode 416题
    4. 一和零 leetcode 474题
    5. 盈利计划 leetcode 879题
  3. 序列dp
    1. 740
    2. 1143
    3. 368
    4. 354
  4. 动态规划 + 字符串
    1. 有效的字符串 leetcode 678题
    2. 正则表达式匹配 leetcode 10 题
    3. 通配符匹配 leetcode 44 题
    4. 不同的子序列 leetcode 115题

记忆化搜索

背包DP

01背包问题

动态规划之01背包问题

问题描述:给定 n 件物品,物品的重量为 w[i],物品的价值为 c[i]。现挑选物品放入背包中,假定背包能承受的最大重量为 V,问应该如何选择装入背包中的物品,使得装入背包中物品的总价值最大?

例题描述:

给定 3 件物品,物品的重量为 weight[]={1,3,1},对应的价值为 value[]={15,30,20}。现挑选物品放入背包中,假定背包能承受的最大重量 W 为 4,问应该如何选择装入背包中的物品,使得装入背包中物品的总价值最大?

设置一个dp[w] [n]数组,横坐标为背包的容量,纵坐标为物品的个数。

对于编号为 i 的物品:

  • 如果选择它,那么,当前背包的最大价值等于” i 号物品的价值“ 加上 ”减去 i 号物品占用的空间后剩余的背包空间所能存放的最大价值“,即dp [i] [k] = value[i] + dp[i-1] [k-weight[i]];
  • 如果不选择它,那么,当前背包的价值就等于前 i-1 个物品存放在背包中的最大价值,即 dp[i] [k] = dp[i-1] [k]

dp[i][k] 的结果取两者的较大值,即:

dp[i][k] = max(value[i] + dp[i-1][k-weight[i]], dp[i-1][k])
简单解法

代码实现如下:

// 横坐标是容量,纵坐标是几号物品。
public int maxValue(int[] weight, int[] value, int W) {
        int n = weight.length;
        if (n == 0) return 0;
        int[][] dp = new int[n][W + 1];
        // 先初始化第 0 行,也就是尝试把 0 号物品放入容量为 k 的背包中
        for (int k = 1; k <= W; k++) {
            if (k >= weight[0]) dp[0][k] = value[0];
            else dp[0][k] = 0;
        }

        for (int i = 1; i < n; i++) {
            for (int k = 1; k <= W; k++) {
                // 存放 i 号物品(前提是放得下这件物品)
                int valueWith_i = (k-weight[i] >= 0) ? (value[i] + dp[i-1][k-weight[i]]) : 0;
                // 不存放 i 号物品
                int valueWithout_i = dp[i-1][k];
                dp[i][k] = Math.max(valueWith_i, valueWithout_i);
            }
        }

        return dp[n-1][W];
    }
压缩空间

观察上面的代码,会发现,当更新dp[i] [..]时,只与dp[i-1] [..]有关,也就是说,我们没有必要使用O(n*W)的空间,而是只使用O(W)的空间即可。

    public int maxValue(int[] weight, int[] value, int W) {
        int n = weight.length;
        if (n == 0) return 0;

        int[] dp = new int[W + 1];
        for (int i = 0; i < n; i++) {
          //只要确保 k>=weight[i] 即可,而不是 k>=1,从而减少遍历的次数
            for (int k = W; k >= weight[i]; k--) {
                dp[k] = Math.max(dp[k - weight[i]] + value[i], dp[k]);
            }
        }
        return dp[W];
    }
训练试题

「力扣」第 416 题:分割等和子集(中等);
「力扣」第 474 题:一和零(中等);
「力扣」第 494 题:目标和(中等);
「力扣」第 879 题:盈利计划(困难);

完全背包问题

有n种物品,每种物品有无限个,每个物品的重量为weight[i],每个物品的价值为value[i]。现在有一个背包,它所能容纳的重量为total,问:当你面对这么多有价值的物品时,你的背包所能带走的最大价值是多少?

为什么不能使用贪心算法进行每次选取最高的价格的?

想要举反例很简单,比如只有两个物品:物品A:价值5,体积5,物品B:价值8:体积7,背包容量为10,物品B的性价比显然要比物品A高,那么用贪心算法必然会选择放入一个物品B,此时,剩余的空间已无法装下A或者B,所以得到的最高价值为8,而实际上,选择放入两个物品A即可得到更高的价值10。所以这里贪心算法并不适用。

状态转移方程

dp[i+1] [j] = Math.max(dp[i+1] [j], dp[i] [j-k * V[i]] + k * P[i]);

横坐标 j 是容量的大小,纵坐标 i 是物品的序号,dp[i] [j] 保存的是当前的最大价值。

 private int[][] dp = new int[P.length + 1][T + 1];
    public void solve() {
        for (int i = 0; i < P.length; i++){
            for (int j = 0; j <= T; j++){
                for (int k = 0; k * V[i] <= j; k++){
                    dp[i+1][j] = Math.max(dp[i+1][j], dp[i][j-k * V[i]] + k * P[i]);
                }
            }
        }
        System.out.println("最大价值为:" + dp[P.length][T]);
    }
}

状态压缩

private int[] newResults = new int[T+1];
private int ksp(int i, int t){
        // 开始填表
        for (int m = 0; m < i; m++){
            for (int n = V[m]; n <= t; n++){
                newResults[n] = Math.max(newResults[n] , newResults[n - V[m]] + P[m]);
            }
        }
        return newResults[newResults.length - 1];
    }

「力扣」上的 完全背包问题:

  • 「力扣」第 322 题:零钱兑换(中等);
  • 「力扣」第 518 题:零钱兑换 II(中等);
  • 「力扣」第 279 题:
  • 「力扣」第 1449 题:数位成本和为目标值的最大数字(困难)。

多重背包问题

有n种物品,每种物品有amount[i]个,每个物品的重量为weight[i],每个物品的价值为value[i]。现在有一个背包,它所能容纳的重量为total,问:当你面对这么多有价值的物品时,你的背包所能带走的最大价值是多少?

区间DP

树形DP

上一篇:0-1背包问题——动态规划,二维dp和一维dp


下一篇:创建类和对象