Algorithm2.1 背包dp

[TOC]

背包dp

背包问题总结

  • 01背包:每种物品仅一个

    • 最大化所有物品的总价值:基础01背包
    • 组成当前容量的物品组合数:装配组合问题、路径问题
  • 完全背包:每种物品有无限个

  • 多重背包:每种物品有具体个数

  • 分组背包:每种物品仅一个,且不同物品有自己所属的组别

  • 多维代价背包:每种物品不仅占用空间,而且占用金钱

  • 多维目的背包:每种物品不仅具有价值1,而且具有价值2

01背包

固定容量 最大价值

题目一

https://www.luogu.com.cn/problem/P1048

题目大意:给定背包容量,将价值总和最大化。

思路:dp[j]表示容量为j的最大价值。将当前dp[0]~dp[j]这j个容量/状态/问题,通过第i个商品转移到dp[-w[i]]~dp[j-w[i]],n个商品就有n次刷新j个状态的过程。

#include <iostream>
using namespace std;
#define maxn 1005
int t, m, val[maxn], c[maxn], dp[maxn]; // dp[j]代表在容量为j的最大价值

int main() {
    scanf("%d%d", &t, &m);
    for (int i = 0; i < m; ++i) scanf("%d%d", &c[i], &val[i]);
    for (int i = 0; i < m; ++i)
        for (int j = t; j >= c[i] ; --j)
            dp[j] = max(dp[j], dp[j-c[i]]+val[i]);
        printf("%d\n", dp[t]);
    return 0;
}

题目二

https://www.luogu.com.cn/problem/P1802

题目大意:给定容量和经验,其中付出固定容量得到胜利的经验,不付出(或者付出部分)容量都得到失败的经验。使最终经验最大化。

思路:首先优化题目,失败的情况下,无论如何都不付出任何容量。转化为最基础的01背包。用dp[j]表示容量为j的最大经验。将当前dp[0]~dp[j]这j个容量/状态/问题,通过第i个商品转移到dp[-w[i]]~dp[j-w[i]]。单独拿出一个状态dp[j],在转换的时候用第i个商品,要么用容量去买,得到胜利的经验;要么直接不用,容量没损耗,经验加上失败的经验。

#include <iostream>
using namespace std;
#define maxn 1005
int n, x, l[maxn], w[maxn], c[maxn], dp[maxn]; // dp[j]表示在药水瓶数为j时所获得的最大经验

int main() {
    scanf("%d%d",&n, &x);
    for (int i = 0; i < n; ++i) scanf("%d%d%d",&l[i], &w[i], &c[i]);
    for (int i = 0; i < n; ++i)
        for (int j = x; j >= c[i]; --j)
            dp[j] = max(dp[j-c[i]]+w[i], dp[j]+l[i]);
    printf("%d\n", dp[x]*5);
    return 0;
}

01背包内圈遍历方向问题

关键:dp[0]dp[m]的赋值是否为1的问题。

理解:从大容量往小容量遍历,因为最后一位dp[m]没有被赋值为1,那么就没办法往前累加,每隔当前第i个商品的容量就要累加一次。反之从小往大遍历,第一位dp[0]是1,那么就会像滚雪球往后累计,每攒够第i个商品的容量,就要累加上一次。

装配组合//路径问题

题目一

https://www.luogu.com.cn/problem/P1164

题目大意:给定若干数,数值相同的数在组合中视作不同。问最大组合数(仅限加法的组合)。

思路:dp[j]表示组合成j时候的组合数。用第i个数来更改容量/状态为dp[0]~dp[j]的这些状态,通过dp[-w[i]]~dp[j-w[i]]进行转移。也就是这些状态之前的组合结果。将n个数,每次更改完的dp[j]都累加功效。

#include <iostream>
using namespace std;
#define maxn 102
#define maxm 10010
int n, m, a[maxn], dp[maxm]; // dp[i]表示容量为i的到达种类

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 0; i < n; ++i) scanf("%d", &a[i]);
    // init
    dp[0] = 1;
    for (int i = 0; i < n; ++i)
        for (int j = m; j >= a[i]; --j)
            dp[j] += dp[j-a[i]];
    printf("%d\n", dp[m]);
    return 0;
}

题目二

题目大意:和题目一区别在于,组合不仅可以加进去,也可以通过减法一块运算进去。

思路:将题目进行转换,就得到了纯加法进行组合的结果。

能否填充 // dp属性挑选与选取

https://www.luogu.com.cn/problem/P1510

题目大意:

1、如果dp属性选择到最大总容积上,dp[i]表示在容积为i时所需要的最小代价,再去比较代价 => 容积太大,不合适

2、如果dp属性选择到最大总代价上,dp[i]表示在代价为i时所呈现的最大容积,再去比较容积 => 合适

#include <iostream>
using namespace std;
#define maxn 10005
int vtotal, n, ctotal, val[maxn], c[maxn], dp[maxn]; // dp[i]表示在总代价为i时所获得的最大体积
// 初始化:
// 状态转移方程:dp[j] = max(dp[j], dp[j-c[i]]+val[i]);

int main() {
    scanf("%d%d%d", &vtotal, &n, &ctotal);
    for (int i = 0; i < n; ++i) scanf("%d%d", &val[i], &c[i]);
    for (int i = 0; i < n; ++i)
        for (int j = ctotal; j >= c[i]; --j)
            dp[j] = max(dp[j], dp[j-c[i]]+val[i]);
    if (dp[ctotal] < vtotal) printf("Impossible\n");
    else{
        for (int i = 0; i < ctotal; ++i)
            if (dp[ctotal-i] >= vtotal) continue;
            else {printf("%d\n", i-1);break;}
    }
    return 0;
}

最小剩余容量 // 最大填充

https://www.luogu.com.cn/problem/P1049

题目大意:

#include <iostream>
using namespace std;
#define maxn 32
int ct, n, c[maxn], dp[maxn]; // dp[j]表示容量为j时所能凑出的最大容量。最后输出dp[j]即可
// 初始化:
// 状态转移方程:dp[j] = max(dp[j], dp[j-c[i]]+c[i]); // 根据能不能放进去,而获得的最大体积

int main() {
    scanf("%d%d", &ct, &n);
    for (int i = 0; i < n; ++i) scanf("%d", &c[i]);
    for (int i = 0; i < n; ++i)
        for (int j = ct; j >= c[i]; --j)
            dp[j] = max(dp[j], dp[j-c[i]]+c[i]);
        printf("%d\n", ct-dp[ct]);
    return 0;
}

多重背包

模版

1、内核还是可以看成是乘倍数的01背包,所以中间遍历容量的时候要反向遍历,也就是从max_m往c[i]去刷新dp值

for (int i = 1; i <= n; ++i)
    for (int j = w; j >= c[i]; --j)
        for (int k = 1; k <= num[i] && k*c[i]<=j; ++k)
            dp[j] = max(dp[j], dp[j-k*c[i]]+k*val[i]);

二进制优化

https://www.luogu.com.cn/problem/P1776

思路:通过二进制来将k这个可能的值用二进制给分解开,数据的两增加到了原来的logn倍,从原来的n^3的遍历缩减成了n^2的遍历加上logn的数据规模扩大。所以最后总的时间复杂度为n^2*logn

【二进制优化的思路很像快速幂,通过任何整数都可以通过2^0,2^1……的时候选取来拟合出来,2的多少次方,相乘也就是上面的指数2的0,2的1这样往后相加,所以也就优化成了最后的logn的复杂度凑出】

1、将原来的多重背包val,c,num拍平,成为一个二维的val,c。

for (int i = 1; i <= n; ++i) {
    scanf("%d%d%d", &ta, &tb, &tc);
    for (int j = 1; j <= tc; j<<=1) {
        c[++idx] = tb*j; val[idx] = ta*j;
        tc-=j;
    }
    if (tc) {c[++idx] = tb*tc;val[idx] = ta*tc;}
}

2、将多重背包拍成了01背包之后,直接进行01背包的模版

for (int i = 1; i <= idx; ++i)
    for (int j = m; j >= c[i]; --j)
        dp[j] = max(dp[j], dp[j-c[i]]+val[i]);

完整AC代码

#include <iostream>
using namespace std;
#define maxn 1000100
int n, m ,ta, tb ,tc, c[maxn], val[maxn], idx = 0, dp[maxn];

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; ++i) {
        scanf("%d%d%d", &ta, &tb, &tc);
        for (int j = 1; j <= tc; j<<=1) {
            c[++idx] = tb*j; val[idx] = ta*j;
            tc-=j;
        }
        if (tc) {c[++idx] = tb*tc;val[idx] = ta*tc;}
    }
    for (int i = 1; i <= idx; ++i)
        for (int j = m; j >= c[i]; --j)
            dp[j] = max(dp[j], dp[j-c[i]]+val[i]);
        printf("%d\n", dp[m]);
    return 0;
}

单调队列优化

??? waiting

01背包 & 完全背包 & 多重背包混合

https://www.luogu.com.cn/problem/P1833

1、思路1:在遍历的时候,遍历到哪种种类背包就进行哪种遍历 => 完全背包直接完全遍历,01背包和多重背包直接用多重背包去解。(误

		for (int i = 1; i <= n; ++i)
        if (!c[i])
            for (int j = c[i]; j <= m; ++j) dp[j] = max(dp[j], dp[j-c[i]]+val[i]);
        else
            for (int j = m; j >= c[i]; --j)
                for (int k = 1; k <= num[i] && j >= k*c[i]; ++k)
                    dp[j] = max(dp[j], dp[j-k*c[i]]+k*val[i]);

2、思路2:直接利用二进制优化,把完全背包和多重背包拍成01背包,原理就是通过完全背包看成一个无穷大的,例如99999等。

for (int i = 1; i <= n; ++i) {
    scanf("%d%d%d", &ta, &tb, &tc);
    if (!tc) tc = 9999999;
    for (int j = 1; j <= tc; j<<=1) {
        c[++idx] = j*ta; val[idx] = j*tb;
        tc -= j;
    }
    if (tc) {c[++idx] = tc*ta; val[idx] = tc*tb;}
}
for (int i = 1; i <= idx; ++i)
    for (int j = m; j >= c[i]; --j)
        dp[j] = max(dp[j], dp[j-c[i]]+val[i]);

完整AC代码:

#include <iostream>
using namespace std;
#define maxn 1000100
int bgh, bgm, edh, edm, m, n, ta, tb, tc, val[maxn], c[maxn], idx, dp[maxn];

int main() {
    scanf("%d:%d %d:%d", &bgh, &bgm, &edh, &edm);
    if (bgm > edm) {edm += 60; edh--;}
    m = (edh-bgh)*60+(edm-bgm);
    scanf("%d", &n);
    for (int i = 1; i <= n; ++i) {
        scanf("%d%d%d", &ta, &tb, &tc);
        if (!tc) tc = 9999999;
        for (int j = 1; j <= tc; j<<=1) {
            c[++idx] = j*ta; val[idx] = j*tb;
            tc -= j;
        }
        if (tc) {c[++idx] = tc*ta; val[idx] = tc*tb;}
    }
    for (int i = 1; i <= idx; ++i)
        for (int j = m; j >= c[i]; --j)
            dp[j] = max(dp[j], dp[j-c[i]]+val[i]);
        printf("%d\n", dp[m]);
    return 0;
}

https://www.luogu.com.cn/problem/P2623

  • 01背包(价值由体积的函数值决定的背包)
  • 完全背包
  • 多重背包混合
#include <iostream>
using namespace std;
const int maxn = 2e3+9;
int n, m, dp[maxn], tp[maxn], val[maxn], c[maxn], num[maxn];

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; ++i) {
        scanf("%d", tp+i);
        if (tp[i] == 2) scanf("%d%d%d", val+i, c+i, num+i);
        else scanf("%d%d", val+i, c+i);
    }
    for (int i = 1; i <= n; ++i) {
        if (tp[i] == 1) {
            for (int j = m; j >= 1; --j) dp[j] = max(dp[j], val[i]*j*j-c[i]*j);
        } else if (tp[i] == 2) {
            for (int j = m; j >= c[i]; --j)
                for (int k = 1; k <= num[i] && k*c[i] <= j; ++k)
                    dp[j] = max(dp[j], dp[j-k*c[i]]+k*val[i]);
        } else {
            for (int j = c[i]; j <= m; ++j) dp[j] = max(dp[j], dp[j-c[i]]+val[i]);
        }
    }
    printf("%d\n", dp[m]);
    return 0;
}

组合背包

https://www.luogu.com.cn/problem/P1757

题目大意:就是普通01背包,但是给每个物品都添加了自己所属的组别。

1、此时遍历所有物品的话,就要按照组别->每个组别所包含的物品来遍历。

2、组别的目的就是加约束,每一组最多多少个,限制一个的话,可以通过把最内层遍历为该组别的遍历,中间设置为容量的遍历

for 所有组别
	for 容量从最大到0
		for 该组别中的所有物品
			if 当前容量大于该物品的容量

AC代码:

#include <iostream>
using namespace std;
const int maxn = 1010;
int dp[maxn], c[maxn], v[maxn], grp[maxn][maxn], cnt[maxn]; // grp[i][j]表示第i组,第j个商品在总的索引里边的位次
int ctotal, n, tc, tval, tgrp, grpmax;

int main() {
    scanf("%d%d", &ctotal, &n);
    for (int i = 1; i <= n; ++i) {
        scanf("%d%d%d", c+i, v+i, &tgrp);
        grpmax = max(grpmax, tgrp);
        cnt[tgrp]++;
        grp[tgrp][cnt[tgrp]] = i;
    }
    for (int i = 1; i <= grpmax; ++i)
        for (int j = ctotal; j >= 0; --j)
            for (int k = 1; k <= cnt[i]; ++k)
                if (j-c[grp[i][k]]>=0) dp[j] = max(dp[j], dp[j-c[grp[i][k]]]+v[grp[i][k]]);
            printf("%d\n", dp[ctotal]);
    return 0;
}

多维代价 // 多维目的 – 背包

二维代价,一个目的

https://www.luogu.com.cn/problem/P1855

多(2)维代价,所以各自的dp数组都是2个维度

一个目的,所以只需要开一个dp数组来作为目的

1、dp[]数组遍历的就是代价元素,所以有几个代价,就增加几个维度,二维就是dp[代价1][代价2],三维就是dp[代价1][代价2][代价3]

2、本题dp[i][j]表示在代价1为i,代价2位j的时候,所获的的最大的数量。

#include <iostream>
using namespace std;
#define maxn 205
int n, m, t, c_m[maxn], c_t[maxn], dp[maxn][maxn];

int main() {
    scanf("%d%d%d", &n, &m, &t);
    for (int i = 1; i <= n; ++i) scanf("%d%d", &c_m[i], &c_t[i]);
    for (int i = 1; i <= n; ++i)
        for (int j = m; j >= c_m[i] ; --j)
            for (int k = t; k >= c_t[i]; --k)
                dp[j][k] = max(dp[j][k], dp[j-c_m[i]][k-c_t[i]]+1);
            printf("%d\n", dp[m][t]);
    return 0;
}

https://www.luogu.com.cn/problem/P1507

同上,只不过通过添加了val来求最大值,而上面只是最大数量,val默认是1.

scanf("%d%d%d", &v, &m, &n);
for (int i = 1; i <= n; ++i) scanf("%d%d%d", &c_v[i], &c_m[i], &val[i]);
for (int i = 1; i <= n; ++i)
    for (int j = v; j >= c_v[i]; --j)
        for (int k = m; k >= c_m[i]; --k)
            dp[j][k] = max(dp[j][k], dp[j-c_v[i]][k-c_m[i]]+val[i]);
        printf("%d\n", dp[v][m]);

二维代价,两个目的

https://www.luogu.com.cn/problem/P1509

多(2)维代价,所以各自的dp数组都是2个维度

两个目的,要开两个dp数组来作为目的

#include <iostream>
using namespace std;
const int maxn = 105;
int n, rmb[maxn], rp[maxn], tim[maxn], totalRmb, totalRp; // 两个代价
int dpNum[maxn][maxn], dpTime[maxn][maxn]; // 两个目的

int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; ++i) scanf("%d%d%d", rmb+i, rp+i, tim+i);
    scanf("%d%d", &totalRmb, &totalRp);
    for (int i = 1; i <= n; ++i)
        for (int j = totalRmb; j >= rmb[i]; --j)
            for (int k = totalRp; k >= rp[i]; --k)
                if (dpNum[j][k] < dpNum[j-rmb[i]][k-rp[i]]+1) {
                    dpNum[j][k] = dpNum[j-rmb[i]][k-rp[i]]+1;
                    dpTime[j][k] = dpTime[j-rmb[i]][k-rp[i]]+tim[i];
                } else if (dpNum[j][k] == dpNum[j-rmb[i]][k-rp[i]]+1)
                    dpTime[j][k] = min(dpTime[j][k], dpTime[j-rmb[i]][k-rp[i]]+tim[i]);
    printf("%d\n", dpTime[totalRmb][totalRp]);
    return 0;
}
Posted on Feb 1, 2021