0/1背包入门题解
背包问题的题目集来自CSDN上的夜深人静讲算法栏目。
HDU2602
最最基础的背包问题。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e3 + 5;
int main()
{
int t;
cin >> t;
while (t--)
{
int dp[maxn] = {0};
int n, v;
cin >> n >> v;
int val[maxn], vol[maxn];
for (int i = 1; i <= n; i++) cin >> val[i];
for (int i = 1; i <= n; i++) cin >> vol[i];
for (int i = 1; i <= n; i++)
for (int j = v; j >= vol[i]; j--)
dp[j] = max(dp[j], dp[j-vol[i]]+val[i]);
cout << dp[v] << endl;
}
}
设计状态
\(dp[i][j]\)表示前\(i\)个物品恰好放入容量为\(j\)的背包中的最大价值。
转移方程
二维转移方程来表示可以为\(dp[i][j] = max(dp[i-1][j], dp[i-1][j-vol[i]]+val[i])\)
滚动数组优化
我们发现每次得到\(dp[i][j]\)只需要用到“上一层”的数据,所以进行降维来优化空间:
\(dp[i]\)表示体积为i的背包中最多能容纳的价值。那么可以得到转移方程:
现在看核心式子:
for (int i = 1; i <= n; i++)
for (int j = v; j >= vol[i]; j--)
dp[j] = max(dp[j], dp[j-vol[i]]+val[i]);
在这里我们的第一层枚举了\(i\),即当前进行到的物品的位置。
但是当dp跑到\(dp[j]\)时,\(dp[j-vol[i]]\)已经产生,假如\(j\)足够大,则\(dp[j-vol[i]-vol[i]]\)必定也已经产生。
所以照这样跑下去,就是一个物品可以无限次选择的情况。但是在一个物品只能选择一次的情况下。我们希望在跑到\(dp[i]\)时\(dp[i-vol[i]]\)不要出现。
因此我们需要把第二层循环中从大到小进行状态转移,这是降维的核心优化。
HDU1203
只是将上一种背包问题中的求最大值变成了求最小值。
添加的内容就是一个初始化,还有一个把转移方程中的\(max\)换成\(min\)的过程。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e4 + 5;
double dp[maxn];
int vol[maxn];
double val[maxn];
int main()
{
int n, m;
while (cin >> n >> m && (n || m))
{
for (int i = 0; i <= n; i++) dp[i] = 1;
for (int i = 1; i <= m; i++) cin >> vol[i] >> val[i];
for (int i = 1; i <= m; i++) val[i] = 1-val[i];
for (int i = 1; i <= m; i++)
for (int j = n; j >= vol[i]; j--)
dp[j] = min(dp[j], dp[j-vol[i]]*val[i]);
printf("%.1lf%%\n", 100-(dp[n]*1.0*100));
}
}
值得注意的是这里的转移方程中的*可以通过log优化成加法,不过意义并不是很大。
同时,这里的数据大小也体现出了降维优化的必要性,如果变成二维数组肯定是塞不下的。
HDU1171
背包板子。
将存在性问题化成最大值问题的关键是将一个事物的体积看作权值。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e3+5;
int dp[maxn*5*50];
int v[maxn], m[maxn];
int main()
{
int n;
while (~scanf("%d", &n))
{
if (n < 0) break;
memset(dp, 0, sizeof(dp));
int sum = 0;
for (int i = 1; i <= n; i++) {cin >> v[i] >> m[i]; sum += v[i] * m[i];}
for (int i = 1; i <= n; i++)
for (int k = 1; k <= m[i]; k++)
for (int j = sum/2; j >= v[i]; j--)
dp[j] = max(dp[j], dp[j-v[i]]+v[i]);
cout << sum - dp[sum/2] << " " << dp[sum/2] << endl;
}
}
P2925
01背包存在性问题板子。
HDU1864
比较麻烦的一道01背包板子题。包括但不限于输入输出处理,浮点化整形,化01背包。
值得学习的一题。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1000 * 30 * 100 + 5;
int dp[maxn];
int cnt; //合法发票数
int val[maxn]; //每张合法发票的额度
int main()
{
double v;
int n;
while (scanf("%lf%d", &v, &n) == 2 && n)
{
cnt = 0;
int max_val = (int)(v * 100);
for (int i = 1; i <= n; i++)
{
int num;
char type; //商品类型
double va = 0, vb = 0, vc = 0;
scanf("%d", &num);
bool flag = true; //合法发票
while (num--)
{
scanf(" %c:%lf", &type, &v);
if (type == 'A') va += v;
else if (type == 'B') vb += v;
else if (type == 'C') vc += v;
else flag = false; //含有其他种类商品
}
if (flag && va <= 600 && vb <= 600 && vc <= 600 && va + vb + vc <= 1000)
val[++cnt] = (int)((va + vb + vc) * 100);
}
memset(dp, 0, sizeof(dp));
for (int i = 1; i <= cnt; i++)
for (int j = max_val; j >= val[i]; j--)
dp[j] = max(dp[j], dp[j - val[i]] + val[i]); //经过优化的一维数组类型的背包方程
printf("%.2lf\n", (dp[max_val]) / 100.0);
}
return 0;
}
HDU2126
考虑状态:最大值,方案数。建立二维背包:\(dp[i][j]\)分别表示容量为\(i\)方案数为\(j\)有多少种。
状态转移方程:\(dp[j][k] += dp[j-val[i][k-1]\),意思很简单,保证前一个i已经被处理过,所以容量为j-val[i]的背包肯定已经得到,这时把所有的可能性累加起来就好了。
还有一个需要注意的点是初始化:\(dp[i][0]\)表示没有任何一种方案,也即没有任何一个物品,所以有1种。
#include <bits/stdc++.h>
using namespace std;
void solve()
{
int n, m;
cin >> n >> m;
int dp[505][35] = {0};
int val[35];
for (int i = 1; i <= n; i++) cin >> val[i];
sort(val+1, val+1+n);
int ans = 0, sum = 0;
for (int i = 1; i <= n; i++)
{
sum += val[i];
if (sum > m && !ans) {ans = i-1; break;}
}
if (val[1] > m) {printf("Sorry, you can't buy anything.\n"); return;}
else if(sum <= m) {printf("You have 1 selection(s) to buy with %d kind(s) of souvenirs.\n", n); return;}
for (int i = 0; i <= m; i++) dp[i][0] = 1;
for (int i = 1; i <= n; i++)
for (int j = m; j >= val[i]; j--)
for (int k = ans; k >= 1; k--)
dp[j][k] += dp[j-val[i]][k-1];
printf("You have %d selection(s) to buy with %d kind(s) of souvenirs.\n", dp[m][ans], ans);
}
int main()
{
int t;
cin >> t;
for (int i = 1; i <= t; i++)
{
solve();
}
}
HDU2955
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn = 1e2 + 5;
const int maxsum = 1e6 + 5;
int vol[maxn];
double val[maxn];
double dp[maxsum];
void solve()
{
double pi;
int n;
memset(dp, 0, sizeof(dp));
memset(vol, 0, sizeof(vol));
memset(val, 0, sizeof(val));
scanf("%lf %d", &pi, &n);
int sum = 0;
for (int i = 1; i <= n; i++)
{
scanf("%d %lf", &vol[i], &val[i]);
sum += vol[i];
}
dp[0] = 1.0;
for (int i = 1; i <= n; i++)
for (int j = sum; j >= vol[i]; j--)
dp[j] = max(dp[j], dp[j-vol[i]]*(1.0-val[i]));
for (int i = sum; i >= 0; i--)
{
//cout << dp[i] << endl;
if (dp[i] >= (1.0-pi)) {printf("%d\n", i); break;}
}
}
int main()
{
int t;cin>>t;
while(t--){
solve();
}
}
普普通通的概率dp,但是因为我忘关流同步导致wa了n多发,maxsum的范围是个小坑点。
PKU2184
很绕的一道题。
#include <iostream>
#include <algorithm>
#include <stdio.h>
#define PII pair<int, int>
using namespace std;
const int INF = 1e6;
const int MAXM = 1e5 + 5;
const int MAXN = 111;
PII a[MAXN];
int dp[MAXM];
int n;
int main()
{
while (scanf("%d", &n) != EOF)
{
int sum = 0;
for (int i = 0; i < n; i++)
{
scanf("%d%d", &a[i].first, &a[i].second);
if (a[i].first > 0) sum += a[i].first;
}
sort(a, a + n);
for (int i = 0; i <= sum; i++) dp[i] = -INF;
dp[0] = 0;
for (int i = n - 1; i >= 0; i--)
{
int f = a[i].first, s = a[i].second;
if (f < 0 && s < 0) continue;
if (f >= 0)
{
for (int j = sum - f; j >= 0; j--)
dp[j + f] = max(dp[j + f], dp[j] + s);
} else {
for (int j = -f; j <= sum; j++)
dp[j + f] = max(dp[j + f], dp[j] + s);
}
}
int ans = 0;
for (int i = 0; i <= sum; i++)
if (dp[i] >= 0)
ans = max(ans, i + dp[i]);
printf("%d\n", ans);
}
return 0;
}
这道题让我逐渐意识到背包问题的本质,其实就是在当有两个变量时,要求限制其中的某一个变量,来求某一个线性组合的最值。zheda
这道题目中要求对s,f有限定条件:\(sum_s>=0,sum_f>=0,\)来求\(s+f\)的最大值。关键在于求\(sum_s\)和\(sum_f\)的过程。
这道题一个很重要的地方就是负数的处理,需要先进行排序。
附上另一种更常规的处理方法。
#include <stdio.h>
#include <string.h>
#include <iostream>
#include <algorithm>
using namespace std;
typedef pair<int, int> P;
const int INF = 0x3f3f3f3f;
const int maxn = 1e2 + 5;
const int maxval = 2e6 + 5;
int a[maxn], b[maxn];
int dp[maxval+5];
int main()
{
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i++)
scanf("%d%d", &a[i], &b[i]);
memset(dp, -INF, sizeof(dp));
dp[100000] = 0;
for (int i = 1; i <= n; i++)
{
if (a[i] > 0)
{
for (int j = maxval; j >= a[i]; j--)
dp[j] = max(dp[j], dp[j - a[i]] + b[i]);
}
else
{
for (int j = 0; j <= maxval + a[i]; j++)
dp[j] = max(dp[j], dp[j - a[i]] + b[i]);
}
}
int ans = 0;
for (int i = 100000; i <= maxval; i++)
{
if (dp[i] > 0)
ans = max(ans, dp[i] + i - 100000);
}
cout << ans << endl;
return 0;
}
这种做法比上一种更加便于理解:把负数化成正数,然后将分界点100*1000作为0点:dp[100000]=0。
若a[i]<0,则j-a[i]其实是j+(-a[i]),为了让dp[j+(-a[i])]==0,所以需要从左往右枚举。
P1507
#include <bits/stdc++.h>
using namespace std;
const int maxn = 4e2 + 5;
int read()
{
char ch=getchar();
int x=0,f=1;
while(ch<'0'||ch>'9'){if(ch=='-') f=-1;ch=getchar();}
while(ch>='0'&&ch<='9') x=x*10+(ch^48),ch=getchar();
return x*f;
}
int main()
{
int maxv = read(), maxm = read();
int n = read();
int v[maxn], m[maxn], cal[maxn];
int dp[maxn][maxn] = {0};
for (int i = 1; i <= n; i++)
{
v[i] = read(), m[i] = read(), cal[i] = read();
}
for (int i = 1; i <= n; i++)
for (int j = maxv; j >= v[i]; j--)
for (int k = maxm; k >= m[i]; k--)
dp[j][k] = max(dp[j][k], dp[j-v[i]][k-m[i]] + cal[i]);
cout << dp[maxv][maxm] << endl;
}
普普通通的二维背包问题,有几个变元就有几个**“除了i枚举个数循环”**以外的循环。
`
#HDU4502
坑点:相同的区间价值要更新较大的那一个。写起来有点像最短路的松弛操作,不过本质上还是dp问题,和一般的背包不同的是,这里的forj循环不需要考虑方向的问题,因为不用在意同一段区间被选择多次。
题目要求工资最大值,所以dp[i]表示的就是工资最大值,然后对于某一天i,对它之前的每一天j进行枚举。
```c++
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e2 + 5;
int read()
{
char ch=getchar();
int x=0,f=1;
while(ch<'0'||ch>'9'){if(ch=='-') f=-1;ch=getchar();}
while(ch>='0'&&ch<='9') x=x*10+(ch^48),ch=getchar();
return x*f;
}
int mp[maxn][maxn];
int dp[maxn];
void solve()
{
int m = read(), n = read();
memset(dp, 0, sizeof(dp));
memset(mp, 0, sizeof(mp));
for (int i = 1; i <= n; i++)
{
int t1 = read(), t2 = read();
mp[t1][t2] = max(mp[t1][t2], read());
}
for (int i = 1; i <= m; i++)
for (int j = i; j >= 1; j--)//for (int j = 1; j <= i; j++)
dp[i] = max(dp[i], dp[j-1] + mp[j][i]);
cout << dp[m] << endl;
}
int main()
{
int t = read();
while (t--)
{
solve();
}
}
HDU3448
痛苦的一题,三维数组开不出来,翻了题解才知道用dfs艹过去,结果dfs又写错了好几遍,最后搬其他题解的代码作为dfs解背包问题的板子来用了。
#include <bits/stdc++.h>
using namespace std;
int n, k, w, ans, a[50];
void dfs(int t, int num, int sum)
{
ans = max(ans, sum);
if (num == k + 1) return;
if (sum + a[num] <= w && t + 1 <= n) dfs(t + 1, num + 1, sum + a[num]);
dfs(t, num + 1, sum);
}
int main()
{
int i, sum;
while (scanf("%d%d", &n, &w) != EOF)
{
sum = 0;
scanf("%d", &k);
for (i = 1; i <= k; i++) scanf("%d", &a[i]);
sort(a + 1, a + k + 1, greater<int>());
for (i = 1; i <= n; i++) sum += a[i];
if (sum <= w)
{
printf("%d\n", sum);
continue;
}
ans = 0;
dfs(0, 1, 0);
printf("%d\n", ans);
}
return 0;
}
HDU2660
刚被3448TLE到抑郁,找个水题做一做又因为memset的原因卡了十来分钟。
要改改数组初始化的习惯了。
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn = 20 + 5;
ll read()
{
char ch=getchar();
ll x=0,f=1;
while(ch<'0'||ch>'9'){if(ch=='-') f=-1;ch=getchar();}
while(ch>='0'&&ch<='9') x=x*10+(ch^48),ch=getchar();
return x*f;
}
int dp[1000][20];
struct Pac
{
int val, vol;
}pac[maxn];
void solve()
{
ll n = read(), k = read();
for (int i = 1; i <= n; i++) pac[i].val = read(), pac[i].vol = read();
ll w = read();
memset(dp, 0, sizeof(dp));
for (int i = 1; i <= n; i++)
for (int j = w; j >= pac[i].vol; j--)
for (int m = 1; m <= k; m++)
dp[j][m] = max(dp[j][m], dp[j-pac[i].vol][m-1] + pac[i].val);
cout << dp[w][k] << endl;
}
int main()
{
int t;cin>>t;
while(t--){
solve();
}
}
PKU1157
典型的dp,从小到大找就能找出dp式子\(dp[i][j] = max(dp[i-1][j-1]+a[i][j], dp[i][j-1])\)
然后顺着推就行了,感觉跟背包关系不大。

浙公网安备 33010602011771号