我有一个毛病,一个算法要是没有数学证明我很难接受它。即使背过的代码也就忘了。就像今天要讲的01背包的滚动数组,我还是推出了数学证明才把它背下来了。
题目:洛谷 P1048 [NOIP2005 普及组] 采药 01背包
先上代码:
// 最简单的01背包
#if 0 // 没有优化,n^2
#include <iostream>
#include <algorithm>
using namespace std;
int T, M;
int t[105], v[105], f[105][1005];
int main()
{
cin >> T >> M;
for(int i = 1; i <= M; ++i)
cin >> t[i] >> v[i];
for(int i = 1; i <= M; ++i)
{
for(int j = 1; j <= T; ++j)
{
if(t[i] <= j)
f[i][j] = max(f[i-1][j],
f[i-1][j-t[i]] + v[i]);
else f[i][j] = f[i-1][j];
}
}
cout << f[M][T] << endl;
return 0;
}
#else
// ------------------------华丽的分割线------------------------
// 滚动数组优化:
// e.g. Fibonacci数列。
// 直接f[i%3] = f[(i-1)%3] + f[(i-2)%3]
// 时间上无优势,可降维节省空间。
#include <iostream>
#include <algorithm>
using namespace std;
int T, M;
int t[105], v[105], f[1005];
int main()
{
cin >> T >> M;
for(int i = 1; i <= M; ++i)
cin >> t[i] >> v[i];
for(int i = 1; i <= M; ++i)
{
for(int j = T; j >= t[i]; --j) // 一定倒序!
{
f[j] = max(f[j], f[j - t[i]] + v[i]);
}
}
cout << f[T] << endl;
return 0;
}
#endif
首先我们要搞清楚从二维降到一维可能存在的隐患是什么。采用二维数组的一个好处就是,每一种草药只能采0/1次,不会出现一种草药被采了许多次的情况。而变为一维数组后,由于缺乏另一维的约束,就可能出现同一种药被采了很多次的情况。打比方说,这组测试数据:
T=20 M=3
t1=21 v1=100
t2=19 v2=1
t3=1 v3=2
不能说明问题。当
的时候,假如我们使用正序循环:
for(int j = t[i]; j <= T; ++j)
那么,j从1到20,由于背包空间(即题目中所说的时间)总是够的,最后f数组将是这样
2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40
可以看到,当时,第三种草药采了二十次!
当我们更新f[j]的时候,我们根本没有考虑f[j-t[i]]到底包不包含第i种草药。
但是我们如果改成倒序
for(int j = T; j >= t[i]; --j)
可以看到结果已经正常了。
2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3
为什么呢?因为,我们采用倒序,则
,f[j-t[i]]后于f[j]被更新,可以保证f[j-t[i]]一定不包含第i种草药。
这样就完成了对滚动数组的数学证明。