一. 本质
递归转递推。
二. 前提
- 问题具有最优子结构性质。如果问题的最优解所包含的 子问题的解也是最优的,我们就称该问题具有最优子结 构性质。
- 无后效性。当前的若干个状态值一旦确定,则此后过程 的演变就只和这若干个状态的值有关,和之前是采取哪 种手段或经过哪条路径演变到当前的这若干个状态,没有关系。
三.动规解题的一般思路
- 将原问题分解为子问题
即将原问题的规模缩小,得到该问题的子问题,再将子问题进一步细化。 - 确定状态
用几个变量能表示当前的状态,并能通过当前的状态定义推出更大的子问题。 - 确定一些初始状态(边界状态)的值
如dp[1]=1或2等; - 确定状态转移方程
由子问题转换到当前问题;
###四.相关模型
1.路径模型(E.G.数字三角形)
特点:能够明确的在一个矩阵或其它图形中体现出来,并且能由其它的点转移过来。
状态:dp[i][j]表示走到点(i,j)时的最优解。(该题即代表从下到上走到(i,j)时的最大 总值)
转移:本题:dp[i][j]=max(a[i][j]+dp[i+1][j];a[i][j]+dp[i+1][j+1])
目标状态:dp[1][1],即从下到上走到顶端时的最优(大)解。
时间:O(nn)
时间优化:无
空间:O(nn)
空间优化:O(n)(滚动数组):
dp[i]表示如果当前选择了该行的第i个数,那么能够得到的最大的路(和)。直接用已经存在的dp里面的值来自我更新即可。
实现代码:
int dp[i][j];
for(int i=1;i<=n;i++) //初始化
dp[n][i]=a[n][i];
for(int i=n-1;i>=1;i--)
for(int j=1;j<=I;j++)
dp[i][j]= max(a[i][j]+dp[i+1][j];a[i][j]+dp[i+1][j+1]);
输出方式:
可另外开一个way[n][n],其中way[i][j]存到i+1行时是走左边还是右边。最后即可从第一行到第n行模拟就可以了。
例题:
(1).数字三角形
(2).花店橱窗布置
(3).滑雪
2.最长上升子序列
特点:没有一个固定的起点,需要自己枚举。然后在枚举出来的起点与终点之 间枚举之前算出来的最优值,然后优中取优即可。
状态:dp[i]表示从i到末尾(包括i)的最长上升子序列的长度。
转移:dp[i]=max(dp[k]+1,dp[k])
目标状态:max(dp[all])
时间:O(n*n)
时间优化:无
空间:O(n)
空间优化:无
实现代码:
int dp[MAXN+5],ans;
for(int i=1;i<=n;i++)
dp[i]=1; //初始化
for(int i=n-1;i>=1;i--)
{
for(int j=i+1;j<=n;j++)
dp[i]=max(dp[j]+1,dp[i]);
ans=max(dp[i],ans); //ans即为答案
}
输出方式:
可以将dp[n][n]改为dp[n][n][2],使用类似于father节点的方式来存某个状态中的最优子问题(答案)。
dp[i][j][0]仍为之前的dp数组,dp[i][j][1]是它的最优子问题的位置。
例题:
(1).最长上升子序列
(2).拦截导弹(最长上升子序列+最长下降子序列)
(3).嵌套矩形
(4).渡轮问题
(5).合唱队形
(6).尼克的任务
3.最长公共子序列
特点:
需要多种情况的分类讨论,并结合当前两个串的处理位置的关系(相等或不同),不同时又需讨论如何将指针向前移动。
状态:
dp[i][j]表示A串从i位置、B串从j位置开始,向前找能找到的最长上升子序列。
转移:dp[i][j]=:if(A[i]==B[j]) dp[i][j]= dp[i-1][j-1]+1;
if(A[i]!=B[j]) dp[i][j]=max(dp[i-1][j]+1,dp[i][j-1]+1);
目标状态:dp[n][n]
时间:O(nm) //n代表A串的长度,m代表B串的长度
时间优化:无
空间:O(nm)
空间优化:无
实现代码:
int dp[MAXN+5][MAXN+5];
dp[0][1]=0,dp[1][0]=0,dp[0][0]=0;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
if(A[i]==B[j])
dp[i][j]=dp[i-1][j-1]+1;
else
dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
输出方式:
例题:
(1).求最长公共子序列
4.0-1背包(E.G.:满足背包容量的最大值)
特点:
当前位置(i)的最优解不一定包括当前所指向的物品(i),但与上一个情况的有关(i-1)。
状态:
dp[i][tot]表示从第一个到第i个满足总体积<=tot的最优解(最大值)。
转移:
if V[i]<=tot
则dp[i][tot]=max{ dp[i-1][tot-V[i]] , dp[i-1][tot] )
else
dp[i][tot]=dp[i-1][tot]
目标状态:dp[n][totV]。
时间:O(ntotV)
时间优化:无
空间:O(ntotV)
空间优化:O(totV)
将dp[n][totV]变为dp[totV],dp[tot]表示当满足总体积<=tot时的最大值。
单独考虑某一个物品,考虑其是否能放进体积为tot的背包中。但要从后往前枚举tot,防止同一种情况下一个物品被用两次(与完全背包正好相反)。
目标状态:dp[totV];
实现代码:
普通实现(二维)
int dp[MAXN+5][MAXV+5];
for(int i=1;i<=n;i++)
dp[i][0]=0;
for(int i=1;i<=totV;i++)
dp[0][i]=0;
dp[0][0]=0;
for(int tot=1;tot<=totV;tot++)
for(int i=1;i<=n;i++)
if(V[i]<=tot)
dp[i][tot]=max(dp[i-1][tot-V[i]]+val[i],dp[i-1][tot]);
// 选它 不选它
else
dp[i][tot]=dp[i-1][tot];
// 由于当前物品不可能放进去,那么就是不选它的情况。
空间优化实现
int dp[MAXV+5];
dp[0]=0;
for(int i=1;i<=n;i++)
for(int tot=totV;tot-V[i]>=0;tot--)
dp[tot]=max(dp[tot-V[i]]+val[i],dp[tot]);
5.区间DP
特点:
考虑的是一段区间以内的影响,同时区间的左右端点也需要枚举,在不优化的时候复杂度高达O(nn),但是通常可以套各种优化。
状态:(加法问题)
dp[i][j] //用了i个加号,当前位置在j
转移:
dp[i][j]=max{dp[i-1][k]+num(k,j)}
目标状态:dp[m][n]//有m个加号,string的长度为n
时间:O(nm)
时间优化:(加法问题好像无法优化掉一样)
通常可以用单调队列or斜率优化(有时可以强行)or高级数据结构,甚至是CDQ分治来优化到O(nlogn)
空间:O(nm)
空间优化:O(m)(与时间一起优化掉)
实现代码:
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
for(int k=1;k<=j-2;k++)
dp[i][j]=max(dp[i][j],dp[i-1][k]+GetNum(k,j));
例题2:邮局
状态:dp[i][j]表示当前已经处理完了前面的i个村庄,其中有j个有邮局
转移:dp[i][j]=min{dp[k-1][j-1]+sigma{pos[p|p∈[k,j]]-pos[(j+k)/2]}}
目标状态:dp[n][m]//有n个村庄,m个邮局
时间:O(nnm);
空间:O(nm)
实现代码:
for(int i=1;i<=p;i++)
for(int j=i;j<=v-(p-i);j++)
for(int k=i;k<=j;k++)
{
int mid=(j+k)/2;
int tot=0;
for(int m=k;m<=j;m++)
tot+=abs(pos[m]-pos[mid]);
if(dp[k-1][i-1]+tot<dp[j][i])
dp[j][i]=dp[k-1][i-1]+tot;
}
6.区间DP2.0
特点:
定义的时候往往都要定义为O(nn)的形式,表示某一个区间内的最优答案。通常的形式是O(nn)枚举。且通常是枚举长度->枚举起始位置->枚举中间位置。
状态:(石子合并版本一)dp[i][j]表示将i到j这一段的石子合并的最小代价。
转移:dp[i][j]=min{dp[i][k]+dp[k+1][j]+sum[i][j]}
目标状态:dp[1][n]//总共有n堆石子
时间:O(nnn)
时间优化:无
空间:O(n*n)
空间优化:无
实现代码:
for(int i=1;i<=n;i++)
dp[i][i]=a[i];
for(int len=2;len<=n;len++)
for(int i=1;i+len-1<=n;i++)
{
j=i+len-1;
dp[i][j]=INF;
for(int k=i;k<=j;k++)
dp[i][j]=min(dp[i][k]+dp[k+1][j],dp[i][j]);
dp[i][j]+=sum[j]-sum[i-1];
}
例题2:乘法游戏
状态:dp[i][j]表示从i到j抽空至只剩下i和j两个数时,得到的最大值。
转移:dp[i][j]=max{dp[i][p]+dp[p][k]+a[i]a[p]a[j]}
时间:O(nnn)
空间:O(n*n)
实现代码:
for(int i=2;i<=n-1;i++)
dp[i-1][i+1]=a[i-1]*a[i]*a[i+1];
for(int i=1;i<=n-1;i++)
dp[i][i+1]=0;
for(int k=3;k<=n-1;k++)
for(int i=1;i<=n;i++)
{
int j=i+k;
if(j>n)
break;
for(int p=i+1;p<=i+k-1;p++)
dp[i][j]=min(a[i]*a[i+k]*a[p]+dp[i][p]+dp[p][j],dp[i][j]);
}
7.单调队列&单调栈优化
虽然有时是单调队列,有时又是单调栈,但是我更倾向于叫它单调队列(想成是deque…)。
单调队列优化首先是最简单的优化,与dp的优化无关。如POJ2559,让你求一个数列里面每个数向两边扩展最多能扩展到哪里(只要大于等于当前的数的数值就可以扩展一格)。
比较经典的思路就是左右各做一遍。举例来说从左往右做的时候,维护一个单调上升的队列,每次从队尾弹出旧的元素,发现最后留下的队尾的元素必定是大于当前枚举到的数的。假设队尾的元素的位置是j,那么从j+1到当前的位置都是合法的(大于等于当前元素)。从右往左扫的时候也是一样的。这样子处理完了之后后,把两边的答案拼接起来就是当前位置的答案了。
比如这张图中就是j+1到i的位置对于i来说都是合法的。
然而这只是最简单的单点队列优化…但是大多数题目的第一步优化就是这样子的,所以说是很有用的呢。
之前在学的时候做了一道神奇的题目。按道理来说,通常是要对dp式进行变形后,才能进行单调队列优化。而最常见的做法是转化为前缀和之差的形式。但是这道题,也就是优化多重背包(求最大价值总和),它的变形是将dp式同时减去一个值,使得进行转移的部分变得有规律,然后通过那个有规律的值进行转移。
这是最初始的转移式:
f
[
i
]
[
j
]
=
m
a
x
(
f
[
i
−
1
]
[
j
−
k
∗
v
[
i
]
]
+
k
∗
v
[
i
]
)
f[i][j]=max(f[i-1][j-k*v[i]]+k*v[i])
f[i][j]=max(f[i−1][j−k∗v[i]]+k∗v[i])
然后我们进行变化之后:
f
′
[
j
]
=
m
a
x
(
f
[
j
−
k
∗
v
[
i
]
]
−
k
′
∗
v
[
i
]
+
(
k
+
k
′
)
∗
v
[
i
]
)
f'[j]=max(f[j-k*v[i]]-k'*v[i]+(k+k')*v[i])
f′[j]=max(f[j−k∗v[i]]−k′∗v[i]+(k+k′)∗v[i])
其中的k’为之前的已经加进去的当前物品的个数
这样还不是最神奇的部分。然后我们可以将j按照mod v[i]的余数进行分类,将余数相同的部分化成一堆,每一堆中进行转移。
证明如下:
若当前枚举到的状态为j,那么可以对当前造成贡献的状态为j-1v,j-2v,j-3*v…这些数mod v的余数都是j mod v。然后就证到了转移只会发生在同一个余数相同的块内。
然后我们就可以根据余数进行分类枚举。首先枚举余数,就相当于枚举了一个块,然后使用一个优先队列,维护一个单减的双向队列,每次先把当前的数值(设余数为d),也就是
d
+
k
∗
v
d+k*v
d+k∗v(k为系数),与队尾进行比较(注意,队列中存储的数值为
f
[
i
−
1
]
[
d
+
k
∗
v
]
−
k
∗
w
f[i-1][d+k*v]-k*w
f[i−1][d+k∗v]−k∗w)。然后再看队首,因为是多重背包,不是完全背包,所以说它的转移是有范围限制的。那么就需要从队首弹出无法转移到的元素(因为当前都无法从那里转移,那么之后也不可能从那里转移)。这样子处理完了之后,然后就从队首取出元素,自然就是我们需要的最优转移了。(转移的之后要记得加上当前的物品的总个数(其实就是枚举的系数))
优化之后,传统的多重背包问题,变成了
O
(
n
∗
t
o
t
V
)
O(n*totV)
O(n∗totV)。
然后最经典的当然是斜率优化了,它应该也算是单调队列优化的一种吧。
斜率优化也往往是通过对转移表达式进行转化后,设当前的决策点为i,转移点为j,那么对于i来说,所有的关于i的变量此时都是一个常量,所有的关于j的变量才是真正不定的。
那么我们可以把变量和常量分离开来,变量就可以形成一个表达式。
接着就假设两个转移点j1和j2,假设
j
1
<
j
2
j1<j2
j1<j2,然后再假设j1的转移比j2的转移要优,就可以得到一个斜率的不等式。
假设是
Y
[
j
1
]
−
Y
[
j
2
]
X
[
j
1
]
−
X
[
j
2
]
<
C
o
n
s
t
V
a
l
[
i
]
\dfrac{Y[j1]-Y[j2]}{X[j1]-X[j2]}<ConstVal[i]
X[j1]−X[j2]Y[j1]−Y[j2]<ConstVal[i],然后就可以写作
T
(
j
1
,
j
2
)
<
C
i
T(j1,j2)<Ci
T(j1,j2)<Ci。
然后就可以根据X和ConstVal的单调性进行讨论,自然是两个都具有单调性最好…
(我一般是在队尾加入元素,在队头取出元素,其实可以根据个人喜好而定)
例如最大化平均值这道题,虽然和优化DP没设么关系,却是斜率优化的一道好题。
实际上就是让
p
r
e
s
u
m
[
i
]
−
p
r
e
s
u
m
[
j
]
i
−
j
(
j
<
i
)
尽
量
大
\dfrac{presum[i]-presum[j]}{i-j}(j<i)尽量大
i−jpresum[i]−presum[j](j<i)尽量大。
然后就可以维护一个下凸包,然后我们可以直观地发现它其实就是求与当前曲线的切线即可。
但是又因为这里的presum具有单增的性质,所以我们可以从队首弹出后,就不再需要将其放回去了。也就是说,每一个元素最多会进去一次,出来一次,做到了O(n)。否则的话,我们还需要二分,然后就变成了O(nlogn)。
然而斜率优化并不是如此简单的。当X不具有单调性的时候,在插入的我们也需要二分;如果ConstVal不具有单调性或者说与不等号的方向相反时(也就是要保证随着i往后移,限制条件会越来越严,也是为了保证每个元素只会进出一次),我们就需要在取出的时候进行二分。
对于后者而言,需要二分的证明如下:
假如说当前的更新的点为i,然后我们所维护的图像为斜率单增的一条曲线,那么必定有如下的性质:
T
(
j
1
,
j
2
)
<
T
(
j
2
,
j
3
)
<
T
(
j
3
,
j
4
)
.
.
.
.
.
.
<
C
o
n
s
t
v
a
l
[
i
]
T(j1,j2)<T(j2,j3)<T(j3,j4)......<Constval[i]
T(j1,j2)<T(j2,j3)<T(j3,j4)......<Constval[i]
C
o
n
s
t
v
a
l
[
i
]
<
T
(
j
1
′
,
j
2
′
)
<
T
(
j
2
′
,
j
3
′
)
<
T
(
j
3
′
,
j
4
′
)
.
.
.
.
.
.
.
Constval[i]<T(j1',j2')<T(j2',j3')<T(j3',j4').......
Constval[i]<T(j1′,j2′)<T(j2′,j3′)<T(j3′,j4′).......
然后我们就发现ConstVal[i]一定是卡在斜率的值的中间的。假如我们的定义是T(j1,j2)大于constval表示j1比j2优,然后就有j4’比j5’优,j3’比j4’优,j2’比j3’优,j1’比j2’优…以此类推,然后我们就发现j1’其实是最优的转移。那么我们就可以二分j1’的位置,从而获得最佳转移点。
这样应该就解决了大多数情况的斜率优化了。
关于斜率优化是维护上凸包还是下凸包的问题:
首先假设当前的转移点是j1,j2,j3,然后有j1<j2<j3。以下讨论的前提都是X()和constval具有单调性的时候。
首先假设j2在某一时刻能够成为最优的转移点,那么就有j2比j1和j3都优,然后就能够得到两个不等式,再和之间的不等式进行比较,若出现矛盾,则不能够是上/下凸包。两个都试一下。
当不具有单调性的时候,往往通过CDQ分治或者是二分/三分查找来解决。
8.矩阵加速优化
首先应该有矩阵乘法的定义。
设相乘的两个矩阵是A和B,得到的目标矩阵是C
那么有:
C
[
i
]
[
j
]
=
∑
(
A
[
i
]
[
k
]
∗
B
[
k
]
[
j
]
)
C[i][j]=\sum(A[i][k]*B[k][j])
C[i][j]=∑(A[i][k]∗B[k][j])
那么这就要求A和B的行和列的数量应该是相等的。实际上“乘法”的定义是可以自己定义的,比如你可以魔改成:
C
[
i
]
[
j
]
=
m
a
x
(
A
[
i
]
[
k
]
+
B
[
k
]
[
j
]
)
C[i][j]=max(A[i][k]+B[k][j])
C[i][j]=max(A[i][k]+B[k][j])
从而适应各种各样的题目。
至于快速幂的话,就和普通的快速幂是一样的。(所以说我们更愿意将矩阵的乘法用重载运算符重载掉,方便调用)。同时我们需要注意一点的是,矩阵乘法的复杂度通常为
O
(
n
3
∗
l
o
g
n
)
O(n^3*logn)
O(n3∗logn)的,谨慎使用。
其实对于矩阵乘法优化来说,最难的是发现可以这样优化和怎样构造矩阵。
首先,发现矩阵的话,我们可以发现,如果把A矩阵看成系数矩阵的话,转移式其实是线性的,也就是说,只有类似于
a
n
s
=
A
1
∗
B
1
+
A
2
∗
B
2
+
A
3
∗
B
3
+
A
4
∗
B
4......
ans=A1*B1+A2*B2+A3*B3+A4*B4......
ans=A1∗B1+A2∗B2+A3∗B3+A4∗B4......的形式才能够进行矩阵优化。
接着就是如何构造矩阵了。这是难点中的难点。
首先是一个简单的例子,快速求斐波那契数列第i项。(i极大)
构造的矩阵可以是:
然后就可以求了。
下面是两种比较特殊的情况。
首先是矩阵套矩阵:
例:给一个n*n的矩阵A,然后求S=A + A^2 + A^3 + …+ A^k.
这道题我们可以构造如下的矩阵:
然后就可以套矩阵套矩阵快速幂的板了。
然后是分块矩阵快速幂。最为经典的还是“沼泽鳄鱼”这道题。
题意大概就是一个无向图,一些食人鱼在进行有周期的运动(周期在2到4之间)。你要从s走到t,中途不能在一个点停留,一共走K个单位之间,问有多少种走法。
因为周期只能是2,3,4,所以说每12个单位时间后,所有食人鱼肯定都回到了原来的位置,也就是说整张图的食人鱼的位置分布最多有12种,也就是说12个单位时间是一个周期。然后就根据12对K进行分块,就可以做到
O
(
K
12
∗
n
3
∗
12
+
n
∗
n
∗
n
∗
(
K
m
o
d
12
)
)
O(\dfrac{K}{12}*n^3*12+n*n*n*(K mod 12))
O(12K∗n3∗12+n∗n∗n∗(Kmod12))的时间内解决。
当然我们要处理12个单位时间的总的邻接矩阵,把可以走的邻接矩阵的食人鱼所在的位置变为0再乘到totG里面去就可以了。
至于后面的k%12的部分暴力处理就好了。
差不多就是这样了,如果说还有什么地方博主写错或者写掉了,请帮忙提醒一下哦~
博主后期也会继续补充的呢