给你一个由若干 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
参考示例1的解释,我们可以遍历分割点把01字符串分成左边和右边,然后找到最大的得分,很显然遍历分割点的时间复杂度是O(n)的。
得分的求法:
暴力
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;
}
};
几张卡牌排成一行,每张卡牌都有一个对应的点数。点数由整数数组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
考虑贪心是不可行的,因为有可能拿走了当前最大之后另一边比较深入的地方拥有非常大的分数而恰好拿走后剩下的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,不管选择的先后顺序而分为从哪边深入进行选择的,有可能的答案选择为:
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;
}
};
给你一个列表nums
,里面每一个元素都是一个整数列表。请你依照下面各图的规则,按顺序返回nums
中对角线上的整数。
输入:nums = [[1,2,3],[4,5,6],[7,8,9]]
输出:[1,4,2,7,5,3,8,6,9]
输入: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
一看到题目,兴高采烈看上去就像是模拟,按照实际对角线的顺序在二维数组中进行遍历,有就加入到答案数组没有就跳过。
但是很可惜的是,对二维数组进行模拟的话这个跳过步骤也是要进行计算的,不然无法知道那个i,j位置是否有数字,那么看一下数据量,虽然nums总共最多有105的数字,但是对于模拟来说是要把整个二维数组的对角线都要列出来,看到行的数据量是105,那么最暴力的当然就要10^10直接爆炸。
看到nums中的总个数限制的话我们就要想到只对nums进行一次遍历而不去遍历不存在的二维数组位置。
遍历一次我们可以得到的信息只有:
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;
}
};
给你一个整数数组 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] 。
示例 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
从题意可以分析出我们需要做的就是从一个完整的序列中舍弃尽量尽量多尽量大的负数来使得总体的序列总和更大,同时需要满足删除完之后的两边位置不能相差k。那么可以想到对于某个负数我们可以选择保留或者舍弃,而且是否可以舍弃是要根据前后进行判断的,那么这种有01状态且与其他状态关联的就想到用动态规划的思想。
考虑当前的nums[i]:
根据以上情况考虑之后,可以发现,对于某个位置上的无论正负数,这个位置的状态都是把当前的数并上前面的最大状态或者是当前的数(因为前面的序列最大贡献都为负了),那么我们只需要维护当前位置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问题下的状态为:
nums = [10,-2,-10,-5,20], k = 2问题下的状态为
以上要非常注意的就是因为限制而出队和最大值更新的顺序,必须要先把被限制的出队然后再看是否更新最大值
需要更新n个状态,且每个状态更新是直接根据队首queue.front来获取的为O(1),而清空状态pop掉或者queue置空只需要O(1),因为每个元素最多被push和pop一次,所以总的时间复杂度是O(n)。
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;
}
};