201904动规总结——天降神坑

emmm…动规上的差不多了,写份总结。
动规,全称动态规划,英文缩写dp…
停停停,干嘛呢干嘛呢,写总结不是写介绍动规!
啊?哦…
那按动规的分类说吧…

一、线性(入门)dp

具有线性“阶段”划分的动规。
动规入门,比较简单,主要就是锻炼我们找如何找状态转移方程。
经典题目:LIS问题,LCS问题,数字三角形。
(三道入门题目,也是基本模板,不会陌生吧)
好吧,(我这么善良)还是给一下代码吧

LIS问题

一道升级版的题目及代码献上
在这里插入图片描述
代码是很久前的没错了

#include<bits/stdc++.h>
using namespace std;
#define so1(a,n) sort(a+1,a+n+1)
#define so2(a,n) sort(a+1,a+n+1,mycmp)
#define ll long long
#define f1(i,l,r) for(int i=l;i<=r;i++)
#define f2(i,r,l) for(int i=r;i>=l;i--)
#define me(a,b) memset(a,b,sizeof(a))
ll n,a[700010],t,k[5],lis[700010],l,r;
bool mycmp(int x,int y)
{
	return x<y;
}
int sr(int f)
{
	if(f==1||f==4)
	    return 999999999;
	else
	    return -999999999;
}
int sc(int f,ll st,ll nd)
{
	if(f==1||f==4)
	    return min(st,nd);
	else
	    return max(st,nd);
}
bool check(int f,ll x,ll y)
{
	if(f==1)
	    return x<y;
	if(f==2)
	    return x>y;
	if(f==3)
	    return x>=y;
	if(f==4)
	    return x<=y;
}
void GG(int f)
{
	k[f]=1;
	f1(j,1,n)
	{
		lis[j]=sr(f);
		l=1;
		r=k+1;
		while(l+1<r)
		{
			t=(l+r)>>1;
			if(check(f,lis[t],a[j]))
			    l=t;
			else
			    r=t;
		}
		if(check(f,lis[l],a[j]))
		    l=r;
		k=max(k,l);
		lis[l]=sc(f,lis[l],a[j]);
	}
	return;
}
void init()
{
	cin>>n;
	f1(i,1,n) cin>>a[i];
}
void work()
{
	f1(i,1,4)
	    GG(i);
}
void print()
{
	f1(i,1,4)
		cout<<k[i]<<endl;
}
int main()
{
	freopen("data.in","r",stdin);
	freopen("data.out","w",stdout);
	init();
	work();
	print();
	return 0;
}
LCS问题

题面可能和最经典的有些不同
在这里插入图片描述
代码是很久前的没错了++,虽然没上面那篇早

#include<bits/stdc++.h>
using namespace std;
#define s1(a,n) sort(a+1,a+n+1)
#define s2(a,n) sort(a+1,a+n+1,mycmp)
#define ll long long
#define sg string
#define st struct
#define f1(i,l,r) for(int i=l;i<=r;++i)
#define f2(i,r,l) for(int i=r;i>=l;--i)
#define f3(i,a,b) for(int i=a;i;i=b)
#define me(a,b) memset(a,b,sizeof(a))
#define sf scanf
#define pf printf
#define fo freopen 
int f[5010][5010]={},q=0,l0,l1;
sg s[2];
char ch;
bool mycmp(int x,int y){
	return x>y;
}
void init(){
	while(cin>>ch){
		if(ch>='A'&&ch<='Z') s[q]+=ch;
		else if(ch=='.') q++;
	}
	l0=s[0].size();
	l1=s[1].size();
}
void work(){
	f1(i,1,l0){
		f1(j,1,l1){
			if(s[0][i-1]==s[1][j-1]) f[i][j]=max(f[i][j],f[i-1][j-1]+1);
			else f[i][j]=max(f[i-1][j],f[i][j-1]);
		}
	}
}
void print(){
	pf("%d",f[l0][l1]);
}
int main(){
	fo("lcs.in","r",stdin);
	fo("lcs.out","w",stdout);
	init();
	work();
	print();
	return 0;
}
数字三角形

这题是标准的了
在这里插入图片描述
啊,这个早到我连宏定义都没有的时候了…刚好方便阅读

#include<bits/stdc++.h>
using namespace std;
int n,a[1010][1010],sum[1010][1010];
int dg(int x,int y)
{
	if(sum[x][y]>=0) return sum[x][y];
	else if(y==n)    sum[x][y]=a[x][y];
	else             sum[x][y]=max(dg(x+1,y),dg(x+1,y+1))+a[x][y];
	return sum[x][y];
}
int main()
{
	freopen("numtri.in","r",stdin);
	freopen("numtri.out","w",stdout);
	cin>>n;
	for(int i=1;i<=n;i++)
	    for(int j=1;j<=i;j++)
	        cin>>a[i][j];
	memset(sum,-1,sizeof(sum));
	cout<<dg(1,1);
}

貌似线性dp一般是作为签到题的吧…
也没什么好多说的了,过了。

二、背包dp

这是线性dp中一类重要but特殊的模型,最重要的几个具体提一下

1、01背包

模型(可能会有另外一些附加条件,这里给出最基本的):
给定 n n n个物品,其中第 i i i个物品的体积为 v [ i ] v[i] v[i],价值为 w [ i ] w[i] w[i]。有一容积为m的背包,要求选择一些物品放入背包,使得物品总体积不超过 m m m的前提下,物品总价值和最大。
解法呢,就是用“已经处理的物品数”作为dp阶段,以“背包中已经放入的物品总体积”作为附加维度。
即用 f [ i ] [ j ] f[i][j] f[i][j]表示从前i个物品中选出总体积不超过j的物品放入背包时物品最大价值和。
转移方程(条件为 i f ( j &gt; = v [ i ] ) if(j&gt;=v[i]) if(j>=v[i])):
f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] , f [ i − 1 ] [ j − v [ i ] ] + w [ i ] ) f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i]) f[i][j]=max(f[i1][j],f[i1][jv[i]]+w[i])
前者表示不选第 i i i个物品,后者表示选第 i i i个物品
初始值:
f [ 0 ] [ 0 ] = 0 f[0][0]=0 f[0][0]=0,其余均为负无穷。
目标:
f [ n ] [ m ] f[n][m] f[n][m]
如果发现 i i i阶段的状态只与 i − 1 i-1 i1阶段有关,为减少空间我们可以用“滚动数组”(空间复杂度从 O ( n m ) O(nm) O(nm)降为了 O ( m ) O(m) O(m))。
但注意,01背包中我们需要使用倒序循环才能保证第 i i i个物品不会再第二次放入背包,即保证每个物品是唯一的。
好了,差不多了,下一块…

2、完全背包

模型:
给定 n n n种物品,其中第 i i i种物品的体积为 v [ i ] v[i] v[i],价值为 w [ i ] w[i] w[i],并且有无数个。有一个容积为 m m m的背包,要求选择若干个物品放入背包,使得物品总体积不超过 m m m的前提下,物品的价值总和最大。
考虑传统的二维线性dp,用 f [ i ] [ j ] f[i][j] f[i][j]表示前 i i i种物品中选出总体积不超过 j j j的物品时价值最大为多少。
转移方程(条件为 i f ( j &gt; = v [ i ] ) if(j&gt;=v[i]) if(j>=v[i])):
f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] , f [ i ] [ j − v [ i ] ] + w [ i ] ) f[i][j]=max(f[i-1][j],f[i][j-v[i]]+w[i]) f[i][j]=max(f[i1][j],f[i][jv[i]]+w[i])
前者表示不选第 i i i个物品,后者表示选一个第 i i i种物品放入背包
初值:
f [ 0 ] [ 0 ] = 0 f[0][0]=0 f[0][0]=0,其余均为负无穷。
目标:
f [ n ] [ m ] f[n][m] f[n][m]
这里是不是感觉和01背包差不多呢?
所以它也可以和01背包一样省略第一维进行滚动数组。
接下来就是揭示它们的不同了。
完全背包需要用正序循环而非逆序,这对应着每种物品可以放入无限次。
当然,一般你会01背包了,完全背包也就差不多了。
ok,下一块…

3、多重背包

模型:
给定 n n n种物品,其中第 i i i种物品的体积为 v [ i ] v[i] v[i],价值为 w [ i ] w[i] w[i],并且有 c [ i ] c[i] c[i]个。有一个容积为 m m m的背包,要求选择若干个物品放入背包,使得物品的总体积不超过 m m m的前提下,物品的价值总和最大。
这类题一般来说有三种常见方法:
(1)直接拆分
把第 i i i种物品看作独立的 c [ i ] c[i] c[i]物品,转化为01背包计算,就是时间复杂度么…会有一点点大,小数据可以过,大数据不一定。
(2)二进制拆分法
听名字挺高级的啊
咳,关注点错了哈,切正题切正题…
众所周知,从 2 0 , 2 1 , 2 2 , ⋅ ⋅ ⋅ , 2 k − 1 2^0,2^1,2^2,···,2^{k-1} 20,21,22,,2k1,这 k k k个数中选出若干个相加能表示出 0 0 0 2 k − 1 2^k-1 2k1之间的任何整数。于是,我们可以把数量为 c [ i ] c[i] c[i]的第 i i i种物品拆成体积为 2 0 ∗ v [ i ] , 2 1 ∗ v [ i ] , ⋅ ⋅ ⋅ , 2 p ∗ v [ i ] , r [ i ] ∗ v [ i ] 2^0*v[i],2^1*v[i],···,2^p*v[i],r[i]*v[i] 20v[i],21v[i],,2pv[i],r[i]v[i] p p p表示满足 2 0 + 2 1 + ⋅ ⋅ ⋅ + 2 p &lt; = c [ i ] 2^0+2^1+···+2^p&lt;=c[i] 20+21++2p<=c[i]的最大整数, r [ i ] = c [ i ] − 2 0 − 2 1 − ⋅ ⋅ ⋅ − 2 p r[i]=c[i]-2^0-2^1-···-2^p r[i]=c[i]20212p),这 p + 2 p+2 p+2个物品就可以凑成 0 0 0 c [ i ] ∗ v [ i ] c[i]*v[i] c[i]v[i]间所有能被 v [ i ] v[i] v[i]整除的数,并且不可能大于 c [ i ] ∗ v [ i ] c[i]*v[i] c[i]v[i],等价于原题中可以使用 0 0 0 c [ i ] c[i] c[i]次效率较高
(3)单调队列
一种很神奇的方法,就是使用单调队列优化,使得时间复杂度低到 O ( n m ) O(nm) O(nm)只是我没怎么用过…所以不过多解释啦 )。
下一块…

4、分组背包

模型(好重复啊… ):
给定 n n n组物品,其中第 i i i 组中有 c [ i ] c[i] c[i]个物品。第 i i i组的第 j j j个物品的体积为 v [ i ] [ j ] v[i][j] v[i][j],价值为 w [ i ] [ j ] w[i][j] w[i][j]。有一容积为 m m m的背包,要求选择若干个物品放入背包,使得每组至多选择一个物品并且物品总体积不超过 m m m的前提下,物品的价值总和最大。
所以接下来也会有点重复
任然先考虑原始的线性dp,为满足“每组至多选择一个物品”,要利用“阶段”线性增长的特征,把“物品组数”作为dp的“阶段”,使用过了第 i i i组的物品就将状态转移到第 i + 1 i+1 i+1组, f [ i ] [ j ] f[i][j] f[i][j]用来表示前 i i i组中选出总体积不超过 j j j的物品放入背包,物品的最大价值和。
转移方程:
f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] , f [ i − 1 ] [ j − v [ i ] [ k ] ] + w [ i ] [ k ] ) ( 1 &lt; = k &lt; = c [ i ] ) f[i][j]=max(f[i-1][j],f[i-1][j-v[i][k]]+w[i][k])(1&lt;=k&lt;=c[i]) f[i][j]=max(f[i1][j],f[i1][jv[i][k]]+w[i][k])(1<=k<=c[i])
初值和目标应该不用我说了吧。
注意,对于每一组内 c [ i ] c[i] c[i]个物品的循环应放在倒序循环内层才能保证正解,顺序千万不能混淆。
另外还有一些不一一说明了,反正总的是有背包九讲,另外5种分别提一下:混合前3种背包,二维费用背包,有依赖的背包,泛化物品,问法的变化。
然后,就过了吧。

三、区间dp

说实话其实我这块不太熟…(所以我就简单套路过一下)
区间的话,有点特殊,它是以“区间长度”作为dp的“阶段”,使用两个坐标来描述维度。区间dp的决策往往就是划分区间的方法,一般以长度为1的区间开始dp。
哦,突然想起来一点,在做区间dp,以及分组背包和树形dp(这三者特别要注意,但并不是说另外的就不需要在意了)时,务必务必分清阶段、状态、决策,这三个应按照从内到外的顺序依次实现。
对于区间dp在特意善意提醒一下,如果顺着无法实现的区间dp请把思路倒过来试试。
然后,就是在敲代码时注意优化。
推荐几道例题:石子合并,Polygon,金字塔;
这里来说一道很入门的题目

石子合并

题面供上
在这里插入图片描述
代码,代码…没找到,重敲吧…
那就索性把宏定义删了
然后敲起来特别不顺手了

#include<bits/stdc++.h>
using namespace std;
int n,s[310],f[310][310],sum[310]={};
void init(){
	scanf("%d",&n);
	memset(f,10,sizeof(f));
	for(int i=1;i<=n;++i){
		scanf("%d",&s[i]);
		sum[i]=sum[i-1]+s[i];
		f[i][i]=0;
	}
}
void work(){
	for(int i=2;i<=n;++i)
		for(int j=1;j<=n+1-i;++j){
			int t=i+j-1;
			for(int k=j;k<=t-1;++k)
				f[j][t]=min(f[j][t],f[j][k]+f[k+1][t]);
			f[j][t]+=sum[t]-sum[j-1];
		}
}
void prin(){
	printf("%d",f[1][n]);
}
int main(){
	//freopen("Stone.in","r",stdin);
	//freopen("Stone.out","w",stdout);
	init();
	work();
	prin();
	return 0;
}

诶,真的有点不习惯了…
我都这样了,总得对得起我把代码看懂吧…

四、树形dp

自我感觉的话,这一块的代码比较雷同吧,都是靠模板打天下…
简单点说,一般这种dp都会给定一棵有 n n n个节点的树(通常是无根树,也就是有 n − 1 n-1 n1条无向边)。我们可以任选一个节点为根节点,从而定义出每个节点的深度和每棵子树的根。dp时,一般以节点从深到浅(子树从小到大)的顺序作为dp的“阶段”,第一维通常为节点编号(代表以该节点为根的子树)。我们一般会采用递归的方式实现。对于一个节点,先递归在它的每个子节点上进行dp;回溯时,再从子节点转移向父节点进行状态转移。
先讲道经典例题

没有上司的舞会

题面来了
在这里插入图片描述
现敲代码,结果忘删宏定义了,那我还是删一下吧…

#include<bits/stdc++.h>
using namespace std;
const int maxn=6000+10;
struct lol{
	int x,y;
}e[maxn];
int n,h[maxn],ans=0,Top[maxn],f[maxn][2],v[maxn]={},l,k,cnt=1;
bool mc(int x,int y){
	return x>y;
}
void ein(int a,int b){
	++ans;
	e[ans].y=b;
	e[ans].x=Top[a];
	Top[a]=ans;
}
void init(){
	scanf("%d",&n);
	for(int i=1;i<=n;++i)
		scanf("%d",&h[i]);
	for(int i=1;i<=n-1;++i){
		scanf("%d%d",&l,&k);
		ein(k,l);
		v[l]=1;
	}
}
void gg(int t){
	f[t][1]=h[t];
	for(int i=Top[t];i;i=e[i].x){
		gg(e[i].y);
		f[t][0]+=max(f[e[i].y][0],f[e[i].y][1]);
		f[t][1]+=f[e[i].y][0];
	}
}
void work(){
	while(v[cnt])
		cnt++;
	gg(cnt);
}
void prin(){
	printf("%d",max(f[cnt][0],f[cnt][1]));
}
int main(){
	//freopen("text.in","r",stdin);
	//freopen("text.out","w",stdout);
	init();
	work();
	prin();
	return 0;
}

这题其实不难的,应该都看得懂。
然后再拓展两种题型

1、背包类树形dp

经典题目选课,有兴趣自行了解。
这种题型又被称为有树形依赖的背包问题,实际上就是背包和树形dp的结合。除了以节点编号作为dp的阶段,还会把当前背包体积作为第二维状态,要处理的实际上就是一个分组背包的问题。
顺带提一下,这类题目还可以按照一个“左儿子右兄弟”的方法,把多叉树转化为二叉树来做,但注意千万不要把父子关系和兄弟关系混淆。

2、二次扫描与换根法

这一块其实我一般不用,所以说不了太多什么,可自行搜索,见谅。
经典题目Accumulation Degree(中文翻译一般为积蓄程度),可进行了解。
用这个方法代替源点枚举可在较小的时间复杂度内解决整个问题,是tle情况下推荐尝试的。
emmm…接下来的双进程dp和平面dp我就不拓展太多啦,简单带一下。
其实是真的没多少时间也懒得继续写啦…
双进程dp顾名思义,就是双向进行的dp,一般会加一维来进行;平面dp吗,就是在一个二维空间上进行dp,思路也和正常的dp差不多。
另外就是什么环形与后效性处理、状压dp、四边形不等式、计数类dp、数位统计dp,以及优化dp中的倍增优化dp、数据结构优化dp、单调队列优化dp、斜率优化dp等。
毕竟我太弱了,还没学过,就不说了。
所以,接下来是老套路…
Over,感谢各位奆佬捧场,GYF感激不尽(orz)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值