线性dp
线性dp
引入
引入1:斐波那契数列
//递归
int f(int n)
{
if(n==0||n==1) return 1;
else return f(n-1)+f(n-2);
}
//递归版本需要每次计算比它小的,很浪费时间,重复计算子问题
//解决方案:
//空间换时间,将已经计算过的记录下来避免重复计算
//记忆化搜索版本
int calc(int n)
{
if(f[n]!=0) return f[n];
return f[n]=calc(n-1)+calc(n-2);
}
记忆化搜索:
用数组等将已经计算过的东西记录下来,在下一次要使用的时候直接用已经算出的值,避免重复运算,去掉重复的搜索树
引入2:走楼梯
要爬n阶楼梯,一次可以爬1/2阶,有多少种方法爬完?
f(i)表示走到第i级台阶的方案数
f[n]=f[n-1]+f[n-2]
若规定xi 级(m个)台阶不能走,就将 f[xi]=0 即可
引入3:蜜蜂只能爬向右侧相邻的蜂房,计算从蜂房a爬向蜂房b的可能路线数
f(i) 表示从a爬到i的方案数
f(i)=0 , i<a;
f(i)=1, i=a;
f(i)=f(i-1)+f(i-2),i>a
数字三角形
有一个三角形的数塔,求从第一层到达底层的路径最大值
13 13
11 8 11 8
12 7 26 如何存储?——> 12 7 26
6 14 15 8 6 14 15 8
12 7 13 24 11 12 7 13 24 11
转化为a[i][j]数组存储,那么从a[i][j] 可以走到 a[i+1][j] 和 a[i+1][j+1] 两个位置
f[i][j] 表示从起点开始走到第i行第j列点的总数
所以f[i][j] = a[i][j] + max( f[i-1][j] , f[i-1][j-1] )
for(int i=1;i<=n;i++)
{
for(int j=1;j<=i;j++)
{
f[i][j]=max(f[i-1][j],f[i-1][j-1])+a[i][j];
}
}
再对最后一行的f值进行扫描,最大的一个就是结果
代码如下
#include <bits/stdc++.h>
using namespace std;
int r;
int a[1010][1010],f[1010][1010];
int main()
{
cin>>r;
for(int i=1;i<=r;i++)
{
for(int j=1;j<=i;j++)
{
cin>>a[i][j];
}
}
f[1][1]=a[1][1];
for(int i=2;i<=r;i++)
{
for(int j=1;j<=i;j++)
{
f[i][j]=a[i][j]+max(f[i-1][j],f[i-1][j-1]);
}
}
int maxn=f[r][1];
for(int i=2;i<=r;i++)
{
maxn=max(maxn,f[r][i]);
}
cout<<maxn<<endl;
return 0;
}
也可以倒着走(从下往上),这样f[1][1]就是结果了,代码如下
#include <bits/stdc++.h>
using namespace std;
int r;
int a[1010][1010],f[1010][1010];
int main()
{
cin>>r;
for(int i=1;i<=r;i++)
{
for(int j=1;j<=i;j++)
{
cin>>a[i][j];
}
}
for(int k=1;k<=r;k++)
f[r][k]=a[r][k];
for(int i=r-1;i>=1;i--)
{
for(int j=1;j<=i;j++)
{
f[i][j]=a[i][j]+max(f[i+1][j+1],f[i+1][j]);
}
}
cout<<f[1][1]<<endl;
return 0;
}
原理
分类加法原理:
做一件事,完成它有n类办法,在第一类办法中有m1种不同的方法,在第二类中有m2种不同的方法,在第n类中有mn中不同的办法,那么完成这件事共有N=m1+m2+......+mn种不同方法
分步乘法原理:
做一件事,完成它有n个步骤,做第一步有m1种不同的方法,做第二步有m2种不同的方法,做第n步有mn中不同的办法,那么完成这件事共有N=m1*m2*......*mn种不同的办法
概念
定义:
动态规划是解决多阶段决策过程最优化问题的一种方法
阶段:
把问题分成几个相互联系的有顺序的几个环节,这些环节即称为阶段
状态:
某一阶段的出发位置称为状态。通常一个状态包含若干阶段
决策:
从某阶段的一个状态演变到下一个阶段的某状态的选择
策略:
由开始到终点的全过程中,由每段决策组成的决策序列称为全过程的策略
状态转移方程:
前一阶段的终点就是后一阶段的起点,前一阶段的决策导出了后一阶段的状态,这种关系描述了由i阶段到i+1阶段的演变规律,称为状态转移方程。
形如:f[i] = f[i-1] + f[i-1] 等等
适用的基本条件
具有相同子问题:
首先,我们必须要保证这个问题能够分解出几个子问题,并且能够通过这些子问题来解决这个问题。
其次,将这些子问题作为一个新的问题,他也能分解成为相同的子问题进行求解。
也就是说,假设我们一个问题被分解为了A,B,C三个部分。那么这A,B,C分别也能被分解为A',B',C'三个部分,而不是D,E,F三个部分。
满足最优子结构:
问题的最优解包含着他的子问题的最优解。即不管前面的策略如何,此后的决策必须是基于当前状态(由上一次决策产生)的最优决策。
e.g.
3 1 1
1 -> 1 ->2 -> 2 ->3 -> 1 ->4
0 2 2
求使得路径长度%4最小的路径
f[i][j] (j=0,1,2,3) 表示从起点出发到i,路径长度%4余数可不可能为j
满足无后效性:
过去的步骤只能通过当前状态影响未来的发展,当前的状态是历史的总结。这条特征说明动态规划只适用于解决当前决策与过去状态无关的问题。状态出现在策略任何一个位置,它的地位相同,都可实施同样策略,这就是无后效性的内涵。
这是动态规划中极为重要的一点,如果当前问题的具体决策,会对解决其他未来的问题产生影响,如果产生影响,就无法保证决策的最优性。
一般步骤
第一步,结合原问题和子问题确定状态:
我是谁?我在哪儿?题目在求什么?要求出这个值,我们需要知道什么? 什么是影响答案的因素?
一维描述不完就二维,二维不行就三维四维。
状态的参数,一般有
-
描述位置的:前(后)i,第i到第j单位。坐标为(i,j),第i个之前(后)且必须取等。
-
描述数量的:取i个,不超过i个,至少i个等。
-
描述对后有影响的:状态压缩的,一些特殊的性质
第二步,确定转移方程:
- 检查参数是否足够
- 分情况:最后一次操作的方式,取不取,怎么样取,——前一项是什么
- 初始边界是什么
- 注意无后效性,比如说,求A 就要求B,求B就要求C,而求C就要求A,这就不符合无后效性了
根据状态枚举最后一次决策(即当前状态怎么来的)就可以确定出状态转移方程
第三步,考虑需不需要优化:
第四步,确定编程实现方式:
- 递推
- 记忆化搜索
路径条数问题
n*m的棋盘上左上角有一个过河卒,需要走到右下角,可以向右或者向下走,计算出从左上角到达右下角的路径条数
f[i][j] 表示从(0,0) 出发到(i,j) 的路径条数
f[i][j] = f[i-1][j] + f[i][j-1]
第一步:确定状态——原问题是什么? 子问题是什么?
原问题:从(0,0)走到(n,m)的路径数
子问题:从(0,0)走到(i,j)的路径数
f[i][j]表示从左上角走到(i.j)点的路径条数
第二步:确定状态转移方程和边界
f[i][j] = f[i-1][j] + f[i][j-1] [][j-1]
f[i][1] = f[1][i] = 1
第三步:考虑是否需要优化
第四步:确定实现方法
(本题其实可以直接用排列组合求)
//记忆化搜索
int calc(int i,int j)
{
if(f[i][j]!=0)
return f[i][j];
else if(i==1||j==1)
return f[i][j]=1;
else return f[i][j]=calc(i-1,j)+calc(i,j-1);
}
代码如下: 记忆化搜索里面,务必不要手抖,是赋值=,不是判断== 符号
#include <bits/stdc++.h>
using namespace std;
int n,m;
int f[1010][1010];
int calc(int i,int j)
{
if(f[i][j]!=0) return f[i][j];
else if(i==1||j==1) return f[i][j]=1;
else return f[i][j]=calc(i-1,j)+calc(i,j-1);
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
f[i][j]=calc(i,j);
}
}
cout<<f[n][m]<<endl;
return 0;
}
过河卒
棋盘上A点(0,0)有一个过河卒,需要走到B点(n,m),可以向下或者向右走。在C点(给出)有一个马,在马所在的点和所有跳跃一步可到达的点称为对方马的控制点。要求计算从A到B点的路径条数,假设马的位置是固定不动的,并不是卒走一步马走一步
用f[i][j] 表示从(0,0)点到(i,j)点的路径总数,
f[i][j]=f[i-1][j]+f[i][j-1]
同时需要注意到马的位置是不可以走的,所以对于c点f值为0,同时对于马走日字能到达的8个点也要赋值为0
但是是从(0,0)点开始的,第一行只可能由左边移动而来,第一列只可能由上边移动而来
所以需要特判 而不是简单赋值 f[0][0]=1;
在第一行的时候i==0,f[i][j]=f[i][j-1];
在第一列的时候j==0,f[i][j]=f[i-1][j];
我们可以使用记忆化搜索存储状态
#include <bits/stdc++.h>
using namespace std;
const int maxn=25;
long long f[maxn][maxn];
int bi,bj,ci,cj;
long long calc(int i,int j)
{
if(i==0&&j==0) return f[i][j]=1;
if(i==ci&&j==cj) return f[i][j]=0;
else if(i==ci-1&&j==cj-2) return f[i][j]=0;
else if(i==ci-2&&j==cj-1) return f[i][j]=0;
else if(i==ci-1&&j==cj+2) return f[i][j]=0;
else if(i==ci-2&&j==cj+1) return f[i][j]=0;
else if(i==ci+1&&j==cj-2) return f[i][j]=0;
else if(i==ci+2&&j==cj-1) return f[i][j]=0;
else if(i==ci+1&&j==cj+2) return f[i][j]=0;
else if(i==ci+2&&j==cj+1) return f[i][j]=0;
else if(i==0) return f[i][j]=f[i][j-1];
else if(j==0) return f[i][j]=f[i-1][j];
else return f[i][j]=f[i][j-1]+f[i-1][j];
}
int main()
{
cin>>bi>>bj>>ci>>cj;
for(int i=0;i<=bi;i++)
{
for(int j=0;j<=bj;j++)
{
f[i][j]=calc(i,j);
}
}
cout<<f[bi][bj]<<endl;
return 0;
}
同样,这题我们也可以考虑用滚动数组来实现,以防止数据过大MLE
#include <bits/stdc++.h>
using namespace std;
const int maxn=25;
long long f[2][maxn];
int bi,bj,ci,cj;
long long calc(int i,int j)
{
if(i==0&&j==0) return f[i%2][j]=1;
if(i==ci&&j==cj) return f[i%2][j]=0;
else if(i==ci-1&&j==cj-2) return f[i%2][j]=0;
else if(i==ci-2&&j==cj-1) return f[i%2][j]=0;
else if(i==ci-1&&j==cj+2) return f[i%2][j]=0;
else if(i==ci-2&&j==cj+1) return f[i%2][j]=0;
else if(i==ci+1&&j==cj-2) return f[i%2][j]=0;
else if(i==ci+2&&j==cj-1) return f[i%2][j]=0;
else if(i==ci+1&&j==cj+2) return f[i%2][j]=0;
else if(i==ci+2&&j==cj+1) return f[i%2][j]=0;
else if(i==0) return f[i%2][j]=f[i%2][j-1];
else if(j==0) return f[i%2][j]=f[(i-1)%2][j];
else return f[i%2][j]=f[i%2][j-1]+f[(i-1)%2][j];
}
int main()
{
cin>>bi>>bj>>ci>>cj;
for(int i=0;i<=bi;i++)
{
for(int j=0;j<=bj;j++)
{
f[i%2][j]=calc(i,j);
}
}
cout<<f[bi%2][bj]<<endl;
return 0;
}
好像,滚动数组就是正常数组再把i%2
传球游戏
n个同学站成圆圈,一个同学手里拿球,每个同学可以把球传给左右的两个同学中的一个。
有多少种不同的方式,可以使得从小蛮手里开始传的球,传了m次以后,又回到小蛮手里,
为了最后一次到1号手里,倒数第二次必然在2或者n手里
第i次传球,球在第j个人手里,那么第i-1次传球,球就在j-1或者j+1个人的手里
f[i][j] = f[i-1][j-1] + f[i-1][j+1]
f[0][1] = 1
第一步:确定状态——原问题是什么?子问题是什么?
原问题:从1开始传球第m步球回到1的方法数
子问题:从1开始传球第i步球到达j的方法数
f[i][j]表示第i次传球之后球在第j个人手上的方法数
第二步:确定状态转移方程和边界
f[i][j] = f[i-1][j-1] + f[i-1][j+1]
f[0][1] = 1
注意由于是一个环,j=1 时左边(j-1) 为n, j=n 时右边(j+1) 为1
第三步:考虑是否需要优化
第四步:确定实现方法
我们可以通过特判j==1和j==n两种情况来实现记忆化搜索
#include <bits/stdc++.h>
using namespace std;
const int maxn=35;
int n,m;
int f[maxn][maxn*2];
int calc(int i,int j)
{
if(j==1) return f[i][j]=f[i-1][n]+f[i-1][j+1];
else if(j==n) return f[i][j]=f[i-1][j-1]+f[i-1][1];
return f[i][j]=f[i-1][j-1]+f[i-1][j+1];
}
int main()
{
cin>>n>>m;
f[0][1]=1;
for(int i=1;i<=m;i++)
{
for(int j=1;j<=n;j++)
{
f[i][j]=calc(i,j);
}
}
cout<<f[m][1]<<endl;
return 0;
}
那我们也可以使用环形结构而不是特判,就是1.......n 1-1=n n+1=1
j-1 -> (j+n-2)%n+1
j+1 -> j%n+1
#include <bits/stdc++.h>
using namespace std;
const int maxn=35;
int n,m;
int f[maxn][maxn*2];
int calc(int i,int j)
{
return f[i][j]=f[i-1][(j+n-2)%n+1]+f[i-1][j%n+1];
}
int main()
{
cin>>n>>m;
f[0][1]=1;
for(int i=1;i<=m;i++)
{
for(int j=1;j<=n;j++)
{
f[i][j]=calc(i,j);
}
}
cout<<f[m][1]<<endl;
return 0;
}
最长不下降子序列
设有一个正整数序列:b1,b2,...bn,对于下标i1<i2<...<im,若有bi1<=bi2<=...<=bim,则称存在长度为m的不下降子序列
缩小范围,最后一次决策是选择了最长不下降子序列的最后一个元素,再往前一次决策时选择了最长不下降子序列倒数第二个元素
f[i] 表示最长不下降子序列的最后一个元素
f[i] 表示前i个数的最长不下降子序列长度,不太方便状态转移
f[i] 表示前i个数中第i个数必须要取的最长不下降子序列长度
f[i] = f[j] +1 (j<i bj<bi)
第一步:确定状态——原问题?子问题?
F[i] 前i个数的最长不下降子序列——求不了啊
不知道这个序列的最后一个元素是哪个,没法转移
F[i] 以第i个数为结尾的最长不下降子序列
第二步:确定状态转移方程
f[i] = max{f[j]+1} (a[j]<=a[i]且j<i)
f[i] = 1
第三步:考虑是否需要优化
o(n^2)
可以使用单调栈或者线段树等数据结构优化到O(nlogn)
第四步:确定实现方法
a[i]
13 7 9 16 38 24 37 18 44 19 21 22
1 1 2 3 4 4 5 4 6 5 6 7
正常遍历
#include <bits/stdc++.h>
using namespace std;
const int maxn=510;
int a[maxn],f[maxn];
int n;
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i];
f[i]=1;
}
for(int i=2;i<=n;i++)
{
for(int j=1;j<i;j++)
{
if(a[j]<=a[i])
f[i]=max(f[j]+1,f[i]);
}
}
for(int i=1;i<=n;i++)
cout<<f[i]<<" ";
return 0;
}
记忆化搜索
#include <bits/stdc++.h>
using namespace std;
const int maxn=510;
int a[maxn],f[maxn];
int n;
int calc(int x)
{
if(f[x]!=0) return f[x];
f[x]=1;
for(int i=1;i<x;i++)
{
if(a[i]<=a[x])
{
f[x]=max(f[i]+1,f[x]);
}
}
return f[x];
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
cin>>a[i];
f[1]=1;
for(int i=2;i<=n;i++)
f[i]=calc(i);
for(int i=1;i<=n;i++)
cout<<f[i]<<" ";
return 0;
}
滑雪
滑雪是向下滑,想知道在一个区域中最长底滑坡。区域由一个二维数组给出,每个数字代表点的高度。一个人可以从某个点滑向上下左右相邻四个点之一
1 2 3 4 5
16 17 18 19 6
15 24 25 20 7
14 23 22 21 8
13 12 11 10 9
假设地图里每一个点(x,y)都有可能作为坡道起点,从(x,y)出发,向上下左右去滑,滑到比他矮的地方,那么该点的滑雪最长距离就是上下左右四个点中能滑到的最长的滑雪坡道距离+1,求每一个点滑下去能滑多远
第一步:确定状态——原问题?子问题?
原问题:从(1, 1)到(n, m)任意一个点滑下的最长路径长度
子问题:从(i, j) 滑下的最长路径长度
f[i][j]表示从 (i, j) 滑下的最长路径长度
第二步:确定状态转移方程 ——由于f[i][j]由上下左右四个方向转移过来
F[i][j]= max{f[i-1][j]+1 (a[i-1][j]<a[i][j])
max{f[i+1][j]+1 (a[i+1][j]<a[i][j])
max{f[i][j-1]+1 (a[i][j-1]<a[i][j])
max{f[i][j+1]+1 (a[i][j+1]<a[i][j])
(初值) :f[i][j]=1 (至少经过自己一个点)
(记忆化搜索)
第三步:考虑是否需要优化
第四步:确定实现方法
实现难点:不知道比他矮的点的f值
解决方案1:按从低到高排序,循环算出每个点的能滑的最长长度
解决方案2:记忆化搜索
#include <bits/stdc++.h>
//#define LOCAL
using namespace std;
typedef long long ll;
#define IOS ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
const double pi=acos(-1.0);
const int INF=1000000000;
//const int maxn=1000005;
int r,c,maxn=0;
int a[110][110],f[110][110];
int calc(int i,int j)
{
if(i<1||j<1||i>r||j>c) return -1;
if(f[i][j]!=0) return f[i][j];
f[i][j]=1;
if(a[i-1][j]<a[i][j]) f[i][j]=max(f[i][j],calc(i-1,j)+1);
if(a[i][j-1]<a[i][j]) f[i][j]=max(f[i][j],calc(i,j-1)+1);
if(a[i][j+1]<a[i][j]) f[i][j]=max(f[i][j],calc(i,j+1)+1);
if(a[i+1][j]<a[i][j]) f[i][j]=max(f[i][j],calc(i+1,j)+1);
return f[i][j];
}
int main()
{
IOS;
#ifdef LOCAL
freopen("input.txt","r",stdin);
freopen("output.txt","w",stdout);
#endif
cin>>r>>c;
memset(a,0x3f3f,sizeof(a));
for(int i=1;i<=r;i++)
{
for(int j=1;j<=c;j++)
{
cin>>a[i][j];
}
}
for(int i=1;i<=r;i++)
{
for(int j=1;j<=c;j++)
{
f[i][j]=calc(i,j);
maxn=max(maxn,f[i][j]);
}
}
cout<<maxn<<endl;
return 0;
}
最大子串和
给一个有正有负的序列,求一个子串(连续的一段),使得和最大
f[i]表示前i个数第i个数必须选的情况
一定要选第i个数,那么就是判断要不要前面的那一串
f[i]=max( f[i-1]+a[i],a[i] )
注意负数的情况,所以maxnn要取到负极大值
#include <bits/stdc++.h>
//#define LOCAL
using namespace std;
typedef long long ll;
#define IOS ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
const double pi=acos(-1.0);
const int INF=1000000000;
const int maxn=200010;
ll n,maxnn=-0x3f3f;
ll f[maxn],a[maxn];
int main()
{
IOS;
#ifdef LOCAL
freopen("input.txt","r",stdin);
freopen("output.txt","w",stdout);
#endif
cin>>n;
for(int i=1;i<=n;i++)
cin>>a[i];
for(int i=1;i<=n;i++)
{
f[i]=max(f[i-1]+a[i],a[i]);
maxnn=max(f[i],maxnn);
}
cout<<maxnn<<endl;
return 0;
}
最长公共子序列
给定两个序列X和Y,当另一序列Z既是X的子序列,又是Y的子序列,称Z为X和Y的公共子序列。
子问题:X的前i个字母和Y的前j个字母的公共子序列
第一步:确定状态——原问题? 子问题?
fi][j] 表示前一个字符串的前i位与后一个字符串的前j位的最长公共子序列长度
第二步:确定状态转移方程
当x[i]==y[j], f[i][j]=f[i-1][j-1]+1
当x[i]!=y[j],f[i][j]=max( f[i- 1][j] ,f[i][j-1])
a[1]==b[1] f[1][1]= 1
else f[1][1]= 0
第三步:考虑是否需要优化
第四步:确定实现方法
下列解法二维数组不能开得很大,会爆炸,所以我们可以考虑滚动数组
#include <bits/stdc++.h>
//#define LOCAL
using namespace std;
typedef long long ll;
#define IOS ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
const double pi=acos(-1.0);
const int INF=1000000000;
const int maxn=105;
int n,maxnn=0;
int a[maxn],b[maxn],f[maxn][maxn];
int main()
{
IOS;
#ifdef LOCAL
freopen("input.txt","r",stdin);
freopen("output.txt","w",stdout);
#endif
cin>>n;
for(int i=1;i<=n;i++)
cin>>a[i];
for(int i=1;i<=n;i++)
cin>>b[i];
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
if(a[i]==b[j]) f[i][j]=f[i-1][j-1]+1;
else f[i][j]=max(f[i][j-1],f[i-1][j]);
}
}
cout<<f[n][n]<<endl;
return 0;
}