一天天的为了啥。|

lgh_2009

园龄:1年粉丝:1关注:0

简单dp 学习笔记

1. 背包

1.1 背包模型的概述

n 种物品,每种物品有若干个。拿一件物品付出 wi 代价获得 vi 价值,问最多花费 V 的代价能获得的最大价值。

1.2 0/1背包

考虑每种物品只有一个的情况。

我们设 fi,j 表示前 i 个物品,花费了 j 的最大价值。

于是得出 dp 方程:fi,j=max(fi1,j,fi1.jwi+vi)

我们发现,fi,j 都是从 fi1,j 转移而来,所以考虑压掉 i。需要发现,在转移时要倒序转移,使得每个物体只被选一次。

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

1.3 完全背包问题

即每种物品有无数个。

考虑同样的 dp 转移,只是将倒序枚举改为正序,这样就变成选无限个了。

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

1.4 多重背包问题

给出每种物品有 si 个,怎么做?

一个最简单的想法是直接把 si 个看成不同的 si 种,但是这样的复杂度是 O(s×V) 的,不可接受。

考虑倍增,将物品拆成 2021,22,,2logs 个,复杂度变成了 O(logs×V)

int cnt=0;
for(int i=1,v,w,s;i<=n;i++){
rd(w,v,s);
for(int j=1; j<=s; j<<=1) w[++cnt]=j*w,v[cnt]=j*v,s-=j;
if(s) w[++cnt]=s*w,v[cnt]=s*v;
}
for(int i=1; i<=cnt; i++)
for(int j=V; j>=w[i]; j--)
f[j]=max(f[j],f[j-w[i]]+v[i]);

1.5 “恰好为 V”

问能否从中选取若干件总价值恰好为 V

fi,j 表示前 i 件物品能否取到恰好 j

转移同理。

1.6 有依赖的背包

P1064

考虑依赖关系,对子集进行讨论,枚举子树哪些被选。

1.7 背包的方案计数

考虑如果不需要最优解,直接将取 max 改为求和即可。

否则记 gi 表示 fi 最大时的方案数,看从哪里转移过来输出即可。

2. 区间 dp

区间 dp 经典方程式:fi,j=max(fi,k+fk+1,j+cost(i,j))

P1880

断环为链,直接做。

3. 树形 dp

3.1 simple

P1352

fi,0/1 表示选/不选 i 的最优情况。

于是可以知道 fi,0=max(fj,0,fj,1)+ai,fi,1=fj,0+ai,其中 ji 的儿子。

dfs 转移。

3.2 树上背包

P2014

建图,发现是一个森林。

我们设 fu,i,j 表示以 u 号点为根的子树中,已经遍历了 u 号点的前 i 棵子树,选了 j 门课程的最大学分。

转移的过程结合了树形 DP 和 背包 的特点,我们枚举 u 点的每个子结点 v,同时枚举以 v 为根的子树选了几门课程,将子树的结果合并到 u 上。

记点 x 的儿子个数为 sx,以 x 为根的子树大小为 siz_x,可以写出下面的状态转移方程:

fu,i,j=maxv,kj,ksiz_vfu,i1,jk+fv,sv,k

注意上面状态转移方程中的几个限制条件,这些限制条件确保了一些无意义的状态不会被访问到。

f 的第二维可以很轻松地用滚动数组的方式省略掉,注意这时需要倒序枚举 j 的值。

可以证明,该做法的时间复杂度为 O(nm)

3.3 换根 dp

换根 dp 通过预处理换根的贡献,使得仅用两次 dfs 可以处理一些问题。

通常在出现在无根树或者需要对每个点计算答案时考虑换根。

P3478"

不妨令 u 为当前结点,v 为当前结点的子结点。首先需要用 si 来表示以 i 为根的子树中的结点个数。su=1+sv
考虑计 fu 为以 u 为根时,所有结点的深度之和。

考虑让根从 u 换到 v,这样 v 的子树中的点的 dep--, v 字数外的点的 dep++,可以知道 fv=fu+n2×sv

直接转移即可。

4.DAG 上 dp

考虑拓扑排序,从入度为 0 的点开始记忆化搜索,这样可以方便的计算答案。

5.状压 dp

5.1 朴素的状压 dp

如果遇到 n 较小的题目,考虑用二进制表示每个位置的状态。

考虑 P2704,记录当前有炮兵的位置,在计数时对其他的地方加上贡献即可。

5.2 一些常用的优化技巧:

两边 dp(与 meet in middle 同理)、阈值分治。

5.3 子集 dp

有时候需要枚举一个数的子集来转移给这个数。

但是如何快速的枚举子集的子集?

考虑这样一个东西:

int full=1<<n;
for(int s=0; s<full; s++)
for(int s1=s; s1; s1=(s1-1)&s)
......

考虑这个 s1 的意义:依次消去 s 的每一位,故为枚举子集。

这个东西用二项式定理证出来是 O(3n) 的。

本文作者:lgh_2009

本文链接:https://www.cnblogs.com/lgh-blog/p/18046725

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   lgh_2009  阅读(6)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起