leetcode刷题思路-----背包
背包问题一直是比较头疼的一类算法,但按照模板来走,比较简单。
以零钱兑换为例,其本质还是dp的状态转换。
1.基本思路
int[][] dp = new int[coins.length+1][amount+1];
//初始化状态
for(int i=; i<=; i++){
dp[0][] = ;
dp[][0] = ;
}
//先遍历硬币,再遍历里面的总数
for(int i=; i<=coins.length; i++){
for(int j=; j<=amount; j++){
//按照提议选择从哪种状态转换而来
dp[i][j] = (dp[i-1][j],dp[i][j-coins[i-1]);
}
}
return dp[coins.length][amount];
- 对于数组要注意大小,一般加一比较稳妥:
//数组第一维i代表选择了前i个硬币的状态,j代表当前凑的钱的面值
int[][] dp = new int[coins.length+1][amount+1];
- 对于带0的初始化要规定好,比如有些初始状态不为0:
for(int i=; i<=; i++){
dp[0][] = ?;
dp[][0] = ?;
}
- 状态转移一般两种:1.来自上次的结果 2.加入当前硬币
//选择上一次的状态
dp[i][j] = dp[i-1][j];
//当加入当前硬币coins[i-1]的状态,相当于没用这个硬币而去凑amount-这个硬币的状态
dp[i][j] = ...dp[i-1][j-coins[i-1]];
//如果硬币是无限的,则第二个为i,大家可以仔细思考
dp[i][j] = ...dp[i][j-coins[i-1]];
具体例子:
1.leetcode518 零钱兑换2
/*
给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。
请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。
假设每一种面额的硬币有无限个。
* */
private static int change2(int[] coins,int amount){
int[][] dp = new int[coins.length+1][amount+1];
//当金额为0时,那只有一种组合就是什么都不做,coins选空集,组合数为1;
for (int i = 0; i <= coins.length; i++) {
dp[i][0] = 1;
}
//开始遍历
for (int i = 1; i <=coins.length; i++) {
for(int j=1; j<=amount; j++){
//对于数组越界的状态要有判断
if(j<coins[i-1]){
//只能按照前i-1个硬币去凑
dp[i][j] = dp[i-1][j];
}else{
//这次不仅仅可以用i-1个硬币去凑,这次加入当前硬币去凑
//相当于凑j-当前硬币的组合数和之前之和
//例如1,2去凑10,有若干种方法,再来一个5,其实多了哪些组合呢?
//相当于1,2,5去凑10-5的面额的组合数(硬币是无限的)
dp[i][j] = dp[i-1][j]+dp[i][j-coins[i-1]];
}
}
}
return dp[coins.length][amount];
}
2.优化
对于二维dp,面试时会要求状态压缩,而原理就是在于滚动更新:
//本质是直接在旧的dp更新新值
dp[i] = dp[i]+...;
private static int better_change2(int[] coins,int amount){
int[] dp = new int[amount+1];
dp[0] = 1;
//注意到i都由上一次转换而来,直接去掉i循环的数组,滚动起来
for (int i = 1; i <=coins.length; i++) {
//对于不更新的i直接跳过
//注意!!!这次从前向后遍历,因为第二个dp[i][j-coins[i-1]]先更新
for(int j=coins[i-1]; j<=amount; j++){
dp[j] += dp[j-coins[i-1]];
}
}
return dp[amount];
}
其他题大家可以按照此套路继续探索。