LeetCode 第186场周赛 题解

文章目录

  • a.分割字符串的最大得分
    • a.题目
    • a.分析
    • a.参考代码
  • b.可获得的最大点数
    • b.题目
    • b.分析
    • b.参考代码
  • c.对角线遍历 II
    • c.题目
    • c.分析
    • c.参考代码
  • d.带限制的子序列和
    • d.题目
    • d.分析
    • d.参考代码

a.分割字符串的最大得分

a.题目

给你一个由若干 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

示例 3

输入:s = “1111”
输出:3

提示

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

a.分析

参考示例1的解释,我们可以遍历分割点把01字符串分成左边和右边,然后找到最大的得分,很显然遍历分割点的时间复杂度是O(n)的。

得分的求法:

  • 由于n只有500这么小,所以可以直接把分割之后的左右两边的字符串分别进行一次遍历,左边的遇到0就+1,右边的遇到1就+1。每次暴力获取得分的都在固定分割点下进行,每次都要花费O(n)的时间进行计算得分,因此总的时间复杂度是O(n^2)的。
  • 考虑如何直接可以知道固定分割点的左右分数,观察发现左右分数其实是一个区间和的问题,因此可以使用前缀和进行预处理,对字符串的1进行计数,那么0的计数就是区间的长度减去区间内的1的计数。前缀和预处理时间花费为O(n),获取区间和为O(1),所以最终总的复杂度为O(n)。

a.参考代码

暴力

class Solution {
public:
	int maxScore(string s) {
		int ans=0;
		//定义i为左边的最后一个位置 由于左边非空 因此i从0开始 由于右边非空 因此i最大为字符串长度-1
		for(int i=0;i<s.size()-1;i++)
			ans=max(ans,getLeft(s,i)+getRight(s,i+1));	//获取最大得分
		return ans;
	}
	int getLeft(string s,int end)
	{
		int sum=0;
		for(int i=0;i<=end;i++)if(s[i]=='0')sum++;
		return sum;
	}
	int getRight(string s,int begin)
	{
		int sum=0;
		for(int i=begin;i<s.size();i++)if(s[i]=='1')sum++;
		return sum;
	}
};

前缀和优化

class Solution {
public:
	int maxScore(string s) {
		vector<int> cnt;
		cnt.push_back(s[0]-'0');
		for(int i=1;i<s.size();i++)cnt.push_back(cnt[i-1]+s[i]-'0');
		//以上进行1的前缀和的计算
		int ans=0;
		//定义i为左边的最后一个位置 由于左边非空 因此i从0开始 由于右边非空 因此i最大为字符串长度-1
		for(int i=0;i<cnt.size()-1;i++)
		{
			int left=i+1-cnt[i];	//当前左边长度为i+1 cnt[i]为左边的1得分 相减为左边的0的得分
			int right=cnt[s.size()-1]-cnt[i];	//直接右边1的得分
			ans=max(ans,left+right);	//更新答案
		}
		return ans;
	}
};

b.可获得的最大点数

b.题目

几张卡牌排成一行,每张卡牌都有一个对应的点数。点数由整数数组cardPoints给出。
每次行动,你可以从行的开头或者末尾拿一张卡牌,最终你必须正好拿 k 张卡牌。
你的点数就是你拿到手中的所有卡牌的点数之和。
给你一个整数数组cardPoints和整数 k,请你返回可以获得的最大点数。

示例 1

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

示例 2

输入:cardPoints = [2,2,2], k = 2
输出:4
解释:无论你拿起哪两张卡牌,可获得的点数总是 4 。

示例 3

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

示例 4

输入:cardPoints = [1,1000,1], k = 1
输出:1
解释:你无法拿到中间那张卡牌,所以可以获得的最大点数为 1 。

示例 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

b.分析

考虑贪心是不可行的,因为有可能拿走了当前最大之后另一边比较深入的地方拥有非常大的分数而恰好拿走后剩下的k步无法获得,比如[666,2,3,9999,1,1]和k为3,贪心会一直拿走左边较大的而右边的无法拿到。
从贪心进一步可得知每一步拿走一边之后将会使得另一边的某个深入无法选择为答案,比如[1,2,3,4,5,6]和k为3,拿了1之后为[2,3,4,5,6]和k为2,因此将会失去选择4的机会。
如果从步骤考虑先后的角度思考的话,每个k都会产生选择之后的两种情况然后再进行取舍,那么对于10^5的数据状态量将会爆掉,而其实先后顺序对于本题是不重要的,所以我们需要从答案集合的角度来进行思考:
对于[1,2,3,4,5]和k=3,不管选择的先后顺序而分为从哪边深入进行选择的,有可能的答案选择为:

  • [1,2,3] 和 []
  • [1,2] 和 [5]
  • [1] 和 [4,5]
  • [] 和 [3,4,5]
    可以看出答案集合仅有k+1个,而这个状态量是完全可以接受枚举状态求最大值的,而且计算总得分也非常简单,只需要按照例子的顺序先把左边k次全拿,然后每次枚举时候从尾端加入而从左边的末尾减去即可,枚举集合的时间复杂度是O(n),计算得分的时间复杂度是先是O(n)预处理再每次O(1)一加一减,所以总的时间复杂度是O(n)

b.参考代码

class Solution {
public:
	int maxScore(vector<int>& c, int k) {
		int n = c.size();
		int sum = 0;
		for(int i=0;i<k;i++)sum+=c[i];	//预处理答案集合的第一个
		int ans = sum;
		//i为从右边开始加入 k-1为左边的末尾位置
		for(int i=n-1;k;i--,k--){
			sum-=c[k-1];
			sum+=c[i];
			ans = max(ans,sum);
		}
		return ans;
	}
};

c.对角线遍历 II

c.题目

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

示例 1
LeetCode 第186场周赛 题解_第1张图片

输入:nums = [[1,2,3],[4,5,6],[7,8,9]]
输出:[1,4,2,7,5,3,8,6,9]

示例 2
LeetCode 第186场周赛 题解_第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

输入:nums = [[1,2,3],[4],[5,6,7],[8],[9,10,11]]
输出:[1,4,2,5,3,8,6,9,7,10,11]

示例 4

输入:nums = [[1,2,3,4,5,6]]
输出:[1,2,3,4,5,6]

提示

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

c.分析

一看到题目,兴高采烈看上去就像是模拟,按照实际对角线的顺序在二维数组中进行遍历,有就加入到答案数组没有就跳过。
但是很可惜的是,对二维数组进行模拟的话这个跳过步骤也是要进行计算的,不然无法知道那个i,j位置是否有数字,那么看一下数据量,虽然nums总共最多有105的数字,但是对于模拟来说是要把整个二维数组的对角线都要列出来,看到行的数据量是105,那么最暴力的当然就要10^10直接爆炸。
看到nums中的总个数限制的话我们就要想到只对nums进行一次遍历而不去遍历不存在的二维数组位置。
遍历一次我们可以得到的信息只有:

  • 每个位置上的值
  • 每个位置的行列i,j
    很显然值是我们需要用到的对于对角线没有什么规律和用处,那么就只有i和j有用了
    观察对角线上的i,j值发现同一条对角线上的位置的i+j的值是相等的,而且i+j越小这条对角线就应该越先被加入,再来看同一条对角线上的先后顺序,可以得出同一条对角线上的j越小这个位置就应该越先被加入。
    因此,我们可以对遍历出来的i,j数据放到一个三元组(i,j,i+j),因为pair排序是先对first进行排序,因此我们可以把遍历出来的数据丢进一个三元组数组里面,三元组的顺序为**{i+j,{j,i}},然后对这个数组进行排序**即可得出按照对角线遍历顺序得出的三元组,最后根据该顺序把对应i,j位置上的值按顺序加入到答案数组即可。总的时间复杂度只有遍历的O(n),n为nums的总个数。

c.参考代码

class Solution {
public:
	vector<int> findDiagonalOrder(vector<vector<int>>& nums) {
		vector<int> ans;
		int n=nums.size();
		vector<pair<int,pair<int,int>>> v;	//(i+j,j,i)三元组
		for(int i=0;i<n;i++)
			for(int j=0;j<nums[i].size();j++)
				v.push_back({i+j,{j,i}});
		sort(v.begin(),v.end());	//根据分析升序排序之后就直接得到顺序的三元组
		for(int i=0;i<v.size();i++)ans.push_back(nums[v[i].second.second][v[i].second.first]);
		return ans;
	}
};

d.带限制的子序列和

d.题目

给你一个整数数组 nums 和一个整数 k ,请你返回非空序列元素和的最大值,子序列需要满足:子序列中每两个相邻的整数nums[i]nums[j],它们在原数组中的下标ij满足 i < jj - i <= k
数组的子序列定义为:将数组中的若干个数字删除(可以删除 0 个数字),剩下的数字按照原本的顺序排布。

示例 1

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

示例 2

输入:nums = [-1,-2,-3], k = 1
输出:-1
解释:子序列必须是非空的,所以我们选择最大的数字。

示例 3

输入: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

d.分析

从题意可以分析出我们需要做的就是从一个完整的序列中舍弃尽量尽量多尽量大的负数来使得总体的序列总和更大,同时需要满足删除完之后的两边位置不能相差k。那么可以想到对于某个负数我们可以选择保留或者舍弃,而且是否可以舍弃是要根据前后进行判断的,那么这种有01状态且与其他状态关联的就想到用动态规划的思想。
考虑当前的nums[i]:

  • 如果它是正数:
    • 肯定把正数保留
    • 是否和前面已经确定的的sum[i-k~i-1]关联:
      • 存在sum[i-k~i-1]>0 选择最大的和它并在一起
      • 所有sum[0~i-1]<=0 前面的序列可以当作丢弃 因为前面序列的子问题对当前的数值是负贡献 直接以当前的数为开始
  • 如果它是负数:
    • 肯定 作为状态当然也是要保留的啦~
    • 是否和前面已经确定的的sum[i-k~i-1]关联:
      • 存在sum[i-k~i-1]>0 选择最大的和它并在一起 只不过并完之后会变小
      • 所有sum[0~i-1]<=0 前面的序列可以当作丢弃 因为前面序列的子问题对当前的数值是负贡献 直接以当前的数为开始

根据以上情况考虑之后,可以发现,对于某个位置上的无论正负数,这个位置的状态都是把当前的数并上前面的最大状态或者是当前的数(因为前面的序列最大贡献都为负了),那么我们只需要维护当前位置i前面的i-k到i-1的最大值即可,而这个最大值是动态的,也就是说会根据i的移动而使得前面的某些状态要舍弃,然后由次大值顶上这样的,这类维护滑动窗口最大值可以想到的就是单调队列或者单调栈,当然也可以用线段树维护区间最大值,但是由于这里i的状态一直往后滑动往后走的,所以也没有必要用线段树,而且线段树在这题复杂度还较高。

单调队列的做法:
在状态i之前维护一个长度为k的单调递减队列,即队首永远是最大i-k到i-1的状态。
可以发现i-k到x-1(x为最大状态的位置)这段状态是没有用的,因为x的最大状态距离i又近而且值又大,所以站在i考虑的话永远都是直接选择x位置的最大状态。
而在x之后的较小状态(因为单调递减)的作用就是随着i的往后,有可能x位置离开了i-k~i-1这个区间,那么就需要这个较小状态来代替它成为队首即当前的最大状态。
很显然在一个正数加上前面无论正负的最大状态之后的状态肯定要比最大状态要大,那么这个时候就可以直接把队列清空然后让该状态成为最大状态。
最终的答案要在每个状态间找到最大值。

举例
nums = [10,2,-10,5,20], k = 2问题下的状态为:

  • i=0: queue:[] status:10
  • i=1: queue:[10] status: 12
  • i=2: queue:[10,12] status: 2 最大值更新 所以之前的清空了
  • i=3: queue:[12,2] status: 17
  • i=4: queue:[12,2,17] status: 37 最大值更新而且12因为k的限制而出队

nums = [10,-2,-10,-5,20], k = 2问题下的状态为

  • i=0: queue:[] status:10
  • i=1: queue:[10] status: 8
  • i=2: queue:[10,8] status: 0
  • i=3: queue:[10,8,0] status: 3 10因为k的限制而出队
  • i=4: queue:[8,0,3] status: 23 最大值更新而且8因为k的限制而出队

以上要非常注意的就是因为限制而出队和最大值更新的顺序,必须要先把被限制的出队然后再看是否更新最大值
需要更新n个状态,且每个状态更新是直接根据队首queue.front来获取的为O(1),而清空状态pop掉或者queue置空只需要O(1),因为每个元素最多被push和pop一次,所以总的时间复杂度是O(n)。

d.参考代码

public:
	int constrainedSubsetSum(vector<int>& nums, int k) {
		int ans = INT_MIN;
		queue<pair<int,int>> q;		//二元组(pos,num)
		//求出所有的状态
		for(int i=0;i<nums.size();i++){
			if(!q.size()){
				//事实上只有刚开始队列才为空 可以简化代码
				q.push({i,nums[i]});
				ans = max(ans,nums[i]);
			}
			else{
				int now = nums[i];
				now = max(now,now + q.front().second);	//当前的状态值
				if(q.front().first+k==i)q.pop();	//因为k的限制在下一个i+1状态将用不到它
				if(q.front().second<now)while(q.size())q.pop();		//在完成限制的pop后才进行最大值更新
				q.push({i,now});	//此时的加入肯定是单调的
				ans = max(now,ans);		//需要在每个状态间找最大值
			}
		}
		return ans;
	}
};

你可能感兴趣的:(leetcode,周赛)