背包
母题1:01背包
考虑 \(f[i,j]\) 表示前 \(i\) 个物品体积为 \(j\) 的答案,答案可以从前 \(i-1\) 个物品继承或选取当前物品,\(f[i,j]=\max(f[i-1][j],f[i-1][j-v]+w)\)。
母题2:完全背包(from 1)
状态同上,转移本来应该为 \(f[i,j]=\max(f[i-1][j-v]+w,f[i-1][j-2v]+2w,\dots,f[i-1][j-kv]+kw)\)(设 \(j-kv\ge 0,j-(k+1)v<0\))。发现 \(f[i,j-v]=\max(f[i-1][j-2v]+w,f[i-1][j-3v]+2w,\dots,f[i-1][j-kv]+(k-1)w)\),恰好是少了一个 \(w\),于是我们可以用 \(f[i,j]=\max(f[i][j-v]+w)\)。
可以用有限背包计数问题的思想:加入一个数,对所有数+1两类。
母题3:多重背包(from 1),理解方式
转移应该为 \(f[i,j]=\max(f[i-1,j-v]+w,f[i-1,j-2v]+2w,\dots,f[i-1,j-sv]+sw)\)(这里不管超出的情况)。
可以直接暴力,也有两种优化:
- 二进制优化,很好理解,对于一个物品若可选 \(5\) 个,可以拆成两个物品 \(1\) 倍和 \(4\) 倍,然后做
01
背包,带一只 \(\log\)。 - 单调队列优化,不好理解,就是对于 \(j\bmod v\) 同一个余数的情况一起处理,发现对于每种物品就是长度为 \(s\) 的定长滑动窗口求最值模型。P1776
母题4:分组背包(from 1)
就是某些物品在一个组内,组内只能选其一。
转移可以用 \(f[i,j]=f[i-1,j-v_k]+w_k\)(注意,同一组的物品对应一个 \(i\),这样从上一层转移可以保证不会出现一组内的物品选多个)。
母题5:有依赖的背包问题(树上背包 \(v\neq 1\) 的情况)/树上背包(from 4),重要!
acwing:https://www.acwing.com/activity/content/problem/content/1281/
y总写法:https://www.acwing.com/activity/content/code/content/118840/
不推荐y总写法,直接在输入时读入更方便,代码如下:
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 110;
int n, m;
int v[N], w;
int h[N], e[N], ne[N], idx;
int f[N][N];
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
void dfs(int u)
{
for (int i = h[u]; ~i; i = ne[i]) // 循环物品组
{
int son = e[i];
dfs(e[i]);
// 分组背包
for (int j = m; j >= 0; j -- ) // 循环体积
for (int k = 1; k <= j - v[u]; k ++ ) // 循环决策
f[u][j] = max(f[u][j], f[u][j - k] + f[son][k]);
}
}
int main()
{
cin >> n >> m;
memset(h, -1, sizeof h);
int root;
for (int i = 1; i <= n; i ++ )
{
int p;
cin >> v[i] >> w >> p;
f[i][v[i]]=w;// 将物品u加进去
if (p == -1) root = i;
else add(p, i);
}
add(0,root);
dfs(0);
cout << f[0][m] << endl;
return 0;
}
/*
加入0->root的解释:
假如只有一个点,如果在输入时插入,且 V>v1,又因为dfs不执行了,就会输出0,所以还是要把0作为根(如果按照y总写法最后加入就不需要)。(同样有多个点时也可能出现这种情况)或者说最后取一遍max。
*/
优化
可以参考 blog :
- https://www.cnblogs.com/ouuan/p/BackpackOnTree.html(严谨证明以后(希望有以后)再看)
- https://blog.csdn.net/m0_73386348/article/details/131838916(这篇博客分析有误,但是总结树形DP的分类是不二之选)
- https://www.cnblogs.com/purplevine/p/16751390.html(已经联系博主深刻理解了,先看这篇博客,一下分析基于此)
还有一篇博客:https://www.cnblogs.com/maple276/p/18005291,简单情况:https://www.cnblogs.com/maple276/p/12870572.html
解释一下正确性:\(j\) 的下界是 \(\max(k - siz[u], 1)\)(这里的 \(j,k\) 按照 blog 3.),\(1\) 不用解释,\(k-siz[u]\) 是因为已处理的子树最多只有 \(siz[u]\) 个物品,如果 \(v\) 侧选了小于 \(k-siz[u]\) 个物品,无论 \(u\) 侧选多少物品都无法总共选出 \(k\) 个物品。\(j\) 的上限是 \(\min(k, siz[v])\) 则是 \(j\) 处无法选出多于 \(k\) 个物品,也无法选出多于自己有的物品数的物品。而对于 \(k\) 的上界是因为子树内最多 \(siz[u]+siz[v]\) 个物品(注意这样枚举的话 \(dp\) 数组的含义发生变化,本来是可以出现什么 \(f[x][siz[u]+siz[v]+1]\) 可以通过 \(f[x][?]\) 过来,现在没了,所以本来是求 \(\le V\) 的,现在变成恰好为 \(V\),对于“选课”答案还是 \(f[0][m]\),因为保证 \(m\le n\),所以可以装满,又因为学分为正,所以 \(f[0][m]\) 最优。)
由于是 01
背包且 \(v=1\),所以 \(m\le n\),否则直接选完完事(因为价值 \(>0\)),所以下面我们都考虑 \(m=n\) 的极端情况,此时只有一个变量 \(n\)。
注意到树上背包有两种写法,一种直接输入到 \(dp\) 数组,另一种先处理子孙,在 dfs
函数中最后加入(需要偏移),为了与网上的博客保持一致防止错误,全部写成第一种。
注意一下复杂度分析讨论的都是“选课”一题的情况,\(v=1\),对于有依赖的背包问题,由于 \(sz_x\) 存储的是 \(\sum v\times [在子树内]\),不可以这样分析。
注意链接中的代码 \(j,k\) 的含义可能与上文恰好相反,以下代码邻接表那里数组多开了一倍的数组存边(因为这道题是有向边)。
- 选课,把 \(0\) 作为根,\(m+=1\),直接输入 \(f[i][1]\),然后标准形式。数据加强,加一个二维映射成一维。
- 有依赖的背包问题,\(v\neq 1\)输入的是 \(f[i][v_i]=w_i\),同样要构建虚拟节点(见开头),然后标准形式。
只加上界优化不能保证复杂度是 \(O(n^2)\),必须要一起加!虽然能过选课加强版,但是根据 blog 3. 可以卡成链,就变为 \(O(\dfrac{1}{3}N^3)\) 的复杂度(常数根据平方和 \(n\) 趋于极限计算,见豆包AI,只有我自己可以看)。
1.的上下界优化:https://www.luogu.com.cn/record/173156416
1.的仅上界优化:https://www.acwing.com/problem/content/submission/code_detail/36537651/
2.的仅上界优化:https://www.acwing.com/problem/content/submission/code_detail/36537651/
2.的上下界优化:https://www.acwing.com/problem/content/submission/code_detail/36533419/
注意两道题枚举时要写成 j<=k-v
,因为必须保证当前必选。其中 \(v\) 为节点 \(x\) 的体积,两道题一个是 \(1\),一个是不定的。
容易发现,1. 是 2. 的特殊情况而已。
注意到 2. 最后需要取 max
(这里取 \(\max\) 可以同时解决开头代码注释内的问题和这里的问题,所以不用加虚拟节点了,我们就直接最后取一遍 \(\max\)),原因是 2. 的条件下不一定装满,而 1. 在上面解释过了。
两道题都需要尽量输入的时候就处理到物品的体积和价值到对应的 \(dp\) 数组里,因为树上背包的一般形式是:
// O(n^3)
for(int k = siz[u] + siz[v]; k >= 0; k--){
dp[u][k] = inf;
for(int j = 1; j <= siz[v] && j <= k; j++){//本题中应是j<=k-当前的v
dp[u][k] = min(dp[u][k], dp[u][k - j] + dp[v][j]);
}
}
siz[u] += siz[v];
// O(n^2)
for(int k = siz[u] + siz[v]; k >= 0; k--){
dp[u][k] = inf;
for(int j = max(k - siz[u], 1); j <= min(siz[v], k); j++){//同上
dp[u][k] = min(dp[u][k], dp[u][k - j] + dp[v][j]);
}
}
siz[u] += siz[v];
实测:上界优化效果可能比下界更好。
优化的复杂度分析
先别管树和抽象的倒序(倒序仅仅是01背包的需要),就只考虑两个背包:假设我们要把背包 \(f\) 和背包 \(g\) 合起来,那么就是 \(f[i],g[j]\rightarrow k[i+j]\),我们先假设是当前点 \(u\) 的子节点 \(s_1\sim s_m\)(\(s\) 表示子树大小)合并,那么发现第一次是 \(1+s_1\) 与 \(s_2\),第二次是 \(1+s_1+s_2\) 与 \(s_3\),大概就是 \(s\) 两两相乘,考虑 \(siz[a] * siz[b]\) 就是 \(a\) 子树内和 \(b\) 子树内各取一个点的方案数,任取一对 \((a’,b’)\),它们仅会在 \(lca(a’, b’)\) 处产生贡献,即 \(lca(a,b)\) 处,因为这里能产生贡献的 a’, b’ 必须位于不同的子树,但是,\(a,b\) 的 lca
就是现在考虑到的点。如果不加下界优化,当 \(j\) 很小时,选在 \(u\) 这个地方的体积就会超出去,而这是无意义的,上下界优化实际上保证的就是不会出现冗余情况,保证每个点对合并一次。
类似的证明:https://www.cnblogs.com/maple276/p/12870572.html 的开头部分
母题6:退背包(少见,就见过两道题)
https://www.cnblogs.com/wscqwq/p/17725571.html
根据物品插入无顺序,所以可以将要删去的数当作刚刚插入,就可以正着删除(因为是倒着插入)。
以上就是6个模型,接下来用x.表示母题x的变式
6
类背包,表示状态类似于,体积变为余数,考虑 \(+,\times\)。1.
8
本质还是背包,分类讨论,状态是由差值表示。1.
[26]
利用背包求解博弈论,体积变为余数。需要知道基本的必胜态、必败态。
[-20]
1.+矩阵快速幂。
[ABC287F] Components
5.+使用刷表法更简单,求连通块是多少的答案(这一维是背包)、树形考虑当前节点选不选丢入状态。
飞扬的小鸟
母题1,2的结合。主要2.
体积=高度。
大部分转移是2.,一个是1.,所以2.得用临时数组存储,再和1.取优。
碰顶特判。
AcWing 1155. Emiya 家今天的饭
背包问题拆解成前后两个部分,第一部分类似于01背包(用来满足前两个性质),可以使用前缀和优化;第三个性质难以满足,故再次DP把主导菜与其他菜差加入状态。第二部分还是01背包,维护差值。
AcWing 233. 换教室
最短路需要考虑相邻的情况,所以状态里加上一维。
通过期望的性质拆解贡献,配合最短路。1.
[ABC320F] Fuel Round Trip
有点像背包,但是考虑到对称点的限制,我们需要新立状态,按照一对一对的点,而不是前多少个点。1.
粉刷匠
首先是一个经典的线性DP的预处理,然后做一遍分组背包。4.
AcWing 487. 金明的预算方案
暴力枚举所有情况,拆物品(与5.思想不同,5.是拆分体积)分组背包即可。4.
有限背包计数问题
与根号分治结合,后半部分2.有划分数的思想:只有加入一个数,对所有数+1两类。
3.(统计数量所以用类前缀和)+2.
货币系统
需要证明找的 \(b\in a\)。
先排序,用小的组合出大的(因为小的不能用大的组合),就是给定一些物品,问凑出的个数,考虑2.,改变一下属性为 bool
。
每次遇到一个物品就看它能不能被凑出(能凑出就没必要存在了)。
A 马
考虑最后一匹马,活动只有三种,因此直接把活动设计到状态中。
总结
背包问题一般比较难的都是1.(可以和各种其他母题结合)(7很明显是完全背包,而且转移就是基本的转移,比母题还简单)。
转移有时需要分类讨论。
多种背包可能同时出现,一个背包问题可能可以拆解成好几个部分,两个部分有可能都是DP,也可能有一个是其他算法。
还可以出现与树形DP等结合使用。
多重背包类型的题目用单调队列(最优)或(类)前缀和(计数)。
分组背包拆分物品可以按照体积或者组合种类。
解法
- 写出转移式,确定种类。(多种见hint)
- 什么数值作为前 \(i\) 个物品,挖掘什么数值作为体积。
可以明确的按照前几个物品算出答案,并且可以从题目中挖掘出体积。
题目中出现的数值可以成为“体积”。
如果类似于斐波那契数列这样的转移,可以使用矩阵快速幂优化。
hint:如果1.+2.等多种背包在一个状态里转移(通过step1.判断),组合往往需要临时数组辅助分出两类转移。(题目分成两部分两次背包不属于此范畴)。
题目如果有多种约束,可以分类(如AcWing 1155. Emiya 家今天的饭,前两个好满足一类,第三个不好满足一类),利用容斥原理或其他技巧。
背包+求期望利用线性性拆解。
状态里可能有当前位置 \(i\) 是否操作的属性。
状态的设计需要考虑求解的需求(如最短路对于最后一个位置选不选的要求)。
对称结构可以按照对称的一对来定义前 \(i\) 个。
定义
描述: 有 \(N\) 件物品和一个容量为 \(V\) 的背包。第 \(i\) 件物品 的费用是 \(c[i]\),价值是 \(w[i]\)。求解将哪些物品装入背包可使价值总和最大。
解析:\(N\) 件物品可以看做是有 \(N\) 种备选方案,容量为 \(V\) 的背包可以看做是成本为 \(V\),\(c[i]\) 是每种方案的成本,价值是每种方案带来的效益。那么问题就变成了,在不超过总成本V的情况下,使用哪些备选方案,可以使效益最大。
根据定义可知,背包问题 V3 不属于背包问题。