LeetCode第186场周赛(Weekly Contest 186)解题报告

今天周日,但是要补休,五一的。所以参赛人员少,而且题目也比较简单。

第一题:枚举。

第二题:滑动窗口 或者 前缀后缀和。

第三题:模拟。

第四题:DP 或者 贪心。

详细题解如下。


1.分割字符串的最大得分(Maximum Score after Splitting A String)

          AC代码(C++)

2. 可获得的最大点数(Maximum Points You Can Obtain from Cards)

          AC代码(方法一  滑动窗口  C++)

          AC代码(方法二  前缀和与后缀和  C++)

3.对角线遍历 II(Diagonal Traverse II)

          AC代码(C++)

4.带限制的子序列和(Build Array Where You Can Find the Maximum Exactly K Comparisons)

          AC代码(方法一  DP  C++)

          AC代码(方法二  贪心  C++)


LeetCode第186场周赛地址:

https://leetcode-cn.com/contest/weekly-contest-186/


1.分割字符串的最大得分(Maximum Score after Splitting A String)

题目链接

https://leetcode-cn.com/problems/maximum-score-after-splitting-a-string/

题意

给你一个由若干 0 和 1 组成的字符串 s ,请你计算并返回将该字符串分割成两个 非空 子字符串(即 左 子字符串和 右 子字符串)所能获得的最大得分。

「分割字符串的得分」为 左 子字符串中 0 的数量加上 右 子字符串中 1 的数量。

示例 1:

输入:s = "011101"
输出:5 
解释:
将字符串 s 划分为两个非空子字符串的可行方案有:
左子字符串 = "0" 且 右子字符串 = "11101",得分 = 1 + 4 = 5 
左子字符串 = "01" 且 右子字符串 = "1101",得分 = 1 + 3 = 4 
左子字符串 = "011" 且 右子字符串 = "101",得分 = 1 + 2 = 3 
左子字符串 = "0111" 且 右子字符串 = "01",得分 = 1 + 1 = 2 
左子字符串 = "01110" 且 右子字符串 = "1",得分 = 2 + 1 = 3

示例 2:

输入:s = "00111"
输出:5
解释:当 左子字符串 = "00" 且 右子字符串 = "111" 时,我们得到最大得分 = 2 + 3 = 5

示例 4:

输入:s = "1111"
输出:3

提示:

  • 2 <= s.length <= 500
  • 字符串 s 仅由字符 '0' 和 '1' 组成。

解题思路

根据题意,我们可以枚举前一个字符串可能的长度,也就是从 0 到 n - 2。

那么我们在每一个可能的长度中,怎么在 O(1) 中快速计算出,前一个字符串中的  0 和 后一个字符串中 1 的个数呢?

我们可以用一个变量,一直记录前一个 字符串中 0 的个数。

那么后面字符串 1 的个数 = 后面字符串长度 - (全部 0 - 前面的 0)。也就是,后面字符串的长度去掉其出现 0 的个数,即为后面字符串中 1的个数。

所以总的时间复杂度是 O(n)

AC代码(C++)

class Solution {
public:
    int maxScore(string s) {
        int n = s.size();
        int total = 0;  // 所有 0 的个数
        for(int i = 0;i < n; ++i)
        {
            if(s[i] == '0') ++total;
        }
        
        int ans = 0;
        int cnt = 0;
        for(int i = 0;i < n - 1; ++i)  // 枚举前一个字符串的长度
        {
            if(s[i] == '0') ++cnt;   // 前面字符串中 0 的个数
            int last = n - i - 1 - (total - cnt);  // 此时后面字符串中 1 的个数
            ans = max(ans, cnt + last);  // 算出所有枚举情况中,最大的那一个答案。
        }
        return ans;
    }
};

 


2. 可获得的最大点数(Maximum Points You Can Obtain from Cards)

题目链接

https://leetcode-cn.com/problems/maximum-points-you-can-obtain-from-cards/

题意

几张卡牌 排成一行,每张卡牌都有一个对应的点数。点数由整数数组 cardPoints 给出。

每次行动,你可以从行的开头或者末尾拿一张卡牌,最终你必须正好拿 k 张卡牌。

你的点数就是你拿到手中的所有卡牌的点数之和。

给你一个整数数组 cardPoints 和整数 k,请你返回可以获得的最大点数。

示例 1:

输入:cardPoints = [1,2,3,4,5,6,1], k = 3
输出:12
解释:第一次行动,不管拿哪张牌,你的点数总是 1 。但是,先拿最右边的卡牌将会最大化你的可获得点数。最优策略是拿右边的三张牌,最终点数为 1 + 6 + 5 = 12 。

示例 3:

输入:cardPoints = [9,7,7,9,7,7,9], k = 7
输出:55
解释:你必须拿起所有卡牌,可以获得的点数为所有卡牌的点数之和。

示例 5:

输入:cardPoints = [1,79,80,1,1,1,200,1], k = 3
输出:202

提示:

  • 1 <= cardPoints.length <= 10^5
  • 1 <= cardPoints[i] <= 10^4
  • 1 <= k <= cardPoints.length

解题思路

方法一、滑动窗口(问题的转换)

根据题目的意思,一开始想的是 DP,但是后来发现 我们要选出来的部分是前面 或者 后面,那么剩下的部分,就是中间连起来的。

那么,我们利用一个窗口,大小是 n - k,我们取这个连续窗口中的最小值(那么就转换为了,求出了我们取出前面或者后面部分中的最大值)

所以我们的目的就是,通过计算出,滑动窗口的值,得到我们要取出的值(总值 - 滑动窗口部分的值),然后使得这部分的值最大,那么根据滑动窗口的计算,是 O(n) 就可以计算出所有可能的值了。

方法二、前缀和、后缀和

上一个方法是将问题转换成了中间部分的值。

那么如果就是利用前缀和后缀怎么计算呢?

我们可以枚举,可能的前缀长度(那么根据总长度是 k,就可以得到 后缀长度),那么怎么在 O(1) 时间内,算出 前缀和,与 后缀部分和呢(因为如果不是 O(1),那么暴力计算,需要 O(k),那么总时间复杂度就会超时)。

计算前缀和,我们可以利用一个数组,来记录,这样子,当我们得到前缀长度的时候,就可以马上计算出 前缀和了。

类似的,后缀和也是一样。

AC代码(方法一  滑动窗口  C++)

class Solution {
public:
    int maxScore(vector& cardPoints, int k) {
        int sum = 0;
        for(auto c : cardPoints) sum += c;  // 先算出总和
        int ans = 0;
        int n = cardPoints.size();

        int cur = 0;   // 当前窗口的值
        k = n - k;
        for(int i = 0;i < k; ++i) cur += cardPoints[i];
        
        ans = sum - cur;
        for(int i = k;i < n; ++i)
        {
            cur = cur + cardPoints[i] - cardPoints[i - k];  // 每一次移动窗口后,新进来后面的,出去了最前面的
            ans = max(ans, sum - cur); 
        }
        return ans;
    }
};

AC代码(方法二  前缀和与后缀和  C++)

const int MAXN = 1e5 + 50;
class Solution {
public:
    int pre[MAXN], last[MAXN];  // 前缀和 与 后缀和
    int maxScore(vector& cardPoints, int k) {
        int n = cardPoints.size();
        // 计算前缀和
        pre[0] = 0;
        for(int i = 1;i <= n; ++i) pre[i] = pre[i - 1] + cardPoints[i - 1];
    
        // 计算后缀和
        last[0] = 0;
        for(int i = 1;i <= n; ++i) last[i] = last[i - 1] + cardPoints[n - 1 - i + 1];
        
        int ans = 0;
        for(int i = 0;i <= k; ++i)  // 枚举所有可能的前缀长度,然后计算得到对应取出的前缀和与后缀和
        {
            int cur = pre[i] + last[k - i];
            ans = max(ans, cur);
        }
        
        return ans;
        
    }
};

3.对角线遍历 II(Diagonal Traverse II)

题目链接

https://leetcode-cn.com/problems/diagonal-traverse-ii/

题意

给你一个列表 nums ,里面每一个元素都是一个整数列表。请你依照下面各图的规则,按顺序返回 nums 中对角线上的整数。

示例 1:

【示例有图,具体看链接】
输入:nums = [[1,2,3],[4,5,6],[7,8,9]]
输出:[1,4,2,7,5,3,8,6,9]

示例 2:

【示例有图,具体看链接】
输入:nums = [[1,2,3,4,5],[6,7],[8],[9,10,11],[12,13,14,15,16]]
输出:[1,6,2,8,7,3,9,4,12,10,5,13,11,14,15,16]

示例 3:

输入:croakOfFrogs = "croakcrook"
输出:-1
解释:给出的字符串不是 "croak" 的有效组合。

提示:

  • 1 <= nums.length <= 10^5
  • 1 <= nums[i].length <= 10^5
  • 1 <= nums[i][j] <= 10^9
  • nums 中最多有 10^5 个数字。

 

解题分析

一开始,以为是直接从左边和下边(即对角线元素的开始)开始,然后枚举所有数,但是这样子的时间复杂度是,O(n * m),那么根据题目就会超时。

根据示例 2 发现,并不是一个完整的 n * m 数组,而是一个动态长度的。(同时数据中保证了,最多只有 10 ^ 5 个数),因此,只要是枚举数据,而不是枚举 n,m。就不会超时。

那么我们分析出,从一个 n * m (其中 m 是每一行数组的最大长度),总共是可能有 n + m - 1 个对角线(首先是左边 的 n 个,然后下边的 m - 1 个)。

然后我们发现了,同一对角线上的 i + j 是相同值。

因此,我们用一个二维动态数组,行数是 n + m - 1,每一个后面该表示对角线上的元素。

这里要注意,我们存进去的时候,是一个倒过来的,也就是,对于 i 行这里面存的,比如示例 2 中第二个对角线,存进去是 2 4。也就是,我们处理的是,是一行一行的数据处理,所以是对角线上的元素,是反过来存储的

所以我们将  n + m - 1 数据存进 一维数组的时候,每一行的数据是反过来 的。

【其中也是可以用 unordered_map ,或者 vector g[n + m - 1]  】

AC代码(C++)

class Solution {
public:
    vector findDiagonalOrder(vector>& nums) {
        int n = nums.size();
        int m = 0;
        for(auto c : nums) m = max(m, int(c.size()));
        
        vector > tep(n + m - 1, vector (1, 0)); // 总共 n + m - 1 行,先把第一列都存好
        for(int i = 0;i < nums[0].size(); ++i) tep[i][0] = nums[0][i];
        
        for(int i = 1;i < n; ++i)  // 开始从原数组的第二行开始
        {
            for(int j = 0;j < nums[i].size(); ++j)
            {
                tep[i + j].push_back(nums[i][j]);
            }
        }
        
        vector ans;
        
        for(int i = 0;i < n + m - 1; ++i)
        {
            for(int j = tep[i].size() - 1;j >= 0; --j)  // 对于每一行,我们是反过来的,所以是从后往前放入 ans
            {
                if(tep[i][j] == 0) break;
                ans.push_back(tep[i][j]);
            }
        }
        return ans;
    }
};

4.带限制的子序列和(Build Array Where You Can Find the Maximum Exactly K Comparisons)

题目链接

https://leetcode-cn.com/problems/constrained-subset-sum/

题意

给你一个整数数组 nums 和一个整数 k ,请你返回 非空 子序列元素和的最大值,子序列需要满足:子序列中每两个 相邻 的整数 nums[i] 和 nums[j] ,它们在原数组中的下标 i 和 j 满足 i < j 且 j - i <= k 。

数组的子序列定义为:将数组中的若干个数字删除(可以删除 0 个数字),剩下的数字按照原本的顺序排布。

示例 1:

输入:nums = [10,2,-10,5,20], k = 2
输出:37
解释:子序列为 [10, 2, 5, 20] 。

示例 4:

输入:nums = [10,-2,-10,-5,20], k = 2
输出:23
解释:子序列为 [10, -2, -5, 20] 。

提示:

  • 1 <= k <= nums.length <= 10^5
  • -10^4 <= nums[i] <= 10^4

解题分析

方法一、动态规划DP

看了题目,其实很像是一个动态规划的问题,只是加了额外的限制条件。

1)设 dp[ i ] 表示 对于 第 i 个数字,取该数的情况下,最大和 结果

  • 要求是取该数,这是因为,我们只有取了这个数,才有对于后面的数可以取(同时也表明这个数前面 k 个中,对应有取)

2)状态转移方程:

dp[ i ] = nums[ i - 1] +max(dp[ j ] )

其中 j 是 j < i,同时,我们是从 j -> i 的,那么按照要求 i - j <= k,所以  i - k <= j < i。

如果直接算的话,总共复杂度是 O(n * k),那么就会超时了。

所以要进行优化,如何快速的求 max(dp[ j ] )

我们可以用一个 优先队列 来存 i 前面的哪些 dp[ j ] 和 对应下标 j。

那么我们就用优先队列,就可以取到最大值

但是有一个地方要注意,我们 取出的最大 dp[ j ],有要求 i - k <= j。(所以我们可以先利用判断,找到一个 满足 j 这个条件的最大值)

3)初始值

根据 示例 2,我们知道至少要取一个数,所以初始值就是 dp[ all i ] = nums[i - 1],也就是对于每个 dp ,初始值就是单单取了自身。

4)答案结果

由于 dp[ i ] 表示 对于 第 i 个数字,取该数的情况下,最大和 结果,但是我们具体不知道 i 这个数取了没,因此,结果应该是 所有 dp[ i ] 中的最大值。

方法二、贪心

其实我们可以发现,如果我们从后往前取,那么当 i 取的时候,说明了 i - j <= k 中的所有 j ,下一次都可以取。

那么对于下一次而言,我们取出最大的来加

然后每一次加,都重复(加过的出来),这个过程(直到没有 数可以再取,也就是队列为空)。然后每一次都得其最大值来记录。最后就是答案。

那么由于每一次也都是找最大得来加,因此,我们用 优先队列。

因此,问题就主要要,从后往前,第一个取得是谁

那我们知道,最后一个要取得值,一定 >= 0,那么从后往前,第一个 >= 0得数

这里有一个问题,假如没有呢?比如 示例 2,所以对于每一个数,我们都认为其是最大值,那么如果都没有 >= 0得,我们也就取出了其中最大的作为最后答案。

这道题主要就是,每一个点,都进 and 出 队列一次。

AC代码(方法一  DP  C++)

const int MAXN = 1e5 + 50;

#define pii pair
#define mk(x, y) make_pair(x, y)

class Solution {
public:
    
    int dp[MAXN];   // dp[i] = nums[i] + max{dp[j]}  其中 i - j <= k -> i - k <= j < i 
    priority_queue, less > pq;
    
    int constrainedSubsetSum(vector& nums, int k) {
        int n = nums.size();
        for(int i = 0;i < n; ++i) dp[i] = nums[i];
        
        while(!pq.empty()) pq.pop();
        pq.push(mk(dp[0], 0));
        for(int i = 1;i < n; ++i)
        {
            while(!pq.empty() && i - pq.top().second > k) pq.pop();
            if(!pq.empty())
            {
                dp[i] = max(dp[i], nums[i] + pq.top().first);
            }
            pq.push(mk(dp[i], i));
            // cout << dp[i] << endl;
        }

        int ans = -MAXN;  // 最小值,应该是 -1e4,但是我们设的 MAXN 大,所以直接利用即可。
        for(int i = 0;i < n; ++i) ans = max(ans, dp[i]);
        
        return ans;
    }
};

AC代码(方法二  贪心  C++)

const int MAXN = 1e5 + 50;

#define pii pair
#define mk(x, y) make_pair(x, y)

class Solution {
public:
    int vis[MAXN];
    
    priority_queue, less> pq;
    
    int constrainedSubsetSum(vector& nums, int k) {
        memset(vis, 0, sizeof(vis));
        int ans = -MAXN;
        
        int cur = 0;
        int n = nums.size();
        int i;
        for(i = n - 1; i >= 0; --i)
        {
            ans = max(ans, nums[i]);
            vis[i] = 1;
            if(nums[i] >= 0)
            {
                cur = nums[i];
                break;
            }
        }
        if(i - 1 < 0) return ans;
        
        int e = i - 1;
        
        while(!pq.empty()) pq.pop();
        for(i = e; i >= max(0, e + 1 - k);--i)
        {
            vis[i] = 1;
            pq.push(mk(nums[i], i));
        }

        while(!pq.empty())
        {
            // cout << cur << " " << pq.top().first << " " << pq.top().second << endl;
            cur += pq.top().first;
            ans = max(ans, cur);
            
            int ee = pq.top().second - 1;
            int ss = max(0, ee + 1 - k);
            pq.pop();
            if(ee >= 0)
            {
                // 对于 i 取了,那么这个 i 满足得 i - j <= k 中得所有 j 都应该加入。但是已经加入就不用加入了,所以用 vis 来标记
                // 同时优化一下,就是对于 vis,我们 j 都是从最开始满足得往后加,那么只要需要了 vis[j] = 1,所以后面都已经加过了,就可以直接跳出了。
                for(int j = ss;j <= ee; ++j)  
                {
                    if(vis[j]) break;
                    vis[j] = 1;
                    pq.push(mk(nums[j], j));
                }
            }
        }
        
        return ans;
    }
};

 

你可能感兴趣的:(LeetCode刷题记录及题解,#,LeetCode比赛)