1. 01背包
1.1 题目
有N件物品和一个容量为V的背包。第i件物品的费用是w[i],价值是v[i],求将哪些物品装入背包可使价值总和最大。
1.2 特点
每种物品仅有一件,可以选择放与不放。
1.3 基本的状态转移方程
f[i][j] = max(f[i − 1][j], f[i − 1][j − w[i]] + v[i])
1.4 基本模板
for(int i = 0; i < h; i++)
{
for(int j = c; j >= v[i]; j--)
{
dp[j] = max(dp[j], dp[j - v[i]] + v[i]);
}
}
1.5沾题
1.P2925 [USACO08DEC]干草出售Hay For Sale
简单01背包有一点优化:背包装满后就没必要再继续循环了。
#include<bits/stdc++.h>
using namespace std;
const int maxn = 50010;
int c, h, v[maxn], dp[maxn];
int main()
{
scanf("%d%d", &c, &h);
for(int i = 0; i < h; i++) scanf("%d", &v[i]);
for(int i = 0; i < h; i++)
{
for(int j = c; j >= v[i]; j--)
{
dp[j] = max(dp[j], dp[j - v[i]] + v[i]);
if(dp[j] == j) continue;
}
if(dp[c] == c) // 优化装满后退出
{
break;
}
}
printf("%d\n", dp[c]);
return 0;
}
1.6 常数优化
for(int i = 0; i < h; i++)
{
int sum = 0;
for(int k = i; k < h; k++) sum += v[i];
int maxx = max(c - sum, v[i]);
for(int j = c; j >= maxx; j--)
{
dp[j] = max(dp[j], dp[j - v[i]] + v[i]);
}
if(dp[c] == c)
{
break;
}
}
1.7初始化细节
(1)恰好装满背包,初始化dp[0] = 0,其余F[1 ... V] 均设为-INF
(2)未要求将背包装满,只是价格尽量大 F[0 ... V] 均设为0
2.完全背包
2.1 题目
有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的费用是w[i],价值是v[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
2.2 基本思路
这个问题非常类似于01背包问题,所不同的是每种物品有无限件。也就是从每种物品的角度考虑,与它相关的策略已并非取或不取两种,而是有取0件、取1件、取2件……等很多种。如果仍然按照解01背包时的思路,令f[i][j]表示前i种物品恰放入一个容量为V的背包的最大权值。仍然可以按照每种物品不同的策略写出状态转移方程,像这样:
f[i][j] = max(f[i − 1][j − k ∗ w[i]] + k ∗ v[i]) ∣ 0 <= k ∗ w[i] <= V
二维状态转移方程
f[i][j] = max(f[i − 1][j], f[i][j − w[i]] + v[i])
2.3 模板
for(int i = 0; i < n; i++)
for(int j = w[i]; j <= W; j++)
{
dp[j] = min(dp[j], dp[j - w[i]] + v[i]);
}
2.4 沾个题
#include<bits/stdc++.h>
using namespace std;
const int INF = 0x3f3f3f3f;
const int maxn = 10010;
int t, W, w[maxn], v[maxn], dp[maxn], n;
int main()
{
scanf("%d", &t);
while(t--)
{
int w1, w2;
scanf("%d%d", &w1, &w2);
fill(dp, dp + maxn, INF);
W = w2 - w1;
scanf("%d", &n);
for(int i = 0; i < n; i++)
{
scanf("%d%d", &v[i], &w[i]);
}
dp[0] = 0;
for(int i = 0; i < n; i++)
for(int j = w[i]; j <= W; j++)
{
dp[j] = min(dp[j], dp[j - w[i]] + v[i]);
}
if(dp[W] == INF)
{
printf("This is impossible.\n");
}
else
{
printf("The minimum amount of money in the piggy-bank is %d.\n", dp[W]);
}
}
return 0;
}
3 多重背包
3.1题目
有N种物品和一个容量为V的背包。第i种物品最多有p[i]件可用,每件费用是w[i],价值是v[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
3.2 复杂度的多重背包问题
void Zero(int w, int p)
{
for(int j = W; j >= w; j--)
{
dp[j] = max(dp[j], dp[j - w] + p);
}
}
void Complete(int w, int p)
{
for(int j = w; j <= W; j++)
{
dp[j] = max(dp[j], dp[j - w] + p);
}
}
void Multiple(int c, int w, int p)
{
if(c * w >= W)
{
Complete(w, p);
return;
}
int k = 1;
while(k < c)
{
Zero(k * w, k * p);
c = c - k;
k = 2 * k;
}
Zero(c * w, c * p);
}
3.3可行性问题O(VN)的算法
当问题是每种有若干的物品能否他Inman给定容积的背包,只需考虑装满背包的可行性O(VN)复杂度。
基本思想:设F[i, j]表示用了前i种物品填满容量为j的背包后最多还剩几个第i种物品可用。若F[i, j] = -1表示这种状态不太可行,若可行则满足F[i , j] >=0 && F[i, j] <= Mi
memset(dp, -1, sizeof(dp));
dp[0][0] = 0;
for(int i = 1; i <= n; i++)
{
for(int j = 0; j <= V; j++)
{
if(dp[i - 1][j] >= 0) dp[i][j] = M[i];
else dp[i][j]= -1;
}
for(int j = 0; j <= V - C[i]; j++)
{
if(dp[i][j] > 0)
dp[i][j + C[i]] = max(dp[i][j + C[i], dp[i][j] - 1);
}
}
4 混合多种背包
4.1问题
如果将前面三个背包混合起来,也就是说,有的物品只可以取一次(01背包),有的物品可以取无限次(完全背包),有的物品可以取的次数有一个上限(多重背包),应该怎么求解呢?
4.2 01背包与完全背包的混合
考虑到在01背包和完全背包中给出的伪代码只有一处不同,故如果只有两类物品:一类物品只能取一次,另一类物品可以取无限次,那么只需在对每个物品应用转移方程时,根据物品的类别选用顺序或逆序的循环即可,复杂度是O(VN) O(VN)O(VN)。
4.3再加上多重背包
如果再加上有的物品最多可以取有限次,那么原则上也可以给出O(VN) O(VN)O(VN)的解法:遇到多重背包类型的物品用单调队列解即可。但如果不考虑超过NOIP NOIPNOIP范围的算法的话,用多重背包中将每个这类物品分成O(log(p[i])) O(log(p[i]))O(log(p[i]))个01背包的物品的方法也已经很优了
4.4赋个多校题
题意:他由很多工作0 ,至少取一个,1 最多取一个, 2随意取,让你求在时间之内,求最大的开心值。
1.第一类,至少选一项,即必须要选,那么在开始时,对于这一组的dp的初值,应该全部赋为负无穷,这样才能保证不会出现都不选的情况。
dp[i][j]=max(dp[i][j],max(dp[i][j-w[x]]+p[x],dp[i-1][j-w[x]]+p[x]));
2.第二类,最多选一项,即要么不选,一旦选,只能是第一次选。
dp[i][j]=max(dp[i][j],dp[i-1][j-w[x]]+p[x]);
3.第三类,任意选,即不论选不选,选几个都可以。
dp[i][j]=max(dp[i][j],dp[i][j-w[x]]+p[x]);
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn = 110;
const int INF = 0x3f3f3f3f;
int n, t, x, y, v[maxn], w[maxn], dp[maxn][maxn];
int main()
{
while(scanf("%d%d", &n, &t) != EOF)
{
memset(dp, 0, sizeof(dp));
for(int i = 1; i <= n; i++)
{
scanf("%d%d", &x, &y);
for(int k = 1; k <= x; k++) scanf("%d%d", &v[k], &w[k]);
if(y == 0) // 至少取一个
{
for(int j = 0; j <= t; j++) dp[i][j] = -INF;
for(int k = 1; k <= x; k++)
{
for(int j = t; j >= v[k]; j--)
{
dp[i][j] = max(dp[i][j], dp[i][j - v[k]] + w[k]);
dp[i][j] = max(dp[i][j], dp[i - 1][j - v[k]] + w[k]);
}
}
}
else if(y == 1) // 至多取一个
{
for(int j = 0; j <= t; j++) dp[i][j] = dp[i - 1][j];
for(int k = 1; k <= x; k++)
{
for(int j = t; j >= v[k]; j--)
dp[i][j] = max(dp[i][j], dp[i - 1][j - v[k]] + w[k]);
}
}
else if(y == 2) // 至多取一个
{
for(int j = 0; j <= t; j++) dp[i][j] = dp[i - 1][j];
for(int k = 1; k <= x; k++)
{
for(int j = t; j >= v[k]; j--)
dp[i][j] = max(dp[i][j], dp[i][j - v[k]] + w[k]);
}
}
}
int ans = -1;
ans = max(ans, dp[n][t]);
printf("%d\n", ans);
}
return 0;
}
5 二维费用背包
5.1题目
二维费用的背包问题是指:对于每件物品,具有两种不同的费用;选择这件物品必须同时付出这两种代价;对于每种代价都有一个可付出的最大值(背包容量)。问怎样选择物品可以得到最大的价值。设这两种代价分别为代价1和代价2,第i件物品所需的两种代价分别为w[i]和g[i]。两种代价可付出的最大值(两种背包容量)分别为V和T。物品的价值为v[i]。
5.2模板
for(int i = 0; i < n; i++)
{
for(int j = v; j >= a[i]; j--)
{
for(int k = m; k >= b[i]; k--)
{
dp[j][k] = max(dp[j][k], dp[j - a[i]][k - b[i]] + c[i]);
}
}
}
5.3沾题
#include<bits/stdc++.h>
using namespace std;
const int maxn = 410;
int v, m, n, a[maxn], b[maxn], c[maxn], dp[maxn][maxn];
int main()
{
scanf("%d%d", &v, &m);
scanf("%d", &n);
for(int i = 0; i < n; i++) scanf("%d%d%d", &a[i], &b[i], &c[i]);
for(int i = 0; i < n; i++)
{
for(int j = v; j >= a[i]; j--)
{
for(int k = m; k >= b[i]; k--)
{
dp[j][k] = max(dp[j][k], dp[j - a[i]][k - b[i]] + c[i]);
}
}
}
printf("%d\n", dp[v][m]);
return 0;
}
2.FATE
题意:打游戏有忍耐值,需要在忍耐值之内刷够经验,最多杀n只怪。
#include<bits/stdc++.h>
using namespace std;
const int maxn = 110;
int n, m, k, s, a[maxn], b[maxn], dp[maxn][maxn];
int main()
{
while(scanf("%d%d%d%d", &n, &m, &k, &s) != EOF)
{
memset(dp, 0, sizeof(dp));
for(int i = 0; i < k; i++) scanf("%d%d", &a[i], &b[i]);
for(int i = 0; i < k; i++)
{
for(int j = b[i]; j <= m; j++)
{
for(int x = s; x >= 1; x--)
{
dp[j][x] = max(dp[j][x], dp[j - b[i]][x - 1] + a[i]);
}
}
}
int ans = 0x3f3f3f3f;
for(int i = 1; i <= m; i++)
{
for(int j = 1; j <= s; j++)
{
if(dp[i][s] >= n)
{
ans = m - i;
break;
}
}
if(ans != 0x3f3f3f3f) break;
}
if(ans == 0x3f3f3f3f) printf("-1\n");
else printf("%d\n", ans);
}
return 0;
}
6 分组背包
有N件物品和一个容量为V的背包。第i件物品的费用是w[i],价值是v[i]。这些物品被划分为若干组,每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
f[k][j]=max(f[k−1][j],f[k−1][j−c[i]]+w[i]∣物品i属于组k)
模板:
for(int i = 1; i <= n; i++)
{
for(int j = m; j >= 1; j--)
{
for(int k = 1; k <= j; k++)
{
dp[j] = max(dp[j], dp[j - k] + a[i][k]);
}
}
}
沾题
题意:ACboy做题然后, 用j天后的a[i][j]奖励
#include<bits/stdc++.h>
using namespace std;
const int maxn = 110;
int n, m, a[maxn][maxn], dp[maxn];
int main()
{
while(scanf("%d%d", &n, &m) != EOF && (m + n))
{
memset(dp, 0, sizeof(dp));
for(int i = 1; i <= n; i++)
{
for(int j = 1; j <= m; j++)
{
scanf("%d", &a[i][j]);
}
}
for(int i = 1; i <= n; i++)
{
for(int j = m; j >= 1; j--)
{
for(int k = 1; k <= j; k++)
{
dp[j] = max(dp[j], dp[j - k] + a[i][k]);
}
}
}
printf("%d\n", dp[m]);
}
return 0;
}
求次优解、第K优解
对于求次优解、第K优解类的问题,如果相应的最优解问题能写出状态转移方程、用动态规划解决,那么求次优解往往可以相同的复杂度解决,第K优解则比求最优解的复杂度上多一个系数K。
其基本思想是将每个状态都表示成有序队列,将状态转移方程中的max/min转化成有序队列的合并。这里仍然以01背包为例讲解一下。
首先看01背包求最优解的状态转移方程:
f[i][j]=max(f[i−1][j],f[i−1][j−w[i]]+v[i])
如果要求第K优解,那么状态f[i][j]就应该是一个大小为K KK的数组f[i][j][1...K]。其中f[i][j][k]表示前i ii个物品、背包大小为j jj时,第k优解的值。
“f[i][j]是一个大小为K的数组”这一句,熟悉C语言的同学可能比较好理解,或者也可以简单地理解为在原来的方程中加了一维。显然f[i][j][1...K]
这K个数是由大到小排列的,所以我们把它认为是一个有序队列。然后原方程就可以解释为:f[i][j]这个有序队列是由f[i−1][j]和f[i−1][j−w[i]]+v[i]这两个有序队列合并得到的。
有序队列f[i−1][j] 即f[i−1][j][1...K] ,f[i−1][j−w[i]]+v[i]则理解为在f[i−1][j−w[i]][1...K]的每个数上加上v[i]后得到的有序队列。
合并这两个有序队列并将结果的前K项储存到f[i][j][1...K]中的复杂度是O(K)。最后的答案是f[N][V][K]。总的复杂度是O(VNK)。为什么这个方法正确呢?
实际上,一个正确的状态转移方程的求解过程遍历了所有可用的策略,也就覆盖了问题的所有方案。只不过由于是求最优解,所以其它在任何一个策略上达不到最优的方案都被忽略了。
如果把每个状态表示成一个大小为K的数组,并在这个数组中有序的保存该状态可取到的前K个最优值。那么,对于任两个状态的max运算等价于两个由大到小的有序队列的合并。
另外还要注意题目对于“第K优解”的定义,将策略不同但权值相同的两个方案是看作同一个解还是不同的解。如果是前者,则维护有序队列时要保证队列里的数没有重复的。代码:
int kth(int n, int V, int k) {
for (int i = 1; i <= n; i++) {
for (int j = V; j >= w[i]; j--) {
for (int l = 1; l <= k; l++) {
a[l] = f[j][l];
b[l] = f[j - w[i]][l] + v[i];
}
a[k + 1] = -1;
b[k + 1] = -1;
int x = 1, y = 1, o = 1;
while (o != k + 1 and (a[x] != -1 or b[y] != -1)) {
if (a[x] > b[y]) f[j][o] = a[x], x++;
else f[j][o] = b[y], y++;
if (f[j][o] != f[j][o - 1]) o++;
}
}
}
return f[V][k];
}
综合代码总结
#include <iostream>
#include <cstdio>
#include <complex>
#define A 1000010
using namespace std;
int f[A], w[A], v[A];
/*---------0-1背包----------*/
int knapsack01(int n, int V) {
memset(f, 0xc0c0c0c0, sizeof f); f[0] = 0; //需要装满
memset(f, 0, sizeof f); //不需要装满
for (int i = 1; i <= n; i++)
for (int j = V; j >= w[i]; j--)
f[j] = max(f[j], f[j - w[i]] + v[i]);
return f[V];
}
/*-----------完全背包----------*/
int Fullbackpack(int n, int V) {
for (int i = 1; i <= n; i++)
for (int j = w[i]; j <= V; j++)
f[j] = max(f[j], f[j - w[i]] + v[i]);
return f[V];
}
/*-------多重背包二进制拆分-------*/
int number[A];
int MultiplePack1(int n, int V) {
for (int i = 1; i <= n; i++) {
int num = min(number[i], V / w[i]);
for (int k = 1; num > 0; k <<= 1) {
if (k > num) k = num;
num -= k;
for (int j = V; j >= w[i] * k; j--)
f[j] = max(f[j], f[j - w[i] * k] + v[i] * k);
}
}
return f[V];
}
int newv[A], neww[A], cnt;
int MultiplePack2(int n, int V) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= c[i]; j <<= 1) {
newv[cnt] = j * v[i];
neww[cnt++] = j * w[i];
c[i] -= j;
}
if (c[i] > 0) {
newv[cnt] = c[i] * v[i];
neww[cnt++] = c[i] * w[i];
}
}
for (int i = 1; i <= cnt; i++)
for (int j = V; j >= neww[i]; j--)
f[j] = max(f[j], f[j - neww[i]] + newv[i]);
return f[V];
}
/*------------多重背包单调队列优化------------*/
void MultiPack(int p, int w, int v) {
for (int j = 0; j < cost; j++) {
int head = 1,tail = 0;
for (int k = j, i = 0; k <= V / 2; k += w, i++) {
int r = f[k] - i * v;
while (head <= tail and r >= q[tail].v) tail--;
q[++tail] = node(i, r);
while (q[head].id < i - num) head++;
f[k] = q[head].v + i * v;
}
}
}
/*-----------二维费用背包----------*/
int t[A], g[A], dp[B][B];
int Costknapsack(int n, int V, int T) {
for (int i = 1; i <= n; i++)
for (int j = T; j >= w[i]; j--)
for (int k = V; k >= g[i]; k--)
dp[j][k] = max(dp[j][k], dp[j - w[i]][k - g[i]] + v[i]);
return dp[T][V];
}
/*--------------分组背包--------------*/
int a[B][B];
int Groupingbackpack() {
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
scanf("%d", &a[i][j]);
for (int i = 1; i <= n; i++)
for (int j = m; j >= 0; j--)
for (int k = 1; k <= j; k++)
f[j] = max(f[j], f[j - k] + a[i][k]);
return f[m];
}
/*------------K优解---------------*/
int kth(int n, int V, int k) {
for (int i = 1; i <= n; i++) {
for (int j = V; j >= w[i]; j--) {
for (int l = 1; l <= k; l++) {
a[l] = f[j][l];
b[l] = f[j - w[i]][l] + v[i];
}
a[k + 1] = -1;
b[k + 1] = -1;
int x = 1, y = 1, o = 1;
while (o != k + 1 and (a[x] != -1 or b[y] != -1)) {
if (a[x] > b[y]) f[j][o] = a[x], x++;
else f[j][o] = b[y], y++;
if (f[j][o] != f[j][o - 1]) o++;
}
}
}
return f[V][k];
}
int main(int argc, char const *argv[]) {}