LeetCode第32场双周赛

第一题 1539. 第 k 个缺失的正整数

用一个数组missed记录从小到大、所有arr数组缺失的正整数可以把所有出现过的元素放入一个set里,然后从
1~2000开始枚举(因为arr长度和k的大小最大都是1000,所以答案一定小于2000),如果当前枚举到的这个数在
set中没有出现,说明arr数组中缺失这个数,把这个数加入到missed数组中,如果missed数组的大小为k,说明找到了第k
个缺失的正整数,返回即可。

class Solution {
public:
    int findKthPositive(vector<int>& arr, int k) {
        vector<int> missed;
        set<int> hash;
        for(auto &a : arr) {
            hash.insert(a);
        }
        for(int i = 1; i < 2010; ++i) {
            if(hash.count(i) == 0) {
                missed.push_back(i);
                if(missed.size() == k) {
                    return missed.back();
                }
            }
        }
        return missed.back();
    }
};

第二题 1540. K 次操作转变字符串

如果s能转换为t,说明s的size和t的size相同,又由于转换的字母在s中的位置和在t中的位置是一样的,所以我们可以一个O(n)同时遍历一下s和t,
看一下每个位置上的s[i]要转换成t[i]需要多少步,然后再判断这些步数能否在给定的k内完成。

我们开一个哈希表vector Hash(27); Hash[i]表示需要移位i步的操作的次数,(i从1到26)。

然后我们统计一下所有移位操作的次数。
最后根据哈希表中各个移位操作的次数和k的大小比较一下,就可以判断出来是否能将s转换成t。

比如,如果k<=26,说明移位1~k的操作每个操作只能有一次,如果对于一个需要移位i步的操作Hash[i] > 1(1 <= i <= k),
也就是说我们需要一次以上的移位i步操作才可以将s转换成t,这是不可以的,因为k小于等于26,所以每个移位操作最多进行一次,
这样我们就返回false。
同样的,如果k<=26,我们有一个需要移位i步的操作Hash[i] > 0 (k + 1 <= i < 26),也就是说有需要移位的步数大于k的情况,
也返回false,因为我们无法移位那么多。

如果k > 26,那么我们的选择余地就大一些了,因为一位i步操作有多次的话,我们可以视为分别移步i步,i + 26步,i + 52步。。。。。
所以我们只需要对于所有的i(1 <= i < 26),判断是否i + 26 * (Hash[i] - 1) <= k即可,Hash[i] - 1的减一是因为,第一次移位i步在1 ~ 25范围内.
i + 26 * (Hash[i] - 1) <= k如果成立,代表所有移位i步的操作都可以在k次移步操作内完成,如果成功就返回false。
否则,如果i + 26 * (Hash[i] - 1) > k,则说明k次移位操作内无法满足所有要移步i步的操作,返回false。

代码如下:

class Solution {
public:
    bool canConvertString(string s, string t, int k) {
        if(s == t) {
            return true;
        }
        if(s.size() != t.size()) {                  //如果长度都不相等,就不用判断了
            return false;
        }
        if(k == 0) {                                //不能移步,返回false
            return false;
        }
        int size = s.size();
        vector<int> Hash(27);                       //Hash[i]表示需要切换i次的操作的个数
        for(int i = 0; i < size; ++i) {
            if(s[i] == t[i]) {
                continue;
            } else if(s[i] < t[i]) {
                int shiftTimes = (t[i] - s[i]);
                ++Hash[shiftTimes];
            } else if(s[i] > t[i]) {
                int shiftTimes = 26 - (s[i] - t[i]);
                ++Hash[shiftTimes];
            }
        }
        if(k <= 26) {
            for(int i = 0; i <= k; ++i) {
                if(Hash[i] > 1) {
                    return false;
                }
            }
            for(int i = k + 1; i <= 26; ++i) {
                if(Hash[i] > 0) {
                    return false;
                }
            }
        } else {
            for(int i = 1; i < 26; ++i) {
                if(i + 26 * (Hash[i] - 1) > k) {
                    return false;
                }
            }
        }
        return true;
    }
};

第三题 1541. 平衡括号字符串的最少插入次数


由于一个左括号需要与两个连续的右括号匹配,且左括号必须要在和他匹配的两个连续的右括号之前,所以我们可以这样,
用一个int变量singleLeft来记录当前还未与两个右括号匹配的左括号数量,再用一个res变量记录需要插入的括号的次数。

然后我们遍历一下字符串s,每一个字符只有两种可能。

(1)如果当前字符是左括号'(',则我们增加singleLeft,表示当前还未匹配的左括号多了一个。

(2)如果当前字符是右扩号')',由于两个右括号才能算是一个“匹配单位”,我们先判断一下下一位是否也是右扩号,
* 如果下一位是右扩号,好,我们就把这两个右扩号当作一起的,让i++,表示跳过下一个字母(因为我们知道下一个字母是右括号了,
我们已经把他和当前这个“第一个右括号”绑定了)。
* 如果下一位不是右括号,没事,我们就让res++,表示需要下一位插入一个右括号,才可以让两个右括号连续以便和之前的左括号匹配。
经过上面两步,我们现在肯定有了两个连续的右括号(就算原来没有我们也插入了一个,使得有了两个连续的右括号),然后我们看一下
当前未匹配的左括号的数量singleLeft是否大于0,如果是,则singleLeft--,也就是让一个左括号和刚刚的两个连续的右括号匹配。
而如果singleLeft<=0,说明前面没有左括号和我们刚刚这两个连续的右括号匹配了,则我们需要在前面插入一个左括号进行匹配(因为题目要求
左括号必须在右括号之前)。所以我们让res++。

遍历完字符串s之后,singleLeft可能不为0,表示左括号多余了,还差singleLeft * 2个右括号才能让s平衡。所以res += singleLeft * 2.

代码如下:

class Solution {
public:
    int minInsertions(string s) {
        int singleLeft = 0, res = 0;
        for(int i = 0; i < s.size(); ++i) {
            if(s[i] == '(') {
                ++singleLeft;
            } else if(s[i] == ')'){
                if(i + 1 < s.size() && s[i + 1] == ')') {
                    ++i;
                } else {
                    ++res;
                }
                if(singleLeft > 0) {
                    --singleLeft;
                } else {
                    ++res;
                }
            }
        }
        res += singleLeft * 2;
        return res;
    }
}; 

第四题 1542. 找出最长的超赞子字符串

最简单的做法是暴力找出所有子串,然后更新可以调整为回文子串的字符串的最大长度,但是这样会超时。需要考虑状态压缩。

我们注意到,可以调整为回文子串的字符串都有这么个性质:字符串中每个数字的出现次数都为偶数** 或者 只有一个数字出现次数为奇数,
其余数字的出现次数为偶数

这很好理解,如果所有数字出现次数都是偶数,则字符串可以调整为沿中间两个元素中间的点作为对称轴对称的一个回文字符串。
如果一个数字出现次数为奇数,其他数字出现次数为偶数,则字符串可以将最中间的数字(必然是出现次数为奇数的数字)作为对称轴,剩下的数字对称的
放到这个数字的两边,也能组成回文字符串。

因此,问题就转换为,在原来的字符串中s中找到最大的子串,使得子串中所有数字的出现次数满足上面两个性质其中之一。

由于子串只能由数字组成,而且我们只需要知道子串中每个数字出现次数的奇偶性(而无需知道具体出现次数),因此我们可以对所有数字的出现次数进行状态
压缩,用一个长度为10的二进制数字表示各个数字出现次数的奇偶性(1表示出现次数为奇数,0表示出现次数为偶数),这个二进制数字从低位到高位分别表示
对应数字的出现次数的奇偶性,比如第0位为1表示0出现奇数次,第7位为0表示7出现偶数次。我们用一个int型变量记录出现次数,由于最大只有2的10次方,
因此用int型记录出现次数足够了。

有了所有数字的出现次数,如何判断字符串能否调整为一个回文字符串呢?
首先,我们开一个dp数组,dp[bits] == i表示状态(各数字的出现次数)为bits(bits是一个整数)的最小的i是多少,比如bits = 0,二进制就是0000000000,
表示从s的第一个位置到第i个位置的子串中,所有数字出现次数都为偶数次。
这样,我们如果继续往后遍历,可以快速计算出以当前位置为终点的子串的状态(所有数字的出现次数的奇偶性),比如,我们之前计算好了s[1....i] = bits_i,
也就是说bits_i是表示子串s[1...i]的所有数字的出现次数的整数,然后我们往后遍历到了第j个位置,我们可以先计算出s[1....j] = bits_j,
然后用bits_j ^ bits_{i - 1}(前j个字母组成的子串的状态异或前i-1个字母组成的子串)得到s[i....j]的状态。
这是因为异或的性质是两个相同的数字异或为0,不同的数字异或为1,和这里奇偶性的计算性质正好符合。

anyway,我们可以计算出所有子串的状态(各数字的出现次数)了。
比如我们知道了s[1...i]的状态是bits_i,s[1...k]的状态是bits_k。(i < k),
我们还知道通过bits_k ^ bits_i可以计算出s[i+1...k]的状态,
然后我们判断s[i+1...k]的状态是否满足(1)bits_i == bits_k,即s[i+1...k]中所有数字的出现次数都是偶数次 或
(2)bits_i == bits_k ^ (1 << c),其中0<=c<=9,表示s[i+1...k]中只有一个数字出现奇数次,其他数字都出现偶数次。
这个式子的意思是这样的,bits_k ^ (1 << c)改变了bits_k的(从右到左从低到高)第c位数字的奇偶性,然后改变了这个数字的
奇偶性之后bits_i和bits_k两个状态相等了,说明s[i+1...k]中只有一个数字出现奇数次,其他数字都出现偶数次。

然后我们就是寻找所有子串的状态了,满足状态的子串,考虑更新一下长度。

代码如下:

class Solution {
public:
    int dp[1030];
    int longestAwesome(string s) {
        int n = s.size();
        for(int i = 0; i < 1030; ++i) {                  //初始化
            dp[i] = n + 1;
        }
        int bits = 0, res = 0;                              //bits记录从1~当前遍历到的位置的状态,也就是说bits的二进制表示上的1和0就是各个数字出现次数的奇偶性
        dp[0] = 0;                                       
        for(int i = 1; i <= n; ++i) {
            int c = s[i - 1] - '0';                        //当前遍历到哪一个数字
            bits ^= (1 << c);                              //更新一下bits, bits ^= (1 << c)表示更改一下第c位(即数字c的出现次数的奇偶性)的奇偶性,原来是0改成1,原来是1改成0
            dp[bits] = min(dp[bits], i);                   //更新最小的以bits为状态的前缀,方便后面的计算
            res = max(res, i - dp[bits]);                  //如果前面有一个位置,假设为j吧(j == dp[bits]),满足s[1...j]的状态是bits(和s[1...i]的状态一样),说明字符串s[j+1...i]中所有数字出现次数都为偶数次,更新长度,这个字符串长度就是i - dp[bits]
            for(int k = 0; k < 10; ++k) {                  //还需要计算是不是有只有一个数字出现次数为奇数的子串
                res = max(res, i - dp[bits ^ (1 << k)]);            //bits ^ (1 << k)就是更改原来的bits的第k位数字的奇偶性,和上面一样,如果找到了只有一个数字出现次数为奇数的子串,也要更新长度
            }
        }
        return res;
    }
};

个人觉得,这题的位运算和状态表示、状态压缩、状态转移以及长度的更新真的很难理解(对于我这种不熟悉位运算和状态压缩dp的人来说),建议大家去看一下B站up主喂你脚下有坑第四题的题解

原文地址:https://www.cnblogs.com/linrj/p/13520987.html