【刷题】动态规划——背包问题:背包九讲模板

一、01背包

题目链接

1、朴素做法 O(nm)

f[i][j]表示前i个物品,体积和不超过j的情况下所能拿的最大价值。
考虑第i个物品,两种情况:取和不取
不取:f[i][j] = f[i - 1][j]
取:f[i][j] = f[i - 1][j - v[i]] + w[i]

#include <iostream>
using namespace std;
const int N = 1005, M = 1005;
int n, m, v[N], w[N], f[N][M];
int main() {
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; i++) {
		scanf("%d%d", &v[i], &w[i]);
	}
	for (int i = 1; i <= n; i++) {
		for (int j = 0; j <= m; j++) {
			if (j - v[i] >= 0)
				f[i][j] = max(f[i-1][j], f[i - 1][j - v[i]] + w[i]);
			else
				f[i][j] = f[i-1][j];
		}
	}
	printf("%d\n", f[n][m]);
}

2、滚动数组去掉第一维

观察两个递推式
递推式1:f[i][j] = f[i - 1][j]
递推式2:f[i][j] = f[i - 1][j - v[i]] + w[i]

可以看到递推式f[i]只用到了f[i-1]f[1]~f[i-2]再也不会用到,显然可以将f的i维从1至n缩减成1至2,即f[i]可以覆盖f[i-2]的位置。

更进一步看,f的i维可以缩减成1。

对于递推式1,直接用f[i][j]覆盖掉f[i-1][j]的位置,也就是去掉第一维,i仍然是从小到大,转移仍可以顺利进行,因为 f[j]不会在后面被用到。递推式变成f[j] = f[j],这是一个恒等式,可以不写。

对于递推式2,如果j是从小到大,去掉第一维会有问题,因为f[j]在用f[j-v[i]]时,f[j - v[i]]已经在这层循环被更新过了,也就是f[i][j]用的是f[i][j - v[i]]而不是f[i - 1][j - v[i]],和原来的递推式不等价。
红色的是去掉第一维前,蓝色是去掉第一维后那么如何防止f[j]f[j - v[i]]f[j - v[i]]被更新呢?只需让j从大到小循环即可。
在这里插入图片描述

#include <iostream>
using namespace std;
const int N = 1005, M = 1005;
int n, m, v[N], w[N], f[M];
int main() {
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; i++) {
		scanf("%d%d", &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]);
		}
	}
	printf("%d\n", f[m]);
}

二、完全背包

相比01背包,每个物品能取任意个
题目链接

1、朴素做法 O(nm^2)

枚举第i个物品是取0, 1, 2…个
f[i][j] = f[i - 1][j - k * v[i]] + k * w[i]

#include <iostream>
using namespace std;
const int N = 1005, M = 1005;
int n, m, v[N], w[N], f[N][M];
int main() {
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; i++) {
		scanf("%d%d", &v[i], &w[i]);
	}
	for (int i = 1; i <= n; i++) {
		for (int j = 0; j <= m; j++) {
			for (int k = 0; k * v[i] <= j; k++) {
				f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
			}
		}
	}
	printf("%d\n", f[n][m]);
}

2、去掉内层循环 O(nm)

原本的递推式是
递推式1:f[i][j] = max(f[i-1][j], f[i-1][j-v] + w, f[i-1][j-2v] + 2w, f[i-1][j-3v] + 3w, ...)
观察f[i][j-v]的递推式(下划线方便与上面对齐)
递推式2:f[i][j-v] = max(_________f[i-1][j-v], ____f[i-1][j-2v] + w, _f[i-1][j-3v] + 2w, ...)

对比发现
递推式1的第2项f[i-1][j-v] + w 是 递推式2的第1项f[i-1][j-v] 加上w
递推式1的第3项f[i-1][j-2v] + 2w 是 递推式2的第2项f[i-1][j-2v] + w 加上w
以此类推。

将递推式2代入递推式1,(递推式1的第2项之后全替换成 递推式2的第1项之后 + w)
max(f[i-1][j-v] + w, f[i-1][j-2v] + 2w, f[i-1][j-3v] + 3w ... )
= max(f[i-1][j-v], f[i-1][j-2v] + w, f[i-1][j-3v] + 2w, ... ) + w
= f[i][j-v] + w

故递推式变成f[i][j] = max(f[i-1][j], f[i][j-v]+w)

#include <iostream>
using namespace std;
const int N = 1005, M = 1005;
int n, m, v[N], w[N], f[N][M];
int main() {
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; i++) {
		scanf("%d%d", &v[i], &w[i]);
	}
	for (int i = 1; i <= n; i++) {
		for (int j = 0; j <= m; j++) {
			f[i][j] = max(f[i][j], f[i - 1][j]);
			if (j >= v[i])
				f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]);
		}
	}
	printf("%d\n", f[n][m]);
}

3、滚动数组去掉第一维

和01背包不同,这边f[i][j]就是从f[i][j - v[i]]递推而来,要的是这层循环更新后的数据,因此j从前往后。所以直接去掉第一维即可。

#include <iostream>
using namespace std;
const int N = 1005, M = 1005;
int n, m, v[N], w[N], f[M];
int main() {
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; i++) {
		scanf("%d%d", &v[i], &w[i]);
	}
	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]);
		}
	}
	printf("%d\n", f[m]);
}

三、多重背包

和完全背包不同,多重背包每件物品数量有限制,不能取无限个
题目链接

1、朴素做法 O(nms)

和完全背包差不多,k循环加个条件 k <= 物品个数s[i]即可

#include <iostream>
using namespace std;
const int N = 1005, M = 1005;
int n, m, v[N], w[N], s[N], f[N][M];
int main() {
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; i++) {
		scanf("%d%d%d", &v[i], &w[i], &s[i]);
	}
	for (int i = 1; i <= n; i++) {
		for (int j = 0; j <= m; j++) {
			for (int k = 0;  k <= s[i] && k * v[i] <= j; k++) {
				f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
			}
		}
	}
	printf("%d\n", f[n][m]);
}

2、滚动数组去掉第一维

同01背包,j需要从大到小

#include <iostream>
using namespace std;
const int N = 1005, M = 1005;
int n, m, v[N], w[N], s[N], f[M];
int main() {
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; i++) {
		scanf("%d%d%d", &v[i], &w[i], &s[i]);
	}
	for (int i = 1; i <= n; i++) {
		for (int j = m; j >= 0; j--) {
			for (int k = 0;  k <= s[i] && k * v[i] <= j; k++) {
				f[j] = max(f[j], f[j - k * v[i]] + k * w[i]);
			}
		}
	}
	printf("%d\n", f[m]);
}

3、二进制优化 O(nmlogs)

首先多重背包不能像完全背包一样直接去掉内重循环
f[i][j] = max(f[i-1][j], f[i-1][j-v] + w, f[i-1][j-2v] + 2w, f[i-1][j-3v] + 3w, ... , f[i-1][j-sv] + sw)
f[i][j-v] = max(_________f[i-1][j-v], ____f[i-1][j-2v] + w, _f[i-1][j-3v] + 2w, ... , f[i-1][j-sv] + (s-1)w, f[i-1][j-(s+1)v] + sw)
由于多了最后一项f[i-1][j-(s+1)v] + sw,所以并不能直接用f[i][j-v]去取代f[i][j]递推式第2项之后的最大值。

二进制优化是将物品的数目拆分成多组,各组数目是1,2,4,8,16…,每组至多只能选一次。因为1,2,4,8,16…可以凑出任意正整数,例如7可拆成4+2+1,100可拆成64+32+4,都不选就是0。
假如把s拆成1, 2, 4, 8, …, 2k, c,有c < 2k+1 。首先1, 2, 4, 8, …, 2k 可以凑出[0, 2k+1-1]中的任意数,拿这些数+c后可凑出[c, 2k+1-1+c]的任意数,而2k+1-1+c = s,也就是[c, s],又因为c < 2k+1 ,因此[0, 2k+1-1]和[c, s]包括了[0, s]
综上1, 2, 4, 8, …, 2k 可以凑出0 ~ s。

因此把物品数s拆成多组二进制的形式,并用01背包的做法就可以解决多重背包。这样循环k就从O(s)优化成了O(logs)

#include <iostream>
using namespace std;
const int N = 25005, M = 2005;
int n, m, v[N], w[N], f[M];
int main() {
	scanf("%d%d", &n, &m);
	int cnt = 0, tv, tw, s; 
	for (int i = 1; i <= n; i++) {
		scanf("%d%d%d", &tv, &tw, &s);
		int k = 1;
		while (k <= s) {
			cnt++;
			v[cnt] = tv * k;
			w[cnt] = tw * k;
			s -= k;
			k *= 2;
		}
		if(s > 0) {
			cnt++;
			v[cnt] = tv * s;
			w[cnt] = tw * s;
		}
	}
	n = cnt;
	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]);
		}
	}
	printf("%d\n", f[m]);
}

4、单调队列优化 O(nm)

再看这几个递推式
f[i][j] = max(f[i-1][j], f[i-1][j-v] + w, f[i-1][j-2v] + 2w, f[i-1][j-3v] + 3w, ... , f[i-1][j-sv] + sw)
f[i][j-v] = max(_________f[i-1][j-v], ____f[i-1][j-2v] + w, _f[i-1][j-3v] + 2w, ... , f[i-1][j-sv] + (s-1)w, f[i-1][j-(s+1)v] + sw)

简单表示为
f[j]=max(f[j], f[j-v], f[j-2v], f[j-3v], ... , f[j-sv])
f[j-v] = max(__f[j-v], f[j-2v], f[j-3v], ... , f[j-sv], f[j-(s+1)v])
f[j-2v] = max(_________f[j-2v], f[j-3v], ... , f[j-sv], f[j-(s+1)v], f[j-(s+2)v])
f[j-3v] = max(_________________ f[j-3v], ... , f[j-sv], f[j-(s+1)v], f[j-(s+2)v], f[j-(s+3)v])
其实这相当于一个长度为s+1的滑动窗口求最大值问题,可以在循环j的同时用单调队列求窗口内最大值。
(而完全背包则是求前缀最大值,所以用1个数不断更新记录最大值即可,这个数恰好是f[i][j-v]

具体实现可用单调队列存f[i][j]的下标j(也就是体积)而不是最大值。
用下标可直接算出状态转移式。具体如下:
原本的递推式是f[i][j] = f[i-1][j-kv] + kw。不妨设窗口最大值(单调队列队头)存的下标是j'j' = j - kv,于是k = (j - j') / v ,递推式变为f[i][j] = f[i - 1][j'] + (j - j') / v * w

注意递推式的每项加的w数不同,第1项是0w,第二项是1w,第三项是2w…,j越前加的w越多,因此单调队列进队时需要考虑这点,例如j-2vj-5v就差了3w。具体地,j1j2比较时(j1 > j2),需要比较f[i - 1][j1]f[i - 1][j2] + (j1 - j2) / v * w大小。

同时窗口每向右挪动一格,递推式各项都要+w,但各项+w不会改变窗口最大值是哪一项。

不使用滚动数组

#include <iostream>
using namespace std;
const int N = 1005, M = 20005;
int n, m, v[N], w[N], s[N], f[N][M];
int que[M], head, tail; //队列存的元素是下标(体积),即f[i][j]的j 
int main() {
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; i ++) {
		scanf("%d%d%d", &v[i], &w[i], &s[i]);
	}
	for (int i = 1; i <= n; i ++) {
		for (int j = 0; j < v[i]; j ++) {
			head = 0, tail = -1;
			for (int k = j; k <= m; k += v[i]) {
				// 队列不为空,且队头下标在窗口外,队头出队 
				if (head <= tail && que[head] < k - s[i] * v[i]) head++;
				
				// 进队,考虑不同项加的w个数不同 
				while (head <= tail && 
				f[i-1][k] >= f[i - 1][que[tail]] + (k - que[tail]) / v[i] * w[i]) tail --;
				que[++ tail] = k;
				
				// 更新f[i][k]
				if (head <= tail) {
					f[i][k] = max(f[i][k], f[i - 1][que[head]] + (k - que[head]) / v[i] * w[i]);
				}
			}
		}
	}
	
	printf("%d\n", f[n][m]);
}

滚动数组保留两行

只需保留f[i]f[i - 1]即可,用f表示f[i]g表示f[i - 1]

#include <iostream>
#include <cstring>
using namespace std;
const int N = 1005, M = 20005;
int n, m, v[N], w[N], s[N], f[M], g[M];
int que[M], head, tail; //队列存的元素是下标(体积),即f[i][j]的j 
int main() {
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; i ++) {
		scanf("%d%d%d", &v[i], &w[i], &s[i]);
	}
	for (int i = 1; i <= n; i ++) {
		memcpy(g, f, sizeof(f));
		for (int j = 0; j < v[i]; j ++) {
			head = 0, tail = -1;
			for (int k = j; k <= m; k += v[i]) {
				// 队列不为空,且队头下标在窗口外,队头出队 
				if (head <= tail && que[head] < k - s[i] * v[i]) head++;
				
				// 进队,考虑不同项加的w个数不同 
				while (head <= tail && 
				g[k] >= g[que[tail]] + (k - que[tail]) / v[i] * w[i]) tail --;
				que[++ tail] = k;
				
				// 更新f[i][k]
				if (head <= tail) {
					f[k] = max(f[k], g[que[head]] + (k - que[head]) / v[i] * w[i]);
				}
			}
		}
	}
	
	printf("%d\n", f[m]);
}

四、混合背包问题

物品有3类,分别是只能选1次、选无限次、选si次。相当于01背包、完全背包、多重背包混合。
题目链接

还是分两类,取第i件物品和不取第i件物品。
1、不取第i件物品:f[i][j] = f[i - 1][j]
2、取第i件物品:看第i件物品的类别,如果只选1次用01背包的递推式,选无限次按完全背包的递推式,选si次按多重背包的递推式。
2.1、选1次:f[i][j] = f[i - 1][j - v[i]] + w[i]
2.2、选无限次:f[i][j] = f[i][j - v[i]] + w[i]
2.3、选si次:f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i], f[i - 1][j - 2v[i]] + 2w[i], ...) ,数据量较大可用多重背包的二进制优化

#include <iostream>
using namespace std;
const int MAX_N = 1005 * 10, MAX_V = 1005;
int N, V, v[MAX_N], w[MAX_N], s[MAX_N], f[MAX_V];
int main() {
	scanf("%d%d", &N, &V);
	int cnt = 0, tv, tw, ts; 
	for (int i = 1; i <= N; i++) {
		scanf("%d%d%d", &tv, &tw, &ts);
		if (ts == -1 || ts == 0) {
			cnt++;
			v[cnt] = tv;
			w[cnt] = tw;
			s[cnt] = ts;
		}
		else {
			int k = 1;
			while (k <= ts) {
				cnt++;
				v[cnt] = tv * k;
				w[cnt] = tw * k;
				s[cnt] = k;
				ts -= k;
				k *= 2;
			}
			if(ts > 0) {
				cnt++;
				v[cnt] = tv * ts;
				w[cnt] = tw * ts;
				s[cnt] = k;
			}
		}
	}
	N = cnt;
	for (int i = 1; i <= N; i++) {
		if (s[i] != 0) { // 01或多重背包 
			for (int j = V; j >= v[i]; j--) {
				f[j] = max(f[j], f[j - v[i]] + w[i]);
			}
		}
		else { // 完全背包 
			for (int j = v[i]; j <= V; j++) {
				f[j] = max(f[j], f[j - v[i]] + w[i]);
			}
		}
	}
	printf("%d\n", f[V]);
	
}

更优雅的写法(其实就是不存输入)

#include <iostream>
using namespace std;
const int MAX_N = 1005 * 10, MAX_V = 1005;
int N, V, f[MAX_V];
int main() {
	scanf("%d%d", &N, &V);
	int v, w, s; 
	for (int i = 1; i <= N; i++) {
		scanf("%d%d%d", &v, &w, &s);
		if (s == -1) s = 1;
		if (s) { // 01或多重背包 
			for (int k = 1; k <= s; k *= 2) {
				for (int j = V; j >= k * v; j--) {
					f[j] = max(f[j], f[j - k * v] + k * w);
				}
				s -= k;
			}
			if (s) {
				for (int j = V; j >= s * v; j--) {
					f[j] = max(f[j], f[j - s * v] + s * w);
				}
			}
		} 
		else {// 完全背包 
			for (int j = v; j <= V; j++) {
				f[j] = max(f[j], f[j - v] + w);
			}
		}
	}
	printf("%d\n", f[V]);
}

五、二维费用背包问题 O(nvm)

除了体积限制以外,还有重量限制。
题目链接

f[i][j][k]表示前i个物品,体积不大于j,重量不大于k的价值和最大。
可以分成两种,选第i组物品和不选第i组物品
1、不选第i个物品:f[i][j][k] = f[i-1][j][k]
2、选第i个物品:f[i][j][k] = f[i-1][j - v[i]][k - m[i]] + w[i]
写法基本和01背包相同。也可用滚动数组去掉第一维

#include <iostream>
using namespace std;
const int NAX_N = 1005, MAX_V = 105, MAX_M = 105;
int N, V, M, f[MAX_V][MAX_M];
int main() {
	scanf("%d%d%d", &N, &V, &M);
	int v, m, w;
	for (int i = 1; i <= N; i++) {
		scanf("%d%d%d", &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);
		}
	}
	printf("%d\n", f[V][M]);
}

六、分组背包 O(nm)

每组有物品若干个,同一组内的物品最多只能选一个。
题目链接

1、朴素做法

f[i][j]表示前i物品,体积不大于j的价值和最大。
可以分成两种,选第i组物品和不选第i组物品
1、不选第i组物品:f[i][j] = f[i-1][j]
2、选第i组物品:枚举选的是第i组的哪个物品
f[i][j] = f[i-1][j - v[i][k]] + w[i][k] v[i][k]、w[i][k]表示i组第k个物品的体积、价值
写法基本和多重背包相同。

#include <iostream>
using namespace std;
const int N = 105, S = 105, M = 105;
int n, m, v[S][N], w[S][N], s[N], f[N][M];
int main() {
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; i++) {
		scanf("%d", &s[i]);
		for (int j = 1; j <= s[i]; j++) {
			scanf("%d%d", &v[i][j], &w[i][j]);
		}
	}
	for (int i = 1; i <= n; i++) {
		for (int j = 0; j <= m; j++) {
			f[i][j] = f[i - 1][j];
			for (int k = 1; k <= s[i]; k++) {
				if (j - v[i][k] >= 0)
					f[i][j] = max(f[i][j], f[i - 1][j - v[i][k]] + w[i][k]);
			}
		}
	}
	printf("%d\n", f[n][m]);
}

2、滚动数组去掉第一维

同01背包,j从大到小

#include <iostream>
using namespace std;
const int N = 105, S = 105, M = 105;
int n, m, v[S][N], w[S][N], s[N], f[M];
int main() {
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; i++) {
		scanf("%d", &s[i]);
		for (int j = 1; j <= s[i]; j++) {
			scanf("%d%d", &v[i][j], &w[i][j]);
		}
	}
	for (int i = 1; i <= n; i++) {
		for (int j = m; j >= 0; j--) {
			for (int k = 1; k <= s[i]; k++) {
				if (j - v[i][k] >= 0)
					f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
			}
		}
	}
	printf("%d\n", f[m]);
}

七、有依赖背包问题 O(nm^2)

购买某个物品必须要把它的前置物品给买了。
题目链接

由于物品之间有相互依赖关系,这个依赖关系呈树形,因此考虑用树形DP解决。

f[i, j]表示:以第i个物品为根节点的子树,选物品i,总体积不超过j的方案的最大价值。
这边条件加上必须选取i是因为只有购买了i才能考虑购买i的子树

考虑最后状态如何转移,不像01背包那样只要考虑取或不取上一个,这边k个孩子可以选取其中任意个,那么可能的方案就是2k种,将这些方案都考虑一遍显然会超时。

因此不再考虑要选取几个孩子,而是考虑选k体积方案的孩子,其中0 <= k <= m
因为体积j的最优方案必然是从某一个k转移过来,而这边k有m+1种,这就类似分组背包问题,将m+1个k看成1组,从这组中挑选1个转移。

不妨设i的孩子是s1、s2、…、sn,因此转移方程式就变成
f[i][j] = f[i][j-k] + max(f[s1][k], f[s2][k], ..., f[sn][k])

这样乍一看好像是只挑了一个孩子,不符合我们取任意几个孩子的决策,其实不然。这只是针对当前体积从这个孩子里取最大的,体积是其他的时候还会包括选其他孩子的方案。
举个例子,不妨假设最优解是挑s1s2,他们的体积分别是k1k2
那么选s1体积k1的方案,在递归s1时已经包括在f[i][j-k2]中了;递归s2f[i][j-k2]再加上选s2体积k2的方案就得到f[i][j]

PS1:这边的j要从大到小枚举,不然同一组的物品可能选多次

PS2:因为f[i, j]为必须购买i,因此j循环范围是从V-v[i]到0,要先预留物品i的空间

PS3:在遍历完所有孩子后,v[i] <= j <=V的部分必须加上物品i,即f[i][j] = f[i][j - v[i]] + w[i],注意不是f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]),因为不买i是不符合f的定义的

PS4:在遍历完所有孩子后,要对j<= v[i]的部分清0,因为这部分体积的方案只能买子树而不能买物品i,不符合题意

#include <iostream>
#include <cstring>
using namespace std;
int N, V, v[105], w[105], f[105][105];
int head[105], val[105], nxt[105], len;

void add_node(int a, int b) {
	val[len] = b;
	nxt[len] = head[a];
	head[a] = len ++;
}

void dfs(int u) {
	for (int i = head[u]; i != -1; i = nxt[i]) {
		int son = val[i];
		dfs(son);
		
		for (int j = V - 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 j = V; j >= v[u]; j--) f[u][j] = f[u][j - v[u]] + w[u];
	for (int j = v[u] - 1; j >= 0; j--) f[u][j] = 0;
}

int main() {
	scanf("%d%d", &N, &V);
	int p, root;
	memset(head, -1, sizeof(head));
	for (int i = 1; i <= N; i ++) {
		scanf("%d%d%d", &v[i], &w[i], &p);
		if (p == -1) {
			root = i;
			continue;
		}
		add_node(p, i);
	}
	
	dfs(root);
	printf("%d\n", f[root][V]);
	return 0;
} 

八、泛化物品

在这里插入图片描述

九、背包问题问法的变化

1、不同条件的背包(体积限制改成不少于/恰好是,求价值由最大改为最小等)

1、初始化不同。
初始化只需看i=0(一个都不选)的情况,i>0可由i=0递推而来。
“不超过” 的初始化要将f[0][j]全部设为0,是因为体积不超过j,包括了体积为0的选法
“恰好是” 的初始化只有f[0][0]=0,其余都是无穷,因为一个都不选体积只能是0,不能是其他值。
“至少是” 的初始化只有f[0][0]=0,其余都是无穷。因为一个都不选体积只能是0,所以只有体积至少是0才是合法的。

2、j的循环范围不同
“不超过”和“恰好是”的情况下,j - v[i] < 0是无意义的,体积不超过-1和恰好是-1都是不可能的选法,因为体积最小就是0。
”至少是“的情况下,j - v[i] < 0的时候仍然可以进行状态转移,因为体积至少是-1、-2、-3也是有意义的,它包括了体积>=0的选法。可将负数的部分答案记在f[i][0]

体积条件写法
不超过f全初始化为0;循环时保证j-v[i]>=0
恰好是f[0]=0,其余无穷;循环时保证j-v[i]>=0
至少是f[0]=0,其余无穷;循环时不必保证j-v[i]>=0

ps1.初始化取正无穷还是负无穷取决于问题求价值最大值还是最小值,求最大取负无穷,求最小取正无穷。
ps2. 如果不是求价值问题,初始化0和无穷可根据题意更改,例如求方案数往往f[0][0] = 1,其余是0。

2、背包问题求具体方案(字典序最小)

题目链接
以01背包为例,状态转移只有两种可能
不取第i件物品:f[i][j] = f[i - 1][j]
取第i件物品:f[i][j] = f[i - 1][j - v[i]] + w[i]
如果取和不取的得到的答案一样,那就选取的方案,这样输出的字典序会更小。(字典序只要相同位置小就更小,跟长度无关,’\0’当成最小的,例如123<21,12<123)因为选了i在放这位,肯定比不选i,选i之后的放在这位更好。例如i是3,选了i是1235,不选i是125,1235 < 135。

具体实现时,比较f[i][j]f[i - 1][j - v[i]] + w[i] 的值,相同则输出i。但这样就是从后往前推,输出的方案会优先包括n更大的,也就是将字典序最大的解倒序输出
要想输出字典序最小的解,只需要把输入顺序进行反向,输出N - i + 1即可,也就是把第1个物品当成第n个,第2个物品当成第n - 1个…

#include <iostream>
using namespace std;
const int MAX_N = 1005, MAX_V = 1005;
int N, V, f[MAX_N][MAX_V], v[MAX_N], w[MAX_N];
int main() {
	scanf("%d%d", &N, &V);
	for (int i = N; i >= 1; i--) {
		scanf("%d%d", &v[i], &w[i]);
	}
	for (int i = 1; i <= N; i++) {
		for (int j = 1; j <= V; 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 j = V;
	for (int i = N; i >= 1; i--) {
		if (j >= v[i] && f[i][j] == f[i - 1][j - v[i]] + w[i]) {
			printf("%d ", N - i + 1);
			j -= v[i];
		}
	}
}

广为流传的貌似是另一种写法,不改变输入顺序和输出,改变递推式推导的方向,从第一个往后推,也可以达到相同的效果。

#include <iostream>
using namespace std;
const int MAX_N = 1005, MAX_V = 1005;
int N, V, f[MAX_N][MAX_V], v[MAX_N], w[MAX_N];
int main() {
	scanf("%d%d", &N, &V);
	for (int i = 1; i <= N; i++) {
		scanf("%d%d", &v[i], &w[i]);
	}
	for (int i = N; i >= 1; i--) {
		for (int j = 1; j <= V; 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 j = V;
	for (int i = 1; i <= N; i++) {
		if (j >= v[i] && f[i][j] == f[i + 1][j - v[i]] + w[i]) {
			printf("%d ", i);
			j -= v[i];
		}
	}
	return 0; 
}

3、背包问题求体积为V的方案数

把递推式的max改成相加即可。
01背包可参考:数字组合【求体积恰好为V的方案数】
完全背包可参考:买书【体积恰好是V求方案数】

4、背包问题求最优解(最大价值)的方案数

求的是取到最大价值的方案数
题目链接

开一个数组g[i, j]表示f[i, j]取最大值的方案数。

考虑是第i个物品取的价值大还是不取的价值大。
不取价值大:f[i, j] = f[i - 1, j]g[i, j] = g[i-1, j]
取价值大:f[i, j] = f[i-1, j-v[i]] + w[i]g[i, j] = g[i-1, j-v[i]]
取和不取一样大:g[i, j] = g[i-1, j] + g[i-1, j-v[i]]

注意这边g[i, j]只表示f[i, j]的方案数,条件是“前i个物品,体积恰好是j的最大价值方案数”,可能f[i', j']也能达到最优解,这种方案数记录在g[i', j']中,因此要把所有能达到最优解的f对应的g累加起来

#include <iostream>
using namespace std;
int N, V;
int f[1005], g[1005];
const int mod = 1e9 + 7;
int main() {
	scanf("%d%d", &N, &V);
	int v, w;
	g[0] = 1;
	int max_f = 0;
	for (int i = 1; i <= N; i ++) {
		scanf("%d%d", &v, &w);
		for (int j = V; j >= v; j --) {
			if (f[j] < f[j - v] + w) {
				f[j] = f[j - v] + w;
				g[j] = g[j - v];
			}
			else if(f[j] == f[j - v] + w) {
				g[j] = (g[j] + g[j - v]) % mod;
			}
		}
	}
	int ans = 0;
	for (int i = 0; i <= V; i ++) {
		if (f[i] == f[V]) ans = (ans + g[i]) % mod;
	}
	printf("%d\n", ans);
	return 0;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值