线性dp

线性dp指有线性关系的一类dp,但是有时候很难看出来是线性的。之前说过的迷宫类DP也属于这个范畴,所以当然,它的难度通常属于dp中最低的,放轻松

LIS(最长上升子序列)

题目传送门

I. 怎么解?

dp

Ⅱ. 解题思路?

状态表示:f[i]

集合表示:\(f[i]\)表示从\(1到i\)中的LIS长度
属性:max

状态转移:

for (int i = 1; i <= n; i ++ )
{
    f[i] = 1;
    for (int j = 1; j <= i; j ++ ) // 判断合法
        if(m[j] < m[i]) 
		f[i] = max(f[i], f[j] + 1);
 
}

完整代码奉上

#include <iostream>
#define INF 0x7f7f7f7f
using namespace std;
const int N = 1010;

int dp[N], a[N], ans = -INF;//DP

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

LCS(最长公共子序列)

题目传送门

I. 怎么解?

dp

Ⅱ. 解题思路?

状态表示:f[i][j]

集合表示:\(f[i][j]\)表示从\(a[i]\)\(b[j]\)的LCS长度
属性:max

状态转移:分为四种情况:


if(a[i] == b[j])
{
    就直接选a[i]和b[j],因此长度就是上次长度加一,
    f[i][j] = f[i-1][j-1] + 1
}
else if(a[i] != b[j])
{
    再分三种情况:a选b不选,a不选b选,ab都不选,最后取max
    
    1. ab都不选
    {
        即保持之前的状态
        f[i][j] = f[i-1][j-1]
    }
    2. a选b不选
    {
        即只考虑a[i]到b[j-1]
        f[i][j] = f[i][j-1]
    }
    3. a不选b选
    {
        同上
        f[i][j] = f[i-1][j]
    }
    
}

实际上ab都不选的情况包含在2和3中,因此ab都不选可以不用考虑

完整代码奉上

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010;
int f[N][N], n, m;
char a[N], b[N];
int main()
{
    scanf("%d%d", &n, &m);
    scanf("%s%s", a+1, b+1); // 下标从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] = f[i-1][j-1] + 1;
        }
    printf("%d", f[n][m]);
    return 0;
}

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

较难

题目描述

给定两个整数序列,写一个程序求它们的最长上升公共子序列。

当以下条件满足的时候,我们将长度\(N\)的序列\(S\_1,S\_2,...,S\_N\) 称为长度为\(M\)的序列\(A\_1,A\_2,...,A\_M\)的上升子序列:

存在\(1≤i_1\)

输入

每个序列用两行表示,第一行是长度\(M(1≤M≤500)\),第二行是该序列的\(M\)个整数\(A\_i(-2^{31}≤A\_i<2^{31} )\)

输出

在第一行,输出两个序列的最长上升公共子序列的长度\(L\)。在第二行,输出该子序列。如果有不止一个符合条件的子序列,则输出任何一个即可。

输入数据 1

5
1 4 2 5 -12
4
-12 1 2 4

输出数据 1

2
1 4

此题分为两问

Q1

第一问是典型的线性dp,是LIS和LCS的结合,但是状态的表示比较难想

状态表示:

  • f[i][j]代表所有a[1 ~ i]b[1 ~ j]中以b[j]结尾的公共上升子序列的集合;

属性:max

状态转移:

for(i = 1 ~ n, j = 1 ~ m)

if(不包含a[i])
{
    f[i - 1][j];
}
else
{
	// 根据子序列倒数第二个数的取值继续划分集合
	for(k ~ j - 1) // k为子序列倒数的第二个数
		f[i - 1][k] + 1;
}

直接写出代码:

for(int i = 1; i <= n; i++)
{
	for(int j = 1; j <= m; j ++)
    {
        f[i][j] = f[i - 1][j];
        if(a[i] == b[j]) // 是公共的
        {
            int mx = 1;
        	for(int k = 1; k <= j - 1; k ++)
            	if(a[i] > b[k] && mx < f[i - 1][k] + 1) // 是上升的
                	mx = f[i - 1][k] + 1;
            f[i][j] = max(f[i][j], mx);
        }
        
    }
}

聪明的你也许发现了,这样需要三层循环,而实际上,我们可以将其优化至两层循环。

不难发现,每轮的mx实际上都记录的是上一个阶段的,因此等价代换得到正解

for(int i = 1; i <= n; i ++)
{
    int mx = 0;
    for(int j = 1; j <= m; j++)
    {
        f[i][j] = f[i - 1][j];            
        if(a[i] > b[j] && f[i - 1][j] > mx) mx = f[i - 1][j];
        else if(a[i] == b[j]) f[i][j] = mx + 1;
    }
}

Q2

关于第二问,我们只需记录每个状态的前驱,最后顺着前驱往前找即可

完整代码奉上

#include <iostream>
#include <cstring>
#include <algorithm>
#define INF 0x3f3f3f3f
using namespace std;
const int N = 500;
typedef long long LL;

// init
struct qaq
{
    int num, nx1, nx2;
}f[N][N]; // 存前驱
int a[N], b[N], ans2[N], idx;

int main()
{
    // input
    int n1, n2;
    cin >> n1;
    for(int i = 1; i <= n1; i ++) cin >> a[i];
    cin >> n2;
    for(int i = 1; i <= n2; i ++) cin >> b[i];
    
    swap(a, b);
    swap(n1, n2);
    
    // dp
    for(int i = 1; i <= n1; i ++)
    {
        qaq maxv = (qaq){0, 0, 0};
        for(int j = 1; j <= n2; j++)
        {
            f[i][j] = (qaq){f[i - 1][j].num, i - 1, j};            
            if(a[i] > b[j] && f[i - 1][j].num > maxv.num) maxv = (qaq){f[i - 1][j].num, i - 1, j};
            else if(a[i] == b[j]) f[i][j] = (qaq){maxv.num + 1, maxv.nx1, maxv.nx2};
        }
    }
    
    // ans_1
    int ans = 0, temp;
    for(int i = 1; i <= n2; i++) 
    {
        if(ans < f[n1][i].num)
        {
            ans = f[n1][i].num;
            temp = i;
        }
    }
    cout << ans << endl;

    if(ans == 0) return 0;
    // ans_2
    int i = n1, j = temp;
    while(i && j)
    {
        qaq t = f[i][j];
        if(f[t.nx1][t.nx2].num == t.num - 1)
			ans2[++idx] = a[i];

        i = t.nx1;
        j = t.nx2; // 下一个前驱
    }

    // output
    for(int i = idx; i >= 1; i --) cout << ans2[i] << " ";
    puts("");

    return 0;
}

最短编辑距离

题目传送门

I. 怎么解?

dp

Ⅱ. 解题思路?

状态表示:f[i][j]
集合表示:a[i]到b[j]的最短编辑距离
属性:min

状态转移:

分四种情况讨论
if(a[i] == b[j])
{
    跳过,不用编辑,因此操作次数不用增加
    f[i][j] = f[i-1][j-1]
}
else if(a[i] != b[j])
{
    再分三种情况讨论,取min
    
    1. 删除
    {
        如果选择删除,删除的必然是a[i],因为之前的已经匹配
        那么a[1 - i-1]和b[1-j]匹配
        f[i][j] = f[i-1][j] + 1
    }
    2. 插入
    {
        如果选择插入,插入的肯定是b[j]
        那么a[1-i]和b[1- j-1]匹配
        f[i][j] = f[i][j-1] + 1
    }
    3. 替换
    {
        相当于删除+插入,所以二者的方程都要匹配
        f[i][j] = f[i-1][j-1] + 1
    }
}

完整代码奉上

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
#define INF 0x3f3f3f3f
const int N = 1010;
int f[N][N];
char a[N], b[N];
int main()
{
    int la, lb;
    scanf("%d%s", &la, a+1); // 从1开始方便
    scanf("%d%s", &lb, b+1);
    
    for(int i = 1; i <= la; i++)
        for(int j = 1; j <= lb; j++)
        f[i][j] = INF; // 因为是min所以是正INF
    
    for(int i = 1; i <= la; i++) f[i][0] = i; // 从a[1-i]变到空需要i次删除
    for(int i = 1; i <= lb; i++) f[0][i] = i; // 从空变到b[1-i]需要i次插入
    
    for(int i = 1; i <= la; i++)
    {
        for(int j = 1; j <= lb; 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);
        }
    }
    printf("%d", f[la][lb]);
    return 0;
}

扩展题目

LIS扩展怪盗基德的滑翔翼

LIS扩展合唱队形

posted @ 2022-07-22 23:42  MoyouSayuki  阅读(53)  评论(0编辑  收藏  举报
:name :name