DP动态规划学习笔记——高级篇上
说了要肝的怎么能咕咕咕呢?
不了解DP或者想从基础开始学习DP的请移步上一篇博客:DP动态规划学习笔记
这一篇博客我们将分为上中下三篇(这样就不用咕咕咕了...),上篇是较难一些树形DP,中篇则是数位和状压DP,下篇则是各种DP的优化手段。
——正片开始—— (为啥我最近的博客都喜欢写这个)
背包类树形DP,树形DP里一种很鬼畜的题目。
简单点讲就是:树上的分组背包。不知道分组背包的也请前往上一篇学习。
我们先来看一道板子题:选课
然后我们一起分析一下这道题(最好自己先想一想),由于每门课的先修课只有一门,所以我们很容易想到用树形结构储存这种关系——>只能从父节点到子节点。但是,这一题的数据会形成一个森林。想一想,为什么?
没错,可能不止一门课没有先修课。但是我们只学过树形DP,怎么办呢?简单,我们自己整一棵树出来呗。
但是原来的父子关系又不能变,怎么整呢?我们新建一个“虚拟课程”0出来,作为“没有先修课的课程”的先修课。也就是说我们把森林里的每一棵树都连到一个新的根节点——0节点上,这样我们就得到了一棵树。
这个想法是不是很妙?然后这题就转换成了一个在树上运行的分组背包。
然后我们来设状态。不会?那你别学了。上一篇博客看了没?快去看。
我们上一篇讲过了,树形DP一般以每个节点x作为第一维...
然后呢?(锤),分组背包啊。哦...
于是我们就得到状态了:设f[x,j]表示我们从以x为根的子树中选j门课能获得的最高学分。
很简单是不是?状态都出来了还不知道方程咩?那我们一起来推一推吧。
我们定义几个变量方便描述,设son(x)表示x的子节点集合,siz(x)表示x的子节点个数,如果选修x这门课,那我们就对于任意y∈son(x),可以从以y为根的的子树中选出若干门课(记为ci)来修...在满足Σci=t-1的基础上,我们尽量要修最高的学分。我们有siz(x)组物品,每组物品有j-1个,其中第i组物品的第k个物品的体积为k,价值为f[y,k],背包的总容积为j-1。
我们从每组中选出至多一个物品,即每个子节点最多转移一个状态到x。我们修完x后,还可以选j-1门课,所以我们要在“物品体积”不超过j-1的情况下,使学分(价值)最大。也就是说我们把“上多少y内的课”作为物品。
然后我们进行树形DP,得到答案:
void dp(int x){ f[x][0]=0; for(int i=0;i<son[x].size();i++){ int y=son[x][i]; dp(y); for(int j=m+1;j>=1;t--)//虚拟节点必选,所以是m+1 for(int k=0;k<=j-1;k++)//组内的物品,第x组第k个物品(选k门课)的体积为k f[x][j]=max(f[x][j],f[x][j-k]+f[y][k]); } }
主函数部分,我们读入每个f[i][1]作为第i门课的价值——>解释:从以i为子树的课中选1门——>价值就是f[i][1]。
接下来我们还是继续讨论树形DP。
我们目前做的树形DP题都是在有根树上进行树形DP的,如果题目给定是一棵无根树呢?
难道我们需要对以每个点为根都做一次树形DP统计答案吗?当然不是,要是这么做还不如暴力呢。
一般对于这种题目,我们用二次扫描(换根法)来解决。
这种DP又被称为换根DP,什么?你想要题?
没有,(爆锤狗头)...拿去 Accumulation Degree。
这好像是我第一次给POJ上的题,皮欧勾的题我都没怎么刷过,不过貌似质量挺好的。
但是我永远喜欢洛谷.jpg,emm,你说你看不懂?Chrome自带翻译,不皮了不皮了。
喏。
给你一个树形的水系(废话那么多懒得翻译了)。
有一个有n个节点,n-1条河道的树形水系,每个河道有一个最大容水量c[x][y]c[x][y]表示点x到y的最大容水量,源点可以源源不断出水,以源点作为根节点的树的叶子结点可以无限接纳水,而一个节点水的流量等于流过其儿子节点的水的流量之和,儿子节点水的流量不能超过其与父亲连边的最大容水量,询问最大的源点水流量,n≤2×10^5。
其实简单点说就是求树形结构上的最大流,但是我们不知道源点。为什么不用网络流?嘘,数据太狗了。
于是我们来快乐地DP啊。不给我源点?我每个点都DP一遍。
要是这样能过我还写它干嘛。
不过还是先讲讲怎么DP吧。假设我们知道根节点是x。
那么我们来拆问题了。你看这个问题它又大又烦,不如把它拆掉。
考虑用树形DP拆问题。对于每个节点x,我们发现它只能流向自己的子树,于是可以这样设状态,我们用f[x]表示在以x为根的子树中,以x为源点流向子树的最大流量。然后我们对于每个子树都可以拆成小子树,再拆,再拆...就到叶节点了,问题就得解了。
切,刚刚还那么傲娇的大问题现在还不是被脱的一件不剩,看到DP的魅力了吧?再大的问题,只要你是DP题,我就能把你拆掉。
然后我们就很自然的得到了转移方程式:
这里我不多写一个latex下面的就会炸不知道为什么,你们就当没看见那个error吧...抱歉抱歉
$$f[x]= \sum\limits_{\text{y∈son(x)}} \begin{cases} min(f[x],c(x,y))& \text{x=0}\\ c(x,y)& \text{x!=0} \end{cases}$$
$$f[x]= \sum\limits_{\text{y∈son(x)}} \begin{cases} min(f[x],c(x,y))& \text{x=0}\\ c(x,y)& \text{x!=0} \end{cases}$$
Latex新手写上面那个公式写了半个小时...
DP代码:
void dp(int x){ vis[x]=1; f[x]=0; for(int i=head[x];i;i=nxt(i)){ int y=to(i); if(vis[y])continue; dp(y); if(deg[y]==1)f[x]+=val(i); else f[x]+=min(f[y],val(i)); } }
如果我们直接枚举源点...时间复杂度是O(n^2)的,接受不了(不是我接受不了,是出题人接受不了)
(如果我自己要求,O(n^n)的算法我都开开心心用...)
于是我们使用换根DP来O(n)的解决这道题目。
我们任选一个源点作为根,记为rt。然后我们进行一次上面的DP,得到f数组。
设g[x]表示把x作为源点,流向整个水系,流量最大是多少。对于根节点rt,显然有f[rt]=g[rt]。
假设g[x]已经被正确地求出了。开始了,万能的假设法...
我们考虑一下它的子节点y,g[y]我们并不知道,我们来分析一下g[y]。
如果我们把根换成y,那么g[y]包含两部分:
1. 从y流向以y为根的子树的流量,即我们上面算出来的f[x]
2. 从y沿着原父节点x流向水系中的其他部分的流量
为什么可以像2这样流呢?因为我们把根换成y了。这是很多博客没有提到的,容易让人看得很懵逼。
我们把x作为源点的总流量是g[x],从x流向y的流量就是min(f[y],c(x,y))。
这个很好理解吧,y是x的子节点,x流向子树的最大流量是f[y],流量限制是c(x,y),哪个小流量就是哪个。
所以从x流向除了y以外其他部分的流量就是两者之差。这个也很好理解吧。
于是我们把y作为源点,先流向x,再流向其他部分的流量就是两者之差和c(x,y)之间的最小值。
同样的,对于度数为1的x节点,我们也要特判——>节点x没法流向其他地方了。
于是我们得到了g[x]的计算方法:
又要写Latex...我裂开了
$$g[y]=f[y]+ \begin{cases} min( g[x]-min( f[y], c(x,y) ), c( x,y ) )& \text{x的度数>1}\\ c(x,y)& \text{x的度数=1} \end{cases}$$
g[y]就是把根从x换成y后流量的计算结果,由于这是一个从上至下的递推方程,所以我们可以通过一遍dfs得出g数组。
又到了你们最爱的放代码时间:
void dfs(int x){ vis[x]=1; for(int i=head[x];i;i=nxt(i)){ int y=to(i); if(vis[y])continue; if(deg[x]==1)g[y]=f[x]+val(i); else g[y]=f[y]+min(g[x]-min(f[y],val(i)),val(i)); dfs(y); } }
解释一下,val(i)就是上面说的c(x,y),我把它当作边权存了起来。
然后我们在main函数里面这样搞就可以得到答案了。
int rt=1; dp(rt); memset(vis,0,sizeof vis); g[rt]=f[rt]; dfs(rt); int ans=-1; for(int i=1;i<=n;i++) ans=max(ans,g[i]);
OK,那么希望你们通过这道例题,对换根DP有了一个初步的了解。
其实换根法的思想就是通过差值来更新答案,而不必一遍遍枚举计算重复信息。
那么,我们中篇再见(咕咕咕)。