动态规划_最长上升子序列

Posted hot-machine

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了动态规划_最长上升子序列相关的知识,希望对你有一定的参考价值。

原型:最长上升子序列

分析

  • 状态表示:所有以a[i]结尾的严格单调上升的子序列的Max长度
  • 状态划分依据:以最后一个不同的点
  • 状态方程:dp[i] = max(dp[i], dp[j] + 1), j必须要小于i
for(int i = 1; i <= n; i++) {
    dp[i] = 1;
    for(int j = 1; j < i; j++) {
        if(a[i] > a[j]) {
            dp[i] = max(dp[i], dp[j] + 1);
        }
    }
}
int res = 0;
for(int i = 1; i <= n; i++) res = max(res, dp[i]);
printf("%d ", res);

寻找路径

memset(tr, -1, sizeof tr);

for(int i = 1; i <= n; i++) {
    dp[i] = 1;
    for(int j = 1; j < i; j++) {
        if(a[i] > a[j]) {
            // 有更新
            if(dp[i] < dp[j] + 1) {
                dp[i] = dp[j] + 1;
                tr[i] = j;
            }
        }
    }
}

int ans = 0, sign = -1;
for(int i = 1; i <= n; i++) {
    if(ans < dp[i]) {
        ans = dp[i];
        sign = i;
    }
}
// 找到最终的以sign为结尾,长度为ans的序列
printf("%d %d
", ans, sign);
int i = sign;
while(~i) {
    cout << a[i] << " ";
    i = tr[i];
}

变形扩展

1017. 怪盗基德的滑翔翼

  • 初始方向不确定,考虑从两个方向各找一遍(从左到右、从右到左)
  • 最长下降子序列可转化成逆向的最长上升子序列

AcWing 1014. 登山

  • 求严格上升到某一节点之后严格下降这一整段的序列的长度最大值
  • 分别从左到右、从右到左求最长上升子序列长度,遍历每个节点,以此节点作为拐点,所形成的序列长度求最大值:res = max(res, f[i] + g[i] - 1)

AcWing 1012. 友好城市

技术图片

  • 以南边的城市为自变量,将其排序,与之对应的因变量即北边的城市坐标,应该满足跟随自变量的递增而单调递增,即抽象出最长上升子序列问题
#include <iostream>
#include <algorithm>

using namespace std;
const int N = 5100;
int dp[N];
struct city {
    int s, n;
}cities[N];

int main() {
    int n; scanf("%d", &n);
    for(int i = 1; i <= n; i++) scanf("%d%d", &cities[i].s, &cities[i].n);
    
    sort(cities, cities + n + 1, [&](city a, city b) { return a.n < b.n;});
    
    for(int i = 1; i <= n; i++) {
        dp[i] = 1;
        for(int j = 1; j < n; j++) {
            if(cities[j].n < cities[i].n) {
                dp[i] = max(dp[i], dp[j] + 1);
            }
        }
    }
    int res = 0;
    for(int i = 1; i <= n; i++) res = max(res, dp[i]);
    printf("%d", res);
    return 0;
}

1016. 最大上升子序列和

  • 状态表示:以该元素为结尾的上升子序列的和
for(int i = 1; i <= n; i++) {
        dp[i] = a[i];
        for(int j = 1; j < i; j++) {
            if(dp[i] > dp[j]) {
                dp[i] = max(dp[i], dp[j] + a[i]);
            }
        }
    }

1010. 拦截导弹 : LIS + 贪心

  • 第一问LIS问题
  • 第二问:求覆盖所有节点的子序列的个数的最小值
贪心思路:
  • 从前往后扫描每个数,对于每个数有以下两种情况
    1.现有的的所有子序列的结尾的数都小于当前这个数 -> 创建一个新的子序列
    2.将当前数放到结尾大于等于这个数的最小子序列后面(别耽误别人,尽量节约 haha)
贪心法求最长上升子序列(O(n^2))超时

数据范围:需要(O(nlogn))

(1 leq N leq 100000)
(-10^9 leq 数列中的数 leq 10^9)

  • 贪心策略:尽可能使得每个子序列中的结尾元素越小越好

  • 具体措施:针对每个数,将这个数接到结尾元素最大的小于此元素的子序列后

/*
数组a存储所有数据
数组q[i]表示序列长度为i的子序列的最后一个数
*/ 
int len = 0;
for(int i = 0; i < n; i++) {
    // 找到满足小于a[i]的最大值
    int l = 0, r = len;
    while(l < r) {
        int mid = l + r + 1 >> 1;
        if(q[mid] < a[i]) l = mid;
        else r = mid - 1;
    }
    len = max(len, r + 1);
    q[r + 1] = a[i];
}
cout << len;
Dilworth定理
  • Dilworth定理:对于一个偏序集,最少链划分等于最长反链长度。

  • Dilworth定理的对偶定理:对于一个偏序集,其最少反链划分数等于其最长链的长度。

以上的铺垫用于解决拦截导弹问题:转化成求最长上升子序列长度

187. 导弹防御系统: LIS + 暴搜

最长公共子序列:原型

  • 所有在第一个序列的前i个字母中出现,且在第二个序列的前j个字母中出现的子序列
  • Max
  • 以a[i], b[j]分别选与不选划分为四种状态
  • dp[i - 1][j - 1]
  • dp[i - 1][j]:不一定有b[j],因此包含前面的状态(在第一个序列的前i - 1个字母出现,且在第二个序列的前j - 1个字母中出现),但状态表示的是子序列的长度最大值,因此状态存在重复,不会影响最终结果
  • dp[i][j - 1]:与dp[i - 1][j]的情况类似
  • dp[i - 1][j - 1] + 1:子序列中包含a[i], a[j]
for(int i = 1; i <= n; i++) {
    for(int j = 1; j <= m; j++) {
        dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
        if(a[i] == b[j]) dp[i][j] = max(dp[i][j], dp[i - 1][j - 1] + 1);
    }
}
cout << dp[n][m];

最长公共上升子序列

  • 状态表示:所有第一个序列的前i个字母,和第二个序列的前j个字母构成的, 且以b[j]结尾的公共上升子序列的Max
  • 状态计算
    1.所有不包含a[i]的公共上升子序列: dp[i - 1][j]
    2.所有包含a[i]的公共上升子序列

对于包含a[i]的公共上升子序列,将其根据倒数第二个字母是什么进行划分:

  • b[1]:最大长度为1
  • b[2]:最大长度为dp[i - 1][1] + 1
  • ...
  • b[j - 1]: dp[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);
        }
    }
}

从中发现,每次如果找到公共子序列之后,都要计算一下dp[i][1 ~ j - 1]

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]);
printf("%d
", res);





以上是关于动态规划_最长上升子序列的主要内容,如果未能解决你的问题,请参考以下文章

动态规划-LIS最长上升子序列

动态规划——最长上升子序列

动态规划——最长公共上升子序列(C++)

动态规划模板1|LIS最长上升子序列

线性状态动态规划 P1020 导弹拦截最长上升子序列

最长上升子序列(LIS)动态规划