文章目录
2404. 出现最频繁的偶数元素
题目描述
给你一个整数数组 nums
,返回出现最频繁的偶数元素。
如果存在多个满足条件的元素,只需要返回 最小 的一个。如果不存在这样的元素,返回 -1
。
-
1 <= nums.length <= 2000
-
0 <= nums[i] <= 10^5
示例
思路
遍历数组,更新答案即可。
2405. 子字符串的最优划分
题目描述
给你一个字符串 s
,请你将该字符串划分成一个或多个 子字符串 ,并满足每个子字符串中的字符都是 唯一 的。也就是说,在单个子字符串中,字母的出现次数都不超过 一次 。
满足题目要求的情况下,返回 最少 需要划分多少个子字符串*。*
注意,划分后,原字符串中的每个字符都应该恰好属于一个子字符串。
示例
思路
比较简单的贪心。这题的意思相当于要在原字符串上切上若干刀,切断之后,每一段独立的子串中的字符不会出现重复。问最少需要切多少刀。
可以这样考虑,反正一共要切若干刀。我们不妨从左到右,依次确认每一刀的位置。
一个很直观的想法是,要想使得切的刀的数量最少,那么每次应该在尽可能远的位置进行切割。即,每次切割的时候,要走的尽可能远,直到遇到了重复字符,才进行切割。我们采用这样的贪心策略来进行操作,就能保证最后划分出的子字符串的个数最少。
贪心的题目,重点在于对贪心策略正确性的证明。
下面我尝试简单的证明一下。使用反证法。
若起点是i
,往右走,能走到的最远的位置是j
。这表明了s[j + 1]
这个字符,与区间[i, j]
内的某个字符重复。而区间[i, j]
内的每个字符都只出现一次。
假设我们此时切割的位置不选j
,而选j
之前的某个位置k
,那么此时能够使得总的切割次数更小吗?
我们假设上图中橙色部分的区间的最小切割次数为t1
;而蓝色部分的区间,假设其最小切割次数t2
。
则一定有t2 >= t1
。为什么呢?
对蓝色部分,首先考虑其和橙色部分重叠的区间,先把这部分区间切割了,则最少需要切割t1
次。我们假设取t1 = 2
,设切割后的第一段区间为s1
,如下图。
然后我们把前面的一段区间(红色部分),纳入考虑范围。(将红色的这段区间,和s1
进行合并)。
由于加上了一段区间,那么字符肯定是变多了。最好的情况是:新增的这段红色区间内的字符,不会和s1
中的字符发生重复(即合并后的新区间中,每个字符都只出现一次)。那么我们可以直接把这一段区间,与s1
合并产生一个新的区间,而不用额外增加切割次数,这样能维持t2 = t1
。
那么坏一点的情况是,红色区间中的字符本身有重复,或与s1
中的字符有重复,那么对于合并后的区间,我们需要额外增加切割次数,那么此时一定有t2 > t1
。
所以,对于[i, n]
的切割,我们取到最远的位置j
作为切割点,一共的切割次数是1 + t1
。
而对于任何< j
的切割点,其总的切割次数是1 + t2
。
而由于t2 >= t1
恒成立,那么我们取最远的位置j
作为切割点,一定能使得对于[i, n]
的切割,切割次数最小。
所以我们的贪心策略是正确的。只需要从左到右遍历,每次取尽可能远的位置作为切割点即可。
2406. 将区间分为最少组数
题目描述
给你一个二维整数数组 intervals
,其中 intervals[i] = [lefti, righti]
表示 闭 区间 [lefti, righti]
。
你需要将 intervals
划分为一个或者多个区间 组 ,每个区间 只 属于一个组,且同一个组中任意两个区间 不相交 。
请你返回 最少 需要划分成多少个组。
如果两个区间覆盖的范围有重叠(即至少有一个公共数字),那么我们称这两个区间是 相交 的。比方说区间 [1, 5]
和 [5, 8]
相交。
示例
思路
这道题是一个区间的合并问题。要将数个区间进行分组,同一组内的区间不能相交。问最少划分多少个组。
这种什么最多最少,那我感觉我又要开始贪了!
要想划分的组数越少,那么很直观的,每一组中包含的区间数量应该尽可能的多。
那么如何能让一个组包含的区间尽可能多呢?
我们对每一个组,维护一下这个组中最右侧区间的右端点,称其为这个组的right
。要想这个组能容纳尽可能多的区间,那么要保证该组的right
增长的尽可能的慢。
我们首先对所有区间,按照左端点从小到大排序,然后用一个小根堆,来维护当前的所有组,对于所有组,维护其right
属性即可。
然后遍历所有区间;判断,若当前小根堆堆顶的第一个组(right
值最小的 组,都和当前这个区间相交),则需要新建一个组来存放当前区间;或者当前小根堆为空,表明当前不存在任何组,则需要新建一个组。
若当前小根堆的第一个组能够容纳当前区间,则插入进去,并更新这个组的right
属性。
至于这种做法为什么是正确的,我也不清楚。哈哈,反正贪对了。具体证明等后续看了y总的视频讲解再补充了。
2407. 最长递增子序列 II
题目描述
给你一个整数数组 nums
和一个整数 k
。
找到 nums
中满足以下要求的最长子序列:
- 子序列严格递增
- 子序列中相邻元素的差值不超过
k
。
请你返回满足上述要求的 最长子序列 的长度。
子序列 是从一个数组中删除部分元素后,剩余元素不改变顺序得到的数组。
-
1 <= nums.length <= 10^5
-
1 <= nums[i], k <= 10^5
示例
思路
这题是经典的最长递增子序列(LIS)的变种。多加了一个约束:子序列中相邻元素相差不能超过k
。周赛当天我直接套用LIS的思路,采用贪心加二分。
提交发现只能通过78个用例
我们来看一下这组数据:[1,3,3,4]
很明显,满足相邻元素相差不超过k = 1
,的最长递增子序列,是[3,4]
,长度为2。
而我们的贪心策略,是用f[i]
表示长度为i
的子序列,结尾最小的值。对于这组数据,f[1] = 1
,而1
结尾,则无法与后面的3和4进行合并。而我们的贪心策略f[i]
维护的是末尾最小值,就无法使得f[1]
更新为3。只有f[1]
能够更新为3后,才能与后面的4进行结合,得到长度为2的递增子序列。
那么换一种思路,那我对所有位置作为起始位置,都求一遍LIS,可以吗?
这样能过掉上面的那组数据[1,3,3,4] 1
。但提交后发现还是WA。
WA的数据是:[10,3,20,2,16,12] 4
。我的代码的输出是1,而正确答案应该是2([10, 12]
)。
分析发现,当对第一个位置求LIS时,其会被后面的3给更新掉,使得以i = 0
这个位置求解LIS时的f[1]
变为了3,则无法和后面的12进行组合。可以看到,二分是行不通的。因为无法按照维护末尾最小值这种贪心思路来求得正确答案。
于是再来盯着最朴素的
对于最长上升子序列问题,我们用动态规划的思路是:
设f[i]
表示,结尾数字为nums[i]
的所有子序列中,最长子序列的长度。
对于结尾数字为nums[i]
的子序列,我们如何进行状态转移呢?考虑倒数第二个数字,倒数第二个数字可以取nums[i - 1]
,nums[i - 2]
,nums[i - 3]
,…
那么。状态转移方程为:f[i] = max{ f[j] } + 1
,其中j
的取值范围是[0, i - 1]
,注意必须满足nums[i] > nums[j]
的位置,才能参与到状态转移当中。
这道题目,再最长上升子序列的基础上,添加另一个额外约束:相邻元素相差不超过k
,那么状态转移方程中的条件,除了需要满足nums[i] > nums[j]
,还需要满足nums[i] - nums[j] < k
。
可以发现只需要增加一个条件即可。但是这道题的数据范围是10^5
,动态规划的复杂度是,会超时。所以我们需要对状态转移的过程进行优化。主要是求解max {f[j]}
这一步,朴素的动归中,使用的是遍历,这种求区间内的最值,可以使用线段树这种数据结构,在的复杂度完成求解,即可把整个算法的复杂度降低为 。但这个数据结构我还需要补课!
看了一下题解,大概都是说线段树的(其中有个讲分治+单调队列的,横竖都看不懂),无奈这个数据结构我还没学过。先去补课!
------把线段树的基本原理补了一遍。这里大概说一下,线段树的具体笔记等后续学了算法提高课后再做补充。
线段树的基本原理描述如下:
线段树首先是一颗二叉树,并且通常是一颗近似完美的二叉树(除了最后一层节点,其余部分是一颗满二叉树)。线段树通常是用来维护一段区间内的某种信息的。
假设我们用一颗线段树来表示区间
[1,10]
,表示的信息是区间的和。那么这颗线段树的根节点表示整个区间[1,10]
,根节点的值为区间[1,10]
的和,根节点有2个子节点,2个子节点会把根节点所代表的区间平均分成两份,左子节点表示区间[1,5]
,右子节点表示区间[6,10]
,然后每个节点所代表的区间,再继续往下分,直到不可再分为止(树上的节点表示的区间是单点的区间)。如下图所示。
其主要优点是可以对区间内的信息进行动态修改。使得查询某段区间的时间复杂度为,修改某段区间时间复杂度也为。对于上面的,维护区间和,其实可以使用前缀和,能够在的复杂度内求得某段区间的和,但是前缀和的局限性在于,无法对区间内的值做修改,一旦做了修改,需要用的复杂度来更新整个前缀和数组。
所以线段树的适用场景,通常是用来求解区间问题,并且区间内的数据可以做动态修改。比如给定一个数组nums
,其长度为n
。再给出m
次操作,每次操作有两种选择:
- 查询下标在
[L, R]
范围内的最大值 - 修改某个下标
i
的元素的值
这类问题,便是线段树的经典应用。
由于这道题,我们可以用动态规划的思路来做,但是动归的过程中需要求解一个max {f[j]}
,这个过程便可以用线段树来优化,将时间复杂度从 降低到 ,使得整体复杂度降为。
我们先将整个动态规划的思路用代码实现一下。注意对于状态f
的定义,第二个维度,我们定义时是结尾的数字,而不是下标,这与前面的描述不一致,主要是为了方便使用线段树。
这份代码提交会报TLE,因为其复杂度达到了 ,对于求解max {f[i-1][j']}
这一步,我们用线段树来优化。
max {f[i - 1][j']}
实际就是求解区间[j - k, j - 1]
的最大值,故可以用线段树来优化。
线段树的存储通常用一维数组(类似于用一维数组来存储堆),需要开的节点个数,经过计算,需要开整个区间点的数量的4倍。
总结
本次周赛表现不错,前3题在半个小时内就都做出来了,然后最后一题坐牢了1个小时。第4题属于没办法,没学过线段树。用线段树是比较好想的,也有其他的不使用线段树的解法,有待后续再研究了。