一、概述
1.问题描述
刷卡买菜,给定卡上余额,每种菜的价格,求消费后可以得到的最小余额,有约束如下:
1)卡上余额不足5时,不能消费,即使余额足以支付物品
2)卡上余额大于等于5时,为所欲为,即使余额被刷成负值
2.问题链接
3.问题截图
图1.1 问题截图
二、算法思路
乍一看下,以为是普通的01背包问题,只不过需要加几个条件判断语句,通过测试用例才检测出想法的错误。参考了网上的思路后,在思路基础上加上了对其正确性的证明,总结如下。以下简称菜为物品。
1.错误思路
由于这个系列到目前为止都是背包问题,在看完题目后,我感觉好像就是01背包问题,在考虑不周以及抱有某种侥幸心理的情况下,我总结出了以下状态转移方程:
设F[i, m]是前i个物品在卡上余额为m的情况下的解答,即消费后的最小余额,a[i]是第i个物品的价格,那么有如下推导:
1) 若F[i-1, m]>=5, 则表明此时可以直接装第i件物品,即F[i, m]=F[i-1,m]-a[i]
2) 所F[i-1, m]<5, 则此时表明不能装第i件物品,
若此时a[i]<=m, 有F[i, m]=max(F[i-1, m], F[i-1, m-ai])
若此时a[i]>m, 有F[i, m]=max(F[i-1, m], m-ai)
以上是我刚开始基于01背包得到的状态转移方程,当代码不能通过后,我很困惑:如此严谨的思路究竟哪里有问题?直到这个测试用例运行后:
m=10, n=3, 3个物品价格为:2, 50, 100
按照上述思路运行此测试用例就可以很容易的发现思路的错误:
在”2) 所F[i-1, m]<5,”时,若此时a[i]>m, 上述的状态转移方程会有一些情况考虑不到。此时正确的做法应该是从前i-1件物品中选择若干件物品,保证余额不小于5,然后再装入物品i,而上述思路直接装入了物品i。
在我兴致冲冲的改完这个bug后,发现还是无法通过,只得重新回去分析一开始为了省事儿而得出的状态转移方程,首先可以证明1)是正确的,这个证明需要借鉴下一部分正确思路的结论:”在保证余额足够装入一件物品(>=5)的情况下最后装入价格最大的物品可以得到最小的余额。”,如果此时第i件物品是价格最大的物品,那么根据上述结论,可以得到最小余额;如果不是的话,那么如果将第i件物品和之前装入的最大价格的物品替换,假设为物品x,也就是让i先装入,此时的F数组依然>=5,而此时剩余价格最大的x未装入,即依然可以得到最大值,证毕。
那么问题肯定出在2)了,并且出在条件”ai<m时”,依然通过一个简单的测试用例,检测出了这个转移式的缺陷。
m=10,n=3,3个物品价格为:2,4,6
可以发现,这个错误和物品装入顺序有关,当条件”ai<m时”,先后装入i会出现不同结果的可能,所以此时要在两者中取较小者。
更正这个错误后,算法通过了,相对于正确思路的出的算法,在时间和空间复杂度上都具有一定的差距。
2.正确思路及其正确性的证明
正确思路的做法:首先在留出5的余额基础上,装入价格由小到大的前i-1件物品,得到这样的前i-1件物品可以花费的最大价格和(不超过留出5后的余额),然后在装入价格最大的一件物品得到最后的答案。
为什么这样做正确呢?会不会出现这么一种情况:选择价格最大的物品构成最大价格和,然后选择其中某件物品x,从而得到更低的余额呢?
不会的,因为如果使用价格最大的物品构成最大价格和并保留某件物品x作为最后一件物品可以得到最优解,那么我们可以在构造最大价格和的过程中把装入价格最大物品换成装入x,那么很明显这种组合(最后装入价格最大的物品)得出的结果是一样的,即也是最优解,即算法思路是正确的。
总结,算法设计时一定要注意,一开始的投机取巧的算法设计可能会导致了后来成倍的时间去修复它所带来的隐患!
3、正确思路的算法实现
#include <iostream> // for cin, cout, endl
#include <algorithm> // for sort
using std::cin;
using std::cout;
using std::endl;
using std::sort;
void input(int&, int&);
int compute(int&, int&);
void output(int&);
const int MAX_DISHES = 1000; // the max num of dishes
int dishes[MAX_DISHES]; // hold input dishes
int ans[MAX_DISHES+1-5]; // ans for answer, +1-5 for index from 0 to 1000-5
int main()
{
int m, n;
int res; // res for result
while (cin>>n && n!=0){
input(m, n);
res = compute(m, n);
output(res);
}
}
int max2(int a, int b)
{
if (a > b)
return a;
else
return b;
}
void input(int& m, int& n)
{
for (int i=0; i<n; ++i)
cin >> dishes[i];
cin >> m;
}
int compute(int& m, int& n)
{
if (m < 5)
return m;
int idx = m-5; // available money set to m-5
int i, j;
int tmp;
// sort for dishes, cost from low to high
sort(dishes, dishes+n);
// initialize ans array, all element to 0, indicate the max sum of cost of first 0 dish
for (i=0; i<=idx; ++i)
ans[i] = 0;
// get the max sum of cost of first n-1 dishes, the sum of cost must not be exceed the current money
for (i=0; i<n-1; ++i)
for (j=idx; j>=dishes[i]; --j){
tmp = ans[j-dishes[i]] + dishes[i];
if (tmp <= j)
ans[j] = max2(ans[j], tmp);
}
return (m-ans[idx]-dishes[n-1]);
}
void output(int& res)
{
cout << res << endl;
}