文章目录
完全背包
1. 完全背包(DP 42)
题目描述

解题思路
背包可以不装满
- 状态表示:dp[i][j]表示前i个物品中总体积不超过j,所有选法中最大价值
- 状态转移方程:对第 i 种物品只有两种大选择:
- 情况 1:不选第 i 件物品:
dp[i][j] = dp[i-1][j] - 情况 2:选至少 1 个第 i 件物品:既然已经选了 1 个,剩下空间
j-v[i]依然可以继续随便选第 i 件,等价于:在前 i 种物品、容量 j-v[i] 的最优方案基础上,再加一个 i 号物品dp[i][j] = dp[i][j-v[i]] + w[i] - 与01背包区别:
- 01背包:
dp[i][[j] = dp[i-1][j-v[i]]+w[i]; - 完全背包:
dp[i][j] = dp[i][j-v[i]]+w[i];
- 01背包:
- 以上两种情况取最大值
dp[i][j] = max(dp[i-1][j],dp[i][j-v[i]]+w[i]),要先判断j-v[i]是否存在
- 情况 1:不选第 i 件物品:
- 初始化:
- i = 0时,表示不选任何物品,体积不超过j的价值,全部都是0,初始化可以省略
- j = 0时,表示体积为0,最大价值也是0,初始化可以省略
背包必须装满
- 状态表示:dp[i][j] 表示在前i个物品中总体积为j的最大价值,如果找不到体积为j的方案,就令
dp[i][j] = -1 - 状态转移方程:
dp[i][j] = max(dp[i-1][j],dp[i][j-v[i]]+w[i]),要判断j-v[i]>0 也要判断dp[i][j-v[i]]>0,表示这种状态存在 - 初始化:
- i = 0:表示不选任何物品,可以凑出体积为j的最大价值,j = 0时,价值为0,j>0时没有方式,所以初始化为-1
- j = 0:表示在前i个物品中选出体积为0的最大价值,都是0,可以省略初始化
优化:利用滚动数组进行空间优化,删除第一维度
- dp[i][j]会使用到i-1行和当前行的j-v[i]列,因此空间优化要从左向右填表
- 01背包只用到上一行的数据,使用后覆盖,因此从右向左填表
代码实现
java
import java.util.Scanner;
// 注意类名必须为 Main, 不要有任何 package xxx 信息
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int n = in.nextInt();
int V = in.nextInt();
int[] v = new int[n];
int[] w = new int[n];
for(int i = 0;i<n;i++){
v[i] = in.nextInt();
w[i] = in.nextInt();
}
//方案一
int[][] dp1 = new int[n+1][V+1];
for(int i = 1;i<=n;i++){
for(int j = 1;j<=V;j++){
dp1[i][j] = dp1[i-1][j];
if(j-v[i-1]>=0)
dp1[i][j] = Math.max(dp1[i][j-v[i-1]]+w[i-1],dp1[i][j]);
}
}
System.out.println(dp1[n][V]);
//方案二
int[][] dp2 = new int[n+1][V+1];
for(int j = 1;j<=V;j++)
dp2[0][j] = -1;
for(int i = 1;i<=n;i++){
for(int j = 1;j<=V;j++){
dp2[i][j] = dp2[i-1][j];
if(j-v[i-1]>=0 && dp2[i][j-v[i-1]]>=0)
dp2[i][j] = Math.max(dp2[i][j-v[i-1]]+w[i-1],dp2[i][j]);
}
}
System.out.println(Math.max(0,dp2[n][V]));
}
}
- 空间优化:
java
import java.util.Scanner;
// 注意类名必须为 Main, 不要有任何 package xxx 信息
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int n = in.nextInt();
int V = in.nextInt();
int[] v = new int[n];
int[] w = new int[n];
for(int i = 0;i<n;i++){
v[i] = in.nextInt();
w[i] = in.nextInt();
}
//方案一
int[] dp1 = new int[V+1];
for(int i = 1;i<=n;i++){
for(int j = v[i-1];j<=V;j++){
dp1[j] = Math.max(dp1[j-v[i-1]]+w[i-1],dp1[j]);
}
}
System.out.println(dp1[V]);
//方案二
int[] dp2 = new int[V+1];
for(int j = 1;j<=V;j++)
dp2[j] = -1;
for(int i = 1;i<=n;i++){
for(int j = v[i-1];j<=V;j++){
if( dp2[j-v[i-1]]>=0)
dp2[j] = Math.max(dp2[j-v[i-1]]+w[i-1],dp2[j]);
}
}
System.out.println(Math.max(0,dp2[V]));
}
}
2. 零钱兑换(LC 322)
题目描述

解题思路
- 状态表示:前i个硬币中,总和为j的最小硬币个数
- 状态转移方程:
- 不选i:
dp[i][j] = dp[i-1][j] - 选i:
j>coins[i]且dp[i][j-coins[i]]存在,dp[i][j] = dp[i][j-coins[i]]+1 - 二者取最小值
- 不选i:
- 初始化:
- j = 0时 表示从前i个元=硬币中选出总和为0的硬币,个数为0,初始化可以省略
- i = 0时 表示不选任何硬币使总和达到j,不存在。因为状态转移方程中取最小值,索引要初始化为无穷大,而状态转移方程中要进行加法操作,MAX_VALUE会溢出,因此要初始化成
0X3f3f3f3f,足够大,不至于溢出
- 返回值:不存在的情况都初始化为0x3f3f3f3f,因此返回时要判断,如果大于这个数,说明不存在,返回-1;否则返回dp[n][amout]
代码实现
java
public int coinChange(int[] coins, int amount) {
int n = coins.length;
int[][] dp = new int[n+1][amount+1];
for(int j = 1;j<=amount;j++)
dp[0][j] = 0x3f3f3f3f;
for(int i = 1;i<=n;i++){
for(int j = 1;j<=amount;j++){
dp[i][j] = dp[i-1][j];
if(j>=coins[i-1])
dp[i][j] = Math.min(dp[i][j],dp[i][j-coins[i-1]]+1);
}
}
return dp[n][amount]>=0x3f3f3f3f?-1:dp[n][amount];
}
java
public int coinChange(int[] coins, int amount) {
int n = coins.length;
int[] dp = new int[amount+1];
for(int j = 1;j<=amount;j++)
dp[j] = 0x3f3f3f3f;
for(int i = 1;i<=n;i++){
for(int j = coins[i-1];j<=amount;j++){
dp[j] = Math.min(dp[j],dp[j-coins[i-1]]+1);
}
}
return dp[amount]>=0x3f3f3f3f?-1:dp[amount];
}
3. 零钱兑换II(LC 518)
题目描述

解题思路
- 状态表示:dp[i][j]表示在前i个硬币中选择总和为j时的硬币总和数
- 状态转移方程:
- 不选i:dp[i][j] = dp[i-1][j]
- 选i:j >= coins[i],dp[i][j] = dp[i][j-coins[i]]
- 二者相加
- 初始化:
- j = 0时,表示前i个硬币中总和为0的选法,就是都不选,有一种选法,初始化为1
- i = 0时,表示不选任何硬币总和为j,不存在,因此为0,省略初始化
代码实现
java
public int change(int amount, int[] coins) {
int n = coins.length;
int[][] dp = new int[n+1][amount+1];
for(int i = 0;i<=n;i++)
dp[i][0] = 1;
for(int i = 1;i<=n;i++){
for(int j = 1;j<=amount;j++){
dp[i][j] = dp[i-1][j];
if(j>=coins[i-1])
dp[i][j]+=dp[i][j-coins[i-1]];
}
}
return dp[n][amount];
}
- 空间优化
java
public int change(int amount, int[] coins) {
int n = coins.length;
int[] dp = new int[amount+1];
dp[0] = 1;
for(int i = 1;i<=n;i++){
for(int j = coins[i-1];j<=amount;j++){
dp[j]+=dp[j-coins[i-1]];
}
}
return dp[amount];
}
4. 完全平方数(LC 279)
题目描述

解题思路
本质上是完全背包问题
- 状态表示:dp[i][j]表示 从前i个完全平方数中选择总和为j的最少数量
- 状态转移方程:
- 不选i: dp[i][j] = dp[i-1][j]
- 选i:j>=ii时,dp[i][j] = dp[i][j-ii]+1;
- 二者取最小值
- 初始化:
- i = 0时,表示不选任何数,总和为j,不存在。因为要取最小值,还涉及到加法,为了防止溢出,初始化为0x3f3f3f3f
- 返回值:一定存在结果,不需要特殊判断
代码实现
java
public int numSquares(int v) {
int n = (int)Math.sqrt(v);
int[][] dp = new int[n+1][v+1];
for(int j = 1;j<=v;j++)
dp[0][j] = 0x3f3f3f3f;
for(int i = 1;i<=n;i++){
for(int j = 1;j<=v;j++){
dp[i][j] = dp[i-1][j];
if(j>=i*i)
dp[i][j] = Math.min(dp[i][j],dp[i][j-i*i]+1);
}
}
return dp[n][v];
}
- 空间优化
java
public int numSquares(int v) {
int n = (int)Math.sqrt(v);
int[] dp = new int[v+1];
for(int j = 1;j<=v;j++)
dp[j] = 0x3f3f3f3f;
for(int i = 1;i<=n;i++){
for(int j = 1;j<=v;j++){
dp[j] = dp[j];
if(j>=i*i)
dp[j] = Math.min(dp[j],dp[j-i*i]+1);
}
}
return dp[v];
}
二位费用的背包问题
二位费用的背包问题有两个限制条件,也分为01背包和完全背包
5. 一和零(LC 474)
题目描述

解题思路
每个元素只涉及选或不选的问题,因此这是01背包
- 状态表示:
dp[i][j][k]表示从前i个元素中挑选,字符0不超过j个,字符1不超过k个的所有选法中,最大长度 - 状态转移方程:
- 不选i:
dp[i][j][k] = dp[i-1][i][k]; - 选i: j>a 且 k > b的情况下
dp[i][j][k] = dp[i-1][j-a][k-b]+1
- 不选i:
- 初始化:i = 0 时,表示不选任何元素,0的个数不超过j,1的个数不超过k的最大长度,就是0,可以省略初始化
代码实现
java
public int findMaxForm(String[] strs, int m, int n) {
int len = strs.length;
int[][][] dp = new int[len+1][m+1][n+1];
for(int i = 1;i<=len;i++){
for(int j = 0;j<=m;j++){
for(int k = 0;k<=n;k++){
dp[i][j][k] = dp[i-1][j][k];
int[] tmp = count(strs[i-1]);
if(j>=tmp[0] && k>=tmp[1])
dp[i][j][k] = Math.max(dp[i][j][k],dp[i-1][j-tmp[0]][k-tmp[1]]+1);
}
}
}
return dp[len][m][n];
}
int[] count (String str){
int zero = 0;
int one = 0;
char[] ss = str.toCharArray();
for(char ch :ss){
if(ch=='0')
zero++;
else
one++;
}
return new int[]{zero,one};
}
- 空间优化
java
public int findMaxForm(String[] strs, int m, int n) {
int len = strs.length;
int[][] dp = new int[m+1][n+1];
for(int i = 1;i<=len;i++){
int[] tmp = count(strs[i-1]);
for(int j = m;j>=tmp[0];j--){
for(int k =n;k>=tmp[1];k--){
dp[j][k] = Math.max(dp[j][k],dp[j-tmp[0]][k-tmp[1]]+1);
}
}
}
return dp[m][n];
}
int[] count (String str){
int zero = 0;
int one = 0;
char[] ss = str.toCharArray();
for(char ch :ss){
if(ch=='0')
zero++;
else
one++;
}
return new int[]{zero,one};
}
6. 盈利计划(LC 879)
题目描述

翻译:有一些工作,从中挑选一部分形成子级,每一项工作都有对应的profit和group,要求这个自己的总共人数<=n,且总利润>=m
解题思路
每个工作只需要考虑选或不选,因此这是01背包问题
- 状态表示:dp[i][j][k]表示前i个计划中挑选,总人数不超过j,总利润至少为k,一共有多少种选法
- 状态转移方程:
- 不选i:dp[i][j][k] = dp[i-1][j][k]
- 选i:
j>=g[i],dp[i][j][k] = dp[i-1][j-g[i]][k-p[i]]k-p[i]可以小于0,也就是说,k<=p[i]的情况是成立的 ,但是在数组中,为了防止越界,不能访问负数下标,只能访问0下标。dp[i-1][j-g[i]][max(0,k-p[i])]
- 以上两种情况相加
- 初始化:当i=0时,表示不选任何任务,此时k=0,dp[0][j][0]表示不选任何任务,人数不超过j,盈利为0,有一种选法,要初始化为1
代码实现
java
public int profitableSchemes(int n, int m, int[] group, int[] profit) {
int mod =(int) 1e9+7;
int len = group.length;
int[][][] dp = new int[len+1][n+1][m+1];
for(int j = 0;j<=n;j++)
dp[0][j][0] = 1;
for(int i = 1;i<=len;i++){
for(int j = 0 ;j<=n;j++){
for(int k = 0;k<=m;k++){
dp[i][j][k] = dp[i-1][j][k];
if(j>=group[i-1])
dp[i][j][k] += (dp[i-1][j-group[i-1]][Math.max(0,k-profit[i-1])]);
dp[i][j][k]%=mod;
}
}
}
return dp[len][n][m]%mod;
}
- 空间优化
java
public int profitableSchemes(int n, int m, int[] group, int[] profit) {
int mod =(int) 1e9+7;
int len = group.length;
int[][] dp = new int[n+1][m+1];
for(int j = 0;j<=n;j++)
dp[j][0] = 1;
for(int i = 1;i<=len;i++){
for(int j = n ;j>=group[i-1];j--){
for(int k = m;k>=0;k--){
dp[j][k] += dp[j-group[i-1]][Math.max(0,k-profit[i-1])];
dp[j][k]%=mod;
}
}
}
return dp[n][m]%mod;
}
似包非包
7. 组合总和(LC 377)
题目描述

解题思路
背包问题解决的问题都是有限制条件下的组合问题,也就是说不需要考虑顺序,而这个题要考虑顺序,严格意义上背包问题不能解决。
- 状态表示: dp[i]表示凑成总和为i有多少种排列数
- 状态转移方程:遍历数组,dp[i] += dp[i-nums[j]]
- 初始化:dp[0]=1,表示空集也算一种排列
代码实现
java
public int combinationSum4(int[] nums, int target) {
int n = nums.length;
int[] dp = new int[target+1];
dp[0] = 1;
for(int i = 1;i<=target;i++){
for(int x:nums){
if(i>=x){
dp[i]+=dp[i-x];
}
}
}
return dp[target];
}
卡特兰数
8. 不同的二叉搜索树(LC 96)
题目描述

解题思路
- 状态表示:dp[i]表示节点个数为i时,二叉搜索树的数量
- 状态转移方程:对于节点个数为i时,从1开始重新遍历,j为根节点,那么左子树的节点个数是j-1;右子树的节点个数是i-j。分部相乘,
dp[i] += dp[j-1]*[i-j] - 初始化:空树可以认为是二叉搜索树 dp[0] = 1;
代码实现
java
public int numTrees(int n) {
int[] dp = new int[n+1];
dp[0] = 1;
for(int i = 1;i<=n;i++){
for(int j = 1;j<=i;j++){
dp[i] +=dp[j-1]*dp[i-j];
}
}
return dp[n];
}