dp(动态规划)总结
Ezio_0420
2018-03-27 18:48:31
# 目录
# 1. dp分类
# 2. dp优化
# 3. 一些经典问题
# 4. 补充题目
------------
## dp分类
### 区间dp
#### 入门题:hdu-4632
http://acm.hdu.edu.cn/showproblem.php?pid=4632
题意:求字符串有多少回文子序列。
解法:
dp[i][j]表示从i到j的子序列中回文子序列的个数。
dp[i][j] = dp[i + 1][j] + dp[i][j - 1] - dp[i + 1][j - 1]
如果s[i] == s[j],则dp[i][j] += dp[i + 1][j - 1] + 1
```cpp
scanf("%s",s);
int k = strlen(s);
for(int i = 0;i<k;i++) dp[i][i]=1;
for(int j=0;j<k;j++){
for(int i=j-1;i>=0;i--){
dp[i][j]=(dp[i][j-1]+dp[i+1][j]-dp[i+1][j-1]+P)%P;
if(s[i]==s[j]) dp[i][j]=(dp[i][j]+dp[i+1][j-1]+1)%P;
}
}
printf("Case %d: %d\n",++cnt,dp[0][k-1]);
memset(dp,0,sizeof(dp));
```
------------
#### POJ – 2955
http://poj.org/problem?id=2955
题意:给出一个括号字符串,问这个字符串最长合法子序列的长度。若A,B合法,则AB;(A)合法。
dp[l][r]表示区间[l,r]的最长合法子序列长度
如果s[l]==s[r],那么dp[l][r]=dp[l+1][r-1]+2
dp[l][r]=max(dp[l][r],dp[l][k]+dp[k+1][r]),枚举断点k
```cpp
if(s[1]=='e') return 0;
memset(f,0,sizeof(f));
int k = strlen(s+1);
for(int j = 1;j<=k;j++){
for(int i = j-1;i>=1;i--){
if((s[i]=='('&&s[j]==')')||(s[i]=='['&&s[j]==']')) f[i][j]=f[i+1][j-1]+2;
for(int p = i+1;p<j-1;p++){
f[i][j]=max(f[i][j],f[i][p]+f[p+1][j]);
}
f[i][j]=max(f[i][j],max(f[i+1][j],f[i][j-1]));
}
}
printf("%d\n",f[1][k]);
```
------------
### 环形dp
#### 石子合并
https://www.luogu.org/problemnew/show/P1880
在一个园形操场的四周摆放N堆石子,现要将石子有次序地合并成一堆.规定每次只能选相邻的2堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。试设计算法,计算出将N堆石子合并成1堆的最小得分和最大得分.
解法:将链复制一倍 a[i+n] = a[i]
dp[i][j]表示合并i-j堆的得分。
按照j-i从小到大依次计算。
------------
### 状压dp
状压dp的思想是利用二进制来表示不同状态以及这些状态之间的相互转换,从而达到解决问题的目的
#### 经典问题:
#### 多米诺骨牌覆盖
题目大意:有一个 n 行 m 列的矩阵,用 1*2 的骨牌(可横放或竖放)完全覆盖,骨牌不能重叠,有多少种不同的覆盖的方法?只需求出覆盖方法总数 mod p 的值即可。m<=5 , p<=10000,1<=n<=10^9.
https://www.vijos.org/p/1194
解法:
F[i][j]表示填满前i-1行,第i行填充情况为j。j为第i行的二进制表示。
时间复杂度n*(2^5)*(2^5)
看到n很大,考虑使用矩阵乘法来加速。
------------
#### noip提高组2016 愤怒的小鸟
https://www.luogu.org/problemnew/show/P2831
题目大意:用过原点的向下开口的抛物线覆盖n个坐标,n<=18。求最少需要的抛物线条数。
解法:
三点确定一条抛物线,枚举过某两个点的抛物线(原点已经确定),再统计有多少点在抛物线上(压缩为状态p[j],在线上为1)
然后f[i]表示猪的消灭状态为i的最小步数(1表示消灭,0表示未消灭)
按照i二进制表示中1的数量从小到大更新
f[i|p[j]]=min(f[i|p[j]],f[i]+1)。
时间复杂度 (n^2)*(2^n)
------------
### 树形dP
顾名思义,树型动态规划就是在“树”的数据结构上的动态规划,平时作的动态规划都是线性的或者是建立在图上的,线性的动态规划有二种方向既向前和向后,相应的线性的动态规划有二种方法既顺推与逆推,而树型动态规划是建立在树上的,所以也相应的有二个方向:
1、叶->根:在回溯的时候从叶子节点往上更新信息
2、根 - >叶:往往是在从叶往根dfs一遍之后(相当于预处理),再重新往下获取最后的答案。
不管是 从叶->根 还是 从 根 - >叶,两者都是根据需要采用,没有好坏高低之分
例题:
#### 1.(hdu1520)
给出一棵树 每个节点有权值 要求父节点和子节点不能同时取 求能够取得的最大值
#### 2.(hdu2196)
给出一棵树,求离每个节点最远的点的距离
#### 3.(poj2378)
给一个树状图,有n个点。求出,去掉哪个点,使得剩下的每个连通子图中点的数量不超过n/2。如果有很多这样的点,就按升序输出。n<=10000
#### 4.(hdu2242)
给出一些点,有值,给出一些边,然后求去掉一条边后将分成连通的两部分,且两部分的差值最小
------------
### 数位dp
顾名思义,数位dp就是在数字的位数上进行dp
#### 例题:
#### bzoj1026 [SCOI2009]windy数
http://www.lydsy.com/JudgeOnline/problem.php?id=1026
windy定义了一种windy数。不含前导零且相邻两个数字之差至少为2的正整数被称为windy数。 windy想知道,
在A和B之间,包括A和B,总共有多少个windy数?100%的数据,满足 1 <= A <= B <= 2000000000
解法:
转换为求解[1,A-1]和[1,B]的答案。
考虑从高位往低位填数
F[i][j][k]:填满第n-i位(1最低位)。n-i位是否和上限相同(相同j为1,否则为0),第i位数字位k。 F[i][j][k]为此情况下的合法方案数。
```cpp
#include<bits/stdc++.h>
using namespace std;
int f[20][2][11];
int solve(int M){
int n,a[20]={0};
int sum=0;
for(n=0;M;M/=10)
a[++n]=M%10;
for(int k=0;k<=10;k++)f[n+1][0][k]=f[n+1][1][k]=0;
f[n+1][1][10]=1;
for(int i=n;i>=1;i--){
for(int k=0;k<10;k++){
f[i][0][k]=f[i][1][k]=0;
for(int l=0;l<=10;l++){
if(abs(k-l)<2&&l!=10)continue;
if(k==0&&l==10)continue;
if(k>a[i]){
f[i][0][k]+=f[i+1][0][l];
f[i][1][k]+=0;
}
if(k==a[i]){
f[i][0][k]+=f[i+1][0][l];
f[i][1][k]+=f[i+1][1][l];
}
if(k<a[i]){
f[i][0][k]+=f[i+1][0][l]+f[i+1][1][l];
f[i][1][k]+=0;
}
}
if(i==1)
sum+=f[i][0][k]+f[i][1][k];
}
f[i][1][10]=0;
f[i][0][10]=f[i+1][1][10]+f[i+1][0][10];
}
return sum;
}
int main(){
int A,B;
cin>>A>>B;
cout<<solve(B)-solve(A-1)<<endl;
return 0;
}
```
------------
### 双线程dp
#### 方格取数
其实挺水的一道题
https://www.luogu.org/problemnew/show/P1004
设有N*N的方格图(N<=9),我们将其中的某些方格中填入正整数,而其他的方格中则放入数字0。如下图所示:
![](https://cdn.luogu.com.cn/upload/pic/16344.png)
某人从图的左上角的A点出发,可以向下行走,也可以向右走,直到到达右下角的B点。在走过的路上,他可以取走方格中的数(取走后的方格中将变为数字0)。此人从A点到B点共走两次,试找出2条这样的路径,使得取得的数之和为最大。
解法:
~~据说可以拿网络流做~~
肯定会首先想到两遍dp,但是这样是错误的,为什么呢。我们思考一下,动态规划算法想要要求出的是最优的解,并且要求无后效性。那么问题来了,有可能我们第一遍dp求出最优解后,会对第二遍dp的过程造成影响,这样就不满足无后效性的要求,因为第一遍取走一些数后会对第二遍的路径选择造成影响。换句话说,我们求出的第一遍的最优解加上第二遍的最优解,可能不如两遍次优解的和。
观察数据范围不是很大(很小好不好),所以我们需要考虑双线dp,也就是四维dp,两条路线一起dp,如果两条路线取走同一个格子的数,就减去一次,这样就能求出总体最优解。
```cpp
#include<bits/stdc++.h>
using namespace std;
int f[10][10][10][10];
int a[10][10];
int main(){
int n,x,y,z;
scanf("%d",&n);
while(1){
scanf("%d%d%d",&x,&y,&z);
a[x][y]=z;
if(!x&&!y&&!z) break;
}
for(int i = 1;i<=n;i++){
for(int j = 1;j<=n;j++){
for(int k = 1;k<=n;k++){
for(int t = 1;t<=n;t++){
f[i][j][k][t]=max(max(f[i-1][j][k-1][t],f[i][j-1][k-1][t]),max(f[i-1][j][k][t-1],f[i][j-1][k][t-1]))+a[i][j]+a[k][t];
if(i==k&&t==j) f[i][j][k][t]-=a[i][j];
}
}
}
}
cout<<f[n][n][n][n];
return 0;
}
```
------------
------------
## dp优化
### 矩阵乘法优化
#### 矩阵乘法入门题:摆花(CH Round30)
~~找到这个oj真心难~~
http://contest-hunter.org:83/contest/CH%20Round%20%2330%20-%20%E6%B8%85%E6%98%8E%E6%AC%A2%E4%B9%90%E8%B5%9B/%E6%91%86%E8%8A%B1
![来源:pb0207](https://cdn.luogu.com.cn/upload/pic/16338.png)
重点是手动推出来原始矩阵和转移方法!!!
------------
### 单调性优化
暂时没学到
------------
### 斜率优化
学长blog:[orz徐文博](https://www.zybuluo.com/KirinBill/note/1034895)
#### 玩具装箱
~~放轻松,跟玩具有关的题都很可爱~~
https://www.luogu.org/problemnew/show/P3195
P教授要去看奥运,但是他舍不下他的玩具,于是他决定把所有的玩具运到北京。他使用自己的压缩器进行压缩,其可以将任意物品变成一堆,再放到一种特殊的一维容器中。P教授有编号为1...N的N件玩具,第i件玩具经过压缩后变成一维长度为Ci.为了方便整理,P教授要求在一个一维容器中的玩具编号是连续的。同时如果一个一维容器中有多个玩具,那么两件玩具之间要加入一个单位长度的填充物,形式地说如果将第i件玩具到第j个玩具放到一个容器中,那么容器的长度将为 x=j-i+Sigma(Ck) i<=K<=j 制作容器的费用与容器的长度有关,根据教授研究,如果容器长度为x,其制作费用为(X-L)^2.其中L是一个常量。P教授不关心容器的数目,他可以制作出任意长度的容器,甚至超过L。但他希望费用最小.
输入格式:
第一行输入两个整数N,L.接下来N行输入Ci.1<=N<=50000,1<=L,Ci<=10^7
输出格式:
输出最小费用
解法:
如果玩具长度的前缀和为sum[]sum[] ,那么dp方程为:
f[i]=min{f[j]+(i-j-1-L+sum[i]-sum[j])^2} ,0<=j<i.
考虑斜率优化。
进行美妙的数学变换
可以发现原方程为
f[i]=min{0<=j<i,f[j]+((i+sum[i]-L-1)-(sum[j]+j))^2}
设a[i]=i+sum[i]-L-1,b[j]=sum[j]+j ,
则f[i]=min{0<=j<i,f[j]+(a[i]-b[j])^2}
即f[i]=min{0<=j<i,f[j]+b[j]^2-2a[i]b[j]}+a[i]^2
再设X[j]=b[j],Y[j]=f[j]+b[j]^2,
~~其实就是展开一下然后移移项对吧~~
则可以发现a[] 和X[] 都单调递增,所以斜率是单调递减的。
~~直线的斜率初中就学过~~
用单调队列维护凸壳
~~emmm凸壳就是用向量叉乘随便搞一下~~
每次从队首不断出队以找出最优决策。
~~没了就这些~~
```cpp
#include <cstdio>
const int MAXN=50005;
int n,l;
long long k[MAXN],dp[MAXN];
inline long long sqr(register long long x){return x*x;}
//intercept
inline long long y_itcpt(int i){
return sqr(k[i])+(l*k[i]<<1)+dp[i];
}
inline long long slope(int i){return -(k[i]<<1);}
inline long long cal(int i,int j){
return slope(j)*k[i]+y_itcpt(j);
}
//intersection
inline double itsct(int i,int j){
return (double)(y_itcpt(j)-y_itcpt(i))/(slope(i)-slope(j));
}
inline long long slopeDP(){
static int dq[MAXN];
int head=0,tail=0;
for(int i=1;i<=n;++i){
while(head<tail && cal(i,dq[head])>=cal(i,dq[head+1]))
++head;
dp[i]=cal(i,dq[head])+sqr(k[i]-l);
while(head<tail && itsct(i,dq[tail-1])<=itsct(dq[tail],dq[tail-1]))
--tail;
dq[++tail]=i;
}
return dp[n];
}
int main(){
scanf("%d%d",&n,&l);
++l;
for(int i=1;i<=n;++i){
scanf("%d",&k[i]);
k[i]+=k[i-1];
}
for(int i=1;i<=n;++i)
k[i]+=i;
printf("%lld",slopeDP());
return 0;
}
```
------------
------------
## 一些经典问题
### 背包问题
https://blog.csdn.net/stack_queue/article/details/53544109
------------
### 最长上升子序列
存在n方算法和nlogn算法
nlogn算法利用库函数进行二分查找从而加速
若相邻两个数可以相等,则用lower_bound,若不可以相等,则用upper_bound
定理:最少链划分数等于最长反链长度
#### 洛谷P1020 导弹拦截
https://www.luogu.org/problemnew/show/P1020
解法:如果你知道上述定理,这就是个裸题,两遍LIS切
------------
### 最长公共子序列
#### 模板:
https://www.luogu.org/problemnew/show/P1439
若两个串长n!=m
则只能用O(nm)的算法
a[i]==b[j]?dp[i][j]=dp[i-1][j-1]+1:dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
如果是两个长度相等的串,则考虑将LCS转化为LIS
我们考虑将第一个串置换,法则就是a[i]->i
那么我们按照这个法则将第二个串置换出来,这样的话,求两者的LCS,就等价于在此置换法则下求第二个串的LIS。于是我们就得到了O(nlogn)的做法,求一下LIS就OK了。
------------
------------
## 补充题目:
#### 1.多米诺覆盖问题
##### 简单版
[poj2411](http://poj.org/problem?id=2411)
题目描述:用1*2 的矩形通过组合拼成大矩形,求拼成指定的大矩形有几种拼法。
n<=11,m<=11
解法:
轮廓线dp,状压dp
首先要熟悉位运算
首先使m<=n
把每一个节点用一个m位二进制表示,每次考虑不放,往左放,往上放的状态转移
时间复杂度O(2^m*(nm))
注意:数组不要开成2^m的,否则会很慢
~~具体原因我也不清楚~~
ac代码
```cpp
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int maxn=12;
int n,m,cur;
long long dp[3][(1<<maxn)+3];
inline void update(int a,int b){
if(b&(1<<m)) dp[cur][b^(1<<m)]+=dp[cur^1][a];
}
int main(){
while(scanf("%d%d",&n,&m)){
if(!n){
if(!m) return 0;
}
if(n<m) swap(n,m);
cur=0;
dp[0][(1<<m)-1]=1;
for(int i = 0;i<n;i++){
for(int j = 0;j<m;j++){
cur^=1;
memset(dp[cur],0,sizeof(dp[cur]));
for(int k = 0;k<(1<<m);k++){
update(k,k<<1);
if(i && !(k&(1<<m-1))) update(k,(k<<1)^(1<<m)^1);
if(j && !(k&1)) update(k,(k<<1)^3);
}
}
}
printf("%lld\n",dp[cur][(1<<m)-1]);
}
}
```
##### 升级版
[voj1194](https://www.vijos.org/p/1194)
题目大意:同简单版,只不过数据范围变成最多5列,但是行数达到1e9
解法:状压dp
预处理出行与行之间的转移关系,用矩阵乘法加速