线性DP学习笔记

文章目录

  • 前言
  • 序列模型
    • 最长上升子序列(LIS)
    • 最长公共子序列(LCS)
    • 最长上升公共子序列(LCIS)
  • 字符串编辑模型
    • 最短编辑距离
    • 编辑距离
  • 求和模型
    • 大盗阿福(序列相关)
    • 摘花生(矩阵相关)
  • 总结

前言

线性dp,是较常见的一类动态规划问题,其是在线性结构上进行状态转移,这类问题不像背包问题、区间DP等有固定的模板。
线性动态规划的目标函数为特定变量的线性函数,约束是这些变量的线性不等式或等式,目的是求目标函数的最大值或最小值。
因此,除了少量问题(如:LIS、LCS、LCIS等)有固定的模板外,大部分都要根据实际问题来推导得出答案。
我总结了几种比较普遍的线性dp模型,包括序列模型字符串编辑模型求和模型这三种模型,以下解法都是最简单的没有经过优化的解法,因为这样更容易看懂。分析DP问题的方法采用了闫氏DP分析法

序列模型

最长上升子序列(LIS)

问题描述
线性DP学习笔记_第1张图片
解题思路
线性DP学习笔记_第2张图片
AC代码

#include 
#include 
#include 
using namespace std;
const int N = 1010;

int n;
int a[N];
int f[N];

int main()
{
    cin>>n;
    for (int i = 1; i <= n; i ++ ) cin>>a[i],f[i]=1;
    for(int i=1;i<=n;i++)
    for(int k=1;k<i;k++){
        if(a[i]>a[k]) f[i]=max(f[i],f[k]+1);
    }
    int res=1;
    for(int i=1;i<=n;i++) res=max(res,f[i]);
    cout<<res;
    return 0;
}

最长公共子序列(LCS)

问题描述
线性DP学习笔记_第3张图片
解题思路
线性DP学习笔记_第4张图片
AC代码

#include 
#include 
#include 
using namespace std;
const int N = 1010;

int n,m;
char a[N],b[N];
int f[N][N];


int main()
{
    cin>>n>>m>>(a+1)>>(b+1);
    for(int i=1;i<=n;i++)
    for(int j=1;j<=m;j++){
        f[i][j]=max(f[i-1][j],f[i][j-1]);
        if(a[i]==b[j]) f[i][j]=max(f[i][j],f[i-1][j-1]+1);
    }
    int res=0;
    for (int i = 1; i <= n; i ++ )
    for (int j = 1; j <= m; j ++ ) res=max(res,f[i][j]);
    cout<<res;
    return 0;
}

最长上升公共子序列(LCIS)

问题描述
线性DP学习笔记_第5张图片
线性DP学习笔记_第6张图片
解题思路
这道题比较难,而且不优化还过不了,就不在图里写解释过程了。
首先这题的状态定义就比较特别:和最长公共子序列相比,这里还多了一个以b[j]结尾的公共子序列
在集合划分上首先的依据的是a[i]是否包含在集合中
1.a[i]不包含在集合中:这个比较好理解就是f[i-1][j]
2.a[i]包含在集合中:首先,如a[i]包含在集合中,那么a[i]==b[j],a[i]的前一个数就是a[i-1],对于b[j]的前一个数,需要继续划分。之所以要继续划分是因为一定要抓住集合定义来做,这里的集合中j表示的是以b[j]结尾的公共子序列,那么在这个公共子序列中它的前一个数是哪个我们不知道,我们需要枚举k个(k=1~j-1)中每个以b[k]结尾的公共子序列,取其中的最大值即可:
子序列中只包含b[j]一个数,长度是1
子序列的第一个数是b[1]的集合,最大长度是f[i - 1][1] + 1

子序列的倒数第二个数是b[j - 1]的集合,最大长度是f[i - 1][j - 1] + 1
按照上述思路实现需要三重循环

for (int i = 1; i <= n; i ++ )
{
    for (int j = 1; j <= n; j ++ )
    {
        f[i][j] = f[i - 1][j];
        if (a[i] == b[j])
        {
            int maxv = 1;
            for (int k = 1; k < j; k ++ )
                if (a[i] > b[k])
                    maxv = max(maxv, f[i - 1][k] + 1);
            f[i][j] = max(f[i][j], maxv);
        }
    }
}

时间复杂度到达了n的三次方。因此需要对代码做等价变形来优化

我们发现每次循环求得的maxv是满足a[i] > b[k]的f[i - 1][k] + 1前缀最大值
因此可以直接将maxv提到第一层循环外面,减少重复计算,此时只剩下两重循环。
最终枚举子序列结尾取最大值即可
闫氏DP分析法的图见下
线性DP学习笔记_第7张图片
AC代码

#include 
#include 
#include 
using namespace std;
const int N = 3005;

int n;
int a[N],b[N];
int f[N][N];

int main()
{
    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++)
    {
        int maxv=1;
        for(int j=1;j<=n;j++)
        {
            f[i][j]=f[i-1][j];
            if(a[i]==b[j]) f[i][j]=max(f[i][j],maxv);
            if(a[i]>b[j]) maxv=max(maxv,f[i-1][j]+1);
        }
    }
    int res=0;
    for (int i = 1; i <= n; i ++ ) res=max(res,f[n][i]);
    cout<<res;
    return 0;
}

字符串编辑模型

最短编辑距离

问题描述
线性DP学习笔记_第8张图片
线性DP学习笔记_第9张图片
解题思路
闫氏DP分析法的过程见下图
1)删除操作:把a[i]删掉之后a[1i]和b[1j]匹配
所以之前要先做到a[1(i-1)]和b[1j]匹配
f[i-1][j] + 1
2)插入操作:插入之后a[i]与b[j]完全匹配,所以插入的就是b[j]
那填之前a[1i]和b[1(j-1)]匹配
f[i][j-1] + 1
3)替换操作:把a[i]改成b[j]之后想要a[1i]与b[1j]匹配
那么修改这一位之前,a[1(i-1)]应该与b[1(j-1)]匹配
f[i-1][j-1] + 1
但是如果本来a[i]与b[j]这一位上就相等,那么不用改,即
f[i-1][j-1] + 0
然后有一些细节问题,就是初始化的时候
如果a序列的长度是i,然后b序列的长度是0,那么想让a变b则需要进行i次删除操作
如果a序列的长度是0b序列的长度是i,那么想让a变b则需要进行i次插入操作

好的那么f[i][j]就由以上三个可能状态转移过来,取个min
线性DP学习笔记_第10张图片
AC代码

#include 
#include 
#include 
using namespace std;
const int N = 1010;

int n,m;
char a[N],b[N];
int f[N][N];

int main()
{
    cin>>n;
    for (int i = 1; i <= n; i ++ ) cin>>a[i];
    cin>>m;
    for (int i = 1; i <= m; i ++ ) cin>>b[i];
    for (int i = 1; i <= n; i ++ ) f[i][0]=i;
    for (int i = 1; i <= m; i ++ ) f[0][i]=i;
    
    for (int i = 1; i <= n; i ++ )
    for (int j = 1; j <= m; j ++ )
    {
        f[i][j]=min(f[i-1][j]+1,f[i][j-1]+1);
        if(a[i]==b[j]) f[i][j]=min(f[i][j],f[i-1][j-1]);
        else f[i][j]=min(f[i][j],f[i-1][j-1]+1);
    }
    cout<<f[n][m];
    return 0;
}

编辑距离

问题描述
线性DP学习笔记_第11张图片
线性DP学习笔记_第12张图片
解题思路
就是上一题编辑距离的一个应用,直接写就行,见如下代码
AC代码

#include 
#include 
#include 
using namespace std;
const int N = 1010;

int n,m;
char a[N][15];
int f[N][N];

int edit_distance(char a[],char b[])//完全就是按照最短编辑距离的写法来写
{
    int lena=strlen(a+1);
    int lenb=strlen(b+1);
    for(int i=1;i<=lena;i++) f[i][0]=i;
    for(int i=1;i<=lenb;i++) f[0][i]=i;
    for(int i=1;i<=lena;i++)
    for(int j=1;j<=lenb;j++)
    {
        f[i][j]=min(f[i-1][j]+1,f[i][j-1]+1);
        if(a[i]==b[j]) f[i][j]=min(f[i][j],f[i-1][j-1]);
        else f[i][j]=min(f[i][j],f[i-1][j-1]+1);
    }
    return f[lena][lenb];
}

int main()
{
    cin>>n>>m;
    for (int i = 1; i <= n; i ++ ) cin>>(a[i]+1);
    while (m -- )
    {
        char str[15];
        int q,res=0;
        cin>>(str+1)>>q;
        for (int i = 1; i <= n; i ++ )
        {
            if(edit_distance(str,a[i])<=q) res++;//对于每一个询问的字符
            //计算它与前面n个字符串的最短编辑距离,如果小于q,计数器=1
        }
        cout<<res<<endl;
    }
    return 0;
}

求和模型

大盗阿福(序列相关)

问题描述
线性DP学习笔记_第13张图片
线性DP学习笔记_第14张图片
解题思路
闫氏DP分析法如下图
这个比较简单,在集合划分的时候,注意到分两种决策:一种是抢劫当前的店铺,另一种是不抢劫
1.抢劫当前店铺:那么在进行状态转移的时候,它的上一个状态就不能从f[i-1]这里转移过来,因为抢劫相邻店铺会报警。所以是f[i]=f[i-2]+a[i]
2.不抢劫当前店铺:当前的f[i]=f[i-1]
两者取max即可
细节上注意初始化,f[1]=a[1],f[2]=max(a[1],a[2])
线性DP学习笔记_第15张图片

AC代码

#include
using namespace std;
const int N=1e5+10;

int n,t;
int a[N];
int f[N];

int main()
{
	cin>>t;
	while(t--)
	{
		cin>>n;
		for(int i=1;i<=n;i++) cin>>a[i];
		f[1]=a[1];
		f[2]=max(a[1],a[2]);
		for(int i=3;i<=n;i++)
		{
			f[i]=max(f[i-1],f[i-2]+a[i]);
		}
		cout<<f[n]<<endl;
	}
	return 0;
}

摘花生(矩阵相关)

问题描述
线性DP学习笔记_第16张图片
线性DP学习笔记_第17张图片
解题思路
闫氏DP分析法见下图
这题也比较简单,状态定义的时候就是二维矩阵的坐标。然后会从两个方向转移过来,从上方和从左边,取一个最大值再加上当前的值即可
线性DP学习笔记_第18张图片
AC代码

#include 
#include 
#include 
using namespace std;
const int N = 1010;

int t,n,m;
int a[N][N];
int f[N][N];

int main()
{
    cin>>t;
    while(t--)
    {
        cin>>n>>m;
        for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) cin>>a[i][j];
        for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
        {
            f[i][j]=max(f[i-1][j],f[i][j-1])+a[i][j];
        }
        cout<<f[n][m]<<endl;
    }
    return 0;
}

总结

线性DP这一块蓝桥杯什么的还是非常喜欢考的,可以考得很难,也可以考得简单,我还要多刷刷题(ง •_•)ง

你可能感兴趣的:(DP学习笔记,蓝桥杯,算法,动态规划)