有关背包九讲的总结分析(持续更新,未完)

背包九讲源自dd大牛的博客《背包九讲》,经yxc大佬讲解和汇总,所有题目在这里

一、0-1背包问题

题目描述

有 N 件物品和一个容量是 m 的背包。每件物品只能使用一次。
第 i 件物品的体积是 vi,价值是 wi
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
数据范围
0<N,m≤1000
0<vi,wi≤1000

问题分析

状态表示:
用f(i, j)表示从前i个物品种选出体积小于等于j的所有选法中的最大价值。

如果f(i-1,j), f(i-1, j-1),……,f(i-1, j-v),……,f(i-1, 0)已经确定
则状态f(i, j)可以由f(i-1, j)或f(i-1, j-v)转移过来。

状态转移:
f ( i , j ) = { f ( i − 1 , j ) , 不 选 第 i 件 物 品 f ( i − 1 , j − v ) + w , 选 择 第 i 件 物 品 f(i, j)=\left\{ \begin{aligned} f(i-1, j), 不选第i件物品\\ f(i-1, j-v)+w, 选择第i件物品 \end{aligned} \right. f(i,j)={ f(i1,j),if(i1,jv)+w,i
状态转移方程为:f(i, j ) = max{f(i-1, j), f(i-1, j-v)+w}

c++代码实现
#include <iostream>
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];//体积为j时,不选择第i个物品的情况
            if(j >= V[i]) {
    
    
            	//状态转移
                //转移方程上面推导过
                f[i][j] = max(f[i][j], f[i-1][j-V[i]]+W[i]);
            }
        }
    }
    cout<<f[n][m]<<endl;

    return 0;
}
滚动数组优化

但是我们发现所有的f(i)的状态都是只由f(i-1)的状态转换而来的,如果我们取消掉f数组的第一维,只保存第二维。
当前i层可以用未被覆盖的i-1层的未被覆盖的数据来进行状态转移
转移方程为:f(j) = max(f(j), f(j-vi)+wi)
但是要注意,我们的第i层和第j层共有一个数组f,而我们需要的是未被覆盖过的第i-1层的数据更新第i层,所有需要逆序枚举体积j

c++代码实现
#include <iostream>
#include <cstdio>
using namespace std;

const int N = 1010;
int n, m;//物品数量和背包体积
int f[N];//表示体积为i的背包最多可以放下的物品价值

int main() {
    
    
    scanf("%d%d", &n, &m);
    int V, W;
    for(int i = 0; i < n; ++i) {
    
    
        scanf("%d%d", &V, &W);
        //0-1背包问题,每个物品最多只能用一次,更新状态就需要用到上一层结果
        for(int j = m; j >= V; --j)
            f[j] = max(f[j], f[j-V]+W);
    }
    cout<<f[m]<<endl;
    
    return 0;
}

二、完全背包问题

问题描述

有 N 件物品和一个容量是 m 的背包。每件物品可以使用无限次。
第 i 件物品的体积是 vi,价值是 wi
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
数据范围
0<N,m≤1000
0<vi,wi≤1000

朴素做法分析

完全背包问题和0-1背包问题的唯一差别就是其每种物品可以用无限次
状态表示
我们同样设f(i, j)表示从前i种物品种选出体积不超过j的所有选法的最大价值
则对于物品i和当前的体积j, 我们可选择0,1,2,…,r个。
(因为不能超出当前背包体积j,所以r = ⌊ l o g 2 ( j ) ⌋ \lfloor log _2(j) \rfloor log2(j))
于是我们可以得到类似于0-1背包的从状态i-1转移到状态i的状态转移方程,只是多了从0—r的一个循环,即
for(int k = 0; j -k * v >=0 ; k++) f[i][j] = max(f(i, j), f(i-1, j-k*v)+k*w)

c++代码实现

#include <iostream>
#include <cstdio>
using namespace std;

const int N = 1010;
int f[N][N];

int main() {
    
    
    int n, m;//n种物品,体积为m
    scanf("%d%d", &n, &m);
    int v, w;
    for(int i = 1; i <= n; ++i) {
    
    
        scanf("%d%d", &v, &w);
        for(int j = 0; j <= m;++j) {
    
    
            for(int k = 0; k*v <= j; ++k) {
    
    
                f[i][j] = max(f[i][j], f[i-1][j-k*v]+k*w);
            }
        }   
    }
    printf("%d", f[n][m]);
    return 0;
}

优化

在朴素做法中的状态转移方程用一个循环表示,其实我们可以将循环展开,就像这样:
f(i, j) = max{ f(i-1, j), f(i-1, j-v)+w, f(i-1, j-2*v)+2*w, ... , f(i-1, j-r*v)+r*w}
同时我们展开另一项f(i, j-v)
因为要保证第二维大于0,所以他最终展开后的最后一项也一定是f(i-1, j-r*v)状态
f(i, j-v) = max{ f(i-1, j-v), f(i-1, j-2*v)+w, ..., f(i-1, j-r*v)+(r-1)*w}
通过上面两个式子我们就可发现,一个更简便的状态转移方程:

f(i, j) = max{f(i-1, j), f(i, j-v)+w}

此时我们只需要中计算过的f(i, j-v)来进行状态转移,而无需计算所有的f(i-1, ...)的状态
这次我们直接考虑像0-1背包的滚动数组一样直接直接用一维数组来优化空间,于是f(j) = max(f(j), f(j-v)+w)
这里我们需要的f(j-v)是第i层的j-v,是被更新过的,所有我们要正序枚举体积。
迭代结束后f(m)就是答案,他代表的就是把所有的n种物品放入体积为m的背包中的最大价值数。

c++代码实现
#include <iostream>

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]<<endl;
    
    return 0;
}

三、多重背包问题

问题描述

有 N 种物品和一个容量是 m 的背包。
第 i 种物品有si个,体积是 vi,价值是 wi
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。

1、朴素做法

适用的数据范围

0<N,m≤100
0<si, vi,wi≤100

思路与分析

首先看到这题类似于完全背包问题,只是每种物品的数量不是无限,而是有限个。
考虑用朴素完全背包的做法,暴力进行s次循环的状态转移。
for(int k = 0; k <= s && j -k * v >=0 ; k++) f[i][j] = max(f(i, j), f(i-1, j-k*v)+k*w)
时间复杂度: O(nms)小数据可以ac

c++代码实现
#include <iostream>
using namespace std;

const int N = 110;
int f[N][N];

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

2、二进制优化

适用的数据范围

0<N≤1000
0<m≤2000
0<si, vi,wi≤2000

思路与分析

如过还是使用之前的朴素做法,O(nms)时间复杂度,最大计算4*109次,1s内不可能完成,需要考虑速度更快的做法。

对于某个s我们需要选择的是s以内的任意一个数量的物品,那么是否可以用更少的数来表示1–s内的所有数呢?
于是,我们将s个数进行拆分,分成1, 2, 4, 8, …,r, s-r
(另r = 2 ⌊ l o g 2 s ⌋ ^{\lfloor log _2s \rfloor} log2s)
这些数是可以每个数至多用一次来表示出1—s以内的所有数,于是问题最后转换成了一个新的0-1背包问题。

c++代码实现
#include <iostream>
#include <cstdio>

using namespace std;
//由于数据范围已经扩展至1000种,2000的容积,2000个物品,朴素做法n*v*s的时间复杂度,会超时
//于是需要优化
//将s个物品进行拆分,拆成1,2,4,8,...s-2^((int)logs)为止,这些数一定可以组成1--s中的所有的数
//将原有问题转换成了新的0-1背包问题
//转换所有物品的时间复杂度nlogs,新的背包中物品的数量nlogs,
//n*logs < 1000*log2000 < 25000
//求解0--1背包问题时间复杂度v*nlogs,可以接受

const int N = 25000;
int f[N], V[N], W[N];
int n, m;
int cnt = 1;
int main() {
    
    
    cin>>n>>m;
    //问题转换
    for(int i = 1; i <= n; ++i) {
    
    
        int v, w, s;
        scanf("%d%d%d", &v, &w, &s);
        for(int j = 1; j <= s; s-=j, j *= 2){
    
    
            V[cnt] = v*j;
            W[cnt++] = w*j;
        }
        if(s > 0) {
    
    
            V[cnt] = v*s;
            W[cnt++] = w*s;
        }
    }
    //0-1背包问题求解
    for(int i = 1; i < cnt; ++i) {
    
    
        for(int j = m; j >= V[i]; --j) {
    
    
            f[j] = max(f[j-V[i]]+W[i], f[j]);
        }
    }
    cout<<f[m]<<endl;
    return 0;
}

3、单调队列优化

适用的数据范围

0<N≤1000
0<m≤20000
0<si, vi,wi≤20000
注:本题最难,最后转换成滑动窗口问题,用单调队列优化,等先更新完滑动窗口问题再写
背包问题三其实是和男人八题中的 Coins 问题是一样的

四、混合背包问题

问题描述

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

物品一共有三类:

第一类物品只能用1次(0-1背包);
第二类物品可以用无限次(完全背包);
第三类物品最多只能用 si 次(多重背包);
每种体积是 vi,价值是 wi
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。

问题分析

状态表示
其实本题只是前面三个基本背包问题的糅合,状态表示仍然是:从前i个物品中选,总体积不超过j的所有选法
状态计算
状态计算仍然沿用上面三类背包问题的状态转移方程, 只需要在获取数据时判断一下是何种问题选择对应的方程求解即可。

(此处对于多重背包问题采用二进制优化转换成0-1背包问题,于是状态转移只有两种了)

此处复习一下两种基本的状态转移方程
0-1背包:
需要上一层的状态,所有这里的体积逆序枚举:
f(j) = max(f(j), f(j-v)+w)
完全背包:
需要本层更新完的状态,顺序枚举体积:
f(j) = max(f(j), f(j-v)+w)
(具体的推导在上面)

C++代码实现

#include <iostream>
#include <cstdio>

using namespace std;

const int N = 1010;
int f[N];

int main() {
    
    
    int n, m;
    scanf("%d%d",&n, &m);
    for(int i = 1; i <= n; ++i) {
    
    
        int v, w, s;
        scanf("%d%d%d", &v, &w, &s);
        if(s == 0) {
    
    //完全背包问题
            for(int j = v; j <= m; ++j) {
    
    
                f[j] = max(f[j], f[j-v]+w);
            }
        }
        else {
    
    
            //0-1或多重背包
            //先将0-1背包看成特殊的多重背包
            if(s == -1) s = 1;
            //将多重背包进行二进制优化转换为0-1背包问题
            int V[N],W[N],cnt = 0;
            for(int k = 1; k < s; s -= k, k *= 2){
    
    
                V[cnt] = k*v;
                W[cnt] = k*w;
                cnt++;
            }
            if(s > 0) {
    
    
                V[cnt] = s*v;
                W[cnt] = s*w;
                cnt++;
            }
            //利用0-1背包的状态方程求解
            for(int k = 0; k < cnt; k++) {
    
    
                for(int j = m; j >= V[k]; j--) {
    
    
                    f[j] = max(f[j], f[j-V[k]]+W[k]);
                }
            }
        }
    }
    //输出结果
    printf("%d\n", f[m]);
    return 0;
}

五、二维费用的背包问题

问题描述

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

每件物品只能用一次。体积是 vi,重量是 mi,价值是 wi

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

问题分析

二维费用的背包问题只是比普通的0-1背包问题多了一个限制条件:质量,于是在状态表示和转移时加上这一条件即可。
状态表示
f(i, j, k)表示所有只从前i个物品中选择,且总体积不超过j,总质量不超过k的所有选法
状态转移
f ( i , j , k ) = { f ( i − 1 , j , k ) , 不 选 择 第 i 件 物 品 f ( i − 1 , j − v i , k − m i ) + w i , 选 择 第 i 件 f(i, j, k)=\left\{ \begin{aligned} f(i-1, j, k), 不选择第i件物品\\ f(i-1, j-v_i, k-m_i)+w_i,选择第i件 \end{aligned} \right. f(i,j,k)={ f(i1,j,k),if(i1,jvi,kmi)+wi,i
f(i, j, k) = max(f(i-1, j, k), f(i-1, j-v, k-m)+w)

C++代码实现

#include <iostream>

using namespace std;

const int N = 110;
int f[N][N];

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

六、分组背包问题

七、有依赖的背包问题

八、背包问题求方案数

九、背包问题求具体方案

猜你喜欢

转载自blog.csdn.net/mwl000000/article/details/108602792