动态规划:子序列系列及其扩展

本文探讨了动态规划在解决数组和字符串问题中的应用,包括最长递增子序列、最长连续递增子数组、最长公共子序列、最长回文子序列以及编辑距离。通过对各种问题的思路分析,展示了动态规划状态转移方程的建立和初始状态的设定,以及如何自底向上计算最优解。同时,文章还讨论了空间优化的可能性。
摘要由CSDN通过智能技术生成

LeetCode300. 最长递增子序列

https://leetcode-cn.com/problems/longest-increasing-subsequence/

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
思路

1.思考状态:找出最优解的性质,明确状态表示什么

dp[i]表示数组nums[0 … i]的最长递增子序列的长度。

2.思考状态转移方程:大问题的最有解如何由小问题的最优解得到

位置i的最长递增子序列等于j从0到i-1各个位置的最长递增子序列 + 1 的最大值。

所以:if (nums[i] > nums[j]): dp[i] = max(dp[j] + 1),其中0 <= j <= i - 1

3.思考初始状态

每一个位置i,对应的dp[i](即最长递增子序列)起始大小至少都是是1.

所以:dp[i] = 1

4.自底向上计算得到最优解

dp[i] 是有0到i-1各个位置的最长递增子序列推导而来,那么遍历i一定是从前向后遍历。

即:

int result = 1;
for (int i = 1; i < size; ++i)
{
    for (int j = 0; j < i; ++j)
    {
        if (nums[i] > nums[j])
        {
            dp[i] = max(dp[i], dp[j] + 1);
        }
    }
    result = max(result, dp[i]);  
}

5.思考是否可以进行空间的优化

代码
class Solution 
{
public:
    int lengthOfLIS(vector<int>& nums) 
    {
        int size = nums.size();
        if (size == 0) return 0;

        vector<int> dp(size, 1);
        int result = 1;
        for (int i = 1; i < size; ++i)
        {
            for (int j = 0; j < i; ++j)
            {
                if (nums[i] > nums[j])
                {
                    dp[i] = max(dp[i], dp[j] + 1);
                }
            }
            result = max(result, dp[i]);  
        }

        return result;
    }
};

LeetCode674. 最长递增子数组

https://leetcode-cn.com/problems/longest-continuous-increasing-subsequence/

给定一个未经排序的整数数组,找到最长递增子数组,并返回该序列的长度。

输入:nums = [1,3,5,4,7]
输出:3
解释:最长递增子数组是 [1,3,5], 长度为3。

思路

1.思考状态:找出最优解的性质,明确状态表示什么

以下标i为结尾的数组的连续递增的子序列长度为dp[i]。

注意这里的定义,一定是以下标i为结尾,并不是说一定以下标0为起始位置。

2.思考状态转移方程:大问题的最有解如何由小问题的最优解得到

如果 nums[i] > nums[i - 1],那么以 i 为结尾的数组的连续递增的子序列长度 一定等于 以 i - 1 为结尾的数组的连续递增的子序列长度 + 1 。

所以:if (nums[i] > nums[i - 1]): dp[i] = dp[i - 1] + 1

注意这里就体现出和LeetCode300. 最长递增子序列的区别!

因为本题要求连续递增子序列,所以就必要比较nums[i]与nums[i - 1],而不用去比较nums[i]与nums[j] (j是在0到i - 1之间遍历)。

既然不用j了,那么也不用两层for循环,本题一层for循环就行,比较nums[i] 和 nums[i - 1]。

3.思考初始状态

以下标i为结尾的数组的最递增子数组长度最少也应该是1,即就是nums[i]这一个元素。

所以:dp[i] = 1

4.自底向上计算得到最优解

dp[i + 1]依赖dp[i],所以一定是从前向后遍历。

int result = 1;
for (int i = 1; i < sz; ++i)
{
    if (nums[i] > nums[i - 1])
    {
        dp[i] = dp[i - 1] + 1;
    }
    result = max(result, dp[i]);
}

5.思考是否可以进行空间的优化

代码
class Solution {
public:
    int findLengthOfLCIS(vector<int>& nums) 
    {
        int sz = nums.size();
        if (sz == 0) return 0;

        vector<int> dp(sz, 1);
        int result = 1;
        for (int i = 1; i < sz; ++i)
        {
            if (nums[i] > nums[i - 1])
            {
                dp[i] = dp[i - 1] + 1;
            }
            result = max(result, dp[i]);
        }

        return result;
    }
};

LeetCode1143. 最长公共子序列

https://leetcode-cn.com/problems/longest-common-subsequence/

给定两个字符串 text1text2,返回这两个字符串的最长公共子序列的长度。若这两个字符串没有公共子序列,则返回 0。

输入:text1 = "abcde", text2 = "ace" 
输出:3  
解释:最长公共子序列是 "ace",它的长度为 3。
思路

1.思考状态:找出最优解的性质,明确状态表示什么

dp[i] [j]表示长度为i的字符串text1和长度为j的字符串text2的最长公共子序列的长度,即[0, i - 1]的字符串text1与[0, j - 1]的字符串text2的最长公共子序列的长度

2.思考状态转移方程:大问题的最有解如何由小问题的最优解得到

主要就是两大情况:text1[i - 1] 与 text2[j - 1]相同,text1[i - 1] 与 text2[j - 1]不相同

  • 如果text1[i - 1] 与 text2[j - 1]相同,那么找到了一个公共元素,所以dp[i] [j] = dp[i - 1] [j - 1] + 1;

  • 如果text1[i - 1] 与 text2[j - 1]不相同,那就看看text1[0, i - 2]与text2[0, j - 1]的最长公共子序列 和 text1[0, i - 1]与text2[0, j - 2]的最长公共子序列,取最大的。

即:if (text1[i - 1] == text2[j - 1]): dp[i] [j] = dp[i - 1] [j - 1] + 1

​ if (text1[i - 1] != text2[j - 1]): dp[i] [j] = max(dp[i - 1] [j], dp[i] [j - 1])

3.思考初始状态

text1[0, i-1]和空串的最长公共子序列自然是0,所以dp[i] [0] = 0;

同理dp[0] [j]也是0。

即:dp[i] [0] = 0

​ dp[0] [j] = 0

4.自底向上计算得到最优解

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

5.思考是否可以进行空间的优化

代码
class Solution 
{
public:
    int longestCommonSubsequence(string text1, string text2) 
    {
        int size1 = text1.size();
        int size2 = text2.size();

        vector<vector<int>> dp(size1 + 1, vector<int>(size2 + 1, 0));
        for (int i = 1; i <= size1; ++i)
        {
            for (int j = 1; j <= size2; ++j)
            {
                if (text1[i - 1] == text2[j - 1])
                {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                }
                else
                {
                    dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }

        return dp[size1][size2];
    }
};

LeetCode718. 最长公共子数组

https://leetcode-cn.com/problems/maximum-length-of-repeated-subarray/

给两个整数数组 AB ,返回两个数组中公共的、长度最长的子数组的长度。

输入:
A: [1,2,3,2,1]
B: [3,2,1,4,7]
输出:3
解释:
长度最长的公共子数组是 [3, 2, 1] 。
思路

1.思考状态:找出最优解的性质,明确状态表示什么

dp[i] [j]表示以下标i - 1为结尾的整数数组A与以下标j - 1为结尾的整数数组B的最长公共子数组的长度

2.思考状态转移方程:大问题的最有解如何由小问题的最优解得到

根据dp[i] [j]的定义,dp[i] [j]的状态只能由dp[i - 1] [j - 1]推导出来。

即:if (A[i - 1] == B[i - 1]): dp[i] [j] = dp[i - 1] [j - 1] + 1;

3.思考初始状态

根据dp[i] [j]的定义,dp[i] [0] 和dp[0] [j]其实都是没有意义的!

但dp[i] [0] 和dp[0] [j]要初始值,因为为了方便递归公式dp[i] [j] = dp[i - 1] [j - 1] + 1,所以dp[i] [0] 和dp[0] [j]初始化为0。

举个例子:A[0]如果和B[0]相同的话,dp[1] [1] = dp[0] [0] + 1,只有dp[0] [0]初始为0,正好符合递推公式逐步累加起来。

4.自底向上计算得到最优解

外层for循环遍历A,内层for循环遍历B。

int result = 0;
for (int i = 1; i <= aSize; ++i)
{
    for (int j = 1; j <= bSize; ++j)
    {
        if (A[i - 1] == B[j - 1])
        {
            dp[i][j] = dp[i - 1][j - 1] + 1;
        }
    result = max(result, dp[i][j]);
    }
}

5.思考是否可以进行空间的优化

代码
class Solution 
{
public:
    int findLength(vector<int>& A, vector<int>& B) 
    {
        int aSize = A.size();
        int bSize = B.size();

        vector<vector<int>> dp(aSize + 1, vector<int>(bSize + 1, 0));
        int result = 0;
        for (int i = 1; i <= aSize; ++i)
        {
            for (int j = 1; j <= bSize; ++j)
            {
                if (A[i - 1] == B[j - 1])
                {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                }
                result = max(result, dp[i][j]);
            }
        }

        return result;
    }
};

LeetCode516. 最长回文子序列

https://leetcode-cn.com/problems/longest-palindromic-subsequence/

给定一个字符串 s ,找到其中最长的回文子序列,并返回该序列的长度。可以假设 s 的最大长度为 1000

输入:
"bbbab"
输出:
4
一个可能的最长回文子序列为 "bbbb"。
思路

1.思考状态:找出最优解的性质,明确状态表示什么

dp[i] [j] 表示s[i…j]的最长回文子序列的长度

2.思考状态转移方程:大问题的最有解如何由小问题的最优解得到

关键逻辑就是看s[i]与s[j]是否相同。

  • 如果s[i]与s[j]相同,那么dp[i] [j] = dp[i + 1] [j - 1] + 2;
    在这里插入图片描述

  • 如果s[i]与s[j]不相同,说明s[i]和s[j]的同时加入不能增加[i,j]区间回文子串的长度,那么分别加入s[i]、s[j]看看哪一个可以组成最长的回文子序列。

    加入s[i]的回文子序列长度为dp[i + 1] [j]。

    加入s[j]的回文子序列长度为dp[i] [j - 1]。

    那么dp[i] [j]一定是取最大的,即:dp[i] [j] = max(dp[i + 1] [j], dp[i] [j - 1]);

在这里插入图片描述

所以: if (s[i] == s[j]): dp[i] [j] = dp[i + 1] [j - 1] + 2
if (s[i] != s[j]): dp[i] [j] = max(dp[i + 1] [j], dp[i] [j - 1])

3.思考初始状态

一个字符的回文子序列长度就是1。

所以:dp[i] [i] = 1

4.自底向上计算得到最优解

从递推公式dp[i] [j] = dp[i + 1] [j - 1] + 2 和 dp[i] [j] = max(dp[i + 1] [j], dp[i] [j - 1]) 可以看出,dp[i] [j]的计算是依赖于dp[i + 1] [j - 1] 、dp[i + 1] [j]和dp[i] [j - 1]

也就是从矩阵的角度来说,dp[i] [j]的计算是依赖与下一行和前一列的数据。 所以遍历i的时候一定要从下到上遍历,遍历j的时候一定要从左到右遍历这样才能保证,下一行的数据,前一列的数据是经过计算的。

在这里插入图片描述

        for (int i = size - 1; i >= 0; --i)
        {
            for (int j = i + 1; j < size; ++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]);
                }
            }
        }

5.思考是否可以进行空间的优化

另一种思路

我们可以将字符串逆序得到另一个字符串,将问题转换为求原始字符串和逆序字符串的最长公共子序列,求解最长公共子序列的思路见前文:LeetCode1143. 最长公共子序列。

代码
class Solution 
{
public:
    //动态规划
    int longestPalindromeSubseq(string s) 
    {
        int size = s.size();
        if(size == 0)
        {
            return 0;
        }

        vector<vector<int>> dp(size, vector<int>(size, 0));
        for (int i = 0; i < size; ++i)
        {
            dp[i][i] = 1;
        }
        
        for (int i = size - 1; i >= 0; --i)
        {
            for (int j = i + 1; j < size; ++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][size - 1];
    }

    //转换为LCS
    /*int longestPalindromeSubseq(string s)
    {
        int size = s.size();
        if(size == 0)
        {
            return 0;
        }

        string sReverse(s);
        for (int i = 0; i < size; ++i)
        {
            sReverse[i] = s[size - i - 1];
        }

        vector<vector<int>> dp(size + 1, vector<int>(size + 1, 0));
        for (int i = 1; i <= size; ++i)
        {
            for (int j = 1; j <= size; ++j)
            {
                if (s[i - 1] == sReverse[j - 1])
                {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                }
                else
                {
                    dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }

        return dp[size][size];
    }*/
};

LeetCode161. 相距为1的编辑距离

https://leetcode-cn.com/problems/one-edit-distance/

给定两个字符串 s 和 t,判断他们的编辑距离是否为 1。

注意:

满足编辑距离等于 1 有三种可能的情形:

  • 往 s 中插入一个字符得到 t
  • 从 s 中删除一个字符得到 t
  • 在 s 中替换一个字符得到 t
输入: s = "ab", t = "acb"
输出: true
解释: 可以将 'c' 插入字符串 s 来得到 t。
输入: s = "cab", t = "ad"
输出: false
解释: 无法通过 1 步操作使 s 变为 t。
思路

字符串s的大小为sSize,字符串t的大小为tSize,不妨假设sSize <= tSize 。

  • 如果tSize - sSize >= 2:编辑距离一定不为1
  • 如果两个字符串下标为i的字符不相等:

​ 1.如果sSize等于tSize:判断s.substr(i + 1)是否等于t.substr(i + 1)。如果

​ 相等则编辑距离为1(更新操作);如果不相等则编辑距离不为1。

​ 2.如果sSize + 1等于tSize:判断s.substr(i )是否等于t.substr(i + 1)。如果

​ 相等则编辑距离为1(插入操作);如果不相等则编辑距离不为1。

在这里插入图片描述

  • 如果两个字符串前sSize个字符相等:

​ 1.如果sSize等于tSize:两个字符串相等,编辑距离不为1。

​ 2.如果sSize + 1等于tSize:编辑距离为1(插入操作)

代码
class Solution 
{
public:
    bool isOneEditDistance(string s, string t) 
    {
        int sSize = s.size();
        int tSize = t.size();

        if (sSize > tSize)
        {
            return isOneEditDistance(t, s);
        }

        if ((tSize - sSize) >= 2)
        {
            return false;
        }
                        
        for (int i =0;i < sSize;++i)
        {
            if (s[i] != t[i])
            {
                if (sSize == tSize)
                {
                    return s.substr(i + 1) == t.substr(i + 1);
                }
                else
                {
                    return s.substr(i) == t.substr(i + 1);
                }
            }
        }

        return (sSize + 1 == tSize);
    }
};

LeetCode72. 编辑距离(LeetCode161. 相距为1的编辑距离进阶)

https://leetcode-cn.com/problems/edit-distance/

给你两个单词 word1word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。

你可以对一个单词进行如下三种操作:

  • 插入一个字符
  • 删除一个字符
  • 替换一个字符
输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')
思路

1.思考状态:找出最优解的性质,明确状态表示什么

dp[i] [j]表示长度为i的字符串word1和长度为j的字符串word2的编辑距离,即[0, i - 1]的字符串word1变换到[0, j - 1]的字符串word2的最少操作数。

2.思考状态转移方程:大问题的最有解如何由小问题的最优解得到

主要就是两大情况:word1[i - 1] 与word2[j - 1]相同,word1[i - 1] 与word2[j - 1]不相同

  • if (word1[i - 1] == word2[j - 1]): dp[i] [j] = dp[i - 1] [j - 1]

​ 因为word1[i - 1]等于word2[j - 1],所以无需进行操作

  • if (word1[i - 1] != word2[j - 1]):

​ dp[i] [j] = 1 + min(dp[i - 1] [j], dp[i] [j - 1], dp[i - 1] [j - 1])

​ 其中,dp[i-1] [j-1] 表示替换操作,dp[i-1] [j] 表示删除操作,dp[i] [j-1]表示插

​ 入操作。

3.思考初始状态

从空串变换到[0, i - 1]的字符串word1的最少操作数为i,所以dp[i] [0] = i

同理,dp[0] [j] = j

4.自底向上计算得到最优解

        for (int i = 1;i <= size1;++i)
        {
            for (int j = 1;j <= size2;++j)
            {
                if (word1[i - 1] == word2[j - 1])
                {
                    dp[i][j] = dp[i - 1][j - 1];
                }
                else
                {
                    dp[i][j] = 1 + min(min(dp[i - 1][j], 
                    dp[i][j - 1]), dp[i - 1][j - 1]);
                }
            }
        }

5.思考是否可以进行空间的优化

代码
class Solution 
{
public:
    int minDistance(string word1, string word2) 
    {
        int size1 = word1.size();
        int size2 = word2.size();

        int dp[size1 + 1][size2 + 1];
        dp[0][0] = 0;
        for (int i = 1;i <= size1;++i)
        {
            dp[i][0] = i;
        }

        for (int j = 1;j <= size2;++j)
        {
            dp[0][j] = j;
        }

        for (int i = 1;i <= size1;++i)
        {
            for (int j = 1;j <= size2;++j)
            {
                if (word1[i - 1] == word2[j - 1])
                {
                    dp[i][j] = dp[i - 1][j - 1];
                }
                else
                {
                    dp[i][j] = 1 + min(min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]);
                }
            }
        }

        return dp[size1][size2];
    }
};

 word2.size();

        int dp[size1 + 1][size2 + 1];
        dp[0][0] = 0;
        for (int i = 1;i <= size1;++i)
        {
            dp[i][0] = i;
        }

        for (int j = 1;j <= size2;++j)
        {
            dp[0][j] = j;
        }

        for (int i = 1;i <= size1;++i)
        {
            for (int j = 1;j <= size2;++j)
            {
                if (word1[i - 1] == word2[j - 1])
                {
                    dp[i][j] = dp[i - 1][j - 1];
                }
                else
                {
                    dp[i][j] = 1 + min(min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]);
                }
            }
        }

        return dp[size1][size2];
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值