一、概述
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;
}