动态规划初步

1、动态规划(Dynamic Programming)

BaseOn : 代码随想录

​ 动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的,

动态规划五部曲:

  1. 确定dp数组(dp table)以及下标的含义
  2. 确定递推公式
  3. dp数组如何初始化
  4. 确定遍历顺序
  5. 举例推导dp数组

2.1 斐波那契数列

509. 斐波那契数

暴力递归:

class Solution {
    public int fib(int n) {
        if(n <= 1) return n;
        return fib(n-1)+ fib(n-2);
    }
}

dp:

  1. dp[i]表示第i个斐波那契值
  2. dp[i] = dp[i-1] + dp[i-2]
  3. dp[0] = 0,dp[1] = 1
  4. 从左往右
  5. image-20211114171007925
class Solution {
    public int fib(int n) {
        if(n <= 1) return n;
        int[] dp = new int[n+1];
        dp[0] = 0;
        dp[1] = 1;
        for(int i = 2;i <= n;i++) dp[i] = dp[i-1] + dp[i-2];
        return dp[n];
    }
}

可以只维护两个数

class Solution {
    public int fib(int n) {
        if(n <= 1) return n;
        int[] dp = new int[2];
        dp[0] = 0;
        dp[1] = 1;
        for(int i = 2;i <= n;i++) {
            int sum = dp[0]+dp[1];
            dp[0] = dp[1];
            dp[1] = sum;
        }
        return dp[1];
    }
}

2.2 爬楼梯

70. 爬楼梯

dp:

  1. 爬到第i层楼梯,有dp[i]种方法
  2. dp[i] = dp[i-1] + dp[i-2] 从i-1上来和从i-2上来
  3. dp[1] = 1,dp[2] = 2
  4. 从左往右
  5. image-20211114203042738
class Solution {
    public int climbStairs(int n) {
        if(n <=2) return n;
        int[] dp = new int[n+1];
        dp[1] = 1;
        dp[2] = 2;
        for(int i = 3;i <= n;i++) dp[i] = dp[i-1] + dp[i-2]; 
        return dp[n];
    }
}

2.3 使用最小花费爬楼梯

746. 使用最小花费爬楼梯

dp:

  1. 爬到第i层,最小花费
  2. dp[i] = min(dp[i-1],dp[i-2]) + cost[i]
  3. dp[0] = cost[0],dp[1] = cost[1]
  4. 从左往右
  5. image-20211114204346293
class Solution {
    public int minCostClimbingStairs(int[] cost) {
        int n = cost.length;
        int[] dp = new int[n+1];
        dp[0] = cost[0];
        dp[1] = cost[1];
        for(int i = 2;i < n;i++) dp[i] = Math.min(dp[i-1],dp[i-2]) + cost[i];
        return Math.min(dp[n-1],dp[n-2]);
    }
}

2.4 不同路径

62. 不同路径

dp:

  1. dp[i] [j],从起点到 (i,j) 有多少种方案
  2. dp[i] [j] = dp[i-1] [j] + dp[i] [j-1]
  3. dp[0] [0] = 0 dp[01] = 1 dp[1 0] = 1
  4. 从左上到右下
  5. image-20211114210351356
class Solution {
    public int uniquePaths(int m, int n) {
        // dp[i][j] = dp[i-1][j]+dp[i][j-1]
        int[][] dp = new int[m][n];
        dp[0][0] = 0;
        // 最左一列
        for(int i = 0;i < m;i++) dp[i][0] = 1;
        // 最上一行
        for(int j = 0;j < n;j++) dp[0][j] = 1;
        for(int i = 1;i < m;i++){
            for(int j = 1;j < n;j++) dp[i][j] = dp[i-1][j] + dp[i][j-1];
        }
        return dp[m-1][n-1];
    }
}

爆搜:

class Solution {
    public int uniquePaths(int m, int n) {
        return dfs(1,1,m,n);
    }
    private int dfs(int i,int j,int m,int n){
        if(i == m && j == n) return 1;
        // 越界
        else if(i > m || j > n) return 0;
        return dfs(i+1,j,m,n) + dfs(i,j+1,m,n);
    }
}

数论:

63. 不同路径 II

dp:

  1. dp[i] [j] 从起点到(i,j)有多少种方案
  2. if 不是障碍物 dp[i] [j] = dp[i] [j-1] + dp[i-1] [j]
  3. dp[0] [0] = 0 出现障碍物前 dp[i,0] = 1,dp[0,j] = 1,之后为0
  4. 从左上到右下,即一行一行的遍历,因为只会用到自己上面一行和左边的数字
  5. [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MtI6QvCe-1637219477435)(https://i.loli.net/2021/11/15/RDY8QkEIqC2gzSB.png)]
class Solution {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        int n = obstacleGrid.length;int m = obstacleGrid[0].length;
        int[][] dp = new int[n][m];
        // dp[i][j] = dp[i-1][j] + dp[i][j-1]
        // 初始化
        // 最左一列和最右一行有一个出现了障碍物,那么之后的都为0
        for(int i = 0;i < n && obstacleGrid[i][0] != 1;i++) dp[i][0] = 1;
        for(int j = 0;j < m && obstacleGrid[0][j] != 1;j++) dp[0][j] = 1;
        // 当前不是障碍物就正常处理,否则为0
        for(int i = 1;i < n;i++){
            for(int j = 1;j < m;j++){
                if(obstacleGrid[i][j] != 1) dp[i][j] = dp[i-1][j] + dp[i][j-1];
            }
        }
        return dp[n-1][m-1];
    }
}

343.整数拆分

343. 整数拆分

dp:

  1. dp[i],将数值i拆分,得到的最大乘积
  2. dp[i] = max( dp[i] ,max( dp[j-i] * j , j * ( j - i ) ))
  3. dp[2] = 1
  4. 从左往右
  5. image-20211115163648955
class Solution {
    public int integerBreak(int n) {
        // dp[i] 拆分数字i,可以得到最大乘机为dp[i]
        // j*(i-j) j*dp[i-j]    j*(i-j) 是将整数拆分成两个数 j*dp[i-j]是拆分成两个及两个以上
        // dp[i] = max(dp[i],max(j*dp[i-j],j*(i-j)))
        //  dp[2] = 1
        int[] dp = new int[n+1];
        dp[2] = 1;
        for(int i = 3;i <= n;i++){
            for(int j = 1;j < i ;j++){
                dp[i] = Math.max(dp[i],Math.max(j*dp[i-j],j*(i-j)));
            }
        }
        return dp[n];
    }
}

96.不同的二叉搜索树

96. 不同的二叉搜索树
class Solution {
    public int numTrees(int n) {
        // dp[i] 有i个节点组成,且1到i互不相同的二叉搜索树的个数
        // dp[i] += dp[j-1]*dp[i-j] 
        // dp[3] = 元素1为头结点搜索树的数量 + 元素2为头结点搜索树的数量 + 元素3为头结点搜索树的数量
        // 元素1为头结点搜索树的数量 = 左子树0个元素,右子树2个元素
        // 元素2为头结点搜索树的数量 = 左子树1个元素,右子树1个元素
        // 元素3为头结点搜索树的数量 = 左子树2个元素,右子树0个元素  左小右大
        // dp[2] = 2个节点组成,且互补相同
        // dp[1] = 1个节点组成,且互不相同
        // dp[3] = dp[0]*dp[2] + dp[1]*dp[1] + dp[2]*dp[0]
        int[] dp = new int[n+1];
        dp[0] = 1;
        dp[1] = 1;
        for(int i = 2;i <= n;i++){
            // 第i个节点需要考虑从1~i作为根节点的情况
            // 一共i个节点,j为首节点,左边有j-1个节点,右边有i-j个节点
            for(int j = 1;j <= i;j++) dp[i] += dp[j-1]*dp[i-j];
        }
        return dp[n];
    }
}

0-1背包问题

​ 有N件物品和一个最多能背重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i]每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ygU60Oyw-1637219477437)(https://i.loli.net/2021/11/16/DCxmIl3oZdbTQt5.jpg)]

0-1背包问题

二维:

  1. dp[i j] 从0~i 中任选、背包容量为j,最大价值
  2. dp[i j] = max(dp[ i-1 , j ],dp[i - 1 , j - weight[i] ] + value[i])
  3. dp[i 0] 容量为0的都为0,还有最上面一行,能放下第一个物品后的价值都为value[1]
  4. 由左上推出右下

一维:

  1. dp[j] 容量为 j ,最大价值
  2. dp[ j ] = max(dp[ j ] , dp[ j - weight[i] ] + value[i])
  3. dp[0] = 0
  4. 滚动数组,右边的从上一伦的左边推出来
#include <bits/stdc++.h>

using namespace std;

const int N = 1e3 + 10;

// 重量、价值
int weight[N],value[N];
// 个数,容量
int n,bag;

void m1();
void m2();

int main(){
    scanf("%d%d",&n,&bag);
    for(int i = 0;i < n;i++) scanf("%d%d",&weight[i],&value[i]);
    // m1();
    m2();
}

void m2(){
    // dp[j] 容量为j的背包,所背的物品价值可以最大为dp[j]。
    vector<int> dp(bag+1,0);
    // dp[j] = max(dp[j],dp[j-weight[i]]+value[i])
    // dp[j] 不取物品 dp[j-weight[i]+value[i] 取第i件物品
    // 初始化
    // dp[0] = 0
    dp[0] = 0;
    for(int i = 0;i < n;i++){
        // 为什么是倒叙?
        // 因为一维dp是滚动数组,其实就是将i-1行复制到第i行
        // 第i行依赖于第i-1行的正上方或者是左上方,如果
        // 左边更新了,会影响到右边
        // 对于每次新的一行,更新右边的值,其实是用上一行的左边的值
        // j 要大于 weight[i] 要不然就是背包容量小于 物品的重量了
        for(int j = bag;j >= weight[i];j--) dp[j] = max(dp[j],dp[j-weight[i]] + value[i]);
    }
    cout << dp[bag] << endl;
    // for(int i = 0;i <= bag;i++) cout << dp[i] << " ";
}

void m1(){
    // dp[i][j] 在0~i件物品中任选,背包容量为j,dp[i][j]是最大价值
    // dp[i][j] = max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i])
    // dp[i-1][j]表示不要第i件物品,dp[i-1][j-weight[j]]+value[i]表示要第i件物品
    vector<vector<int>> dp(n,vector<int>(bag+1,0));
    dp[0][0] = 0;
    // 最右边一列是0
    // for(int i = 0;i < n;i++) dp[i][0] = 0;
    // 最上面一列只有能装下第一件物品才有价值
    for(int j = weight[0];j <= bag;j++) dp[0][j] = value[0];
    
    for(int i = 1;i < n;i++){       // 遍历物品,第0件不用遍历了
        for(int j = 0;j <= bag;j++){
            if(j < weight[i]) dp[i][j] = dp[i-1][j];
            else dp[i][j] = max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]);
        }
    }
    cout << dp[n-1][bag] << endl;
}
416. 分割等和子集
  1. dp[i] 容量为i,能放下的最大价值为dp[i]
  2. dp[ i ] = max(dp[i],dp[i-weight[i]]+value[i])
  3. dp[0] = 0
  4. 滚动数组,右边的从上一伦的左边推出来
class Solution {
    public boolean canPartition(int[] nums) {
        // 背包体积为sum/2
        // 背包的商品的重量是商品的数值、价值也是元素的数值
        // 可以分割表示背包刚好装满

        // dp[i] 容量为i,最大凑成的i的子集
        // dp[i] = max(dp[i],dp[i-nums[i]]+nums[i])
        int n = nums.length,target;
        int sum = 0;
        for(int i = 0;i < n;i++) sum += nums[i];
        // sum不是偶数,凑不出来
        if(sum % 2 != 0) return false;
        target = sum / 2;
        // 容量为target
        int[] dp = new int[target+1];
        dp[0] = 0;
        for(int i = 0;i < n;i++){
            for(int j = target;j >= nums[i];j--) dp[j] = Math.max(dp[j],dp[j-nums[i]]+nums[i]);
        }
        // 背包容量是sum/2,并且最大能装sum/2
        // dp[i] <= i 永远成立
        // i是容量 dp[i]是容量为i的时候,能装进去的最大值
        // 物品的重量 = 物品的质量 = 物品的数值
        return dp[target] == target;
    }
}

第二次写总结:

class Solution {
    public boolean canPartition(int[] nums) {
        int sum = 0,n = nums.length,target;
        for(int i = 0;i < n;i++) sum += nums[i];
        if(sum % 2 != 0) return false;
        else target = sum / 2;
        // 现在就是找nums中能否组合出一个和为target的组合
        // 并且每个元素只能用一次
        // dp[j] 容量为j,凑成最大的字节和
        // dp[j] = max(dp[j],dp[j-nums[i]]+nums[i])
        // 很类似以0-1背包问题,就是一堆物品,一个容量,看最多能装的多大价值的东西
        // 只是这里的重量和价值是一样的而已
        int[] dp = new int[target+1];
        for(int i = 0;i < n;i++){       // 遍历物品
            for(int j = target;j >= nums[i];j--) dp[j] = Math.max(dp[j],dp[j-nums[i]]+nums[i]);
        }
        // dp[target] 容量为 target,看最大能不能凑出 target
        // 不能的话就没希望了,因为dp[j] <= j
        return dp[target] == target;
    }
}
1049. 最后一块石头的重量 II
class Solution {
    public int lastStoneWeightII(int[] stones) {
        // 本题其实就是将石头分成两份最接近的,然后剩下的就是答案
        // dp[i] 容量为i,最多能装dp[i]重量的石头
        // 石头共重sum,那么一半是target
        int n = stones.length,sum = 0,target;
        for(int i = 0;i < n;i++) sum += stones[i];
        target = sum / 2;
        // target是sum/2向下取整
        // 背包容量是石头总重的一半,看最大能装多重
        int[] dp = new int[target+1];
        dp[0] = 0;
        for(int i = 0;i < n;i++){
            for(int j = target;j >= stones[i];j--) dp[j] = Math.max(dp[j],dp[j-stones[i]]+stones[i]);
        }
        // 此时一堆石头是 dp[target],另外一堆是 sum - dp[targer]
        // dp[target] <= target 所以 sum - dp[targer] >= dp[target]
        return sum - dp[target] - dp[target];
    }
}
494. 目标和
class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        // nums 可以分为两部分、一部分做正数 a、一部分做负数 b(b还是一个正数,前面整体加一个符号就是负数了)
        // target = a - b
        // sum = a + b -> b = sum - a
        // target = a - sum + a -> a = (target + sum ) / 2;
        // a 是 nums中的一部分数之和
        // 能在 nums中找到a,那么b自然就能找到
        // 使用0-1背包来思考,dp[a],填满a这么大容积的背包,有dp[a]种方法
        int n = nums.length, sum = 0;
        for(int i = 0;i < n;i++) sum += nums[i];
        // dp[i] 填满i这么大容积的背包、由dp[i]种方法
        // dp[j] += dp[j-num[i]]
        // 搞满容量为3的背包,有dp[3]种方法
        // 如果 num[i] = 2,那么至少dp[5]有dp[3]种
        // dp[0] = 1 装满0,只有一种,装0

        // target比全是正数还大
        if(Math.abs(target) > sum) return 0;
        // 按照上面的推倒、a应该是偶数
        if((target + sum) % 2 == 1) return 0;
        int a = (target + sum) / 2;
        int[] dp = new int[a+1];
        for(int i = 0;i < n;i++){
            for(int j = a;j >= nums[i];j--) dp[j] += dp[j-nums[i]];
        }
        return dp[a];
        // 在求装满背包有几种方法的情况下,递推公式一般为:
        // dp[j] += dp[j - nums[i]];
    }
}

我感觉这几题都在做一件事:从一个序列里面找和为target的、可能是能否为target、可能是为target有多少种。

474. 一和零

重量有两个维度的0-1背包问题

class Solution {
    public int findMaxForm(String[] strs, int m, int n) {
        // dp[i][j] 最多有i个0,j个1的strs的最大子集的大小为dp[i][j]
        // dp[i][j] = max(dp[i][j],dp[i-zeroNum][j-oneNum] + 1);
        // dp[0][0] = 0
        // 这个其实不是二维dp,是物品的重量有两个维度,[i][j]一起构成j
        // 字符串的zeroNum和oneNum相当于物品的重量(weight[i]),字符串本身的个数相当于物品的价值(value[i])
        int[][] dp = new int[m+1][n+1];
        for(String str : strs){         // 遍历物品
            int zeroNum = 0,oneNum = 0;
            for(int i = 0;i < str.length();i++){
                if(str.charAt(i) == '0') zeroNum++;
                else oneNum++;
            }
            for(int i = m;i >= zeroNum;i--){
                for(int j = n;j >= oneNum;j--) dp[i][j] = Math.max(dp[i][j],dp[i-zeroNum][j-oneNum]+1);
            }
        }
        return dp[m][n];
    }
}

完全背包问题

有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。

完全背包和01背包问题唯一不同的地方就是,每种物品有无限件

01背包

for(int i = 0;i < n;i++){	// 遍历商品个数
    // 反向遍历
    for(int j = bag;j >= weight[i];j--) dp[j] = max(dp[j],dp[j-weight[i]]+value[i]);
}

完全背包

for(int i = 0;i < n;i++){	// 遍历商品个数
    // 正向遍历
    for(int j = 0;j <= weight[i];j++) dp[j] = max(dp[j],dp[j-weight[i]]+value[i]);
}
#include <bits/stdc++.h>

using namespace std;

const int N = 1e3 + 10;

// 重量、价值
int weight[N],value[N];
// 个数,容量
int n,bag;

void m1();
void m2();

int main(){
    scanf("%d%d",&n,&bag);
    for(int i = 0;i < n;i++) scanf("%d%d",&weight[i],&value[i]);
    // m1();
    m2();
}

void m2(){
    // dp[j] 容量为j,dp[j]的最大价值
    // dp[j] = max(dp[j],dp[j-weight[i]]+value[i])
    // dp[0] = 0
    vector<int> dp(bag+1,0);
    dp[0] = 0;
    for(int i = 0;i < n;i++){
        for(int j = weight[i];j <= bag ;j++) dp[j] = max(dp[j],dp[j-weight[i]]+value[i]);
    }
    cout << dp[bag] << endl;
}


518. 零钱兑换 II
  1. dp[j] 凑出j面额的组合个数为dp[j]
  2. 之前0-1背包中有一个也是求组合个数 dp[j] += dp[j-coins[i]]
  3. dp[0] = 0
  4. 完全背包方式遍历
class Solution {
    public int change(int amount, int[] coins) {
        // dp[j] 凑出j面额的组合数个数
        // dp[j] += dp[j-coins[i]]
        // dp[0] = 1;
        // 完全背包
        int[] dp = new int[amount+1];
        dp[0] = 1;
        for(int i = 0;i < coins.length;i++){								// 遍历物品
            for(int j = coins[i]; j <= amount;j++) dp[j] += dp[j-coins[i]];	// 遍历背包
        }
        return dp[amount];
    }
}
377. 组合总和 Ⅳ
class Solution {
    public int combinationSum4(int[] nums, int target) {
        // dp[j] 组合成j的组合个数
        // dp[j] += dp[j-nums[i]]
        // dp[0] = 0
        // 完全背包
        int[] dp = new int[target+1];
        dp[0] = 1;
        for(int i = 0;i <= target;i++){         			// 遍历背包
            for(int j = 0;j < nums.length;j++){				// 遍历物品
                if(nums[j] <= i) dp[i] += dp[i-nums[j]];	// 能装的下
            }
        }
        return dp[target];
    }
}

如果求组合数就是外层for循环遍历物品,内层for遍历背包

如果求排列数就是外层for遍历背包,内层for循环遍历物品

70. 爬楼梯

难度简单2006收藏分享切换为英文接收动态反馈

改为:一步一个台阶,两个台阶,三个台阶,…,直到 m个台阶。问有多少种不同的方法可以爬到楼顶呢?

class Solution {
    public int climbStairs(int n) {
        // dp[j] 跳到j层有多少种方式
        // dp[j] += dp[j-num[i]]
        // dp[1] = 1 dp[2] = 2
        // 完全背包,并且是排列,因为(1,2)(2,1)是不同的
        int[] dp = new int[n+1];
        if(n <= 2) return n;
        dp[1] = 1;
        dp[2] = 2;
        for(int i = 3;i <= n;i++){                  // 遍历重量
            for(int j = 1;j <= m;j++) {             // 遍历物品
                if(i >= j) dp[i] += dp[i-j];        
            }
        }
        return dp[n];
    }
}
279. 完全平方数
class Solution {
    public int numSquares(int n) {
        // dp[j] 和为j的完全平方数最少的个数
        // dp[j] = min(dp[j],dp[j-i*i]+1)
        // dp[0] = 
        // 完全背包,因为完全平方数可以随意使用
        int[] dp = new int[n+1];
        for(int j = 1;j <= n;j++) dp[j] = Integer.MAX_VALUE;
        // 物品就是完全平方数
        for(int i = 1; i*i <= n;i++){           // 遍历物品
            for(int j = 1;j <= n;j++){          // 遍历背包容量
                if(i * i <= j) dp[j] = Math.min(dp[j],dp[j-i*i]+1);
            }
        }
        return dp[n];
    }
}
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值