背包(笔记合集)
一般代码只是例子,具体使用依据题目来, DP是一种思想,代码都以属性为最大值等等为例子
01背包
最基本的背包
简单说就是有n个物品和容量为m的包,求其max/min/方案数等等即属性
一般转移方程为f[i][j]
意思为在前i个里容量为j的情况下的要求的属性
(可忽略)一般这里的转移是在f[i][j]
,第i个数取与不取
时间复杂度O(n*m)
一般代码
for (int i = 1; i <= n; i ++ ) // 枚举当前几个物品
{
int v, w;
cin >> v >> w;
for (int j = 1; j <= m; j ++ )
{
f[i][j] = f[i - 1][j];
if (j >= v) f[i][j] = max(f[i][j], f[i - 1][j - v] + w);
}
}
一维优化
for (int i = 1; i <= n; i ++ ) // 枚举当前几个物品
{
int v, w;
cin >> v >> w;
for (int j = m; j >= v; j -- ) // 体积
f[j] = max(f[j], f[j - v] + w);
}
必须倒序枚举体积,我们的f[j - v]用的实际是f[i - 1][j - v], 而正序枚举我们可能会先更新了
f[j - v]使得后面用到f[j - v]时实际用的是f[i][j - v]从而错误, 只要倒序就可以解决此问题
完全背包
即有n种物品,每种无限个
一般的转移方程为 f[i][j] = max(f[i - 1][j], f[i][j - v]);
优化代码
O(n*m)
for (int i = 1; i <= n; i ++ )
{
int w, v;
cin >> v >> w;
for (int j = 0; j <= m; j ++ )
{
f[i][j] = f[i - 1][j];
if (j >= v) f[i][j] = max(f[i][j], f[i][j - v] + w);
}
}
朴素版
O(n*m*k)
for (int i = 1; i <= n; i ++ )
{
int v, w;
cin >> v >> w;
for (int j = 1; j <= m; j ++ )
for (int k = 0; k <= j / v; k ++ )
f[i][j] = max(f[i][j], f[i - 1][j - v * k] + w * k);
}
方程推导
正常的思路:
f[i][j] = max(f[i - 1][j], f[i - 1][j - v] + w, f[i - 1][j - 2v] + 2w,...,f[i][j - kv] + kw)
k∈(0, j/v);
这种复杂度过高,一般接受不了
一般的方程f[i][j] = max(f[i - 1][j], f[i][j - v]);
f[i][j - v]
如何得来? (注意max中的对应)
f[i][j] = max(f[i - 1][j], f[i - 1][j - v], f[i - 1][j - 2v],...,f[i][j - kv])
########f[i][j - v] = max(f[i - 1][j - v], f[i - 1][j - 2v],...,f[i][j - kv])
k是相同的
可得f[i][j] = max(f[i - 1][j], f[i][j - v]);
加上价值就是f[i][j] = max(f[i - 1][j], f[i][j - v] + w);
一维优化
for (int i = 1; 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);
}
这里只能正序枚举,和01背包相反,因为我们转移的实际上是f[i][j - v]
,正序可以提前更新f[j - v]
使他实际上成为f[i][j - v]
符合要求倒序则为f[i - 1][j - v]
不合要求
多重背包
有n种物品但是每种物品有数量限制
朴素代码
for (int i = 1; i <= n; i ++ )
{
int w, v, s;
cin >> v >> w >> s;
for (int j = 0; j <= m; j ++ )
for (int k = 0; k <= min(s, j / v); k ++ )
f[i][j] = max(f[i][j], f[i - 1][j - k * v] + k * w);
}
二进制优化
把物品的个数S拆成一堆数,形成一个单独的物品,最后采用01背包去做
一般采用二进制的方法
因为一堆二进制数加起来可以加成任何数
如0001,0010,0100,1000这四个二进制数相加,可以组成任何小于等于1111的数(正数)
for (int i = 1; i <= n; i ++ )
{
int v, w, s, k = 1;
cin >> v >> w >> s;
while (k <= s)
{
V[ ++ cnt] = v * k;
W[cnt] = w * k;
s -= k; // 这样拆是logn个比较高效
k <<= 1;
}
if (s > 0) // 我们减到最后可能会有剩下的,也加上
{
V[ ++ cnt] = v * s;
W[cnt] = w * s;
}
}
for (int i = 1; i <= cnt; i ++ )
{
int v = V[i], w = W[i];
for (int j = m; j >= v; j -- )
f[j] = max(f[j], f[j - v] + w);
}
一般够用
单调队列优化
口诀:比你小还比你强,你就出列了~~~
时间复杂度为O(nm)
因为一般m都比较大所以常用[[DP的通用优化#滚动数组]]
代码
for (int i = 1; i <= n; i ++ )
{
int w, s, v;
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)
{
if (hh <= tt && q[hh] < k - s * v) hh ++ ;
while (hh <= tt && g[q[tt]] - (q[tt] - j) / v * w <= g[k] - (k - j) / v * w) tt -- ;
q[ ++ tt] = k;
f[k] = max(f[k], g[q[hh]] + (k - q[hh]) / v * w);
}
}
}
思路
首先你要知道在s个物品限制下转移方程为(一般情况s >= m / v
)
这里推荐看我以前的笔记
f[i][j] = max(f[i - 1][j], f[i - 1][j - v] + w, f[i - 1][j - 2v] + 2w, ... , f[i - 1][j - sv] + sw);
f[i][j - v] = max(f[i - 1][j - v], f[i - 1][j - 2v] + 1w, ... , f[i - 1][j - sv] + (s - 1)w, f[i - 1][j - (s + 1)v] + sw);
f[i][j - 2v] = ...
f[i][j - 3v] = ...
...
r = j % v;
f[i][r + v] = max(f[i - 1][r + v], f[i - 1][r] + w)
f[i][r] = f[i - 1][r]
经典的图啊
根据完全背包优化的思路我们可以得到上面的图(也可以看我过去笔记的图片)
我们能发现一个事情,因为有s的限制,在一般情况下,我们没法直接用f[i][j - v]来转移
but我们可以从它的余数出发, f[i][r], 这个是值固定的,往上f[i][r + v]看图
越往上我们会发现,在他们的max内的第一个都在增加v(r + v), 且当s > j / v时它就开始滑动(像f[i][j], f[i][j - v], max的数量一样,但是f[i][j]
向前进了1个v, 且w的值都加了1w, 除了新进来的)
这就想到了单调队列,可以靠它维护这个区间最大值,优化的点,保持队列队头最大,利用这个最大,优化计算量;并且可以计算滑动窗口内的最大值
看看代码,多想想
因为过于复杂还是看y的课比较好。
以前的笔记
如果你还有记忆的话应该会懂得
一般比二进制优化快
二维费用背包
很简单就是有两个费用,多开一维,记录费用即可非常简单
状态一般为 f[i][j][k]
在前i个数内,在两个费用都不超过j和k的情况下的属性
可以和上面三种背包结合,相当于前缀
一般代码(二维费用01背包)
for (int i = 1; i <= n; i ++ )
{
int w, v, t;
cin >> v >> t >> w;
for (int j = m; j >= v; j -- )
for (int k = p; k >= t; k -- )
{
f[j][k] = max(f[j][k], f[j - v][k - t] + w);
}
}
不少于问题
之前没整理到这个
简单说就是对于费用不少于的问题,这也可以和上面三种背包连用,相当于前缀
对于这个直接DP分析即可,关于不少于,直接设f[i][j]
为在前i个里面,费用不少于j的情况
那么第i个可选可不选,不选就是f[i - 1][j]
,选的话是f[i - 1][j - v]
这时候可能会有j < v的情况,这时候也要转移,因为它符合,费用不少于j(大于了当然可以),写成这个就行f[i - 1][max(0, j - v)]
。从相当于从0直接到j。
这时候回头想想,好像之前的当j < v的时候就不转移了(可看看上面代码),这是因为之前状态是费用最大为j,这就是本质的差距。
恰好问题
这是一类小问题,状态设计要改变为,恰好,转移没什么变化,但初始化有变动,一定要先设置为不可能情况,因为对于一种状态
像有许多石头,价值和重量无关,求恰好重量为j时的最大价值,石头有(前面是价值,后面是重量){1, 1},{2, 1},{2, 2}
当f[2][3]转移的时候,f[1][2]这种情况是不可能的,所以f[2][3] = max(f[1][2] + 2)
整个转移是转移不了的,但是价值不大于j时是可以的。这就是它的本质
求方案数
这类其实有点技巧,最稳妥的方法是,把题目改为恰好,这里用一个例题来说明。
求最优方案数
AcWing 11. 背包问题求方案数 - AcWing
题中要求不超过j方案最大价值的方案数,
有两种大方向
- 利用不超过直接求解,因此f初始化都为0,对于每种情况
f[i][j]
最少有1种方案,然后转移时如果可以更新就直接更替cnt,相等就加上cnt,最后直接输出cnt[n][m]
即可 - 利用恰好,把不超过j,改为恰好为j,转移和上面一样,然后在
f[n][j]
中记录,最大值,并把所有的最大值相等的方案加上。
如上代码在上面链接中
因此对于一个问题,除了特殊记录以外,就可以直接转移或利用恰好凑出方案
而转移时如果可以更新就直接更替cnt,相等就加上cnt,这是所有方案问题的最基本的思想,不限于动态规划
关于方案的保存
问题在保存选的方案,像这个AcWing 1013. 机器分配 - AcWing比较典型,除了求解答案,还要输出每个工厂选择的机器数,可以dfs,可以像我这样原路推回去(实际上更多使用这种),
没有固定的方式,这里只说明有此类问题。
枚举的状态
正常背包的转移为f[i][j] = max(f[i][j], f[i - 1][j - v] + w)
我们依赖于第i - 1个物品进行转移,实际上还可以依赖体积,来转移。
思路为
{不选}
{选择体积最大为k的子集,并加上这个点} k <= j
对于正常的背包,这是没有用的,但对于树形等物件间有特殊关系的,有奇效.
AcWing 10. 有依赖的背包问题 - AcWing这题就需要对于每个子树分配体积,来计算
否则复杂度将爆炸
有依赖的问题
分为很多种,例如可能依赖是树形的,可能是线性的,等等。
中心思想就是转移好子集,不能言传。
AcWing 10. 有依赖的背包问题 - AcWing
487. 金明的预算方案 - AcWing题库
上面是较好的一道,也是较难的一道。
下面的较为简单适合入入门。
求具体方案
这种会问你最后选择的方案是是什么,而不是方案数,可能有限制,如选择字典序最小的最优选择方案是什么。这种有两种思路。
- 直接保存方案,利用string,或者vector,在转移过程中保存方案,通过比较方案来选择最小字典序,这种一般较慢,但很稳定。
- 先求出最优方案,再利用最优方案,从大枚举,或从小枚举,推出选择方案。这种较快,但边界和枚举设计需要考虑清楚。
AcWing 12. 背包问题求具体方案 - AcWing
具体代码如上
关于状态转移的奇技淫巧
- 有时候我们不能直接转移状态,而要合成状态,像
f[i + 1][j + v] = ...
这时候可能会有“卡壳”情况,即我们无法直接判断情况,这时候不妨从0出发把for (int i = 1; i <= n; i ++ )
变为for (int i = 0; i < n; i ++ )
这样“退位”,在某些时候有奇效如这题P1156 垃圾陷阱而这就是刷表法,[[刷表法和填表法]]
变化的价值/体积(泛化物品的背包)
对于一类题,它的价值和体积会随着你的选择顺序不同而改变,这时候需要考虑优先级,
常用方法
先设x,y为两个相邻物品,然后列出(如价值)的不等式,像如此价值的情况
先x后y w = (p + c[x]) * b[x] + (p + c[x] + c[y]) * b[y] (1)
先y后x w = (p + c[y]) * b[y] + (p + c[y] + c[x]) * b[x] (2)
我们如果要让x在前的话,那么(1) > (2)
可以解得 c[x] * b[y] > b[x] * c[y]
,然后我们利用这个性质排序,再进行背包处理
做了那么多题,对于背包,正确的物品排序确实很重要。
给出对应的例题试试吧P1417 烹调方案 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
在最值情况下的最值
这类题题意如求在最大价值1的情况下,最小的价值2为多少,即在最大价值1下的最小价值2,
这种算是比较好做,只要在转移出最大价值1的同时,记录最小价值2,更新时替换,相等时对比求最小。注意,前提是最大价值1,所以要对它Dp