第四、五周acm刷题总结 背包
思考了两个周的动态规划,希望能慢慢用自己的方式理解。这两个周里想出来的一个“采集”的模型,在总结背包之前先解释一下。
动态规划和贪心都是局部最优累积成全局最优,是一个不断求最优解的过程,关于贪心,我想可以理解成在总揽全局的前提下,制定一个最优原则每一次局部的抉择都遵循这个原则,并且每走一步,往后的选择空间就少了一份。而动态规划,我想并不能做到总揽全局,另外,新的一步客观上继承上一步,但操作方法不受上一步的制约。操作上是相互独立的,所以动态规划能够考虑到所有情况,时间复杂度也更高一些。
如果把贪心理解成沿着一个方向不断排除冗余部分,不断抓住主要矛盾忽视次要矛盾的过程,理解成一个对最优解的不断保持的过程。那么我想可以把动态规划理解成罗列每一个或者前n个个体,按照一个比较标准检查每一个状态是否有更优解的过程,一个对更优解的采集过程。
这个采集过程怎么说呢,就是先把限制条件按最小单位铺开,比如背包的容量是5千克,那就在一条轴上铺开1到5这5个状态,如果背包容量是5千克,容积是5立方分米,那就把这25个状态在一层正方形平面上铺开。然后用当前现有的条件去遍历铺开的这些状态,要保证所有的条件都遍历一遍。如果当前条件满足当前限制状态,那就可以尝试采集更优解。
在总结背包之前先以最长公共子序列为例解释一下。 两个序列如果尾元素相等,那么他们的最长公共子序列就等于不要尾元素的两个序列的公共子序列加一。也就是说当往一个序列尾部不断增添元素的时候,尾元素和第二个序列的尾元素相等了,那就可以尝试采集更优解了。所以要找两个序列的最长公共子序列,可以模拟这两个序列同时从一个元素开始不断增添元素的过程,当遇到尾元素相等的情况,就检查一下是否有更优解。代码如下
#include <iostream>
#include <string.h>
#include <algorithm>
using namespace std;
const int M = 205;
int main() {
char A[M], B[M];
while (cin >> A + 1 >> B + 1) {
int dp[2][M] = { 0 };
int lenA = strlen(A + 1);
int lenB = strlen(B + 1);
for (int i = 1; i <= lenA; i++) {
for (int j = 1; j <= lenB; j++) {
if (A[i] == B[j])
dp[i & 1][j] = dp[(i - 1) & 1][j - 1] + 1;
else dp[i & 1][j] = max(dp[(i - 1) & 1][j], dp[i & 1][j - 1]);
}
}
cout << dp[lenA & 1][lenB] << endl;
}
return 0;
}
背包问题:
1)01背包:
给出n个物品,背包总容积是vall输入每一个物品的体积和价值,求能装下的最大价值。代码和过程都在图里了:
#include <iostream>
#include <iomanip>
using namespace std;
int n, vall, v[104], w[103], ans[103][10003]; //物品数,总体积,个体体积,个体价值,局部最优dp值
int main()
{
cin >> n >> vall;
for (int i = 1; i <= n; i++)
cin >> v[i] >> w[i];
for (int i = 1; i <= n; i++)
for (int j = 1; j <= vall; j++)
if (j < v[i]) ans[i][j] = ans[i - 1][j];
else ans[i][j] = max(ans[i - 1][j], ans[i - 1][j - v[i]] + w[i]);
for (int i = 0; i <=n; i++) {
for (int j = 0; j <= vall; j++)
cout << setw(4) << ans[i][j];
cout << endl;
}
cout << ans[n][vall] << endl;
return 0;
}
输出如下:
不难发现,每次尝试采集更优解,都是去上一行采集的,所以每次状态转移之后只需要留着上一行的内容就行了,这里对比一下上面的公共子序列,那个要留两行。因为他是同时需要参考在同一列上的两行的内容的,所以最多优化为两行。那么这一个呢,我们可以优化位一行。代码如下:
#include <iostream>
#include <iomanip>
using namespace std;
int n, vall, v[104], w[103], ans[10300];
int main()
{
cin >> n >> vall;
for (int i = 1; i <= n; i++)
cin >> v[i] >> w[i];
for (int i = 1; i <= n; i++) {
for (int j = vall; j >= v[i]; j--) {
ans[j] = max(ans[j], ans[j - v[i]] + w[i]);
cout << setw(4) << ans[j];
}
cout << endl;
}
cout << ans[vall] <<endl;
}
输出如下:
#include <iostream>
#include <iomanip>
using namespace std;
int n, vall, v[10003], w[102201], dp[10002][10003];
int main() {
cin >> n >> vall;
for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];
for (int i = 1; i <= n; i++) {
for (int j = v[i]; j <= vall; j++) {
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - v[i]] + w[i]);
}
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= vall; j++) {
cout << setw(4) << dp[i][j];
}
cout << endl;
}
cout << dp[n][vall] << endl;
return 0;
}
既然优化成一行我们可以只遍历能装下的限制状态(我自行把动规理解成两个状态的相遇转移:条件状态和限制状态),那么在一开始的朴素法中能不能只遍历能装下的时候?
答案是不可以,刚才是突发奇想,其实不可以的,输出如下:
会发现后面几行有数据丢失,那么是不是不影响结果呢?不是的,因为这里采集更优解是从上一行左面某一点采集的,所以上一行左面必须存有之前的数据,会发现这里二维数组反而不如一维数组能够实现这个存储功能,他把之前的数据更新没了。
2)完全背包:
完全背包是指每件物品有无限个,只有一个区别,更优解是从当前行左面某处采集,而不是上一行。也就是不断地检查第i种物品如果能装下,装一个是不是更好?又能装下了,那么再装一个是不是更好?代码如下:
#include <iostream>
#include <iomanip>
using namespace std;
int n, vall, v[10003], w[10003], dp[10003][10003];
int main() {
cin >> n >> vall;
for (int i = 1; i <= n; i++)
cin >> v[i] >> w[i];
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= vall; j++) {
if (j < v[i]) {
dp[i][j] == dp[i - 1][j];
} else {
dp[i][j] = max(dp[i - 1][j], dp[i][j - v[i]] + w[i]);
}
}
}
for (int i = 0; i <= n; i++) {
for (int j = 0; j <= vall; j++)
cout << setw(4) << dp[i][j];
cout << endl;
}
cout << dp[n][vall] << endl;
return 0;
}
结果如下:
同样道理,完全背包也能优化成一行,不过注意有点小不同,代码如下:
#include <iostream>
#include <iomanip>
using namespace std;
int n, vall, v[10004], w[10003], dp[10004];
int main() {
cin >> n >> vall;
for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];
for (int i = 1; i <=n; i++) {
for (int j = v[i]; j <= vall; j++) {
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
cout << setw(4) << dp[j];
}
cout << endl;
}
cout << dp[vall] << endl;
return 0;
}
结果如下:
3)多重背包:
多重背包就是每种物品有多个,不是一个也不是无限个。用到一个减少物品总数的小技巧,叫做二进制优化,就是比如有5个2千克的物品摆在面前,他和有一个两千克的,两个四千克的摆在面前是一样的,后者可以完全代替前者所有拿取可能。直接代码吧,只写01背包优化后的:
#include <iostream>
#include <iomanip>
using namespace std;
const int M = 200005;
int n, vall, ind = 1, v[M], w[M], dp[1000004];
int bit2[25];
void init() {
int t = 1;
for (int i = 0; i <= 21; i++) {
bit2[i] = t;
t *= 2;
}
}
int main() {
init();
cin >> n >> vall;
for (int i = 1; i <= n; i++) {
int a, b, c, cnt = 0;
cin >> a >> b >> c;
while (c) {
if (c > bit2[cnt]) {
v[ind] = a * bit2[cnt];
w[ind] = b * bit2[cnt];
c -= bit2[cnt++];
} else {
v[ind] = a * c;
w[ind] = b * c;
c = 0;
}
ind++;
}
}
for (int i = 1; i <= ind - 1; i++) {
for (int j = vall; j >= v[i]; j--) {
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
cout << setw(4) << dp[j];
}
cout << endl;
}
cout << dp[vall] << endl;
return 0;
}
结果如下:
这里有人把循环条件写错了,写成i <= ind,也没有人指出错误,结果也是37,但是这是不对的,如下:
4)双限制条件的背包:
开头说的那种,现在如果朴素法就是展到一个面上,然后用物品数量的增加升维为一个四棱柱,直接上代码吧。``
#include <iostream>
#include <iomanip>
using namespace std;
int n, vall, mall, v[50], m[50], w[50];// dp[50][405][404];
int dp[405][405];
int main() {
cin >> n >> vall >> mall;
for (int i = 1; i <= n; i++) {
cin >> v[i] >> m[i] >> w[i];
}
/*
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= vall; j++) {
for (int k = 1; k <= mall; k++) {
if (j < v[i] || k < m[i]) {
dp[i][j][k] = dp[i - 1][j][k];
} else {
dp[i][j][k] = max(dp[i - 1][j][k], dp[i - 1][j - v[i]][k - m[i]] + w[i]);
}
}
}
}
*/
for (int i = 1; i <= n; i++) {
for (int j = vall; j >= v[i]; j--) {
for (int k = mall; k >= m[i]; k--) {
dp[j][k] = max(dp[j][k], dp[j - v[i]][k - m[i]] + w[i]);
}
}
}
//cout << dp[n][vall][mall] << endl;
cout << dp[vall][mall] << endl;
return 0;
}
总结:
除了开始时候总结的,补充这几天考虑的空间优化的事情:拿着当现有条件的状态去遍历限制条件的状态时, 当现有条件发生改变可以理解成操作数的增加的时候,这个时候可以理解成用限制条件所构成的空间随着现有条件所被模拟成的时间发生升维遍历的时候,可以检查一下大多数时候没必要升维,而是把最后一维优化掉。如果只需要保留限制条件的某一行状态,或者虽然两行,但是可以共存的状态,那么可以优化为一行,如果要同时用到多行,可以通过取模,模多少就是留多少行。