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 imes 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;
}

习题:炮兵阵地

原文地址:https://www.cnblogs.com/luotao0034/p/14040251.html