HDU2546
题意
电子科大本部食堂的饭卡有一种很诡异的设计,即在购买之前判断余额。如果购买一个商品之前,卡上的剩余金额大于或等于5元,就一定可以购买成功(即使购买后卡上余额为负),否则无法购买(即使金额足够)。所以大家都希望尽量使卡上的余额最少。
某天,食堂中有n种菜出售,每种菜可购买一次。已知每种菜的价格以及卡上的余额,问最少可使卡上的余额为多少。
输入
多组数据。对于每组数据:
第一行为正整数n,表示菜的数量。n<=1000。
第二行包括n个正整数,表示每种菜的价格。价格不超过50。
第三行包括一个正整数m,表示卡上的余额。m<=1000。n=0表示数据结束。
输出
对于每组输入,输出一行,包含一个整数,表示卡上可能的最小余额。
思路
这题不同于往常的01背包题目,在转移时有限制,并且可以超出背包容量(余额可以是负数)
如果直接套模板显然不行
(不过我们还是采用一维滚动数组)
经过观察,我们发现这样一个问题,在0 <= i < 5
的时候,dp[i]恒等于0(饭卡余额低于五块无法购买),也就是内层循环的下界一定不能低于5,然后在大于5的区域就和01背包几乎没有区别,便不难得出转移方程
if (j >= c[i] ) {
dp[j] = max(dp[j], dp[j - c[i]] + c[i]);
} else {
dp[j] = max(dp[j], c[i]);
}
但是如果这样直接交就会WA
究其原因,我们可以看这样一组样例
10
10 25 49 10 3 29 17 38 50 3
166
然后就会发现代码和正解不一样
究其原因,通过博客,发现是这样一个原因: 比如现在余额是11,有两个商品A和B,价格分别是10和1
我们发现如果先买A,就会导致B无法购买,这样最终余额就是1.如果先买B,则还能买A,则最终余额是0.
这和01背包有什么区别呢?因为01背包是不在乎顺序的,放入背包先后的顺序是不会对最终的结果产生影响的.但是这道题会受放入顺序的影响,这样就导致我们的放入需要一些策略.
根据以上的分析我们可以朴素地认为是先放小的再放大的可以不漏下最优解.
有没有数学角度的解释呢?我们不妨以二维数组的角度(便于理解)分析这个顺序问题.
我们更新dp[i][j]
的时候,需要dp[i-1][j-c[i]]
,j
的取值范围是[m,5]
,所以更新所需要查询的范围是[m,5-c[i]]
,也就是查询范围取决于c[i]
,我们不难想到,查询的范围应该是从大到小的 (语出Kuangbin),也就是范围大的应该在dp的最开头被更新,换句话说就是c[ ]
数组应该是递减排列的 .
**这个结论和我们通过逻辑得到的结论相同吗?**我们逻辑上给出的是先放小的,而我们从数学角度的分析是dp转移先处理大的.
关键结论
这两个结论是等价的.01背包问题在处理中很容易产生误导,我们是先放的1号还是先放的n号物品?其实是先放的n号,尽管我们是先处理的1号.可以这样理解:**1号只需要一步就能转移到最终结果.**而n号的决策取决于前一步,前一步然后取决于前前一步…这样就会追溯到最后一步,所以背包问题永远是倒着放的(我居然这才知道).
说到这里也明白了,理论的分析比较复杂,但是最后的结果却很简单,就是给c[]
排个序,从大到小,然后状态转移.
AC代码
// 好题 考察了"无后效性"这一重要性质
// 需要先排序才能得到正解
// 参考资料 https://blog.csdn.net/HTT_H/article/details/43163659?utm_source=blogxgwz8
#include<iostream>
#include<cstring>
#include<string>
#include <algorithm>
using namespace std;
int c[1100];
int dp[1100];
bool cmp(int a, int b) {
return a > b;
}
int main() {
int n, m;
while (cin >> n && n) {
memset(dp, 0, sizeof(dp));
for (int i = 0; i < n; ++i) {
cin >> c[i];
}
cin >> m;
sort(c, c + n, cmp);
for (int i = 0; i < n; ++i) {
for (int j = m; j >= 5; --j) {
if (j >= c[i] ) {
dp[j] = max(dp[j], dp[j - c[i]] + c[i]);
} else {
dp[j] = max(dp[j], c[i]);
}
}
}
cout << m - dp[m] << endl;
}
return 0;
}