背包九讲模板整理

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 沾个题

Piggy-Bank

#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 O\left ( V\sum logM_{i} \right )复杂度的多重背包问题

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赋个多校题

AreYouBusy

题意:他由很多工作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沾题

1.P1507 NASA的食物计划

#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 needs your help

题意: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[]) {}

 

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值