记录动态规划(三)

这次的总结比以前明了多了

115. 不同的子序列力扣

给你两个字符串 s 和 t ,统计并返回在 s 的 子序列 中 t 出现的个数。

题目数据保证答案符合 32 位带符号整数范围。

示例 1:

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

输入:s = "babgbag", t = "bag"
输出:5
解释:
如下所示, 有 5 种可以从 s 中得到 "bag" 的方案。 
babgbag
babgbag
babgbag
babgbag
babgbag

 这题的思路其实也是选和不选的模型,我们这样看,我现在有一个下标i和一个下标j,这两个下标一开始都是0,并且分别指向s和t,进行匹配字符。

假如s[i] == t[j] 那么我们就两个选择,一个是两个都选那么就转移到了上一个状态也就是i - 1 和 j - 1,二是不选i,为什么是不选i而不是不选j,因为你是从s中凑出t,所以对于s的字符你是有选择性的,所以转移的状态就是i - 1 和 j。

假如s[i] != t[j] 那么直接返回i - 1和 j状态,因为i这个字符不能选。

我们先创建一个二维数组,dp[i][j]表示s中前i个和t中前j个有多少能匹配的。

我们再用二维数组去解释以下上面为什么是转移i,而不是转移j

如果图解不理解,可以自己尝试填充数据就懂了

因为你的i用不上,那么你的值从哪里来呢,只能从上一行继承过来。

前面都搞定了,现在就要解决边界问题,我们可以很清楚得到dp[i][0] = 1, dp[0][j] = 0

class Solution 
{
public:
    int numDistinct(string s, string t) 
    {
        int n = s.size(), m = t.size();
        if(n < m) return 0;
        vector<vector<unsigned long long>> dp(n + 1, vector<unsigned long long>(m + 1, 0));
        for(int i = 0; i <= n; ++i) dp[i][0] = 1;
        for(int i = 1; i <= n; ++i)
            for(int j = 1; j <= m; ++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[n][m];
    }
};

这个题还可以优化空间,我们观察得到在这个状态转移方程里面,我们需要左上角数据,上方数据,那么上面的数据很好解决,直接转移就行,而左上的数据需要我们用一个临时变量去记录。

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

 516. 最长回文子序列力扣

给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。

子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。

示例 1:

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

输入:s = "cbbd"
输出:2
解释:一个可能的最长回文子序列为 "bb" 。

 这个题首先用一个中心扩散也可以完成,就是枚举每一下标为中心点或者两个中心点的其中一个,然后用dfs去暴力搜索就行了。但是这里用动态规划更好解决,但是其实动态规划都可以用dfs实现,动态规划就是只归不递,这里就用这个思想,因为递归递进去的时候是举例中心点,所以我们归的时候就是从两边回来。

class Solution 
{
public:
    int longestPalindromeSubseq(string s) 
    {
        int n = s.size();
        vector<vector<int>> dp(n, vector<int>(n, 0));
        for(int i = n - 1; i >= 0; --i)
        {
            dp[i][i] = 1;
            for(int j = i + 1; 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];
    }
};

// 两个不同的方向
class Solution 
{
public:
    int longestPalindromeSubseq(string s) 
    {
        int n = s.size();
        vector<vector<int>> dp(n, vector<int>(n, 0));
        for(int i = 0; i < n; ++i)
        {
            dp[i][i] = 1;
            for(int j = i - 1; j >= 0; --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[n - 1][0];
    }
};

 一维空间,和上一个不同的是,这里需要的是右上角或者左下角,下面这种就是需要右上角

class Solution 
{
public:
    int longestPalindromeSubseq(string s) 
    {
        int n = s.size();
        vector<int> dp(n, 0);
        for(int i = 0; i < n; ++i)
        {
            dp[i] = 1;
            int pre = 0;
            for(int j = i - 1; j >= 0; --j)
            {
                int temp = dp[j];
                if(s[i] == s[j]) dp[j] = pre + 2;
                else dp[j] = max(dp[j], dp[j + 1]);
                pre = temp;
            }
        }
        return dp[0];
    }
};

131. 分割回文串力扣

给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。

回文串 是正着读和反着读都一样的字符串。

示例 1:

输入:s = "aab"
输出:[["a","a","b"],["aa","b"]]
示例 2:

输入:s = "a"
输出:[["a"]]

 这题明显的组合形式的划分,所以这里用dfs,其实就是选和不选的问题,只不过我们选之前得知道我们选的是不是回文字符串,所以我们还需要一个二维数组去记录是不是回文字符串,为什么是二维,因为dp[i][j] 这个代表i -- j是不是字符串。那么这里思路清楚了,首先用一个二维数组去记录是不是回文数字符串,然后再dfs搜索。

class Solution 
{
public:
    vector<vector<string>> ret;
    vector<string> ans;
    vector<vector<bool>> dp;
    int n;
    vector<vector<string>> partition(string s) 
    {
        n = s.size();
        dp = vector<vector<bool>> (n, vector<bool>(n, true));

        for(int i = n - 1; i >= 0; --i)
            for(int j = i + 1; j < n; ++j)
                dp[i][j] = (s[i] == s[j]) && dp[i + 1][j - 1];

        dfs(s, 0);
        return ret;
    }

    void dfs(const string& s, int i)
    {
        if(i == n)
        {
            ret.emplace_back(ans);
            return;
        }
        for(int j = i; j < n; ++j)
        {
            if(dp[i][j])
            {
                ans.emplace_back(s.substr(i, j - i + 1));
                dfs(s, j + 1);
                ans.pop_back();
            }
        }
    }
};

 132. 分割回文串 II力扣

给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是回文。

返回符合要求的 最少分割次数 。

示例 1:

输入:s = "aab"
输出:1
解释:只需一次分割就可将 s 分割成 ["aa","b"] 这样两个回文子串。
示例 2:

输入:s = "a"
输出:0
示例 3:

输入:s = "ab"
输出:1

这个题档次一下子就上来了, 如果用上面那个代码,然后遍历每一行的长度取最小值的话会超时,所以不能使用上述代码,其实这里不需要考虑具体是怎么分,我只需要知道分了多少次就行,这个感觉有点像背包问题,都是从前面的所有状态中找到一种最适合的情况转移过来。看代码就理解了。

class Solution 
{
public:
    int minCut(string s) 
    {
        int n = s.size();
        vector<vector<bool>> dp(n, vector<bool>(n, true));
        for(int i = n - 1; i >= 0; --i)
            for(int j = i + 1; j < n; ++j)
                dp[i][j] = (s[i] == s[j]) && dp[i + 1][j - 1];
        
        vector<int> f(n, INT_MAX);
        for(int i = 0; i < n; ++i)
            if(dp[0][i])
            {
                f[i] = 0;
            }
            else
            // 归根究底还是状态转移
            // 想了想为什么dp[j + 1][i]可以确保dp[0][j]中可以分割字符串,因为这个里是循环上来的
            // 所以f[j]肯定也早就受到了前面的影响
            // 所以还是一个状态转移,只能说太妙了
            {
                for(int j = 0; j < i; ++j)
                    if(dp[j + 1][i]) f[i] = min(f[i], f[j] + 1);
            }
        return f[n - 1];
    }
};

 总结:我只能说太妙了,这些题是几天前写的,当时还是有点懵懵懂懂的,直到今天我看到了灵神的一句话,动态规划就是只归不递,恍然大悟,其实归根究底动态规划就是dfs,每一个状态都对应dfs的每一个状态,所以把握住dfs就离动态规划不远了。其实我现在感觉就是先确定dfs然后可以反推dp,然后再确定边界,如果熟练可以跳过dfs阶段,直接写状态方程,然后再确定边界。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值