背包问题 持续更新中!

背包九讲

一、01背包问题

题目概览

N N N件物品和一个容量是 V V V的背包。每件物品只能使用一次。
i i i件物品的体积是 v i v_i vi,价值是 w i w_i wi
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。

输入格式

第一行两个整数 N , V N,V NV用空格隔开,分别表示物品数量和背包容积。
接下来有 N N N行,每行两个整数 v i , w i v_i,w_i vi,wi用空格隔开,分别表示第 i i i件物品的体积和价值。

输出格式

输出一个整数,表示最大价值。

数据范围

0 < N , V ≤ 1000 0<N,V≤1000 0<N,V1000
0 < v i , w i ≤ 1000 0<v_i,w_i≤1000 0<vi,wi1000

输入样例
4 5
1 2
2 4
3 4
4 5
输出样例:
8

(一)先使用DP去做

思路:

f [ i ] [ j ] f[i][j] f[i][j]表示只看前i个物品,总体积是j的情况下,总价值最大是多少

r e s u l t = m a x ( f [ n ] [ 0 − v ] ) result = max(f[n][0-v]) result=max(f[n][0v])

状态转移公式:

  1. 不选第i个物品, f [ i ] [ j ] = f [ i − 1 ] [ j ] f[i][j] = f[i - 1][j] f[i][j]=f[i1][j];
  2. 选第i个物品, f [ i ] [ j ] = f [ i − 1 ] [ j − v [ i ] ] f[i][j] = f[i - 1][j - v[i]] f[i][j]=f[i1][jv[i]]

f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] , f [ i − 1 ] [ j − v [ i ] ] ) f[i][j] = max(f[i - 1][j],f[i - 1][j - v[i]]) f[i][j]=max(f[i1][j],f[i1][jv[i]])

初始化工作: f [ 0 ] [ 0 ] = 0 f[0][0] = 0 f[0][0]=0


代码实现:
#include<bits/stdc++.h>
using namespace std;

const int N = 1010;

int n,m;
int f[N][N];
int v[N],w[N];

int main(){
    cin >> n >> m;
    
    for(int i = 1;i <= n;i++) cin >> v[i] >> w[i];
    
    for(int i = 1;i <= n;i++)
        for(int j = 0;j <= m;j++){
            f[i][j] = f[i - 1][j];
            if(j >= v[i])
                f[i][j] = max(f[i][j],f[i - 1][j - v[i]] + w[i]);
        }
    
    int res = 0;
    for(int i = 0;i <= m;i++) res = max(res,f[n][i]);
    
    cout << res << endl;        
        
    return 0;
}

(二)进行优化

思路:

目标一:省掉一维空间

一共就二维,所以,我们不妨每一维都试一试。

  1. 省去j那一维

    显而易见,所有的操作都是基于这一维的,如果没有这一维,无法进行转移。
    换一种说法,省去j这一维, f [ i ] f[i] f[i]数组的含义变成了前i个物品,总价值是多少,好像不太行吧。。。

  2. 省去i那一维

    仔细观察,不难发现,所有的状态均是由i - 1那一维转移过来的,所以,可以尝试使用滚动数组
    f [ j ] f[j] f[j]中原先存的是 f [ i − 1 ] [ j ] f[i - 1][j] f[i1][j]的数据,更新后变为 f [ i ] [ j ] f[i][j] f[i][j]的数据
    我们先尝试,强行去掉一维
    主体代码:

    for(int i = 1;i <= n;i++)
        for(int j = 0;j <= m;j++){
            if(j >= v[i])
            f[j] = max(f[j],f[j - v[i]] + w[i]);
        }
    

    那么,就有问题了,由于j是从小到大循环, f [ j − v [ i ] ] f[j - v[i]] f[jv[i]],它好像已经被更新过了,它对应的是原先的 f [ i ] [ j − v [ i ] ] f[i][j - v[i]] f[i][jv[i]],与我们想象的不符。

    所以,我们让 f [ j ] f[j] f[j]先更新,再让 f [ j − v [ i ] ] f[j - v[i]] f[jv[i]]先更新即可。实现也非常简单,只需要把j倒着循环即可。

    目标二:少一个循环

    找答案的循环不需要啦!

    原因:
    初始化的时候,把所有的 f [ i ] f[i] f[i]都初始化成了0

    f [ m ] f[m] f[m]就是体积为 m m m的方案。


代码实现:
#include<bits/stdc++.h>
using namespace std;

const int N = 1010;

int n,m;
int f[N];
int v[N],w[N];

int main(){
    cin >> n >> m;
    
    for(int i = 1;i <= n;i++) cin >> v[i] >> w[i];
    
    for(int i = 1;i <= n;i++)
        for(int j = m;j >= 1;j--){
            if(j >= v[i])
                f[j] = max(f[j],f[j - v[i]] + w[i]);
        }
    
    cout << f[m] << endl;        
        
    return 0;
}

二、完全背包问题

题目概览:

N N N种物品和一个容量是 V V V的背包,每种物品都有无限件可用。
i i i种物品的体积是 v i v_i vi,价值是 w i w_i wi
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。

输入格式

第一行两个整数, N N N V V V,用空格隔开,分别表示物品种数和背包容积。

接下来有 N N N行,每行两个整数 v i v_i vi, w i w_i wi,用空格隔开,分别表示第 i i i种物品的体积和价值。

输出格式

输出一个整数,表示最大价值。

数据范围

0 < N , V ≤ 1000 0<N,V≤1000 0<N,V1000
0 < v i , w i ≤ 1000 0<v_i,w_i≤1000 0<vi,wi1000

输入样例
4 5
1 2
2 4
3 4
4 5
输出样例:
10

思路与解

对比 01 01 01背包,区别在于,每个物品有无限件可以选。

思路:

f [ i ] f[i] f[i]表示总体积是i的情况下,最大价值是多少
r e s u l t = m a x ( f [ 0 … m ] ) result = max(f[0…m]) result=max(f[0m])
刚开始的想法肯定很朴实,会这样写:

for(int i = 0;i < n;i++)
    for(int j = m;j >= v[i];j--)//到这里都和01背包一样
        for(int k = 0;k * v[i] <= j;k++)//依次尝试能放多少
            f[j] = max(f[j],f[j - k * [i]] * w[i]);

但是时间复杂度太大了。。

优化:

for(int i = 0;i < n;i++)
    for(int j = v[i];j <= m;j++)
        f[j] = max(f[j],f[j - v[i]] + w[i]);

因为是从小到大枚举,所以 f [ j − v [ i ] ] f[j - v[i]] f[jv[i]]已经算过了
考虑前 i i i个物品,包括第 i i i个物品,可能里面已经有一些第 i i i个物品了

证明:
数学归纳法

  1. 假设考虑前 i − 1 i-1 i1个物品之后,所有的 f [ j ] f[j] f[j]都是正确的

  2. 来证明:考虑完第 i i i个物品后,所有的 f [ j ] f[j] f[j]也都是正确的
    对于某个 j j j而言,如果最优解中包含 k k k v [ i ] v[i] v[i];
    从小到大枚举的时候,一定会枚举到 f [ j − f ∗ v [ i ] ] f[j - f * v[i]] f[jfv[i]],那么这个状态就会用 f [ j − k ∗ v [ i ] − v [ i ] ] + w [ i ] f[j - k * v[i] - v[i]] + w[i] f[jkv[i]v[i]]+w[i]来更新它。
    枚举到 f [ j − ( k − 1 ) ∗ v [ i ] − v [ i ] ] + w [ i ] f[j - (k - 1) * v[i] - v[i]] + w[i] f[j(k1)v[i]v[i]]+w[i]包含1个 v [ i ] v[i] v[i]

    所以 f [ j ] f[j] f[j]就一定枚举到有 k k k v [ i ] v[i] v[i]的情况


代码实现
#include<bits/stdc++.h>
using namespace std;

const int N = 1010;

int n,m;
int f[N];

int main(){
    cin >> n >> m;
    
    for(int i = 0;i < n;i++){
        int v,w;
        cin >> v >> w;
        for(int j = v;j <= m;j++)
            f[j] = max(f[j],f[j - v] + w);
    }
    
    cout << f[m];//与上一题的原因一样,不需要比较
    
    return 0;
}

三、多重背包问题

题目概览:

N N N种物品和一个容量是 V V V的背包。
i i i种物品最多有 s i s_i si件,每件体积是 v i vi vi,价值是 w i wi wi
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。

输入格式

第一行两个整数, N N N V V V,用空格隔开,分别表示物品种数和背包容积.
接下来有 N N N行,每行三个整数 v i v_i vi, w i w_i wi, s i s_i si,分别表示第 i� 种物品的体积、价值和数量。

输出格式

输出一个整数,表示最大价值。

数据范围

0 < N , V ≤ 100 0<N,V≤100 0<N,V100
0 < v i , w i , s i ≤ 100 0<v_i,w_i,s_i≤100 0<vi,wi,si100(注意,下面会改)

输入样例
4 5
1 2 3
2 4 1
3 4 3
4 5 2
输出样例:
10

(一)最暴力的方法:

思路:

f [ i ] f[i] f[i]总体积是 i i i的情况下,最大价值是多少

for(int i = 0;i < n;i++)
    for(int j = m;j >= v[i];j--)
        f[j] = max(f[j],f[j - v[i]] + w[i],f[j - 2 * v[i]],...)

将f的所有值初始化为 0 0 0,结果为 f [ m ] f[m] f[m]

代码与解
#include<bits/stdc++.h>
using namespace std;

const int N = 105;
int n,m;
int f[N];

int main(){
    cin >> n >> m;
    for(int i = 0;i < n;i++){
        int v,w,s;
        cin >> v >> w >> s;
        for(int j = m;j >= 0;j--)
            for(int k = 1;k <= s && k * v <= j;k++)
                f[j] = max(f[j],f[j - k * v] + k * w);
                
    }
    
    cout << f[m] << endl;
    return 0;
}

(二)二进制优化

题目不变

数据范围

0 < N ≤ 1000 0<N≤1000 0<N1000
0 < V ≤ 2000 0 < V \le 2000 0<V2000
0 < v i , w i , s i ≤ 2000 0<v_i,w_i,s_i\le2000 0<vi,wi,si2000

思路:
  1. 把一个多重背包问题变成一个01背包问题
    我们可以把物品重复 s s s份,放到物品堆里去,每个物品就独立了,成功转化为一个01背包

例如,我们要把7拆成不同的方案
笨方法:1 1 1 1 1 1
每个1都有选和不选

聪明一下,我们只需要1,2,4,就可以表示出来1~7的所有数了

  1. 那么,回归到题目,只需要 l o g ( s ) log(s) log(s)上取整个数即可

    时间复杂度来到了可观的 1000 × 11 × 2000 = 2 × 1 0 7 1000\times11\times2000 = 2 \times 10 ^ 7 1000×11×2000=2×107

代码实现
#include<bits/stdc++.h>
using namespace std;

const int N = 2010;
int n,m;
int f[N];

struct Good{
    int v,w;
};

int main(){
    vector<Good> goods;
    cin >> n >> m;
    for(int i = 0;i < n;i++){
        int v,w,s;
        cin >> v >> w >> s;
        for(int k = 1;k <= s;k *= 2){
            s -= k;
            goods.push_back({v * k,w * k});
        }
        if(s > 0) goods.push_back({v * s,w * s});
    }
    
    for(auto good:goods)
        for(int j = m;j >= good.v;j--)
            f[j] = max(f[j],f[j - good.v] + good.w);
    
    cout << f[m] << endl;
    return 0;
}

(三)单调队列优化

题目不变。

数据范围

0 < N ≤ 1000 0<N≤1000 0<N1000
0 < V ≤ 2000 0 < V \le 2000 0<V2000
0 < v i , w i , s i ≤ 20000 0<v_i,w_i,s_i\le20000 0<vi,wi,si20000

思路:

先回看一下我们最原始的代码:

for(int i = 0;i < n;i++){
        int v,w,s;
        cin >> v >> w >> s;
        for(int j = m;j >= 0;j--)
            for(int k = 1;k <= s && k * v <= j;k++)
                f[j] = max(f[j],f[j - k * v] + k * w);
                
    }

最里面一重循环是我们的决策,01背包是只有01两种决策的,多重背包的话,是从0ss + 1种决策。
观察决策有没有什么性质,能不能用一些数据结构把它优化掉。
答案是可行的。

我们在去循环这个体积的时候,我们把所有的体积归一个类,根据我们的体积 j j j,模上v,把 m o d    v = 0 \mod v=0 modv=0的归为一类, m o d    v = 1 \mod v = 1 modv=1的归为一类,依次类推,分成若干类,每类之间一定是没有交集的,然后他所有类加在一块就是我们的全集。也就是我们把整个从0到 m m m的集合分成了 v v v类,从 m o d    v = 0 \mod v = 0 modv=0一直到 m o d    v = ( v − 1 ) \mod v = (v - 1) modv=(v1),类与类之间相互独立,毫不影响。

所以,在转移的时候,只需要到余数相同的集合中进行转移,不需要到其他类。

在实现的时候,我们要分别考虑每一类,枚举每一个余数,毕竟每个余数相互独立。在枚举的时候,还有一点比较绕。假设我正在枚举体积为j的情况,朴素的状态转移方程即为: f [ j ] = m a x ( f [ j − v ] + w , f [ j − 2 ∗ v ] + w ∗ 2 , . . . , f [ j − k ∗ v ] + k ∗ w ) f[j] = max(f[j - v] + w,f[j - 2 * v] + w * 2,...,f[j - k * v] + k * w) f[j]=max(f[jv]+w,f[j2v]+w2,...,f[jkv]+kw)

我们每次都要找前以上一串中的最大值,共k个数。进一步,当我们再算 f [ j + v ] f[j + v] f[j+v]时,我们只是把这个框向右移了一位,框里还是有 k k k个数。

这时,我们就可以顺理成章的想到,假设 j m o d    v = x j \mod v = x jmodv=x,根据数学知识,我们就可以推断出 ( j − k ∗ v ) m o d    v [ k 为常数 ] (j - k * v) \mod v[k为常数] (jkv)modv[k为常数]的结果也一定是 x x x,所以 f [ j ] f[j] f[j]只会从对 v v v取余相同的状态转移过来。

现在,这个问题已经被转移成了一个求窗口内最大值的问题。
好啦,可以看下一下几个题:
239. 滑动窗口最大值 - 力扣(LeetCode)
P1886 滑动窗口 - 洛谷
154. 滑动窗口 - AcWing题库

但是,还是有区别的,滑动窗口的原题是不会变的,但这里它是会变的,毕竟这里还有w的问题。

所以这里,给他们减去一个等差数列,每多一个 v v v,说明剩余要装入的物品数量就少了一个,总价值也少了一个 w w w
f [ 0 ] 不变 f [ v ] → f [ v ] − 1 ∗ w f [ 2 ∗ v ] → f [ 2 ∗ v ] − 2 ∗ w f[0]不变\\ f[v] → f[v] - 1 * w\\ f[2 * v] → f[2 * v] - 2 * w f[0]不变f[v]f[v]1wf[2v]f[2v]2w
实现过程:第一个循环枚举余数,第二个循环枚举这个余数里的所有数,这个过程可以用单调队列来优化,那么这里就是一个经典的单调队列问题。每次把队首取出来,队列的首部一定是最大的数,然后用最大数去更新一下我当前的数。每次把当前数往队列里插的时候,需要剔除队列中一定不会被用到的元素。然后我们把当前数放到队列离去,最后, f [ m ] f[m] f[m]就是答案。

代码实现
#include<bits/stdc++.h>
using namespace std;

const int N = 20010;

int n,m;
int f[N],g[N],q[N];

int main(){
    cin >> n >> m;
    for(int i = 0;i < n;i++){
        int v,w,s;
        cin >> v >> w >> s;
        memcpy(g,f,sizeof f);
        
        for(int j = 0;j < v;j++){
            int hh = 0,tt = -1;
            for(int k = j;k <= m;k += v){
                f[k] = g[k];
                if(hh <= tt && k - s * v > q[hh]) hh++;
                if(hh <= tt) f[k] = max(f[k],g[q[hh]] + (k - q[hh]) / v * w);
                while(hh <= tt && g[q[tt]] - (q[tt] - j) / v * w <= g[k] - (k - j) / v * w) tt--;
                q[++tt] = k;
            }
        }
    }
    
    cout << f[m] << endl;
    
    return 0;
}

四、混合背包问题

题目概览

N N N种物品和一个容量是 V V V 的背包。

物品一共有三类:

  • 第一类物品只能用1次(01背包);
  • 第二类物品可以用无限次(完全背包);
  • 第三类物品最多只能用 s i s_i si次(多重背包);

每种体积是 v i v_i vi,价值是 w i w_i wi

求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。

输入格式

第一行两个整数, N N N, V V V,用空格隔开,分别表示物品种数和背包容积。

接下来有 N N N行,每行三个整数 v i v_i vi, w i w_i wi, s i s_i si,用空格隔开,分别表示第 i i i种物品的体积、价值和数量。

  • s i = − 1 s_i=−1 si=1 表示第 i i i种物品只能用1次;
  • s i = 0 s_i=0 si=0 表示第 i i i 种物品可以用无限次;
  • s i > 0 s_i>0 si>0 表示第 i i i种物品可以使用 s i s_i si 次;
输出格式

输出一个整数,表示最大价值。

数据范围

0 < N , V ≤ 1000 0<N,V≤1000 0<N,V1000
0 < v i , w i ≤ 1000 0<v_i,w_i≤1000 0<vi,wi1000
− 1 ≤ s i ≤ 1000 −1≤s_i≤1000 1si1000

输入样例
4 5
1 2 -1
2 4 1
3 4 0
4 5 2
输出样例:
8

思路:

只需要判断一下是什么类的,然后按照每一类的进行转移就可以了。把所有的都拆成01背包的形式。

代码实现:
#include<bits/stdc++.h>
using namespace std;

const int N = 1010;

int n,m;
int f[N];

struct Thing{
    int kind;
    int v,w;
};
vector<Thing> things;

int main(){
	cin >> n >> m;
    for(int i = 0;i < n;i++){
        int v,w,s;
        cin >> v >> w >> s;
        if(s < 0) things.push_back({-1,v,w});
        else if(s == 0) things.push_back({0,v,w});
        else{
            for(int k = 1;k <= s;k *= 2){
                s -= k;
                things.push_back({-1,v * k,w * k});
    		}
            if(s > 0) things.push_back({-1,v * s,w * s});
        }
    }
    
    for(auto thing:things){
        if(thing.kind < 0){
            for(int j = m;j >= thing.v;j--) f[j] = max(f[j],f[j - thing.v] + thing.w);
        }
        else 
            for(int j = thing.v;j <= m;j++) f[j] = max(f[j],f[j - thing.v] + thing.w);
    }
    cout << f[m] << endl;
    
    return 0;
}

五、二维费用的背包问题

题目概述

N N N件物品和一个容量是 V V V的背包,背包能承受的最大重量是 M M M

每件物品只能用一次。体积是 v i v_i vi,重量是 m i m_i mi,价值$ w_i$。

求解将哪些物品装入背包,可使物品总体积不超过背包容量,总重量不超过背包可承受的最大重量,且价值总和最大。
输出最大价值。

输入格式

第一行三个整数, N N N, V V V, M M M,用空格隔开,分别表示物品件数、背包容积和背包可承受的最大重量。

接下来有 N N N 行,每行三个整数 v i v_i vi, m i m_i mi, w i w_i wi,用空格隔开,分别表示第 i i i 件物品的体积、重量和价值。

输出格式

输出一个整数,表示最大价值。

数据范围

0 < N ≤ 1000 0<N≤1000 0<N1000
0 < V , M ≤ 100 0<V,M≤100 0<V,M100
0 < v i , m i ≤ 100 0<v_i,mi≤100 0<vi,mi100
0 < w i ≤ 1000 0<wi≤1000 0<wi1000

输入样例
4 5 6
1 2 3
2 4 4
3 4 5
4 5 6
输出样例:
8

六、分组背包问题

题目概述

N N N组物品和一个容量是 V V V 的背包。

每组物品有若干个,同一组内的物品最多只能选一个。
每件物品的体积是 v i , j v_{i,j} vi,j,价值是 w i , j w_{i,j} wi,j,其中$ i$ 是组号, j j j 是组内编号。

求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。

输出最大价值。

输入格式

第一行有两个整数 N N N V V V,用空格隔开,分别表示物品组数和背包容量。

接下来有 N N N 组数据:

  • 每组数据第一行有一个整数 S i S_i Si,表示第$ i$ 个物品组的物品数量;
  • 每组数据接下来有$ S_i 行,每行有两个整数 行,每行有两个整数 行,每行有两个整数v_{i,j},w_{i,j} ,用空格隔开,分别表示第 ,用空格隔开,分别表示第 ,用空格隔开,分别表示第i$ 个物品组的第 j j j个物品的体积和价值;
输出格式

输出一个整数,表示最大价值。

数据范围

0 < N , V ≤ 100 0<N,V≤100 0<N,V100
0 < S i ≤ 100 0<S_i≤100 0<Si100
0 < v i j , w i , j ≤ 100 0<v_{i_j},w_{i,j}≤100 0<vij,wi,j100

输入样例
3 5
2
1 2
2 4
1
3 4
1
4 5
输出样例:
8

七、背包问题求方案数

题目概述

八、背包问题求具体方案

九、有依赖的背包问题

持续更新。。。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值