DP学习笔记
前言
因为自己学的快忘的也快,故在此记录一些,有待补充。
目录
1.背包问题
2.区间DP
3.树形DP
4.状压DP
线性DP
感觉范围很广,没有什么明显的特征,大概简单的大部分人都会吧,不想讲什么。其实是自己水平不够
背包问题
弱鸡只会01背包(bushi),总之是个只能背背板子的蒟蒻。
for(int i=1;i<=n;i++)
for(int j=m;j>=v[i];j--)
f[j]=max(f[j],f[j-v[i]]+w[i]);
记得要倒序枚举,不然一个物品会被用多次,如果正序枚举就变成完全背包了。
还有个二维01背包,就多加一个维度,没什么好说的。
多重背包似乎要二进制优化?打算等学了单调队列直接跳到最优,就先不讲了。分组背包没有做过题,但是树上背包似乎和它比较像,这也不讲。
然后好像就没什么背包了,总之就这么水过去了吧……
区间DP
由于是有点久之前学的了,所以忘得可能有点多,能写多少写多少吧。
经典例题:上古NOI真题石子合并。
口胡分析:观察题目可以看出此具有明显的区间合并性质,可以划分成一个一个区间,然后维护区间的信息,通过DP求解。
for(int L=2;L<=n;L++)
for(int i=1;i<=2*n-L+1;i++)
{
j=i+L-1;
fmin[i][j]=1e9;
for(int k=i;k<j;k++)
{
fmin[i][j]=min(fmin[i][j],fmin[i][k]+fmin[k+1][j]+sum[j]-sum[i-1]);
fmax[i][j]=max(fmax[i][j],fmax[i][k]+fmax[k+1][j]+sum[j]-sum[i-1]);
}
}
嗯,区间DP的题一般都比较模板,只有内部的状态转移方程要变,所以可以背板子了。
注意要先枚举区间长度,然后枚举得到两个端点,再枚举这个区间是由那两个区间合并而来的就行了。
经典断环成链的技巧不要忘记,就是把长度复制成两倍,接着再一个个枚举断开的地方就行。
NOIP真题能量项链,跟石子合并一样板子。
进阶指南上的例题[IOI1998]Polygon是比较好的,有思维含量,值得一做。
二维区间DP以后再学,先立个flag在这。
树形DP
这块大概是NOIP关于DP最重要的部分了,毕竟树这东西总是受到很多人青睐,而且花样也格外的多。
经典例题一:没有上司的舞会
最简单的树形DP了,只要用一个f数组表示每个人和他的所有下属创造的最大价值,并进行简单的分类讨论即可。
f[x][0]表示此人不来,那么可以在它的下属来与不来里选一个价值最大的加上去
也就是f[x][0]+=max(f[y][0],f[y][1])
,其中y是x的下属之一。
f[x][1]表示此人会来,那么他的下属肯定不会来,那么只要使其加上f[y][0]即可。不过别忘了f[x][1]要初始化为x的快乐指数。
读入的时候顺便可以计算一下每个人的入度,入度为零的就是根节点。
最后的答案就是在f[root][0]与f[root][1]里面取最大值。
还有一道很像的题:战略游戏。可以稍微做一做,练练手。
经典例题二:[CTSC1997]选课
树上背包经典题,和分组背包很像。首先可以用f[x][t]表示在以x为根的基础上学t门课程可以得到的最大学分。然后我们可以用分组背包的思想,在一个子节点的子树上选j门课,剩余的选t-j门课,进行状态转移。
因为可能有多门课程没有先修课,所以我们需要建一个虚拟节点0来作为所有没有先修课的课程的根,当然由于是虚拟的,加上根的学分时不用考虑虚拟节点。最终答案即为f[0][m]。
具体实现还得见代码。
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 t=m;t>=0;t--)
for(int j=t;j>=0;j--)
if(t-j>=0)f[x][t]=max(f[x][t],f[x][t-j]+f[y][j]);
}
if(x!=0)
{
for(int t=m;t>0;t--)
f[x][t]=f[x][t-1]+s[x];
}
}
习题:有线电视网
换根DP
树形DP的变种之一,经典模型如下:
给你一棵无根树,要你选一个根,使得所有节点的深度之和最大。
一个最简单的想法就是枚举每一个点为根,分别计算以它为根时的节点深度之和,最后去比较大小。
但是面对 \(10^6\) 的数据规模,这个算法肯定会超时,所以我们要另寻他路。
我们可以先任选一个点作为根,计算出以它为根的所有节点深度之和,并存进f数组。用f[x]来表示以x为根的深度之和。
看图
我们先以1号节点为根,然后观察可以发现,当根从1号节点转换到它的子节点:4号节点时,以4号节点为根的子树中的节点深度都减一,而剩余的节点深度都加一。
我们可以用size[x]代表以x为根的子树里有多少节点,这样我们就可以得到转移的方程式。
\(f[y]=f[x]+n-2*size[y]\)
剩余的节点数即为n-size[y],然后再减去size[y],便得到了如上式子。
size数组可以在一开始求f[1]时就预处理得到,然后我们再通过第二次遍历将所有的f都求出来,最后就可以比较了。
主要部分代码
void dp(int x,int fa)
{
size[x]=1;dep[x]=dep[fa]+1;
for(int i=head[x];i;i=Next[i])
{
int y=ver[i];
if(y==fa)continue;
dp(y,x);
size[x]+=size[y];
}
}
void dfs(int x,int fa)
{
for(int i=head[x];i;i=Next[i])
{
int y=ver[i];
if(y==fa)continue;
f[y]=f[x]+n-2*size[y];
dfs(y,x);
}
}
通过模型还可以造出一些简单的变式,如加上点权的Tree with Maximum Cost以及同时加上点权和边权的Great Cow Gathering G。这些只需在模板上做一点简单的改动即可。
状压DP
全称是状态压缩DP,此类DP的模型是给你一个 \(n\times m\) 的地图,让你计算在里面放不重合的 \(k\) 个图形的方案数。
我的表达能力并不好,所以我们还是先看到状压的入门题:k国王问题
可以发现,这道题的数据范围非常小,但是状态非常多,用搜索肯定会超时,这时候就要使用状压DP了。其实,如果你看到这么小的数据范围,且用搜索难以解决的题,那多半就是状压DP。
状态压缩的思想我们在将bfs的时候就已经说过,即把每一种状态缩成一个数字,这样方便我们进行DP。
状压DP一般使用二进制进行状态压缩,在本题中,我们就可以用一个 \(n\) 位的二进制数来代表压缩后的状态。若第 \(j\) 位为1,则代表这里放了一个国王,为0则没有放。
我们可以用 \(f[x][j]\) 代表第x行,状态压缩后的数字为j来代表方案数。
首先要知道,我们不可能存下所有的状态,那样数组也开不下,因此需要预处理出可以选择的状态,从 \(0-2^n-1\) 的所有状态中,只有每两位1之间都有至少一个0的状态才能被选。
因为由题意可知,若两个1靠在一起,表示有两个国王会互相攻击,那么这种状态不符合题目要求,肯定不能被选。
在预处理结束后,还需要判断当前行的状态与上一行的状态是否符合,也就是我们在进行状态转移时,一定要这一行的状态与上一行的状态不会冲突才可以转移。
注意:状压DP的很多判断都要通过位运算来快速实现,因此一定要用好位运算,并能看懂每种位运算操作所代表的含义
说这么多,可能还是不如代码有效,那就看代码吧
模板题AC代码
#include<iostream>
#include<cstdio>
using namespace std;
long long ans,f[11][151][151];
int n,k,s0,s[155],num[155];
int main()
{
cin>>n>>k;
for(int i=0;i<(1<<n);i++)
{
if(i&(i<<1))continue;
int cnt=0;
for(int j=0;j<n;j++)
if(i&(1<<j))cnt++;
num[++s0]=cnt;
s[s0]=i;
}
f[0][1][0]=1;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=s0;j++)
for(int kk=0;kk<=k;kk++)
if(kk>=num[j])
for(int t=1;t<=s0;t++)
if(!(s[t]&s[j])&&!(s[j]&(s[t]<<1))&&!(s[j]&(s[t]>>1)))
f[i][j][kk]+=f[i-1][t][kk-num[j]];
}
for(int i=1;i<=s0;i++)ans+=f[n][i][k];
printf("%lld",ans);
return 0;
}
习题:炮兵阵地