动态规划——不重复子序列DP法

动态规划——不重复子序列DP法

考虑一个基本模型:

给定一个字符串 S S S,请问 S S S中有多少个不重复的子序列。这里的子序列指,删掉 S S S中某些字符,剩下的字符按照原有的顺序拼成的字符串。

容易想到一个基本的DP解法:

d p i dp_i dpi为以 S i S_i Si结尾(必选)的 S [ 1 , i ] S[1,i] S[1,i]中的子序列的数量。特别的,设 d p 0 = 0 dp_0=0 dp0=0,对应一个空字符串。那么有如下转移方程:

d p i = ∑ j = 0 i − 1 d p j dp_i = \sum_{j = 0}^{i-1}dp_j dpi=j=0i1dpj

但是我们会重复进行记录相同的子序列,因此我们改变一下DP方程:

d p i = ∑ j = k i − 1 d p j dp_i = \sum_{j = k}^{i-1}dp_j dpi=j=ki1dpj

则此问题的答案为 ∑ i = 1 n d p i \sum_{i=1}^n dp_i i=1ndpi

其中 k k k是满足 S k = S i S_k = S_i Sk=Si中最大的 k k k,为什么这样可行呢?如果我们对小于 k k k d p j dp_j dpj进行计数的话,则这个子序列已经被 d p k dp_k dpk所计数过了,因此加到上述和中必然会重复计数,加到 S k S_k Sk后面是可行的,因为我们对原有的子序列的末尾长度增加了一。

模板题:

LeetCode 940

class Solution
{
  public:
    int distinctSubseqII(string s)
    {
        const int mod = 1e9 + 7;
        s = "$" + s;
        vector<int> dp(s.size());
        vector<int> prv(26);
        int ans = 0;
        for (int i = 1; i < s.size(); i++)
        {
            if (prv[s[i] - 'a'] == 0)
                dp[i] = (dp[i - 1] + 1) % mod; // 没有前驱位置,加上1,代表单独出现
            else
                dp[i] = (dp[i - 1] - dp[prv[s[i] - 'a'] - 1]) % mod; // 不重复子序列DP
            ans = (ans + dp[i]) % mod;                               // 更新答案
            dp[i] = (dp[i] + dp[i - 1]) % mod;                       // 维护前缀和
            prv[s[i] - 'a'] = i;                                     // 维护最后出现的位置
        }

        return (ans % mod + mod) % mod;
    }
};

例题

ABC 214F

#include <bits/stdc++.h>

#define FR freopen("in.txt", "r", stdin)
#define FW freopen("out.txt", "w", stdout)

using namespace std;

int pos[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};

typedef long long ll;

const ll mod = 1e9 + 7;

ll dp[200005];

int las[26];

int main()
{
    string str;
    cin >> str;
    dp[0] = 1;

    for (int i = 1; i <= str.size(); i++)
    {
        int curr = str[i - 1] - 'a';
        ll sum = 0;
        for (int j = max(las[curr] - 1, 1); j < i - 1; j++)
        {
            sum = (sum + dp[j]) % mod;
        }
        if (las[curr] == 0)
        {
            sum = (sum + 1) % mod;
        }
        las[curr] = i;
        dp[i] = sum;
    }

    ll ans = 0;
    for (int i = 1; i <= str.size(); i++)
    {
        ans = (ans + dp[i]) % mod;
    }

    cout << ans;
    return 0;
}

LeetCode 1955

此题要求的是可以重复,如果要求不重复呢?

使用前缀和可以优化至 O ( n ) O(n) O(n)

#include <bits/stdc++.h>

#define FR freopen("in.txt", "r", stdin)
#define FW freopen("out.txt", "w", stdout)

using namespace std;

int pos[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};

typedef long long ll;

const ll mod = 1e9 + 7;

ll dp[100005];

int main()
{
    string str;
    cin >> str;
    dp[0] = 1;
    for (int i = 1; i <= str.size(); i++)
    {
        if (str[i - 1] == '0')
        {
            for (int j = i - 1; j >= 0; j--)
            {
                if (j == 0 || str[j - 1] == str[i - 1])
                {
                    dp[i] += dp[j];
                    break;
                }
            }
        }
        else if (str[i - 1] == '1')
        {
            for (int j = i - 1; j > 0; j--)
            {
                if (str[j - 1] == '0')
                {
                    dp[i] += dp[j];
                }

                if (str[j - 1] == '1')
                {
                    dp[i] += dp[j];
                    break;
                }
            }
        }
        else if (str[i - 1] == '2')
        {
            for (int j = i - 1; j > 0; j--)
            {
                if (str[j - 1] == '1')

                {
                    dp[i] += dp[j];
                }

                if (str[j - 1] == '2')
                {
                    dp[i] += dp[j];
                    break;
                }
            }
        }
    }
    ll ans = 0;
    for (int i = 1; i <= str.size(); i++)
    {
        if (str[i - 1] == '2')
            ans += dp[i];
    }

    cout << ans;
    return 0;
}

LeetCode 1857

纯不重复子序列DP法,"0"单独考虑即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值