背包
背包问题
我们可以从集合的角度来看待这个问题,主要是如何表示状态以及进行状态间的转移(决策)。
通常可以考虑两个角度
-
状态表示
- 集合:当前这个状态代表的具体的含义
- 属性:它的特点,如最大最小等
-
状态计算
状态计算本质是对集合的划分,再进一步想其实是对于当前这个集合怎么划分才能不重不漏(不一定不重,但一定不能遗漏)以便于后面进行集合的决策转移,也就是划分成更小的子集使这些子集都可以被计算出来。
优化的根本是有一个东西代表一类东西,而且大部分是在DAG上做递推,我当前这个状态如果从所有能够到他的状态都枚举决策过,那么当前这个状态一定是最优的(最优子结构),因此对于下一个状态就不用再从头开始转移了,他只会用到一些最优子结构来形成一个新的最优子结构。一般枚举状态的时候要满足拓扑序,保证当前这个状态进行转移的时候他的所有的前驱一定都是最优的了,不能有后效性。
01背包
在一些限制下,每个物品最多选一次。
状态表示 :f(i,j) :表示从前i个里面选体积不超过j的最大价值
状态计算:可以通过第i个物品选于不选将集合划分成两个子集,且不重不漏
转移:$f(i, j) = max(f(i-1,j),f(i-1,j-v[i]+w[i]))$。
发现当前这个状态是用到了第$i- 1$层的状态,如果我们想用滚动数组将$f$优化到一维,我们可以倒叙枚举$j$,因为只会从他前面更小的$j$,因为是倒叙枚举的所以前面更小的$j$对应的还是第$i-1$层的状态。
转移:$f(j) = max(f(j),f(j-v[i] + w[i]))$。
完全背包
在一些限制下每个物品可以选无数次。
状态表示:f(i,j): 表示从前i个里面选体积不超过j的最大价值
状态计算:可以通过将第i个物品选多少个将集合划分,且补充不漏
对于第i个物品体积不超过j则有:$f(i,j)=max(f(i-1,j),f(i-1,j-v)+w,f(i-1,j-2v)+2w,...,f(i-1,j-kv)+kw)$
对于第i个物品体积不超过j-1则有:$f(i, j-v)= max(\ \ \ \ f(i-1,j-v)+w,f(i-1,j-2v)+2w,...,f(i-1,j-kv)+kw)$
将下面带入上面的发现$f(i,j)=max(f(i-1,j),f(i, j - v)+w)$
转移:$f(i,j)=max(f(i-1,j),f(i, j - v)+w)$
发现当前状态转移过来用到的是第$i$层的状态,因此滚动数组优化只需要正序枚举体积即可。
转移:$f(j)=max(f(j),f(j-v)+w)$
多重背包
在一些限制下每个物品可以选一定的次数。
思路一(暴力)
发现我们多加一维来表示第i个选多少个即可,或者假设某个物品可以选k次那就把它拆成k个一样的物品即可,时间复杂度为$O(nm)$
核心代码
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 - v[i] * k] + w[i] * k);
思路二(二进制优化)
考虑优化暴力里的第二个方法,不把k拆成k个1,我们把k转化成二进制形式假设为1011,则有$1011=0111+100$,我们可以把它拆成$1,2,4,4$这样,这样是可以组合出$0-11$里的任何一个数的,因为$111$每一位选与不选可以组合出$0-7$中任何一个数,然后对于$100$这个数选与不选相当于让$0- 7$往后平移$4$,所以可以凑出任何一个数。复杂度降到了$O(mlogn)$
核心转化代码
for (int i = 1; i <= n; i ++ ) {
int a, b, s;
cin >> a >> b >> s;
int k = 1;
while (k <= s) {
cnt ++;
v[cnt] = k * a;
w[cnt] = k * b;
s -= k;
k <<= 1;
}
if (s > 0) {
cnt ++;
v[cnt] = s * a;
w[cnt] = s * b;
}
}
思路三 (单调队列优化)
分组背包
在一定限制下每组物品最多选一个。
状态表示:f(i,j):从前i组里选,且体积是j的最大价值
状态计算:我们可以多加一维循环来枚举第i组里选哪个,这样就变成了01背包
转移:$f[i][j]=max(f[i][j],f[i-1][j-v[i][k]]+w[i][k]);$
可以像01背包一样倒着枚举体积优化掉第一维
转移:$ f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);$
有依赖的背包问题
对于一颗树选择某个结点就必须选择他的父节点。
状态表示:f(i,j):以i为根的子树必须选根节点,且体积不超过j的最大价值
状态计算:对于每个结点$u$来说,先递归处理它的子节点当子结点信息正确后再向父节点进行转移,将集合划分成当前结点的这个子节点的体积是多少,可以将每一棵子树看成一个物品组,就变成了一个分组背包问题。
核心代码:
int dfs(int u) {
for (int i = v[u]; i <= m; i ++ ) f[u][i] = w[u]; // 因为u必选所以给成立的加上一个u的价值
for (int i = h[u]; ~i; i = ne[i] ) {
int v = e[i]; dfs(v);
for (int j = m; j >= v[u]; j -- )
for (int k = 0; k <= j - v[u]; k ++ ) // 留给选根结点的体积,这样转移的过程中任意一个f(i,j)中均含有w[u]这个价值,因为向他转移的那个f(u,j-k)中一个含有w[u],所以就满足了依赖性
f[u][j] = max(f[u][j], f[u][j - k] + f[v][k]);
}
}