动态规划中子序列也一直是高频考点,在一条子序列内部操作(递归数组为一维),也有在两条子序列内部操作(递归数组为二维)。

首先回顾一下动态规划五部曲:

  • 1 定义递归数组
  • 2 推导递归公式
  • 3 初始化递归数组
  • 4 确定遍历顺序
  • 5 手动打印数组检查是否正确

首先我们先来两个开胃小菜题目,了解一下子序列类动态规划题目如何下手。

最大子数组和

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

 
  • 1 定义dp[i]为包含第i个元素的最大和
  • 2 dp[i] = max(dp[i-1]+nums[i], nums[i]) 
  • 3 dp[0] = nums[0]
  • 4 i从1到nums.size()

以下为代码:

class Solution {
public:
    int maxSubArray(vector& nums) {
        int m = nums.size();
        vector dp(m);
        dp[0] = nums[0];
        int result = dp[0];
        for(int i = 1; i<m; i++)
        {
            dp[i] = max(dp[i-1]+nums[i], nums[i]);
            if (result < dp[i]) result=dp[i];
        }
        return result;
    }
};

乘积最大子数组

给你一个整数数组 nums ,请你找出数组中乘积最大的非空连续

 
上题我们给出了最大子数组的和,这里我们面对的是最大子数组的乘积,区别在于哪里呢,其实是乘积最大 = max(正数*正数,负数*负数),到这里应该发现这两个题目在递归表达式的区别了吧。
 
本题需要为每一个子数组定义两个状态,最大值和最小值。
  • dp[i][0]为最大子数组乘积最大值,dp[i][1]为最大子数组乘积的最小值。
  • 递归表达式
    • dp[i][0] = max(max(dp[i-1][0] * nums[i], dp[i-1][1]*nums[i]), nums[i]); 
    • dp[i][1] = min(min(dp[i-1][0] * nums[i], dp[i-1][1]*nums[i]), nums[i]);
  • 初始化:dp[0][0] = dp[0][1] = nums[0];
class Solution {
public:
    int maxProduct(vector& nums) {
        int m = nums.size();
        vector<vector> dp(m, vector(2, 0));
        dp[0][0] = nums[0];
        dp[0][1] = nums[0];
        int result = nums[0];
        for(int i=1; i<m; i++)
        {
            dp[i][0] = max(max(dp[i-1][0] * nums[i], dp[i-1][1]*nums[i]), nums[i]);
            dp[i][1] = min(min(dp[i-1][0] * nums[i], dp[i-1][1]*nums[i]), nums[i]);
            result = max(result, dp[i][0]);
        }
        return result;
    }
};

开题介绍

好了,开胃小菜结束了,怎么样,是不是很有继续下去的冲动,不要着急,我们从宏观入手,先了解大概会出现哪些题目,然后细细深入,理解每一个题目。

题目分类:

  • 1 递增子序列
    • 最长连续递增子序列
    • 最长递增子序列
  • 2 公共子序列
    • 最长重复子数组
    • 最长公共子序列
    • 判断子序列
    • 不同的子序列
  • 3 序列的删除操作
    • 两个字符串的删除操作
    • 编辑距离
  • 4 其他
    • 摆动序列
    • 整数求和
    • 完全平方数

========================递增子序列=====================

最长连续递增序列

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

连续递增的子序列 可以由两个下标 l 和 rl < r)确定,如果对于每个 l <= i < r,都有 nums[i] < nums[i + 1] ,那么子序列 [nums[l], nums[l + 1], ..., nums[r - 1], nums[r]] 就是连续递增子序列。

一般来说连续比不连续要好做很多。本题需要最长连续递增子序列的长度,只需要判断第i个元素比第i-1个元素大,即可+1。同时遍历初始化第一个元素,从第二个元素开始遍历。代码如下:

class Solution {
public:
    int findLengthOfLCIS(vector& nums) {
        int m = nums.size();
        if (m <= 1) return m;
        int result = 0;
        vector dp(m, 1);
        for(int i=1; i nums[i-1])
            {
                dp[i] = dp[i-1] + 1;
            }
            if (result < dp[i]) result=dp[i];
        }
        return result;
    }
};

最长递增子序列

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

子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列

不连续他来了,比起连续来说,不连续的子序列需要第i个元素返回去看0到i-1的元素和第i个元素的关系。大家可以体会一下,代码如下:

class Solution {
public:
    int lengthOfLIS(vector& nums) {
        int m = nums.size();
        if (m <= 1) return m;
        vector dp(m, 1);
        int result = 0;
        for(int i = 1; i<m; i++)
        {
            for(int j=0; j<i; j++) 
            { 
                if (nums[i]>nums[j]) dp[i] = max(dp[i], dp[j]+1);
            }
            if (result < dp[i]) result = dp[i];
        }
        return result;
    }
};

========================公共子序列=====================

最长重复子数组

给两个整数数组 nums1 和 nums2 ,返回 两个数组中 公共的 、长度最长的子数组的长度 

子数组:连续;子序列:不连续。这一般是题目的提示,大家记得get到。

该题比上述题目区别的地方在于,输入为两个子数组或者字符串,如果想实现比较,需要创建一个二维递归数组。

  • 递归数组定义:定义dp[i][j]为nums1[:i]数组与nums2[:j]数组的公共最长子数组长度
  • 递归方程:dp[i][j] = dp[i-1][j-1]+1
  • 初始化:全体初始化为0
  • 遍历:注意细节,为了方便处理dp数组的i=0的该行和j=0的一列不做单独处理,i,j都从1开始遍历,同时dp数组的索引i,j都需要-1.
class Solution {
public:
    int findLength(vector& nums1, vector& nums2) {
        int m = nums1.size()+1;
        int n = nums2.size()+1;
        vector<vector> dp(m, vector(n, 0));
        int result = 0;
        for(int i=1; i<m; i++)
        {
            for(int j=1; j<n; j++)
            {
                if (nums1[i-1] == nums2[j-1]) dp[i][j] = dp[i-1][j-1]+1;
                if (result < dp[i][j]) result = dp[i][j];
            }
        }
        return result;
    }
};

最长公共子序列

给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

  • 例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。

两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

子数组:连续;子序列:不连续。这一般是题目的提示,大家记得get到。

当寻找的子序列,最大的区别在于递归方程。

此时我们可以认为dp[i][j]的最大子序列的长度为(text1删除第i个元素与text2的最大子序列的长度)与(text1与text2删除第j个元素的最大子序列的长度)

class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        int m = text1.length() +1 ;
        int n = text2.length() + 1;
        vector<vector> dp(m, vector(n, 0));
        int result = 0;
        for(int i=1; i<m; i++)
        {
            for(int j=1; j<n; 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]);
                if (result < dp[i][j]) result = dp[i][j];
            }
        }
        return result;
    }
};

判断子序列

给定字符串 s 和 t ,判断 s 是否为 t 的子序列。

字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace""abcde"的一个子序列,而"aec"不是)。

该题与上题一致,只是递归方程中只能删减长序列的元素最后最大长度如果等于短字符串的长度,则说明短字符串为长字符串的子序列

class Solution {
public:
    bool isSubsequence(string s, string t) {
        int m = s.size() + 1;
        int n = t.size() + 1;
        vector<vector> dp(m, vector(n, 0));
        for(int i=1; i<m; i++)
        {
            for(int j=1; j<n; j++)
            {
                if (s[i-1] == t[j-1]) dp[i][j] = dp[i-1][j-1] + 1;
                else dp[i][j] = dp[i][j-1];
            }
        }
        return dp[m-1][n-1] == m-1;
    }
};

========================序列的删除操作=====================

两个字符串的删除操作

给定两个单词 word1 和 word2 ,返回使得 word1 和  word2 相同所需的最小步数

每步 可以删除任意一个字符串中的一个字符。

  • 递归数组定义:如上
  • 递归方程:最小步数 = 第一个单词删除一个元素 , 第二个单词删除一个元素,两个单词都删除元素。
  • 初始化:如上
  • 遍历顺序:如上
class Solution {
public:
    int minDistance(string word1, string word2) {
        int m = word1.size() + 1;
        int n = word2.size() + 1;
        vector<vector> dp(m, vector(n, 0));
        for(int i=0; i<m; i++) dp[i][0] = i;
        for(int j=0; j<n; j++) dp[0][j] = j;
        for(int i=1; i<m; i++)
        {
            for(int j=1; j<n; j++)
            {
                if (word1[i-1] == word2[j-1]) dp[i][j] = dp[i-1][j-1];
                else dp[i][j] = min(min(dp[i-1][j]+1, dp[i][j-1]+1), dp[i-1][j-1]+2);
            }
        }
        return dp[m-1][n-1];
    }
};

编辑距离

给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数  。

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

  • 插入一个字符
  • 删除一个字符
  • 替换一个字符

针对操作数来说,对word1进行插入字符操作,等价于word2进行删除操作。

针对操作数来说,对word1进行替换字符操作,等价于word1和word2都进行删除操作,但是只能当做1次操作。

针对操作数来说,对word1的操作数也等于word2的操作数。

故,针对操作数来说,本来可以针对2个单词都可以进行的操作可以被规约为只对word1进行操作,同时将插入、删除、替换都变成删除操作。

class Solution {
public:
    int minDistance(string word1, string word2) {
        int m = word1.size() + 1;
        int n = word2.size() + 1;
        vector<vector> dp(m, vector(n, 0));
        for(int i=0; i<m; i++) dp[i][0] = i;
        for(int j=0; j<n; j++) dp[0][j] = j;
        for(int i=1; i<m; i++)
        {
            for(int j=1; j<n; j++)
            {
                if (word1[i-1] == word2[j-1]) dp[i][j] = dp[i-1][j-1];
			  // 插入 删除  替换
                else dp[i][j] = min(min(dp[i-1][j]+1, dp[i][j-1]+1), dp[i-1][j-1]+1);
            }
        }
        return dp[m-1][n-1];
    }
};

========================其他=====================

摆动序列

如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 摆动序列 。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。

  • 例如, [1, 7, 4, 9, 2, 5] 是一个 摆动序列 ,因为差值 (6, -3, 5, -7, 3) 是正负交替出现的。

  • 相反,[1, 4, 7, 2, 5] 和 [1, 7, 4, 5, 5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。

子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。

给你一个整数数组 nums ,返回 nums 中作为 摆动序列 的 最长子序列的长度 。

该题最大的难点在于递归数组的定义。

在此我们总结一下递归数组的定义的几种形式:

  • 1 一维数组
  • 2 二维数组(每一个元素N个状态)
  • 3 三维数组(每一个元素1个状态)

递归数组定义:本题考查摆动序列,摆动即说明单个元素可能存在多个状态,即波峰和波谷状态。

递归方程:如果第i个元素小于第i-1元素,则第i个元素的波谷所拥有的子序列摆动长度+1,反之,则第i个元素的波峰所拥有的子序列摆动长度+1。

大家好好琢磨,代码如下:

class Solution {
public:
    int wiggleMaxLength(vector& nums) {
        vector<vector> dp(1000, vector(2, 1));
        for(int i=1; i<nums.size(); i++)
        {
            for(int j=0; j<i; j++) { if (nums[i] > nums[j]) dp[i][0] = max(dp[i][0], dp[j][1]+1);
                else if(nums[i] < nums[j]) dp[i][1] = max(dp[i][1], dp[j][0]+1);
            }
        }
        return max(dp[nums.size()-1][0], dp[nums.size()-1][1]);
    }
};

整数拆分

给定一个正整数 n ,将其拆分为 k 个 正整数 的和( k >= 2 ),并使这些整数的乘积最大化。

返回 你可以获得的最大乘积 。

递归数组定义:dp[i]为整数i拆分的乘积最大化。

递归数组:dp[i] = max(max(j*(i-j), j*dp[i-j]), dp[i]) 有两种选择,拆或者不拆。

递归初始化:由于n>=2,故dp[2]=1(2=1+1;1*1=1)。

递归顺序:使用两个循环是因为,第一个循环为i,第二个循环为拆多少。由于n>=2,则i从3开始。

class Solution {
public:
    int integerBreak(int n) {
        // 1 定义动规矩阵
        vector dp(n+1);
        // 3 初始化
        dp[2] = 1;
        // 2 动规方程
        for (int i=3; i<=n; i++)
        {
            for (int j=1; j<i; j++)
            {
                dp[i] = max(max(j*(i-j) , j*dp[i-j]), dp[i]);
            }
        }
        return dp[n];
    }
};

完全平方数

给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。

完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,149 和 16 都是完全平方数,而 3 和 11 不是。

动规数组的定义:dp[i]为整数i的完全平方数的最少数量。

动规方程:循环j可以先找到i内上一个最小的dp[i-j*j],则dp[i] = 1 + min(dp[i-j*j]),示例

  • dp[26] = dp[16] + dp[10] = 1 + dp[10]
  • dp[10] = dp[9] + dp[1] = 1 + dp[1]
  • dp[1] = 1
  • dp[26] = 1 + dp[10] = 1 + 1 +dp[1] = 1+1 +1 = 3

初始化:全部为0。

遍历顺序:第一个循环为了生成必要的前置状态,第二个循环为了找到最小的dp[i-j*j]

以下为代码:

class Solution {
public:
    int numSquares(int n) {
        vector dp(n+1, 0);
        for(int i=1; i<=n; i++)
        {
            int min_value = 1e9;
            // 找最小数量的前置
            for(int j=1; j*j <= i; j++)
            {
                min_value = min(min_value, dp[i-j*j]);
            }
            dp[i] = min_value + 1;
        }
        return dp[n];
    }
};