入门dp总结
写这篇博文主要是为了归纳总结一下dp的有关问题(不定期更新,暑假应该会更的快一些)
会大概讲一下思路,不会事无巨细地讲
另一篇是平时做过的一些dp题,这篇博客里面提到的题都有题解放在那边:https://www.cnblogs.com/henry-1202/p/9211398.html
这个玩意更新会有点慢,比较系统的学过一些dp的问题之后才会来写这个(可能要有人来催更才会写?)
一.最长上升子序列问题(LIS)
大概意思是给一个序列,按从左到右的顺序选出尽可能多的数,组成一个上升子序列(子序列:对于一个序列,在其中删除1个或多个数,其他数的顺序不变)
随便来个序列:1 8 7 4 8 9
它的最长上升子序列是1 4 8 9(当然也可以是1 7 8 9)
对于这个问题,有O(n2)做法和O(nlogn)做法,这里两个做法都会讲到
1.LIS的O(n2)做法
对于一个简单的LIS,你是怎么用肉眼判断的?一个一个往后找,然后数出最长的LIS,是不是?
n2做法就是用这种思路来做的,事实上这个做法就是一个优化后的暴力
设f[i]为以i为结尾的LIS的最大长度(也可以是起点),则有
f[i]=max(f[i],f[j]+1)(a[i]>a[j])
代码如下:
for(int i=1;i<=n;i++){ f[i]=1;//这里不要忘记初始化,这是每个人都很容易忘记的地方 for(int j=1;j<i;j++){ if(a[i]>a[j])f[i]=max(f[i],f[j]+1); } }
当然第二层循环也可以枚举j+1到n,都一样的,if里面的大于号改一下就好(这样的话就是f数组表示以ai为起点的最长上升子序列的)
这五行代码是LIS的基本模型,基本所有LIS的题都是对这五行代码添添改改搞出来的,所以在学习下面的东西之前需要务必确保自己完全搞懂这五行代码了再继续看
确保自己搞懂了之后可以先做一下例题中的合唱队形还有导弹拦截的n2做法
2.LIS的O(nlogn)做法
在学习LIS的nlogn做法之前,你需要两个前置姿势:知道什么是贪心,并且会打二分查找
想想LIS:在一个序列中选出尽可能多的数组成一个上升子序列。
让我们用用贪心的思想:
对于n2做法,我们是在求出以ai为结尾(起点)的LIS,以不断更新f数组的值的方式来维护LIS,而在这个过程中,f数组的值一定是最优的,而对于LIS来说,最优意味着什么?
对于最优的LIS,它的最后一位一定是尽可能小的,因为这样在后面更新的过程中,你才有更大的可能性让这个LIS变得更长
LIS的nlogn做法就是根据这种思想来做的,然后通过二分查找来使效率降到nlogn
相应的,f数组的含义也需要改一下:fi代表长度为i的LIS的最后一位数
在看代码之前请确保你理解了上面的贪心的思路
int m=1;//m是指LIS的长度,m=1是因为LIS的长度至少为1 memset(f,127,sizeof(f));//初始化,这里的127表示的是无穷大 f[1]=a[1];//初始化 for(int i=2;i<=n;i++){ if(a[i]>f[m])f[++m]=a[i];//如果比当前的LIS的最后一位还大那这个LIS就可以变长了 else {//通过二分查找来更新f数组 int l=0,r=m; while(l<r){ int mid=(l+r)>>1; if(a[i]>f[mid])r=mid; else l=mid+1; } f[l]=a[i];//不要忘记更新 } }
想练一下LIS的nlogn做法的话就去试试洛谷的导弹拦截吧,可以去我的另一篇博客看题解qwq,链接在上面
二,最长公共子序列(LCS)
公共子序列是指:一个同时是这两个序列的子序列的序列
有点绕口...反正就是那个意思
例如:
1 2 3 4 5
2 1 3 5 4
LCS的长度显然是3(1,3,4/2,3,4)
于是可以设f[i][j]表示第一个序列的前i位与第二个序列的前j位的LCS
答案就是f[len1][len2]
转移方程其实很好想的:
如果第一个序列的i位与第二个序列的j位相同,那么转移:f[i][j]=max(f[i-1][j],f[i][j-1])+1
如果不同那么考虑继承之前的最优答案:f[i][j]=max(f[i-1][j],f[i][j-1])
这里给例题中的 洛谷P1439[模板]最长公共子序列 的朴素LCS做法
#define ll int #define N 1010 ll n,a[N],b[N],f[N][N]; int main(){ scanf("%d",&n); for(ll i=1;i<=n;i++)scanf("%d",&a[i]); for(ll i=1;i<=n;i++)scanf("%d",&b[i]); for(ll i=1;i<=n;i++){ for(ll j=1;j<=n;j++){ if(a[i]==b[j])f[i][j]=max(f[i-1][j],f[i][j-1])+1; else f[i][j]=max(f[i-1][j],f[i][j-1]); } } printf("%d",f[n][n]); return 0; }
然后这道题的100%做法是很妙的,有兴趣的话可以去做一下(上面的做法只能50分)
三.区间dp
这个我应该会写多一点……
区间dp的定义:顾名思义,就是在对区间进行dp,一般是对小区间进行dp得出最优解,然后通过小区间的最优解得出一整个区间的最优解
大概是这样?反正也差不多,在没有题的情况下再怎么讲也很空泛,上面的定义随便看看就好,知道区间dp是个什么东西就行,具体看下面的例题来理解。
1.矩阵链乘问题
这是紫书和算导都有的东西,不过我没有在OJ上面找到这道题...
首先在讲这道题之前我们要明确矩阵乘法的几个概念
1.两个矩阵能够相乘,当且仅当矩阵A的列数等于矩阵B的行数
2.设矩阵A的规模为n*p,矩阵B的规模为p*m,则这两个矩阵相乘的运算量为n*m*p
2.矩阵乘法满足结合律但不满足分配律
在明白矩阵乘法的概念之后,就可以来看这道题了
题意:给n个矩阵,全部都要乘起来,最后得到一个矩阵,你可以给他们加括号改变运算顺序,求最少的运算量,假设第i个矩阵Ai的规模是pi-1*pi
显然,加不加括号对这道题的运算量的影响是巨大的,举个例子,对于三个矩阵A,B,C,假设他们分别是2*3,3*4,4*5的,那么(A*B)*C的运算量为64,A*(B*C)的运算量为90,,这两种运算方式得出的最终的矩阵其实是一样的,但是运算量差别就很大了。
那么怎么解决这道题?
我们不妨用一般解dp题的思路来看看,把这一整个序列分成多个小的序列(划分子问题)
设l为一个小序列的左端点,r为一个小序列的右端点,那么当这个小序列足够小(l==r),运算量显然为0(你没办法用它自己来乘它自己),这样我们就能得出初始化的情况了
然后假设我们现在正在求解某个子问题,这一次乘法是第k个乘号,那么我们一定已经算出A1*A2*···*Ak和Ak+1*Ak+2*···*An的最优结果,因此对于这一次相乘的结果它一定也是最优的(dp的最优子结构性质)
设我们正在求解的这个子问题的运算量为f[i][j],那么可得f[i][j]=min{f[i][k]+f[k+1][j]+pi-1*pj*pk}(k即上文我们提到的“最优的第k个乘号”)
那么有了转移方程后我们就可以来写这道题了吗?不,我们还需要注意到一个特殊的情况,如果采用递推求解的方式,因为需要满足dp的无后效性的性质,所以我们不能按照i/j的递增/递减顺序来进行运算,而是要以j->i递增的顺序来运算。
当然记忆化搜索就没有这个问题了,不过我不习惯打记忆化搜索,这一方面的话算导有讲到
总的时间复杂度为O(n3)
是的没有代码我懒得写,不过反正就是一个引入,应该不需要代码吧···理解一下思路
真的需要代码的话可以跟我讲下我去搞一下
二.石子合并问题
区间dp的例题,流传甚广的一道题,这里只给O(n3)做法,四边形不等式优化我不会啊>_<
与上一题相同,枚举断点划分子问题,通过子问题的最优解得出整个区间的最优解,区间dp的题都是这种套路
显然对于每个区间f[i][j],一定是由最优断点的结果转移过来的(不难证明:如果当前断点不是最优的,那么如果使用更优的断点得出的结果,也一定比当前断点更优)
于是设f[i][j]表示把i~j这个区间的石子合并为一堆石子的最小/最大花费
答案就是所有区间长度为n的区间的最大值/最小值
然后代码中的具体实现,我个人推荐的做法是枚举区间长度,左端点和断点,右端点可以通过左端点和区间长度的值算出来
还有就是对于石子的个数处理一下前缀和存在数组c中,这样在转移的时候就可以O(1)得到i~j堆石子的合并花费
转移方程即为f[i][j]=max(f[i][j],f[i][k]+f[k+1][j]+c[j]-c[i-1])(对于最小值是min)
#include <cstdio> #include <cstring> #define ll long long #define inf 1<<30 #define il inline #define in1(a) read(a) #define in2(a,b) in1(a),in1(b) #define in3(a,b,c) in2(a,b),in1(c) #define in4(a,b,c,d) in2(a,b),in2(c,d) il int max(int x,int y){return x>y?x:y;} il int min(int x,int y){return x<y?x:y;} il int abs(int x){return x>0?x:-x;} il void swap(int &x,int &y){int t=x;x=y;y=t;} il void readl(ll &x){ x=0;ll f=1;char c=getchar(); while(c<'0'||c>'9'){if(c=='-')f=-f;c=getchar();} while(c>='0'&&c<='9'){x=x*10+c-'0';c=getchar();} x*=f; } il void read(int &x){ x=0;int f=1;char c=getchar(); while(c<'0'||c>'9'){if(c=='-')f=-f;c=getchar();} while(c>='0'&&c<='9'){x=x*10+c-'0';c=getchar();} x*=f; } using namespace std; /*===================Header Template=====================*/ #define N 210 int n,f_min[N][N],f_max[N][N],a[N],c[N]; int main(){ in1(n); for(int i=1;i<=n;i++)in1(a[i]),a[i+n]=a[i]; for(int i=1;i<=2*n;i++)c[i]=c[i-1]+a[i]; for(int len=2;len<=n;len++){ for(int i=1;i<=2*n;i++){ int j=i+len-1;f_min[i][j]=inf; for(int k=i;k<j;k++){ f_min[i][j]=min(f_min[i][j],f_min[i][k]+f_min[k+1][j]+c[j]-c[i-1]); f_max[i][j]=max(f_max[i][j],f_max[i][k]+f_max[k+1][j]+c[j]-c[i-1]); } } } int ans_max=0,ans_min=inf; for(int i=1;i<=n;i++){ ans_max=max(ans_max,f_max[i][i+n-1]); ans_min=min(ans_min,f_min[i][i+n-1]); } printf("%d\n%d\n",ans_min,ans_max); return 0; }
四.最大子段和问题
挺简单的一类dp问题
最大子段和问题是这样的:
给你一个序列,正负不定,你需要求出$a[i]+a[i+1]+···+a[j-1]+a[j]$的最大值$(1<=i<=j<=n)$
分治和dp的经典题
这里只讲dp做法
我们设$f[i]$表示从1到i的最大子段和,那么可以很简单的推出转移方程
$f[i]=max(f[i-1]+a[i],a[i])$
也可以换一种写法
if(f[i-1]>0)f[i]=f[i-1]+a[i];
else f[i]=a[i];
初始化f[1]=a[1]
以上就是最基础的几个dp类型啦(完结撒花)