【算法训练营Day27】动态规划part3

文章目录

完全背包理论基础

完全背包与01背包的唯一区别就在于完全背包的每件物品有无数个,可以放入背包多次。

我们从dp四部曲来分析一下完全背包问题:

  • dp数组及其下标:dp[i][j]表示物品0 ~ i可多次放入到容量为j的背包中最大价值是多少?
  • 递推关系 :我们从01背包的递推对比来看:
    • 01背包的递推关系:关键点在于物品只能用一次,所以如果要放物品,那么肯定是没有该物品,然后把该物品的位置空出来再进行推导。所以dp[i][j]是两种情况取较大值:
      • 放物品i:dp[i - 1][j - weight[i]] + value[i]
      • 不放物品i:dp[i - 1][j]
    • 完全背包的递推关系:关键点在于物品可以使用多次,所以如果要放物品,此时是可以有该物品的,只需要把物品的位置空出来即可。所以dp[i][j]是两种情况取较大值:
      • 放物品i:dp[i][j - weight[i]] + value[i]
      • 不放物品i:dp[i - 1][j]
  • 初始化:根据推导公式以及dp含义,我们可以将容量为0的那一列先初始化为0,注意到递推式中dp[i][j]依赖左边以及上边,所以我们一定要初始化第一行,也就是只放物体0的情况。因为可以多次放物品0,所以dp[0][j] = dp[0][j - weight[0]] + value[0]
  • 遍历顺序:在01背包中是从后往前,这是因为一个物品只能取一次,而在完全背包中是从前往后,因为每个物品可以取多次。

完全背包典例

题目链接:52. 携带研究材料

java 复制代码
import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int n = in.nextInt();
        int bag = in.nextInt();
        int[] weight = new int[n];
        int[] values = new int[n];
        for(int i = 0;i < n;i++) {
            weight[i] = in.nextInt();
            values[i] = in.nextInt();
        }
        int[][] dp = new int[n][bag + 1];
        //初始化
        for(int j = 1;j <= bag;j++) if(j - weight[0] >= 0) dp[0][j] = dp[0][j - weight[0]] + values[0];
        //遍历
        for(int i = 1;i < n;i++) {
            for(int j = 1;j <= bag;j++) {
                if(j - weight[i] >= 0) dp[i][j] = Math.max(dp[i][j - weight[i]] + values[i],dp[i - 1][j]);
                else dp[i][j] = dp[i - 1][j];
            }
        }
        System.out.println(dp[n - 1][bag]);
    }
}

零钱兑换II

题目链接:518. 零钱兑换 II

解题逻辑:

这个题就类似于我们在01背包中做过的,把背包装满有多少种情况。所以他的递推式是相加,而不是取较大值。

dp的四部曲分析和上面的典例基本一样,就不在此赘述。

java 复制代码
class Solution {
    public int change(int amount, int[] coins) {
        int n = coins.length;
        int[][] dp = new int[n][amount + 1];
        //初始化
        for(int i = 0;i < n;i++) dp[i][0] = 1;
        for(int j = 1;j <= amount;j++) if(j - coins[0] >= 0) dp[0][j] = dp[0][j - coins[0]];
        //遍历
        for(int i = 1;i < n;i++) {
            for(int j = 1;j <= amount;j++) {
                if(j - coins[i] >= 0) dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i]];
                else dp[i][j] = dp[i - 1][j];
            }
        }
        return dp[n - 1][amount];
    }
}

一维dp数组:

java 复制代码
class Solution {
    public int change(int amount, int[] coins) {
        int[] dp = new int[amount + 1];
        //初始化
        dp[0] = 1;
        //遍历
        for(int i = 0;i < coins.length;i++) {
            for(int j = 1;j <= amount;j++) {
                if(j - coins[i] >= 0) dp[j] = dp[j] + dp[j - coins[i]];
            }
        }
        return dp[amount];
    }
}

组合总和 Ⅳ

题目链接:377. 组合总和 Ⅳ

这一题和上面的题很相似,也是求把背包塞满有多少种情况。区别在于上一题是求组合数(也就是不考虑顺序),而本题是求排列数(也就是要考虑顺序),两者的处理区别在于:

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

为什么?

  • 先遍历物品,再遍历背包。相当于固定了物品之后,对全容量进行递推,每次外循环新增一个后面的元素,所以dp数组里存的是组合的情况。
  • 先遍历背包,再遍历物品。相当于固定了容量,对物品进行递推,当前容量可以由任意物品组成,进入下一轮容量固定之后,其依赖上一轮的结果,并且此轮也可以由任意物品组成,所以dp数组里面存的是排列的情况。

解题代码如下:

java 复制代码
class Solution {
    public int combinationSum4(int[] nums, int target) {
        int n = nums.length;
        int[] dp = new int[target + 1];
        //递推式:一维dp :dp[j] = dp[j] + dp[j - nums[i]]
        //初始化
        dp[0] = 1;
        //遍历
        for(int j = 1;j <= target;j++) {
            for(int i = 0;i < n;i++) {
                if(j - nums[i] >= 0) dp[j] = dp[j] + dp[j - nums[i]];
            }
        }
        return dp[target];
    }
}

爬楼梯(进阶版)

题目链接:57. 爬楼梯

解题思路同上。

解题代码:

java 复制代码
import java.util.*;

public class Main{
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int bag = in.nextInt();
        int m = in.nextInt();
        int[] dp = new int[bag + 1];
        //递推 dp[j] = dp[j] + dp[j - value[i]]
        dp[0] = 1;
        for(int j = 1;j <= bag;j++) {
            for(int i = 0;i < m;i++) {
                if(j >= i + 1) {
                    dp[j] = dp[j] + dp[j - (i + 1)];
                }
                
            }
        }
        System.out.println(dp[bag]);
    }
}

零钱兑换

题目链接:322. 零钱兑换

解题逻辑:

从dp四部曲分析:

  • dp数组及下标含义:dp[j]表示装满容量为j的背包最少的硬币个数
  • 递推式:dp[i][j] = min(dp[i - 1][j],dp[i][j - values[i]] + 1),简化为一维dp,则dp[j] = min(dp[j],dp[j - values[i]] + 1)
  • 初始化:dp[0] = 0,其余全部初始化为Integer.MAX_VALUE - 1,减1是因为防止递推的时候 + 1导致变为负数从而取最小的时候误取。
  • 遍历顺序:先遍历物品,再遍历背包,使用物品的组合可以解决问题。

代码如下:

java 复制代码
class Solution {
    public int coinChange(int[] coins, int amount) {
        int n = coins.length;
        int[] dp = new int[amount + 1];
        //初始化
        for(int i = 1;i <= amount;i++) dp[i] = Integer.MAX_VALUE - 1;
        //遍历
        for(int i = 0;i < n;i++) {
            for(int j = coins[i];j <= amount;j++) {
                dp[j] = Math.min(dp[j],dp[j - coins[i]] + 1);
            }
        }
        return dp[amount] == Integer.MAX_VALUE - 1 || dp[amount] < 0 ? -1 : dp[amount];
    }
}

或者当遍历到Integer.MAX_VALUE直接跳过,也就是如果取当前元素,那么dp[j - coins[i]]需要先被取到:

java 复制代码
class Solution {
    public int coinChange(int[] coins, int amount) {
        int n = coins.length;
        int[] dp = new int[amount + 1];
        //初始化
        for(int i = 1;i <= amount;i++) dp[i] = Integer.MAX_VALUE;
        //遍历
        for(int i = 0;i < n;i++) {
            for(int j = coins[i];j <= amount;j++) {
                if(dp[j - coins[i]] != Integer.MAX_VALUE) dp[j] = Math.min(dp[j],dp[j - coins[i]] + 1);
            }
        }
        return dp[amount] == Integer.MAX_VALUE ? -1 : dp[amount];
    }
}

完全平方数

题目链接:279. 完全平方数

解题逻辑和上一题一样,代码如下:

java 复制代码
class Solution {
    public int numSquares(int n) {
        int[] dp = new int[n + 1];
        for(int i = 1;i <= n;i++) dp[i] = Integer.MAX_VALUE;
        for(int i = 1;i <= 100;i++) {
            for(int j = i * i;j <= n;j++) {
                if(dp[j - i * i] != Integer.MAX_VALUE) dp[j] = Math.min(dp[j],dp[j - i * i] + 1);
            }
        }
        return dp[n];
    }
}

单词拆分

题目链接:139. 单词拆分

解题逻辑:

从dp四部曲分析:

  • dp数组及下标含义:dp[j]表示s的前j个元素能否被拼出
  • 递推式:也是分为取和不取两种情况。dp[j] = dp[j] || (wordDict.get(i).equals(s.substring(j - wordDict.get(i).length(),j)) && dp[j - wordDict.get(i).length()])
  • 初始化:dp[0] = true
  • 遍历顺序:先遍历背包,再遍历物品,使用物品的排列可以解决问题。

代码逻辑:

java 复制代码
class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        int n = wordDict.size();
        int bag = s.length();
        boolean[] dp = new boolean[bag + 1];
        //初始化
        dp[0] = true;
        for(int j = 1;j <= bag;j++) {
            for(int i = 0;i < n;i++) {
                if(wordDict.get(i).length() <= j) {
                    dp[j] = dp[j] || (wordDict.get(i).equals(s.substring(j - wordDict.get(i).length(),j)) && dp[j - wordDict.get(i).length()]);
                }
            }
        }
        return dp[bag];
    }
}

背包问题总结

关于(一维dp数组)常用的递推公式:

  • 问能否能装满背包(或者最多装多少):dp[j] = max(dp[j], dp[j - nums[i]] + nums[i])
  • 问装满背包有几种方法:dp[j] += dp[j - nums[i]]
  • 问背包装满最大价值:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
  • 问装满背包所有物品的最小个数:dp[j] = min(dp[j - coins[i]] + 1, dp[j])

关于遍历顺序:

  • 01背包
    • 二维dp数组01背包先遍历物品还是先遍历背包都是可以的,且第二层for循环是从小到大遍历。
    • 一维dp数组01背包只能先遍历物品再遍历背包容量,且第二层for循环是从大到小遍历
  • 完全背包
    • 一维dp数组实现,先遍历物品还是先遍历背包需要分情况,且第二层for循环是从小到大遍历。
    • 如果求组合数就是外层for循环遍历物品,内层for遍历背包。
    • 如果求排列数就是外层for遍历背包,内层for循环遍历物品。
相关推荐
炬火初现3 小时前
Hot100-哈希,双指针
算法·哈希算法·散列表
weixin_307779134 小时前
利用复变函数方法计算常见函数的傅里叶变换
算法
共享家95275 小时前
LeetCode热题100(1-7)
算法·leetcode·职场和发展
新学笺6 小时前
数据结构与算法 —— Java单链表从“0”到“1”
算法
同元软控6 小时前
首批CCF教学案例大赛资源上线:涵盖控制仿真、算法与机器人等9大方向
算法·机器人·工业软件·mworks
yiqiqukanhaiba6 小时前
Linux编程笔记2-控制&数组&指针&函数&动态内存&构造类型&Makefile
数据结构·算法·排序算法
PKNLP6 小时前
逻辑回归(Logistic Regression)
算法·机器学习·逻辑回归
可触的未来,发芽的智生7 小时前
新奇特:神经网络的自洁之道,学会出淤泥而不染
人工智能·python·神经网络·算法·架构
放羊郎7 小时前
SLAM算法分类对比
人工智能·算法·分类·数据挖掘·slam·视觉·激光