牛客巅峰赛第7场-牛牛的独特子序列

牛牛的独特子序列


解法1:前缀和 + 二分

思路:

包含三个字母的子序列问题不好考虑,我们来想一下如果简单化为 a , b a, b a,b 两个字母的情况。也即如何在给定字符串中找到最长的形如 a n b n a^n b^n anbn 的子序列。

这个问题的 O ( n ) O(n) O(n) 复杂度方法是,遍历字符串每一个位置 i n d e x index index,以该位置为 a , b a, b a,b 字母的分界线,取该位置以前字母 a a a 出现的次数,以及该位置以后字母 b b b 出现的次数(提前维护前缀和,获取出现次数复杂度为 O ( 1 ) O(1) O(1)),二者取较小值,即可得到满足题意的、以当前位置为分界线的子序列。

回到三个字母的问题,我们也可以利用类似的思想,遍历字符串每一个位置 i n d e x 1 index1 index1,以该位置为 a , b a, b a,b 字母的分界线,然后遍历其后的每一个位置 i n d e x 2 index2 index2,以该位置为 b , c b, c b,c 字母的分界线,按照上面的方法即可求解。但是由于要遍历两个位置,所以时间复杂度为 O ( n 2 ) O(n ^ 2) O(n2),对于本题数据会超时。

本题给定的数量级为 1 0 6 10^6 106,我们考虑 O ( n l g n ) O(nlgn) O(nlgn) 的算法,如果用上面的思路,遍历 a , b a, b a,b 分界线就需要 O ( n ) O(n) O(n) 的复杂度,那么我们考虑用 O ( l g n ) O(lgn) O(lgn) 的复杂度去找 b , c b, c b,c 的分界线。

划重点,我最喜欢的二分算法就是 O ( l g n ) O(lgn) O(lgn)

每次以左右边界求出的 m i d mid mid 位置作为 b , c b, c b,c 字符的分界线,然后根据前缀和分别计算出当前的 a , b , c a, b, c a,b,c 字母出现的次数(在代码中用 c u r a , c u r b , c u r c cura, curb, curc cura,curb,curc 表示),取三者里面的最小值,乘以 3 3 3 得到的就是满足题意的子序列。

由于找 b , c b, c b,c 分界线时,该分界线越向右, b b b 的出现次数越多, c c c 的出现次数越少;向左时刚好相反。所以二分找分界线时,若当前 b b b 出现的次数大于 c c c 出现的次数,那我们将分界线左移(对应着二分右边界左移),若小于则右移。


代码:

class Solution {
public:
    int Maximumlength(string s) {
        int n = s.size();
        vector<int> a(n + 1, 0), b(n + 1, 0), c(n + 1, 0);
        for(int i = 1; i <= n; i++) {
            a[i] = a[i - 1] + (s[i - 1] == 'a');
            b[i] = b[i - 1] + (s[i - 1] == 'b');
            c[i] = c[i - 1] + (s[i - 1] == 'c');
        }
        int ans = 0;
        for(int i = 1; i < n; i++) {
            int res = 0;
            int cura = a[i];
            int l = i + 1, r = n - 1;
            while(l <= r) {
                int mid = l + r >> 1;
                int curb = b[mid] - b[i];
                int curc = c[n] - c[mid];
                res = max(res, min(curb, curc));
                if(curb == curc) {
                    break;
                } else if(curb > curc) {
                    r = mid - 1;
                } else {
                    l = mid + 1;
                }
            }
            ans = max(ans, min(res, cura));
        }
        return ans * 3;
    }
};

复杂度分析:

时间复杂度 O ( n l g n ) O(nlgn) O(nlgn),遍历 a , b a, b a,b 分界点复杂度 O ( n ) O(n) O(n),每次在剩余部分二分找 b , c b, c b,c 分界点复杂度 O ( l g n ) O(lgn) O(lgn)

空间复杂度为 O ( n ) O(n) O(n),维护了三个前缀和序列


另一个二分的思路:

我们也可以利用二分答案,二分找子序列中 a a a 出现的次数(与 b , c b, c b,c 出现次数相同),每次遍历字符串,贪心检查是否满足条件,若满足则增大次数继续检查,若不满足则减少次数继续检查,每次更新当前最大值即可。

二分找次数时间复杂度为 O ( l g n ) O(lgn) O(lgn),每次检查的时间复杂度为 O ( n ) O(n) O(n),故总复杂度仍为 O ( n l g n ) O(nlgn) O(nlgn)

代码如下:

class Solution {
public:
    bool check(int mid, string s) {
        int n = s.size();
        int a = 0, b = 0, c = 0;
        for(int i = 0; i < n; i++) {
            if(a < mid) a += (s[i] == 'a');
            else if(b < mid) b += (s[i] == 'b');
            else c += (s[i] == 'c');
        }
        return c >= mid;
    }
    
    int Maximumlength(string s) {
        int n = s.size();
        int l = 0, r = n, ans = 0;
        while(l <= r) {
            int mid = l + r >> 1;
            if(check(mid, s)) {
                ans = mid;
                l = mid + 1;
            } else {
                r = mid - 1;
            }
        }
        return ans * 3;
    }
};

解法2:前缀和 + 双指针

思路:

利用两个双指针分别作为 a , b a, b a,b b , c b, c b,c 的分界线,初始时,让左指针左边的 a a a 出现次数和右指针右边的 c c c 出现次数相等,假设为 k k k(可以直接初始化为 n / 3 n / 3 n/3),然后通过前缀和 O ( 1 ) O(1) O(1) 检查双指针中间的 b b b 出现次数是否大于等于 k k k,若是,则得到了最长满足条件的子序列;若不是,则左指针左移,右指针右移,使得 a 、 c a、c ac 出现次数为 k − 1 k - 1 k1,继续检查中间的 b b b,以此类推。

双指针法的时间复杂度可以降低到 O ( n ) O(n) O(n) ,但是如果字母数量增大到 4 、 5... 4、5... 45... 个,二分答案法就可以更加方便的得到结果:设共有 N N N 个字母,那么先二分搜索各个字母出现的次数 k k k,然后利用 c h e c k ( ) check() check() 函数检查是否存在满足条件的子序列,再根据检查结果更新搜索区间。时间复杂度为 O ( n l g n ) O(nlgn) O(nlgn)

猜你喜欢

转载自blog.csdn.net/weixin_42396397/article/details/110898906