子串 or 子序列

最长上升子序列

给定一个长度为 nn 的数组 a1,a2,…,ana1,a2,…,an,问其中的最长上升子序列的长度。也就是说,我们要找到最大的 mm 以及数组 p1,p2,…,pmp1,p2,…,pm,满足 1≤p1<p2<⋯<pm≤n1≤p1<p2<⋯<pm≤n 并且 ap1<ap2<⋯<apmap1<ap2<⋯<apm。

输入格式

第一行一个数字 nn。

接下来一行 nn 个整数 a1,a2,…,ana1,a2,…,an。

输出格式

一个数,表示答案。

代码如下:

#include<iostream>
#include<algorithm>
using namespace std;
int a[1001];
int dp[1001];//定义dp表表示以a[i]为结尾的最长上升子序列
int main()
{
    int n;
    cin >> n;
    for(int i = 1; i <= n; i++)
    {
        cin >> a[i];
    }
    dp[1] = 1;//到1的时候最长上升子序列只有1个
    for(int i = 2; i <= n; i++)
    {
        dp[i] = 1;//首先自己至少就是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]);
    }
    cout << res;
    return 0;
}

顺着这一道题,我们要深刻的理解一下根据无后效性来定义状态

无后效性最通俗易懂的方法就是:过去不影响未来,只要求出了过去的某一个值,那么根未来就没有关系了。这个就是无后效性。刚做这一道题的时候我想的状态转移方程是:到达i位置的时候的最长上升子序列。这个状态的定义是错误的,原因是我如果要到第i位置,那么我就必须知道前面有多少个数字比我现在要大,也就是说我到达i位置的时候我还必须回头看看过去的情况,看看以前有多少比我小,这个就是违反了无后效性的。所以正确的状态转移方程是:以a[i]结尾的最长上升子序列,为什么这个是对的,因为假如我以及求出了以a[i - 1]结尾的最长上升子序列的话,那么只需要再判断这个数字是否要小,就可以直接转移出i位置的,不需要了解过去的情况。

此题还有一个很关键的点把我卡住了,就是我到达i位置的时候要回溯搜索小于i位置的所有位置,这个套路很多题也会使用

最长上升字串

和上面一道题一模一样,只不过我们改变一下,把序列改成字串。

代码如下:

#include<iostream>
#include<algorithm>
using namespace std;
int a[1001];
int dp[1001];//定义dp表表示以i结尾的最长上升字串
int main()
{
    int n;
    cin >> n;
    for(int i = 1; i <= n; i++)
    {
        cin >> a[i];
    }
    int res = 0;//用一个变量来记录最大值
    dp[1] = 1;//1位置最长上升字串就是它自己
    for(int i = 2; i <= n; i++)
    {
        dp[i] = 1;//这里代表的是清0的过程
        if(a[i] < a[i - 1])
        {
            res = max(res, dp[i - 1]);
        }
        else
        {
            dp[i] = dp[i - 1] + 1;
        }
    }
    cout << res;
    return 0;
}

这里的dp含义就可以定义为以i结尾了。

最长公共子序列

给定一个长度为 n 的数组 a1,a2,…,an 以及一个长度为 m 的数组 b1,b2,…,bm,问 a 和 b 的最长公共子序列的长度。

也就是说,我们要找到最大的 k 以及数组 p1,p2,…,pk,数组 l1,l2,…,lk 满足 1≤p1<p2<⋯<pk≤n 并且 1≤l1<l2<⋯<lk≤m 并且对于所有的 i(1≤i≤k) ,api=bli。

输入格式
第一行两个整数 n,m。

接下来一行 n 个整数,a1,a2,…,an。

接下来一行 m 个整数,b1,b2,…,bm。

输出格式
输出一个整数,表示答案。
    

先来看代码:

#include<iostream>
#include<algorithm>
using namespace std;
int a[1001];
int b[1001];
int dp[1001][1001];//定义dp表,表示前a数组前i的位置和b数组前j的位置的最大公共子序列
int main()
{
    int n, m;
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
        cin >> a[i];
    for(int i = 1; i <= m; i++)
        cin >> b[i];
    //当i或者j有一个是0的时候,最长公共子序列根本不存在,所以从1开始可以很巧妙的避免补条件
    for(int i = 1; i <= n; i++)
    {
        for(int j = 1; j <= m; j++)
        {
            if(a[i] == b[j])
            {
                dp[i][j] = dp[i - 1][j - 1] + 1;
            }
            else
            {
                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
            }
        }
    }
    cout << dp[n][m];
    return 0;
}

这道题的dp表的含义是:dp[i][j],表示a数组前i个和b数组前j个的最长公共子序列。

转移:如果a[i] == b[j]的话那么说明可以多一个公共子序列,否则的话就缩小范围继续查找。dp[i - 1] [j]或者dp[i] [j - 1]里面的最大值。这里我们定义的含义是以下标i和下标j为计数的。所以我们必须要从int i = 1的时候开始,0表示我这个时候没有序列,但是如果我们做leetcode里面的oj的话,那么我们必须从0开始,这个时候我们就要处理边界情况了,但是我不想处理边界情况,所以出现了下面这个很巧妙的写法:

class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        int N = text1.size();//这里直接对应的就是最后一个字母的下标
        int M = text2.size();

        vector<vector<int>> dp(N + 1, vector<int>(M + 1));


        for(int i = 1; i <= N; ++i)
        {
            for(int j = 1; j <= M; ++j)
            {
                if(text1[i - 1] == text2[j - 1])
                {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                }
                else
                {
                    dp[i][j] = max(dp[i][j], max(dp[i - 1][j], dp[i][j - 1]));
                }
            }
        }  
        return dp[N][M];
    }
};

这里dp表的定义是长度为i和长度为j的最长公共子序列。所以我们比较的时候比较下标应该是tex1[i - 1] == text2[j - 1]最后返回dp[N] [M].这就是巧妙的地方。

最长回文字串

#include<iostream>
#include<algorithm>
using namespace std;
int n;
int a[1001];
int dp[1001][1001];//定义dp表的含义是(i, j)范围之内的最长回文字串的长度
int main()
{
    cin >> n;
    for(int i = 1; i <= n; i++)
        cin >> a[i];
    int res = 1;//res用来记录最长的回文字串的长度
    for(int i = 1; i <= n; i++)
        dp[i][i] = 1;//只有一个数字的时候最长回文字串就是它自己,为1
    for(int i = n - 1; i >= 1; i--)
    {
        dp[i][i + 1] = a[i] == a[i + 1] ? 2 : 0;
        for(int j = i + 2; j <= n; j++)
        {
            if(a[i] == a[j] && dp[i + 1][j - 1])
            {
                dp[i][j] = dp[i + 1][j - 1] + 2;
            }
            else
            {
                dp[i][j] = 0;
            }
            res = max(res, dp[i][j]);
        }
    }
    cout << res;
    return 0;
}

这道题应该话表格方便来理解,然后就是注意这里的条件判断。

最长回文子序列

class Solution {
public:
    int longestPalindromeSubseq(string s) {
        int n = s.size();
        vector<vector<int>> dp(n + 1, vector<int>(n + 1));
        for(int i = 0; i < n; i++)
        {
            dp[i][i] = 1;
            dp[i][i + 1] = s[i] == s[i + 1] ? 2 : 1;
        }
        for(int i = n - 3; i >= 0; i--)
        {
            for(int j = i + 2; j < n; j++)
            {
                //第一种可能,
                if(s[i] == s[j])
                {
                    dp[i][j] = dp[i + 1][j - 1] + 2;
                }
                else
                {
                    dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
                }
            }
        }
        return dp[0][n - 1];
    }
};

回文问题与序列问题总结

对比上面的回文序列子串问题可以发现:如果是子串问题的话,涉及到子串问题的题目都有一个清0和记录最大值的过程,如果是子序列问题的话就不需要了。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

胡桃姓胡,蝴蝶也姓胡

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值