背包问题(背包九讲) acwing视频笔记

背包问题(背包九讲)

(很多细节待校验)

本博客是B站up主大雪菜视频《背包九讲专题》的笔记,代码、题目描述均出自该视频。稍微加入了少量个人理解。另外也想借此机会练习 m a r k d o w n markdown markdown的使用方式。

1. 01背包问题

原题链接 (文中的题目描述截图均来源于AcWing)
共有N个物品,背包容量为V。每个物品的体积为 v i v_i vi,价值为 w i w_i wi,求不超过总容量的情况下能够装的物品价值最大是多少。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8691iPJv-1591533818353)(./1591289405223.png)]
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])
状态转移:
f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] , f [ i − 1 ] [ j − v i ] + w i ) f[i][j] = max(f[i-1][j], f[i-1][j-v_i] + w_i) f[i][j]=max(f[i1][j],f[i1][jvi]+wi)
f [ i ] [ j ] f[i][j] f[i][j]:

  1. 不选第i个物品,f[i][j] = f[i-1][j]
  2. 选第i个物品,f[i][j] = f[i-1][j-v[i]]+w[i]
    f[i][j] = max(1, 2)
    初始化:
    f[0][0] = 0

核心状态转移方程:
f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] , f [ i − 1 ] [ j − v [ i ] ] + w [ i ] ) f[i][j] = max(f[i-1][j], f[i-1][j-v[i]] + w[i]) f[i][j]=max(f[i1][j],f[i1][jv[i]]+w[i])

/* 

*/
#include <iostream>
#include <cstring>
#include <algorithm>
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) {
            if (j>=v[i]) f[i][j] = max(f[i-1][j], f[i-1][j-v[i]] + w[i]);
            else f[i][j] = f[i-1][j];
        }
    }
    
    int res = 0;
    for (int j=0;j<=m;++j) res = max(res, f[n][j]);
    
    cout << res << endl;
    return 0;
}

**【优化代码】**发现状态 f [ i ] f[i] f[i]只跟前一层有关系,这样就可以有一个通用的优化方式——滚动数组,但还可以再进一步优化,不用滚动数组,用一维数组。这里讨论一位数组方法。

int n, m;
int f[N]; // 去掉第一维(前i个物品)。表示体积是j的情况下最大值是多少。
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>=v[i];--j) { 
            f[j] = max(f[j], f[j-v[i]] + w[i]);
/*  j从后往前考虑就能避免i的影响。注意这里等号左边其实是f[i][j],而等号右边为f[i-1][j](请结合二维矩阵的方法),这样就造成了歧义。既然我们舍去了第一维,就要保证第一维不同的情况下等号左右两边能够对应。如何做呢?具体分析见下面的图。
*/
        }
    }
    cout << f[m] << endl;
    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mE2wCxog-1591533818357)(./1591424075749.png)]

我画的丑草图

如果初始化时f全部为0,则最大值一定是f[m]。
如果初始化只有f[0]=0,则需要枚举找一遍max(f[i],i=0…m)。

分析:
设最大结果为 f [ k ] = m a x _ w f[k] = max\_w f[k]=max_w;
那么分析转移过程:
f [ 0 ] = 0 → f [ v [ 0 ] ] = w [ 0 ] → . . . f[0] = 0 \rightarrow f[v[0]] = w[0] \rightarrow ... f[0]=0f[v[0]]=w[0]...
同理:
f [ m − k ] = 0 → f [ m − k + v [ 0 ] ] = w [ 0 ] → . . . f[m-k] = 0 \rightarrow f[m - k + v[0]] = w[0] \rightarrow ... f[mk]=0f[mk+v[0]]=w[0]...

再想,如果是 f [ j ] f[j] f[j]表示容量恰为j时的最大值,初始化又该如何做呢?
f [ 0 ] = 0 , f [ i ] = − I n f ( i > 0 ) f[0] = 0, f[i] = -Inf(i>0) f[0]=0,f[i]=Inf(i>0)

2. 完全背包问题

原题链接 (来源于AcWing)
共有N个物品,背包容量为V。每个物品的体积为 v i v_i vi,价值为 w i w_i wi,求不超过总容量的情况下能够装的物品价值最大是多少。每件物体可以选择无限次。其余设定和01背包问题一致。

核心状态转移方程:
01背包:
f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] , f [ i − 1 ] [ j − v [ i ] + w [ i ] ] ) f[i][j] = max(f[i-1][j], f[i-1][j-v[i]+w[i]]) f[i][j]=max(f[i1][j],f[i1][jv[i]+w[i]])
完全背包:
f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] , f [ i ] [ j − v [ i ] ] + w [ i ] ) f[i][j] = max(f[i-1][j], f[i][j-v[i]]+w[i]) f[i][j]=max(f[i1][j],f[i][jv[i]]+w[i])

两者的空间优化结果表达式是一样的:
f [ j ] = m a x ( f [ j ] , f [ j − w [ i ] ] + w [ i ] ) f[j] = max(f[j], f[j-w[i]]+w[i]) f[j]=max(f[j],f[jw[i]]+w[i])
但是实现的具体操作,前者要求j从大到小,后者要求从小到大。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zOwwOG3h-1591533818361)(./1591429067710.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TkE0C7Kv-1591533818367)(./1591429081147.png)]

《算法笔记》p444-445大佬画的图一目了然

回忆上一题一维数组方法的核心部分:

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

只需要再次让j反过来变化就能解决完全背包问题了!

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

同样也有最后取值的思考。如果初始化时 f [ i ] = 0 , ∀ i ∈ [ 0 , m ] f[i]=0, \forall i\in[0,m] f[i]=0,i[0,m] ,则 f [ m ] f[m] f[m]就是最后的最大值。(仔细想想!也可以看acwing上的讲解)
如果题目问的是:在体积恰好为m时最大价值是多少,则应该在初始化时:
f [ i ] = − I n f , ∀ i ∈ [ 1 , m ] ; f [ 0 ] = 0 f[i]=-Inf, \forall i\in[1,m]; f[0]=0 f[i]=Inf,i[1,m];f[0]=0
另外,关于下式的有效性,可以用数学归纳法证明。详细见acwing。
f [ j ] = m a x ( f [ j ] , f [ j − v [ i ] ] + w [ i ] ) f[j] = max(f[j], f[j-v[i]] + w[i]) f[j]=max(f[j],f[jv[i]]+w[i])

3 多重背包问题

原题链接 (来源于AcWing)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LyIYeUX1-1591533818369)(./1591458483469.png)]
其实 看范围在100以内,则可以用o(n3)的方法来做(也就1000000次计算以内搞定,可以接受)。在01背包的代码上修改如下:

for (int i=1;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]] + 2*w[i], f[j-3*v[i]] + 3*w[i], ...);
        //这里就是再用一重循环

注意,结果要不要再枚举一遍,仍然取决于初始化方式。

  1. f[i]=0 ,则result = f[m].
  2. f[0]=0, f[i]=-Inf, 则result=max(f[0],f[1],f[2]…f[m])
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 110;
int n, m;
int f[N];

int main() {
    cin >> n >> m;
    for (int i=1;i<=n;++i)  
    
    for (int i=0;i<n;++i) {
	    cin >> n >> m >> 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;
}

升级版:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9QuaZoRJ-1591533818372)(./1591459609916.png)]
现在数据范围扩大了,O(n3)的算法在时间上就太长了!优化一下。

思路:
对某个物品,假设是v, m, s。那我们把他拆成s个v, m的物品!这样就转换成了01背包问题。
然而···复杂度其实是没有变的!

二进制优化方法

先考虑一个数学题。设有一个数s,从小于s的数中至少选多少个数,就能通过他们之间的加法来表示出0-s内的全部数呢?
比如s=10, 则选择1,2,4,3

先看1 2 4, 能够表示出0-7, 那么最后选一个3,就能表示0-10的。
估算时间复杂度。
1000*log(2000)2000=2107,c++里面一秒钟能进行107次计算。那这样是符合要求的了。

#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>

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 >> m >> 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;
}

究极版
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GfbAQ0FZ-1591533818374)(./1591513789679.png)]
二进制优化已经无法满足时间要求了。
单调队列优化(有种说法是说双端队列和单调队列是一个意思,但我认为单调队列是可以基于双端队列实现的一种新数据结构,具有一些新的特点)
利用同余的概念。按照每个j去模v得到的余数分组作为一个系列。这个系列的子问题其实就是一个单调队列问题。
f [ j ] = m a x ( f [ j ] , f [ j − v ] + w , f [ j − 2 ∗ v ] + 2 ∗ w , . . . f [ j − k ∗ v ] + k ∗ w ) f[j] = max(f[j], f[j-v] + w, f[j - 2*v] + 2*w, ... f[j - k*v] + k*w) f[j]=max(f[j],f[jv]+w,f[j2v]+2w,...f[jkv]+kw)
f [ 0 ] f[0] f[0]
f [ v ] − 1 ∗ w f[v] - 1*w f[v]1w
f [ 2 v ] − 2 ∗ w f[2v] - 2*w f[2v]2w
…这样理解,因此在比较大小关系的时候,虽然每个数都会变化,但是每个数都加的一样,所以取最大值得到的结果没有问题。

f [ j ] − k ∗ w f[j] - k*w f[j]kw
f [ j + v ] = f [ j ] + w , f [ j − 2 ∗ v ] + 2 ∗ w f[j + v] = f[j] + w, f[j - 2*v] + 2*w f[j+v]=f[j]+w,f[j2v]+2w

#include <iostream>
#include <cstring>
#include <algorithm>

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 >> m >> s;
	    memcpy(g, f, sizeof f); //void *memcpy(void*dest, const void *src, size_t n); 注意,sizeof是一个优先级第二的单目运算符关键字,后面本可以不加括号。但为保险起见不混淆作用对象通常还是加了为好。
	    for (int j = 0; j < n; ++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;
}

4. 混合背包问题

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kOR06lzu-1591533818376)(./1591529756429.png)]
转换成01背包和完全背包问题。

#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>

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;
}

5. 二维费用的背包问题

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Jn8bZ2h0-1591533818378)(./1591530607225.png)]
直观地考虑——增加一维。按照01背包扩展问题理解即可。三重循环。里面两个循环都是从大到小循环。
$$f[i][j]

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 110;
int n, v, m;
int f[N][N];

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

6. 分组背包问题

有 N 组物品和一个容量是 V 的背包。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nKmkpX0J-1591533818381)(./1591531137594.png)]
f [ j ] = m a x ( f [ j ] , f [ j − v [ 0 ] ] + w [ 0 ] , f [ j − v [ 1 ] ] + w [ 1 ] , . . . , f [ j − v [ s − 1 ] ] + w [ s − 1 ] ) f[j] = max(f[j], f[j-v[0]]+w[0], f[j-v[1]]+w[1],...,f[j-v[s-1]]+w[s-1]) f[j]=max(f[j],f[jv[0]]+w[0],f[jv[1]]+w[1],...,f[jv[s1]]+w[s1])
其实可以把它和多重背包问题比较。多重背包问题是分组背包问题的一种特殊情况。多重背包问题可以看成有很多组,每组一个。因此多重背包问题比较特殊、能够采用好几种优化方法,而分组背包问题就只能老老实实地写三重循环了。

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 110;
int n, m;
int f[N], v[N], w[N];

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

7. 有依赖的背包问题

有N个物品和一个容量是V的背包。
在这里插入图片描述
在这里插入图片描述
树形DP问题+背包问题
难点是如何组合起来。
f [ i ] [ j ] f[i][j] f[i][j]表示选择节点i并且体积是j的情况下,以i为根节点的子树的最大收益。

从上往下递归地做。每做一个点时,把它所有子节点的 f [ i ] [ j ] f[i][j] f[i][j]算出来。
可以认为实际上是一个分组背包问题。每个子节点可以看成一个组。
每个点的不同体积就是不同的组。每个组里只能选择一个物品。

分组背包问题——先循环物品,再循环体积,再循环选哪个(决策)
总结起来背包问题就是——循环物品,再循环体积,最后循环决策。
这里,从某个父节点出发,每个儿子节点都是一个物品组。

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 110;
int n, m;
int h[N], e[N], ne[N], idx;
int v[N], w[N], f[N][N];

void add(int a, int b) {
	// a为父节点,b为当前节点.
	// 这个建树过程有点特别啊···而且整个代码尽然能够AC
	// 虽然这种方法很简单···但还是建议建树时老老实实用struct TreeNode以及指针来做吧。。
	e[idx] = b, ne[idx] = h[a], h[a] = idx++;
	// h[e[x]]表示x节点的最新的子节点,h[e[ne[x]]]表示x节点次新的子节点,h[e[ne[ne[x]]]]表示次次新的子节点...
}

void dfs(int u) {
	for (int i = h[u]; i != -1; i = ne[i]) { // 先循环物品(组)
		//这个循环在i=0时应该会进入无限循环吧? 因为ne[0]=0,所以它是如何防止这一点出现呢?
		//莫非是c++有优化机制,使得这种情况下会强行停止循环。
		//想来只有这一种结论了。必然就是,当出现i=0时,会直接停止循环。(但如果将i!=-1改成i!=0则会超出内存Memory Limit Exceeded)
		int son = e[i];
		dfs(son);
		for (int j = m - v[u]; j >= 0; j--) // 再循环体积
			for (int k = 0; k <= j; k++) // 最后循环每一组内的决策(选哪个物品),每种体积对应一种选法。其实就是在决定这个子节点该用哪个体积。不同的体积就对应了不同的物品组。
				f[u][j] = max(f[u][j], f[u][j-k] + f[son][k]);
	}
	for (int i = m; i >= v[u]; i--) f[u][i] = f[u][i - v[u]] + w[u];
	for (int i = 0; i < v[u];  i++) f[u][i] = 0; // 这两行代码不能放到本函数的开头
}

int main() {
	memset(h, -1, sizeof h);
	cin >> n >> m;
	int root;
	for (int i = 1; i <= n; ++i) {
		int p;
		cin >> v[i] >> w[i] >> p;
		if (p == -1) root = i;
		else add(p, i);
	}
	dfs(root);	
	cout << f[root][m] << endl;
	return 0;
}

8. 背包问题的方案数

在这里插入图片描述

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1010, mod = 1000000007, INF = 1000000;

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

int main() {
	cin >> n >> m;
	// 和之前的01背包问题不同,这里稍微调整了f[j]的含义。“恰好”
	// f[j]表示体积恰好是j的情况下最大价值
	// g[j]表示体积恰好是j的情况下方案数是多少
	// 因此他们的初始化方式会有所不同。
	g[0] = 1;
	for (int i = 1; i <= m; i++) f[i] = -INF;

	for (int i = 0; i < n; i++) {
		int v, w;
		cin >> v >> w;
		for (int j = m; j >= v; j--) {
		int t = max(f[j], f[j - v] + w);
		int s = 0;
		if (t == f[j]) s += g[j];
		if (t == f[j - v] + w) s += g[j - v];
		if (s >= mod) s -= mod;
		f[j] = t;
		g[j] = s;
		}
	}
	//注意,因为f[j]的定义不一样了,所以最后求最大值时需要遍历了。
	int maxw = 0;
	for (int i = 0; i <= m; i++) maxw = max(maxw, f[i]); 
	int res = 0;
	for (int i = 0; i <= m; i++) {
		if (maxw == f[i]) {
			res += g[i];
			if (res >= mod) res -= mod;
		}
	}

	cout << res << endl;
	return 0;
}

9. 背包问题的具体方案

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0KFaIPUK-1592057246703)(./1592055082858.png)]
注意字典序是不看位数的。
按照字典序的比较方法:
123 < 31
从前往后一位一位地比较。

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1010;
int n, m;
int f[N][N], v[N], w[N];
// 为了反推我们的方案,就要把所有的方案记录下来。所以需要两维。
// 注意此时定义: f[i][j]表示用前i个物品,容积不超过j的情况下最大价值
int main() {
	cin >> n >> m;
	for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];
	for (int i = n; i >= 1; i--) { 
	// 得从后往前看,所以i逆序。相当于,i=n时表示,只使用物品n的效果。
	// i= n-1表示,只使用物品n和n-1的效果。
		for (int j = 0; j <= m; j++) { // 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 vol = m;
	for (int i = 1; i <= n; i++) { // 贪心
		if (f[i][vol] == f[i + 1][vol - v[i]] + w[i]) {
			cout << i << ' ';
			vol -= v[i];
		}
	}
	return 0;
}

但这个代码似乎通不过acwing。看了好几遍代码和视频里是一样的。郁闷。

背包问题总结

  1. f [ i ] [ j ] f[i][j] f[i][j]是如何从二维优化至一维的。这样在循环时需要将j逆序循环。
  2. f [ i ] [ j ] f[i][j] f[i][j]的定义方式,与相应的初始化方法和最终选择答案的方法
    “恰好体积为j时的最大价值”,则意味着初始化需要将不确定的地方初始化为—INF,最后选择答案时要遍历
    ”体积不超过j时的最大价值",则意味着全部初始化为0即可。最后选择答案时不需要遍历。输出 f [ n ] [ m ] f[n][m] f[n][m]就对了
  3. 先循环物品,再循环体积,最后循环决策
  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值