LeetCode第201场周赛

第一题 1544. 整理字符串

简单模拟,用一个字符串res记录结果,然后遍历原字符串,如果res为空或者当前字母与res的最后一个字母不是同一个字母的大小写形式,
就把当前字母加入到res字符串中,否则,删除res的最后一个字母。

class Solution {
public:
    string makeGood(string s) {
        string res;
        int size = s.size();
        for(int i = 0; i < s.size(); ++i) {
            if(res.empty()) {
                res += s[i];
            } else {
                if((res.back() + 32 == s[i]) || (res.back() - 32 == s[i])) {
                    res.pop_back();
                } else {
                    res += s[i];
                }
            }
        }
        return res;
    }
};

第二题 1545. 找出第 N 个二进制字符串中的第 K 位

这题n最大20,可以直接开一个数组,算出2到n的所有字符串(1不用算),然后返回n的第k个字符即可。

class Solution {
private:
    vector<string> s = vector<string>(21);
public:
    char findKthBit(int n, int k) {
        s[1] = "0";
        for(int i = 2; i <= n; ++i) {
            string invertLastS = "";                  //invertLastS是上一个字符串按位取反的字符串
            for(int j = 0; j < s[i - 1].size(); ++j) {
                if(s[i - 1][j] == '0') {
                    invertLastS += '1';
                } else {
                    invertLastS += '0';
                }
            }
            s[i] = s[i - 1] + "1" + string(invertLastS.rbegin(), invertLastS.rend());
        }
        return s[n][k - 1];
    }
};

第三题 1546. 和为目标值的最大数目不重叠非空子数组数目

数据范围是105,所以O(n2)的算法是不能接受的,必须在O(n)的时间复杂度内计算出来,因此考虑前缀和。

我们遍历一遍数组,同时用一个变量sum记录从数组开头加到当前位置的和,然后我们需要寻找以当前位置作为某段子数组的结尾,是否可以形成
和为target的子数组,这样我们就需要子数组的开头,所以我们需要一个哈希表map<int, int>计算每个位置的前缀和。

所以如果我们遍历到某个位置i,当前和为sum,如果哈希表中存在一个大于0的startPos,Hash[sum - target] = startPos, 那么我们就找到了一个
子数组nums[startPos + 1 ... i],使得这个子数组的和为target。
因为从开头到startPos的和为sum - target, 从开头到i的和为sum, 所以从startPos + 1 ~ i的和为target。

这还不够,因为我们需要的是和为target的最大数目不重叠非空子数组,所以我们需要记录前缀和为sum的最后位置,也就是说,如果两个数p和q
的前缀和都为sum - target,且p在q的位置之后,那么我们加上的子数组应该是p + 1 ~ i 而不是q + 1 ~ i。
这里有贪心的思想在里头,因为我们取一个长度较短的子数组,前面就能留出长度更长的数组,这样数目肯定会更大一些,最起码也是一样大。

因为最终答案是子数组的个数,所以我们用一个数组dp来记录答案,dp[i]表示nums数组的前i个数字(i从1开始)中能找到的和为target的非重叠子数组的最大数目,
这样dp[size](size是nums数组的大小)就是最终的答案。

我们遍历一遍nums数组,首先,前i个数字的和为target的非重叠子数组的最大数目肯定不小于前i-1个数字的和为target的非重叠子数组的最大数目。
所以dp[i] = max(dp[i], dp[i - 1]);

然后求出从开头到当前位置的前缀和sum: sum += nums[i - 1];
然后去哈希表寻找是否存在一个startPos,使得Hash[sum - target] = startPos,如果startPos大于0,
说明找到了一个位置startPos,使得startPos + 1 ~ i的子数组的和为target,那么dp[i] = max(dp[i], dp[startPos] + 1); (+1表示多了一个子数组)
如果没找到,但是sum == target,说明从开头位置到当前位置的和为一个target,那么dp[i] = max(dp[i], 1);
最后当然要在哈希表里记录一下前缀和为sum的最后位置:Hash[sum] = i;

代码如下:

const int N = 1e5 + 10;
vector<int> dp(N);
map<int, int> Hash;
class Solution {
public:
    int maxNonOverlapping(vector<int>& nums, int target) {
        int sum = 0;
        int size = nums.size();
        fill(dp.begin(), dp.begin() + size + 1, 0);
        Hash.clear();
        for(int i = 1; i <= size; ++i) {
            dp[i] = max(dp[i], dp[i - 1]);
            sum += nums[i - 1];    
            int startPos = Hash[sum - target];
            if(startPos > 0) {
                dp[i] = max(dp[i], dp[startPos] + 1);
            } else if(sum == target) {
                dp[i] = max(dp[i - 1], 1);
            }
            Hash[sum] = i;
        }
        return dp[size];
    }
};

第四题 1547. 切棍子的最小成本


区间dp问题。
设cuts数组的大小为m,也就是说一共切m刀,最终会得到m+1段木棍,我们要求出切割m+1段木棍的最小代价。
我们可以反过来看,已经有了m+1段木棍,我们转而去求将所有木棍合并为一段木棍的代价。
这样好理解一点,合并的代价和题目定义的切割的代价是相同的,就是两端小木棍的长度之和。

由于cuts的大小最大为100,所以我们可以开一个二维数组dp[105][105]。
dp[i][j]的定义就是合并第i段木棍和第j段木棍为一根木棍的最小代价。1 <= i, j <= m。 (这里m是木棍的总数量)
最终的答案就是dp[1][m],也就是将所有木棍合并为一根木棍的最小代价和。

对于dp[i][j],如果i和j中间有一个切点k,则第i段到第j段木棍的总长度我们可以理解为是第i段木棍到第k段木棍(视为一段)
第k+1段木棍到第j段木棍(也视为一段)这两段木棍合并而来的。

dp[i][j] 就是 dp[i][k] + dp[k + 1][j] + 合并这两段木棍的代价
合并这两段木棍的代价好算,就是这两段木棍的长度和,也就是第j段木棍右边的切点和第i段木棍左边的切点之差,即cuts[j] - cuts[i - 1];

这样我们就得到了状态转移方程,我们可以枚举每次合并的木棍区间长度(也就是合并多少根木棍),然后还要枚举(当前合并范围内的)第一根木棍i,
通过第一根木棍加上木棍区间长度,得到当前合并范围的最后一根木棍j,然后在这两个木棍范围内枚举切点k。
比如,现在枚举到以第i个木棍为第一根木棍,第j个木棍作为最后一个木棍,我们需要求出合并第i, i + 1, i + 2.....j根木棍的总代价,
我们需要枚举切点为i, i + 1, ......j - 1, 然后分别计算如果k是这些切点的时候总代价是多少,总代价就是合并i, i + 1,....k 和 合并k + 1, k + 2, ... j
的代价加上第i根木棍,第i+1根木棍,。。。。第j根木棍的总长度和(cuts[j] - cuts[i - 1])。
如果计算出来的代价dp[i][k] + dp[k + 1][j] + cuts[j] - cuts[i - 1]比之前记录的代价dp[i][j]小,说明我们找到了一个“更好”的切点,
如果(优先)以这个点切割木棍,能够得到更小的代价。

就这样,枚举完所有可能的合并区间,计算完所有合并的代价之后,最终返回dp[1][m],即合并第1根木棍,第2根木棍,。。。。第m根木棍(这里m为总的木棍数量)
的最小代价。

代码如下:

const int N = 105;
int dp[N][N];
class Solution {
public:
    int minCost(int n, vector<int>& cuts) {
        cuts.push_back(0);                        //把两个端点也加进去,当作“切点”
        cuts.push_back(n);
        sort(cuts.begin(), cuts.end());
        int m = cuts.size() - 1;                 //不加两个端点的时候,木棍数量是cuts.size() + 1,现在cuts加了两个端点,所以木棍数量m是cuts.size() - 1
        for(int i = 1; i <= m; ++i) {            //预处理所有木棍的合并方案,-1表示这个方案不可行
            for(int j = 1; j <= m; ++j) {
                dp[i][j] = -1;
            }
        }
        for(int i = 1; i <= m; ++i) {            //合并第i根木棍到第i根木棍的代价为0,因为就一根木棍,没什么好合并的
            dp[i][i] = 0;
        }
        for(int len = 1; len < m; ++len) {          //枚举合并的区间范围
            for(int i = 1; i <= m; ++i) {           //i是当前合并范围的第1根木棍,从1枚举到m
                int j = i + len;                    //i+len就是当前合并范围内的最后一根木棍
                if(j > m) {                         //一共就m根木棍,超过了就break
                    break;
                }
                for(int k = i; k < j; ++k) {         //枚举切点,从i ~ j - 1
                    if(dp[i][k] != -1 && dp[k + 1][j] != -1) {            //如果当前切割方案可行
                        int cost = dp[i][k] + dp[k + 1][j] + cuts[j] - cuts[i - 1];      
                        if(dp[i][j] == -1 || dp[i][j] > cost) {            //且这样切割的代价更小
                            dp[i][j] = cost;                              //则我们更新切割(合并)第i~j根木棍的代价
                        }
                    }
                }
            }
        }
        return dp[1][m];                        //合并第1~m根木棍的代价就是最终答案
    }
};
原文地址:https://www.cnblogs.com/linrj/p/13517378.html