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
>
=
v
[
i
]
)
if(j>=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[i−1][j],f[i−1][j−v[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
i−1阶段有关,为减少空间我们可以用“滚动数组”(空间复杂度从
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
>
=
v
[
i
]
)
if(j>=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[i−1][j],f[i][j−v[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,⋅⋅⋅,2k−1,这
k
k
k个数中选出若干个相加能表示出
0
0
0到
2
k
−
1
2^k-1
2k−1之间的任何整数。于是,我们可以把数量为
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]
20∗v[i],21∗v[i],⋅⋅⋅,2p∗v[i],r[i]∗v[i](
p
p
p表示满足
2
0
+
2
1
+
⋅
⋅
⋅
+
2
p
<
=
c
[
i
]
2^0+2^1+···+2^p<=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]−20−21−⋅⋅⋅−2p),这
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
<
=
k
<
=
c
[
i
]
)
f[i][j]=max(f[i-1][j],f[i-1][j-v[i][k]]+w[i][k])(1<=k<=c[i])
f[i][j]=max(f[i−1][j],f[i−1][j−v[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
n−1条无向边)。我们可以任选一个节点为根节点,从而定义出每个节点的深度和每棵子树的根。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)。