题目描述
解法
01背包 vs 完全背包
每件物品要么拿那么不拿(最多拿一次)是 01背包 问题,如果每件物品可以重复拿多次是 完全背包 问题,本题显然是完全背包问题
恰好装满 VS 可以不装满
题目有两种可能,一种是要求背包恰好装满,一种是可以不装满(只要不超过容量就行)。而本题是要求恰好装满的。而这两种情况仅仅影响我们dp数组初始化。
- 恰好装满。只需要初始化dp[0] 为 0, 其他初始化为负数(无效状态标记)即可。
- 可以不装满。 只需要全部初始化为 0,即可,
一维 vs 二维
- 二维表示:dp[i][j] 表示只能选择前i个物品,背包容量为j的情况下,能够得到的最大整数的字符串。
- 一维表示:dp[j] 表示只能选择前i个物品,背包容量为j的情况下,能够得到的最大整数的字符串。
那么疑问来了,一维中哪来的 i ?
其实二维写法中的 i 在一维表示中用遍历数组的 i 隐晦表示了,这样使得需要开辟的 dp 数组从二维降到了一维,空间复杂度降低了,但思想是一样的,后面我会给出二维写法和一维写法,对比一下就清楚了。
下面的题解都是二维,二维懂了之后一维就轻松了。
初始状态
dp[0][j] 表示选择前 0 个物品,背包容量为 j 的情况下,能够得到的最大整数的字符串。显然只有在 j = 0 时是有效状态,其余均是无效状态。
无效状态可以是 “0” 或者 “#”,只要能区分清楚即可,为了与题目相接,这里设无效状态为 “0”
那么初始化时 dp[0][0] = “” (有效状态,初始为空串),而dp[0][1…target] 初始化为 “0” (无效状态)。其实如果为了方便,可以在定义数组 dp 时直接全都初始化为 “0”,后面再把不是 “0” 的改回去。
状态转移
- 不选择当前的数位 i
dp[i][j] = dp[i-1][j];
- 至少选择一个当前的数位 i
dp[i][j] = to_string(i) + dp[i][j-cost[i-1]]; (注意这里的 cost[i-1] 表示第 i 位的成本,因为题目说了 cost 下标是从0开始的)
问题来了,为什么 to_string(i) + dp[i][j-cost[i-1]] 表示至少选择一个当前的数位 i 呢?
因为 dp[i][j - cost[i-1] 表示选择前i个物品, 恰好装进容量为j - cost[i]时能获得的最大值, 这包含了此时背包里第i个物品的个数是0个,1个,2个, 或者3个等等的情况。 方程前面 + to_string(i) 表示放了1个第i件物品进去, 那么此时就包含了背包里第i个物品的个数是1个,2个, 或者3个等等的情况,即至少装了1个第i件物品
要么不装,要么至少装1个,就把所有情况都考虑在内了,此时的状态转移方程才是正确的。综上,dp[i][j] = max_str(dp[i-1][j], to_string(i) + dp[i][j-cost[i-1]]); (max_str 函数需要自己写)
返回值
dp[n][target]
复杂度分析
-
二维写法
时间复杂度:O(NV)
空间复杂度:O(NV)
N是总个数,V是最大容量 -
一维写法
复杂度分析
时间复杂度:O(NV)
空间复杂度:O(V)
二维写法
class Solution {
public:
string max_str(const string &a, const string &b)
{
if(a.size() == b.size())
return a > b ? a : b;
else
return a.size() > b.size() ? a : b;
}
string largestNumber(vector<int>& cost, int target) {
int n = cost.size();
//无效状态用 "0" 表示(只要能区分清楚),比如 dp[0][j] 就都是无效状态,除了dp[0][0]
vector<vector<string> > dp(n + 1, vector<string>(target + 1, "0"));
dp[0][0] = "";
// dp[i][j]表示前i个元素, 恰好构成成本为j时, 构成的最大的整数(整数用字符串表示)
for(int i = 1; i <= n; i++)
{
for(int j = 0; j <= target; j++)
{
// 如果这两个要比较字符串都是 "0" 就没有比较的必要了,直接继承 dp[i-1][j] 的结果
if(j < cost[i - 1] || dp[i][j - cost[i - 1]] == "0")
dp[i][j] = dp[i - 1][j];
else
{
string tmp = to_string(i) + dp[i][j - cost[i - 1]];
dp[i][j] = max_str(tmp, dp[i - 1][j]);
}
}
}
return dp[n][target];
}
};
一维写法
class Solution {
public:
string max_str(const string &a, const string &b)
{
if(a.size() == b.size())
return a > b ? a : b;
else
return a.size() > b.size() ? a : b;
}
//一维写法,二维中的 i 用遍历数组的 i 隐晦表示了
string largestNumber(vector<int>& cost, int target) {
int n = cost.size();
//均初始化为无效状态 "0"
vector<string> dp(target + 1, "0");
dp[0] = "";//dp[0][0] 是有效的
// dp[i][j]表示前i个元素, 恰好构成成本为j时, 构成的最大的整数(整数用字符串表示)
for(int i = 1; i <= n; i++)
{
for(int j = cost[i - 1]; j <= target; j++)
{
if(dp[j - cost[i - 1]] != "0")
{
string tmp = to_string(i) + dp[j - cost[i - 1]];
dp[j] = max_str(tmp, dp[j]);
}
}
}
return dp[target];//此时 i 已经等于 n 了,也即此时一维的 dp[target] = 二维的 dp[n][target]
}
};
发现了吗,一维写法只是把二维中 dp[i][j] 的 [i] 去掉,其余几乎一样
背包问题的套路模板在 这里