本文记录作者刷题过程中与动态规划的序列相关的题目。

一、序列问题

1、最长递增子序列

300. 最长递增子序列(中等难度)

给定一个未经排序的整数数组 nums ,找到其中严格递增子序列的最长长度,子序列可以不连续。

输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
class Solution {
public:
    /* 动规经典问题
    【状态定义】dp[j]表示以nums[j]结尾的最长上升子序列长度
    【状态转移】对于任何的i属于[0,j-1],都有
    if (nums[j] > nums[i]) dp[j] = max(dp[j], dp[i] + 1);
    【初始状态】每个j对应的dp[j]都至少是1.
    */
    int lengthOfLIS(vector<int>& nums) {
        if (nums.size() <= 1) return nums.size();
        vector<int> dp(nums.size(), 1);
        int result = 0;
        // 从数组中的第2个数开始遍历,当前遍历数为i
        for (int j = 1; j < nums.size(); j++) {
            // 遍历数组中的[i,j]
            for (int i = 0; i < j; i++) {
                if (nums[j] > nums[i]) dp[j] = max(dp[j], dp[i] + 1);
            }
            result=max(result,dp[j]);// 保存dp数组中的最长的长度
        }
        return result;
    }
};

674. 最长连续递增序列(中等难度)

给定一个未经排序的整数数组 nums ,找到其中严格递增子序列的最长长度,子序列必须连续。

输入:nums = [1,3,5,4,7] 输出:3 解释:最长连续递增序列是 [1,3,5], 长度为3。 尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为 5 和 7 在原数组里被 4 隔开。

点评

class Solution {
public:
    /*动态规划问题
    【状态定义】dp[i]表示以j结尾的数组的连续递增的子序列的长度
    【状态转移】当nums[i+1]>nums[i]时,dp[i+1]=dp[i]+1
    【初始状态】dp[i]=1
    */
    int findLengthOfLCIS(vector<int>& nums) {
        vector<int> dp(nums.size()+1,1);
        int res=1;
        for(int i=0;i<nums.size();++i){
            if(i+1<nums.size()&&nums[i+1]>nums[i]){
                dp[i+1]=dp[i]+1;
                res=max(res,dp[i+1]);
            }
        }
        return res;
    }
};

2、最长重复子序列

718. 最长重复子数组(中等难度)

给两个整数数组 nums1nums2 ,返回 两个数组中 公共的 、长度最长的连续子数组的长度

输入:nums1 = [1,2,3,2,1], nums2 = [3,2,1,4,7]
输出:3
解释:长度最长的公共子数组是 [3,2,1] 。

点评

class Solution {
public:
    /* 连续子序列问题:动态规划
    【状态定义】dp[i][j]表示以i-1结尾的nums1和以j-1结尾的nums2的重复子序列的长度
    (i和j都从1遍历到结尾)
    【状态转移】dp[i][j]由一个方向决定:dp[i-1][j-1]
    nums1[i-1]==nums2[j-1]时,dp[i][j]=dp[i-1][j-1]+1
    【初始状态】全部初始为0
    时间复杂度和空间复杂度为O(nxm)
    */
    int findLength(vector<int>& nums1, vector<int>& nums2) {
        vector<vector<int>> dp(nums1.size()+1,vector<int>(nums2.size()+1,0));
        int res=0;
        for(int i=1;i<=nums1.size();++i){
            for(int j=1;j<=nums2.size();++j){
                if(nums1[i-1]==nums2[j-1]){
                    dp[i][j]=dp[i-1][j-1]+1;
                    res=max(res,dp[i][j]);
                }
               
            }
        }
        return res;
    }
};

1143. 最长公共子序列(中等难度)

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

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

点评

class Solution {
public:
    /* 最长重复子序列,子序列不连续
    【状态定义】dp[i][j]表示长度为[0,i-1]的字符串text1和长度为[0,j-1]的字符串text2的公共子序列的最长长度
    【状态转移】dp[i][j]由三个方向决定:dp[i-1][j-1]、dp[i-1][j]、dp[i][j-1]
    text1[i-1]==text2[j-1]时,dp[i][j]=dp[i-1][j-1]+1
    text1[i-1]!=text2[j-1]时,dp[i][j]=max(dp[i-1][j],dp[i][j-1])
    【初始状态】全部初始化为0
    */
    int longestCommonSubsequence(string text1, string text2) {
        vector<vector<int>> dp(text1.length()+1,vector<int>(text2.length()+1,0));
        for(int i=1;i<=text1.length();++i){
            for(int j=1;j<=text2.length();++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[text1.length()][text2.length()];
    }
};

1035. 不相交的线(中等难度)

在两条独立的水平线上按给定的顺序写下 nums1 和 nums2 中的整数。

当nums1[i] == nums2[j]时可以绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线,同时要求该直线不能相交,即每个数字只能属于一条连线

以这种方法绘制线条,返回可以绘制的最大连线数。

nums1 = [1,4,2], nums2 = [1,2,4] 输出:2 解释:可以画出两条不交叉的线,如上图所示。 但无法画出第三条不相交的直线,因为从 nums1[1]=4 到 nums2[2]=4 的直线将与从 nums1[2]=2 到 nums2[1]=2 的直线相交。

class Solution {
public:
    /*本题本质上就是求nums1和nums2的最长重复子序列的长度,子序列可以不连续
    【状态定义】dp[i][j]表示长度为[0,i-1]的字符串nums1和长度为[0,j-1]的字符串nums2的公共子序列的最长长度
    【状态转移】dp[i][j]由三个方向决定:dp[i-1][j-1]、dp[i-1][j]、dp[i][j-1]
    nums1[i-1]==nums2[j-1]时,dp[i][j]=dp[i-1][j-1]+1
    nums1[i-1]!=nums2[j-1]时,dp[i][j]=max(dp[i-1][j],dp[i][j-1])
    【初始状态】全部初始化为0
    */
    int maxUncrossedLines(vector<int>& nums1, vector<int>& nums2) {
        vector<vector<int>> dp(nums1.size()+1,vector<int>(nums2.size()+1,0));
        for(int i=1;i<=nums1.size();++i){
            for(int j=1;j<=nums2.size();++j){
                if(nums1[i-1]==nums2[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[nums1.size()][nums2.size()];
    }
};

583. 两个字符串的删除操作(中等难度)

给定两个单词 word1word2每步 可以删除任意一个字符串中的一个字符。返回使得 word1word2 相同所需的最小步数

输入: word1 = “sea”, word2 = “eat” 输出: 2 解释: 第一步将 “sea” 变为 “ea” ,第二步将 “eat “变为 “ea”

点评

class Solution {
public:
    /* 本题和1143题目接近:
    求出两个字符串的最长重复子序列的长度,那么除了公共子序列剩余的字符都是要删除的
    【状态定义】dp[i][j]表示长度为[0,i-1]的字符串text1和长度为[0,j-1]的字符串text2的公共子序列的最长长度
    【状态转换】dp[i][j]由三个方向决定:dp[i-1][j-1]、dp[i-1][j]、dp[i][j-1]
    word1[i-1]==word2[j-1]时,dp[i][j]=dp[i-1][j-1]+1
    word1[i-1]!=word2[j-1]时,dp[i][j]=max(dp[i-1][j],dp[i][j-1])
    【初始状态】全部初始化为0
    */
    int minDistance(string word1, string word2) {
        // 计算最长公共子序列的长度
        vector<vector<int>> dp(word1.length()+1,vector<int>(word2.length()+1,0));
        for(int i=1;i<=word1.length();++i){
            for(int j=1;j<=word2.length();++j){
                if(word1[i-1]==word2[j-1]){
                    dp[i][j]=dp[i-1][j-1]+1;
                }
                else{
                    dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
                }
            }
        }
        int max_len=dp[word1.length()][word2.length()];
        return word1.length()-max_len+word2.length()-max_len;
    }
};

3、最大子序列和

剑指 Offer 42. 连续子数组的最大和(简单难度)

53. 最大子数组和(简单难度)

给定一个整数数组 nums ,请返回其中连续子数组的最大和(子数组最少包含一个元素)。

输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。

点评

class Solution {
public:
    /* 最大和子序列问题,子序列必须连续,原始序列中可能存在负值
    【状态定义】dp[i]表示以nums[i]结尾的连续子序列的最大和
    【状态转移】dp[i]由1个方向决定:dp[i-1],其变化决定于nums[i-1]
    共有三种可能:dp[i-1](放弃累加nums[i])、dp[i-1]+nums[i](直接累加nums[i])、nums[i](从i开始重新累加)
    因为是连续序列,所以排除dp[i-1],如果放弃累加nums[i],那么就得从nums[i]重新开始
    dp[i]=max(dp[i-1]+nums[i],nums[i])
    【初始状态】dp[0]=nums[0],因为取最大值,dp[i]的初始值应该尽可能小,至于有多小,可以取nums[0]或INT_MIN
    */
    int maxSubArray(vector<int>& nums) {
        vector<int> dp(nums.size()+1,INT_MIN);
        dp[0] = nums[0];
        int res = dp[0];
        for(int i=1;i<nums.size();++i){
            dp[i]=max(nums[i],dp[i-1]+nums[i]);
            res=max(res, dp[i]);
        }
        return res;
    }
};

4、编辑距离

392. 判断子序列(简单难度)

给定字符串 st ,两个字符串都由小写字母组成,判断 s 是否为 t 的子序列,本题中的s可以是不连续的子序列。

输入:s = "abc", t = "ahbgdc"
输出:true

点评

class Solution {
public:
    /* 子序列判断,子序列可以不连续
    该题可以转换为求s和t的重复子序列的长度,最后判断这个长度是否等于s的长度即可
    【状态定义】 dp[i][j]表示以s[i-1]结尾的字符串s和t[i-1]结尾的字符串t的重复子序列的长度
    【状态转换】dp[i][j]可以由2个方向推导而来:dp[i-1][j-1]、dp[i][j - 1]
    因为s是不连续的子序列,所以s不需要向前回退,t是原序列,可以跳过某些字符不能由dp[i-1][j]推导而来
    s[i-1]==t[i-1]时,t中找到了s对应的字符,dp[i][j] = dp[i-1][j-1] + 1
    s[i-1]!=t[i-1]时,t中没有找到了s对应的字符,dp[i][j] = dp[i][j - 1]
    【初始状态】全部初始化为0
    */
    bool isSubsequence(string s, string t) {
        vector<vector<int>> dp(s.size() + 1, vector<int>(t.size() + 1, 0));
        for (int i = 1; i <= s.size(); i++) {
            for (int j = 1; j <= t.size(); 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];
            }
        }
        for (int i = 1; i <= s.size(); i++) {
            for (int j = 1; j <= t.size(); j++) {
                cout<<dp[i][j];
            }
            cout<<endl;
        }
        if (dp[s.size()][t.size()] == s.size()) return true;
        return false;
    }
};

115. 不同的子序列(困难难度)

给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数,s的子序列可以是不连续的。

输入:s = “rabbbit”, t = “rabbit” 输出:3 解释: 如下图所示, 有 3 种可以从 s 中得到 “rabbit” 的方案。 rabbbit rabbbit rabbbit

点评

class Solution {
public:
    /* s的不连续子序列中,可以得到t的个数
    【状态定义】dp[i][j]表示以i-1为结尾的s子序列中随便删除元素,出现以j-1为结尾的t的个数为dp[i][j]。
    【状态转换】假设dp[i][j] 就是s[i] 和t[j] 索引的元素子序列数量,则状态方程是: 
    s[i] == t[j] 时 dp[i][j] = dp[i-1][j-1] + dp[i-1][j]
    s[i] != t[j] 时 dp[i][j] = dp[i-1][j]
    现在解释
    (1)s[i] == t[j] 时,以s="rara" t = "ra" 为例,当i = 3, j = 1时,s[i] == t[j]。
   此时分为2种情况,s串用最后一位的a + 不用最后一位的a。
   如果用s串最后一位的a,那么t串最后一位的a也被消耗掉,此时的子序列其实=dp[i-1][j-1]
   如果不用s串最后一位的a,那就得看"rar"里面是否有"ra"子序列的了,就是dp[i-1][j]
   所以 dp[i][j] = dp[i-1][j-1] + dp[i-1][j]
   (2)s[i] != t[j] 比如 s = "rarb" t = "ra" 还是当i = 3, j = 1时,s[i] != t[j]
   此时显然最后的b想用也用不上啊。所以只能指望前面的"rar"里面是否有能匹配"ra"的
   所以此时dp[i][j] = dp[i-1][j]
    【初始状态】从递归公式中可知dp[i][0] 和dp[0][j]是必须初始化的
    dp[i][0]表示以i-1为结尾的s可以随便删除元素,出现空字符串的情况个数,其结果一定是1,即删除所有元素
    dp[0][j]表示以-1为结尾的s可以随便删除元素,出现以j-1为结尾t的情况个数,其结果一定是0
    dp[0][0]表示以-1为结尾的s可以随便删除元素,出现空字符串的情况个数,其结果是1
    */
    int numDistinct(string s, string t) {
        vector<vector<uint64_t>> dp(s.size() + 1, vector<uint64_t>(t.size() + 1));
        for (int i = 0; i < s.size(); i++) dp[i][0] = 1;
        for (int j = 1; j < t.size(); j++) dp[0][j] = 0;
        for (int i = 1; i <= s.size(); i++) {
            for (int j = 1; j <= t.size(); j++) {
                if (s[i - 1] == t[j - 1]) {
                    dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
                } else {
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }
        return dp[s.size()][t.size()];
    }
};

72. 编辑距离(困难难度)

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

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

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

示例 1:

输入:word1 = “horse”, word2 = “ros” 输出:3 解释: horse -> rorse (将 ‘h’ 替换为 ‘r’) rorse -> rose (删除 ‘r’) rose -> ros (删除 ‘e’)

点评

class Solution {
public:
    /*
    【状态定义】dp[i][j]表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2的最近编辑距离。
    【状态转移】dp[i][j]是由dp[i-1][j-1]、dp[i - 1][j]、dp[i][j - 1]三个方向推导而来
    具体取决于s1[i-1]和s2[i-1]的关系:
    s1[i-1]==s2[i-1]时,在s1[i-2]和s2[i-2]后面分别添加一个相同的字符,不需要操作,所以dp[i][j]=dp[i-1][j-1]
    s1[i-1]!=s2[i-1]时,增删换三选一,取最小值
        (1)尝试删除s1的一个字符:dp[i][j]=dp[i-1][j]+1:以下标i-2为结尾的s1与j-1为结尾的s2的最近编辑距离 再加上一个操作
        (2)尝试删除s2的一个字符:dp[i][j]=dp[i][j-1]+1:以下标i-2为结尾的s2与j-1为结尾的s1的最近编辑距离 再加上一个操作
        (3)尝试替换s1或者s2的一个字符:替换s1[i-1]或者s2[i-1]使其s1[i-1]==s2[i-1]
        dp[i][j]=dp[i-1][j-1]+1:以下标i-2为结尾的s1与j-2为结尾的s2的最近编辑距离 再加上一个操作
    取最小:dp[i][j] = min({dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]}) + 1;
    【初始状态】
    dp[i][0]表示以下标i-1为结尾的字符串word1和以下标0-1为结尾的字符串word2的最近编辑距离,其值是i
    dp[0][j]表示以下标0-1为结尾的字符串word1和以下标j-1为结尾的字符串word2的最近编辑距离,其值是j
    */
    int minDistance(string s1, string s2) {
        vector<vector<int>> dp(s1.length()+1,vector<int>(s2.length()+1,0));
        for(int i=0;i<=s1.length();++i) dp[i][0]=i;
        for(int j=0;j<=s2.length();++j) dp[0][j]=j;
        // 遍历两个字符串
        for (int i = 1; i <= s1.size(); i++) {
            for (int j = 1; j <= s2.size(); j++) {
                // 如果相等,不操作
                if (s1[i - 1] == s2[j - 1]) {
                    dp[i][j] = dp[i - 1][j - 1];
                }
                // 如果不相等,分别尝试增删换,取最小值
                else {
                    dp[i][j] = min({dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]}) + 1;
                }
            }
        }
        return dp[s1.size()][s2.size()];
    }
};

5、回文子串

647. 回文子串(中等难度)

给定一个字符串 s ,请返回这个字符串中回文子串的数目,本题中的回文子串是正着读和倒过来读一样的字符串,且由原字符串s中的连续字符组成。具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。

输入:s = "abc"
输出:3
解释:三个回文子串: "a", "b", "c"
输入:s = "aaa"
输出:6
解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa"

点评

方法一:暴力破解法(时间复杂度为O(n^3))

//方法一:暴力获取所有子串
int countSubstrings(string s) {
    int res=0;
    // 遍历所有的起始位置
    for (int i = 0; i < s.length(); ++i) {
        // 遍历所有的终止位置
        for (int j = i; j < s.length(); ++j) {
            //判断子串[i,j]是否是回文串
            if (isHui(s.substr(i, j - i + 1)))res++;
        }
    }
    return res;
}
bool isHui(string s){
    int left=0;
    int right=s.length()-1;
    while(left<right){
        if(s[left]!=s[right]){
            return false;
        }
        else{
            left++;
            right--;
        }

    }
    return true;
}

方法二:动态规划法(时间复杂度为O(n^2),空间复杂度为O(n^2)

class Solution {
public:
    /* 回文子串,子串必须连续
    【状态定义】dp[i][j]表示字符串s在[i,j]上是否是回文子串(左右都是闭区间)
    【遍历顺序】对i是从大向小遍历,对j是从小向大遍历,则i相当于右指针,j相当于左指针
    【状态转移】
    子串长度为1,如a。这样的子串是回文子串
    子串长度为2,如aa,这样的子串要判断前后端是否相同,即s[i] == s[j],相同则是回文子串,不相同则不是
    故有s[i]== s[j]&&j-i<=1为true时,dp[i][j] = true;
    子串长度大于2。比如 ababa 这个字符记作串1,把两边的a去掉,也就是bab记作串2,
    可以看出只要串2是一个回文串,那么左右各多了一个a的串1必定也是回文串。
    所以当s[i]==s[j]时,自然要看dp[i+1][j-1]是不是一个回文串。
    故有s[i]== s[j]&&dp[i+ 1][j-1]为true时,dp[i][j] = true;
    【初始状态】全为true或者false都可以
    
    */
    int countSubstrings1(string s) {
        vector<vector<bool>>dp (s.size() + 1, vector<bool>(s.size() + 1, false));
        int res = 0;
        // 右指针i从大到小遍历
        for (int i=s.size()-1; i >= 0; i--) {
            // 左指针j从小到大遍历
            for (int j=i; j<s.size(); j++) {
                // 如果区间[i,j]的长度<=2,s[i]==s[j]
                if(s[i]==s[j]&&j-i<=1){
                    res++;
                    dp[i][j] = true;
                }
                // 如果区间[i,j]的长度>2,s[i]==s[j]且dp[i+1][j-1]为真
                else if(s[i]==s[j]&&dp[i+1][j-1]){
                    res++;
                    dp[i][j] = true;
                }
            }
        }
        return res;
    }
};

方法三:双指针法(时间复杂度为O(n^2),空间复杂度为O(1))

/*
首先确定回文串,就是找中心然后向两边扩散看是不是对称的就可以了。
在遍历中心点的时候,要注意中心点有两种情况。
一个元素可以作为中心点,两个元素也可以作为中心点。
*/ 
int countSubstrings(string s) {
        int res = 0;
        // 遍历字符串s的每个字符,分别以其为中心进行扩散,
        for (int i=0; i<s.size(); i++) {
            res += extend(s, i, i, s.size()); // 以i为中心
            res += extend(s, i, i + 1, s.size()); // 以i和i+1为中心
        }
        return res;
    }
    // 从字符串s的位置i和j分别向两端扩散,
    int extend(string& s, int left, int right, int n) {
        int res=0;
        // 如果left和right扩散后仍然没有出界并且指的字符相等,
        while(left>=0&&right<n&&s[left]==s[right]) {
            left--;
            right++;
            res++;
        }
        return res;
    }

5. 最长回文子串(中等难度)

给你一个字符串 s,找到 s 中最长的回文子串,如果有多个最长回文子串,随意输出一个即可。

输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。

点评

本题和647的代码高度相同。

class Solution {
public:
    string longestPalindrome(string s) {
        vector<vector<bool>>dp (s.size() + 1, vector<bool>(s.size() + 1, false));
        string res;
        int maxLen = 0;
        // 右指针i从大到小遍历
        for (int i=s.size()-1; i >= 0; i--) {
            // 左指针j从小到大遍历
            for (int j=i; j<s.size(); j++) {
                // 如果区间[i,j]的长度<=2,s[i]==s[j]
                if(s[i]==s[j]){
                    if(j-i<=1||dp[i+1][j-1]){
                        dp[i][j] = true;
                        int len=j-i+1;
                        if(len>maxLen) {
                            maxLen=len;
                            res=s.substr(i,len);
                        }
                    }
                }
            }
        }
        return res;
    }
};

516. 最长回文子序列(中等难度)

给你一个字符串 s ,返回其中最长的回文子序列,注意,本题中的回文子序列可以在字符串s中不连续。

输入:s = "bbbab"
输出:4
解释:一个可能的最长回文子序列为 "bbbb" 。

点评

class Solution {
public:
    /* 动态规划法:回文子串,子串不连续
    【状态定义】字符串s在[i,j]范围内最长的回文子序列的长度为dp[i][j]。
    【状态转换】要求dp[i][j]就考虑s[i]与s[j]
    i==j时,dp[i][j]=1
    i!=j时,
        s[i]==s[j]时,看中间回文长度可以直接拼接两边,dp[i][j]=dp[i+1][j-1]+2;
        s[i]!=s[j]时,必定要舍弃其中一个字母,看舍弃哪个最后长度会更大
        dp[i][j]=max(dp[i+1][j], dp[i][j-1]);
    【初始状态】初始化dp[i][i]=1,其他为0,递归公式无法计算i和j相同的情况,需要手动初始化
    【遍历顺序】先i后j,i倒序,j正序(dp[i][j]=dp[i+1][j-1]说明,i+1在i之前计算,j-1在j之前计算)
    【返回形式】返回dp[0][len-1]
    */
    int longestPalindromeSubseq(string s) {
        vector<vector<int>> dp(s.size(), vector<int>(s.size(), 0));
        //for (int i=0; i<s.size(); i++) dp[i][i]=1;
        // 遍历dp
        for (int i=s.size()-1; i>= 0; i--) {
            for (int j=i; j<s.size(); j++) {
                if(i==j){
                    dp[i][j]=1;
                }
                else 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][s.size()-1];
    }
};