2289. 使数组按非递减顺序排列
给你一个下标从 0 开始的整数数组 nums
。在一步操作中,移除所有满足 nums[i - 1] > nums[i]
的 nums[i]
,其中 0 < i < nums.length
。
重复执行步骤,直到 nums
变为 非递减 数组,返回所需执行的操作数。
示例 1:
输入:nums = [5,3,4,4,7,3,6,11,8,5,11]
输出:3
解释:执行下述几个步骤:
- 步骤 1 :[5,3,4,4,7,3,6,11,8,5,11] 变为 [5,4,4,7,6,11,11]
- 步骤 2 :[5,4,4,7,6,11,11] 变为 [5,4,7,11,11]
- 步骤 3 :[5,4,7,11,11] 变为 [5,7,11,11]
[5,7,11,11] 是一个非递减数组,因此,返回 3 。
示例 2:
输入:nums = [4,5,7,7,13]
输出:0
解释:nums 已经是一个非递减数组,因此,返回 0 。
单调栈&dp
提示 1
元素 x x x 会被左边某个比他大的元素 y y y 给删除(如果存在的话)。
我们需要计算在删除 x x x 之前,删除了多少个比 y y y 小的元素,从而算出删除 x x x 的时刻(第几步操作)。
答案可以转换成当前元素前所有(能被删除的)元素被删除的时刻的最大值。
提示 2
以 [ 20 , 1 , 9 , 1 , 2 , 3 ] [20,1,9,1,2,3] [20,1,9,1,2,3] 为例。
时刻一 20 20 20 删掉 1 1 1, 9 9 9 删掉 1 1 1;
时刻二 20 20 20 删掉 9 9 9, 9 9 9 删掉 2 2 2;
时刻三 20 20 20 接替了 9 9 9 的任务,来删除数字 3 3 3。
虽然说数字 3 3 3 是被 20 20 20 删除的,但是由于 20 20 20 立马接替了 9 9 9,我们可以等价转换看作 3 3 3 是被 9 9 9 删除的,也就是它左边离它最近且比它大的那个数。
该等价转换不会影响数字被删除的时刻。
因此元素 x x x 会被左边第一个比他大的元素 y y y 给删除
也就变成找上一个更大元素 -> 从左往右(上一个)遍历单调递减(更大)栈
提示 3
再考虑这个例子 [ 9 , 1 , 2 , 3 , 4 , 1 , 5 ] [9,1,2,3,4,1,5] [9,1,2,3,4,1,5]。
5 5 5 应该被 9 9 9 删除。根据题目要求,在删除 5 5 5 之前,需要把 5 5 5 前面不超过 5 5 5 的元素都删除,然后才能删除 5 5 5。所以在删除 5 5 5 之前,我们需要知道 9 9 9 到 5 5 5 之间的所有元素被删除的时刻的最大值,这个时刻加一就是删除 5 5 5 的时刻。
提示 4
对于一串非降的序列,该序列的每个元素被删除的时刻是单调递增的。(假设序列左侧有个更大的元素去删除序列中的元素)
利用这一单调性,我们只需要存储这串非降序列的最后一个元素被删除的时刻。(单调栈的弹出正确性,因为弹出序列可以看作以当前元素结尾的非降序列)
某一段区间会包含若干个非降序列,也就包含了若干个最后一个元素被删除的时刻,提示 3 中所需要计算的最大值必然在这些时刻中。
提示 5
我们可以用一个单调递减栈存储元素及其被删除的时刻,当遇到一个不小于栈顶的元素 x x x 时**(也即出现了提示4中的非降序列)**,就不断弹出栈顶元素,**并取弹出元素被删除时刻的最大值,**这样就得到了提示 3 中所需要计算的时刻的最大值 m a x T maxT maxT。
然后将 x x x 及 m a x T + 1 maxT+1 maxT+1 入栈。注意如果此时栈为空,说明前面没有比 x x x 大的元素, x x x 无法被删除,即 m a x T = 0 maxT=0 maxT=0,这种情况需要将 x x x 及 0 0 0 入栈。
/**
* [9 1 2 4 3 5 5]
*
* 9
* -----------------------------6
* ------------------------5
* -------------4
* -------------------3
* --------2
* ----1
*
*---------------------------------
* 单调递减队列
*到 1 的时候 9 在队列中 所以 1是 需要被删除的 时间点为 1
*到 2 的时候 9 1 在队列中,要删除2需要先删除1, 时间点为 t(1)+1
*到 4 的时候 9 2 在队列中,要删除4需要先删除2, 时间点为 t(2)+1
*到 3 的时候 9 4 在队列中,要删除3不需要先删除其他的值,所以删除3的时间点为1
*到 5 的时候 9 4 3 在队列中,要删除5 需要先删除 4,3,所以删除5的时间点为t(4)+1
*到 6 的时候 9 5 在队列中,要删除6 需要先删除 5,所以删除6的时间点为t(5)+1
*/
class Solution {
public:
int totalSteps(vector<int>& nums) {
int ans = 0;
stack<pair<int,int>> stk;
for(int i =0;i<nums.size();i++){
int maxpopT = 0;//要删除当前num 的时间点
while(!stk.empty()&&nums[i]>=stk.top().first){//单调递减队列
maxpopT = max(maxpopT,stk.top().second);//左边小于等于num的值都需要在num被删除之前删掉,此时maxT为删掉左边这些小与等于num的数的最大时间
stk.pop();
}
maxpopT = stk.empty()?0:maxpopT+1;//stack不空,左边有比num值大的情况,num需要被删除,为空,说明num不用删除
ans = max(ans,maxpopT);
stk.emplace(nums[i],maxpopT);
}
return ans;
}
};
class Solution {
public:
int totalSteps(vector<int>& nums) {
int ans = 0, n = nums.size();
vector<int> dp(n);
deque<int> stk;
for(int i = 0; i < n; ++i){
int x = nums[i];
while(stk.size() && nums[stk.back()] <= x){
dp[i] = max(dp[i], dp[stk.back()]);
stk.pop_back();
}
++dp[i];
if(stk.empty()) dp[i] = 0;
stk.push_back(i);
}
return *max_element(begin(dp), end(dp));
}
};
316. 去除重复字母
给你一个字符串 s
,请你去除字符串中重复的字母,使得每个字母只出现一次。需保证 返回结果的字典序最小(要求不能打乱其他字符的相对位置)。
示例 1:
输入:s = "bcabc"
输出:"abc"
示例 2:
输入:s = "cbacdcbc"
输出:"acdb"
单调栈&哈希表
单调递增栈 实现 字典序最优
记录字母最后出现位置下标的哈希表 实现 维护字典序最优过程中 每个字母至少出现一次
查询给定字符是否在一个序列中存在的方法。根本上来说,有两种可能:
有序序列: 可以二分法,时间复杂度大致是 O ( N ) O(N) O(N)。
无序序列: 可以使用遍历的方式,最坏的情况下时间复杂度为 O ( N ) O(N) O(N) 。我们也可以使用空间换时间的方式,使用 N N N 的空间 换取 O ( 1 ) O(1) O(1) 的时间复杂度。
对枚举的每个元素
-
如果栈顶元素不是最后一次出现,就按照单增栈规则维护最优字典序,弹出栈顶元素直到符合规则
-
如果栈顶元素是最后一次出现,说明该元素只能出现在最终序列中的该位置,当前元素不管字典序直接入栈
-
如果遇到栈中已存在元素,直接忽略当前元素,因为栈中已是最优字典序,如果按照规则再次弹出维护,最后结果也不会更优,只有可能不变或者更差(栈中要被弹出的某个更大元素必须在那个位置,不能弹出,此时字典序变差)
-
注意怎么查找字符串中有无特定字符/子串?npos
s.find(substr/char) != s.npos //或者 s.find(substr/char) != string::npos
class Solution {
public:
string removeDuplicateLetters(string s) {
//哈希表记录每个字符最后出现位置
unordered_map<char,int> lastpos;
for(int i=0;i<s.length();i++){
lastpos[s[i]] = i;
}
//直接用最后返回结果作为栈操作,push_back()和pop_back()
string ans = "";
for(int i=0;i<s.length();i++){
char c = s[i];
if(ans.find(c)!=ans.npos) continue;//遇到栈中已存在元素,直接忽略
//如果栈顶元素不是必须出现在该处(在原串中最后一次出现),就按字典序维护栈
while(!ans.empty()&&c<ans.back()&&lastpos[ans.back()]>i){
ans.pop_back();
}
ans.push_back(c);
}
return ans;
}
};
也可以栈中加个大于所有元素的哨兵,就不用每次判断栈非空了
class Solution {
public:
string removeDuplicateLetters(string s) {
unordered_map<char,int> lastpos;
for(int i=0;i<s.length();i++){
lastpos[s[i]] = i;
}
string ans = "";
ans.push_back('z'+1);//大于所有元素
for(int i=0;i<s.length();i++){
char c = s[i];
if(ans.find(c)!=ans.npos) continue;
while(c<ans.back()&&lastpos[ans.back()]>i){
ans.pop_back();
}
ans.push_back(c);
}
return ans.substr(1,ans.length()-1);//结果取子串
}
};
402. 移掉k位数字
给你一个以字符串表示的非负整数 num
和一个整数 k
,移除这个数中的 k
位数字,使得剩下的数字最小。请你以字符串形式返回这个最小的数字。
示例 1 :
输入:num = "1432219", k = 3
输出:"1219"
解释:移除掉三个数字 4, 3, 和 2 形成一个新的最小的数字 1219 。
示例 2 :
输入:num = "10200", k = 1
输出:"200"
解释:移掉首位的 1 剩下的数字为 200. 注意输出不能有任何前导零。
示例 3 :
输入:num = "10", k = 2
输出:"0"
解释:从原数字移除所有的数字,剩余为空就是 0 。
字典序 —— 单调栈
和316思路一样:
从左到右遍历维护 单调递增栈 实现 字典序最优
如果不满足单增栈了,就弹栈并维护k直至0结束
class Solution {
public:
string removeKdigits(string num, int k) {
string ans = "";
ans.push_back('0'-1);//哨兵使得不用判断栈空
for(int i=0;i<num.size();i++){
while(k>0&&num[i]<ans.back()){//按照字典序,从左到右遍历维护单调递增栈
ans.pop_back();
k--;
}
//if(num[i]=='0'&&ans.length()==1) continue; 如果0打头,就跳过插入该元素
if(k==0){
ans+=num.substr(i,num.length()-1);
break;
}
ans.push_back(num[i]);
}
while(k>0&&ans.length()>1){
ans.pop_back();
k--;
}
//维护单调栈过程中照常插入0,最后再取从第一个非0开始的子串
int i =1;
while(i<ans.length()){
if(ans[i]!='0') break;
i++;
}
ans = ans.substr(i);//注意substr用法,如果第二个参数不提供,则获取从位置i到字符串结尾的子串
return ans.length()==0?"0":ans;
}
};
456. 132模式
给你一个整数数组 nums
,数组中共有 n
个整数。132 模式的子序列 由三个整数 nums[i]
、nums[j]
和 nums[k]
组成,并同时满足:i < j < k
和 nums[i] < nums[k] < nums[j]
。
如果 nums
中存在 132 模式的子序列 ,返回 true
;否则,返回 false
。
示例 1:
输入:nums = [1,2,3,4]
输出:false
解释:序列中不存在 132 模式的子序列。
示例 2:
输入:nums = [3,1,4,2]
输出:true
解释:序列中有 1 个 132 模式的子序列: [1, 4, 2] 。
示例 3:
输入:nums = [-1,3,2,0]
输出:true
解释:序列中有 3 个 132 模式的的子序列:[-1, 3, 2]、[-1, 3, 0] 和 [-1, 2, 0] 。
暴力
维护 132模式 中间的那个数字 3,因为 3 在 132 的中间的数字、也是最大的数字。我们的思路是个贪心的方法:我们要维护的 1 是 3 左边的最小的数字; 2 是 3 右边的比 3 小并且比 1 大的数字。
从左到右遍历一次,遍历的数字是 n u m s [ j ] nums[j] nums[j] 也就是 132 模式中的 3。根据上面的贪心思想分析,我们用一个变量维护 3 左边最小的元素,然后使用暴力在 n u m s [ j + 1.. N − 1 ] nums[j+1..N−1] nums[j+1..N−1] 中找到 132 模式中的 2 就行。
这个思路比较简单,也能 AC。
class Solution(object):
def find132pattern(self, nums):
N = len(nums)
numsi = nums[0]
for j in range(1, N):
for k in range(N - 1, j, -1):
if numsi < nums[k] and nums[k] < nums[j]:
return True
numsi = min(numsi, nums[j])
return False
单调栈
先正序遍历一遍生成每个元素的左最小值(也就是每个元素的1),然后倒序(从右到左)遍历维护一个单调递减栈,遍历遇到比栈顶元素大的元素时,作为3,此时对该3找到其右边(也就是栈中)最大的2,也即一直弹栈直到栈顶元素比当前元素大,那么弹出的最后一个元素就是 最优的2(对于该3为右边最大的较小值),此时比较2>1即可
**正确性:**如果当前枚举元素不符合单调递减要求,可以作为3,那么弹栈直到找到2的过程中,被弹出的元素不影响正确性,因为最后一个被弹出的元素由于单调递减栈缘故,是最大的2,相较于前面弹出的值拿去与1比大小,是最优的
或者换一个角度看,枚举过程中要找的是。 找 栈中已有的 2 左边 第一个比 2 大的元素 3。这个元素3,是此时的2的最优的3。
**为什么最优?**因为对于每个3而言,1已经是固定的,从右往左遍历只能是递增的,因此贪心策略使得离2最近的3对应的1最小,最有可能贪心得到132组合
因此这个过程两次用到贪心:
- 贪心得到3右边最大的2,也就是弹栈的最后一个元素
- 贪心得到2左边最小的1,也就是找离2最近的3
以上两点合二为一,造就了本题 正序遍历获得每个位置最小前缀+倒序单调递减栈 的解法
#define reversedecreasestack stk
class Solution {
public:
bool find132pattern(vector<int>& nums) {
vector<int> leftmin = {INT_MAX};
stack<int> stk;
stk.push(INT_MAX);
for(int i=1;i<nums.size();i++){
leftmin.push_back(min(leftmin[i-1],nums[i-1]));
}
//for(int n:leftmin) cout<<n<<' ';
for(int i=nums.size()-1;i>=0;i--){
int rightmaxmin = INT_MIN;
while(nums[i]>stk.top())
{
rightmaxmin = stk.top();
stk.pop();
}
if(rightmaxmin>leftmin[i]) return true;
stk.push(nums[i]);
}
return false;
}
};
239. 滑动窗口最大值
给你一个整数数组 nums
,有一个大小为 k
的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k
个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值 。
示例 1:
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值
--------------- -----
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7
单调队列
如下图:当4出现后,在其之前出现的更小值2,1永远也不可能成为滑动窗口内最大值
又因为要随着滑动窗口的推进,弹出窗口头部元素,因此不能只用单调栈,需要有两个元素出口,一个用于在新元素加入时,维护单调性,另一个用于随着时间推移维护元素正确性,因此使用单调队列,即双端队列,从尾部加入元素维护其单调性的同时,从头部弹出窗口外的过期元素
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
vector<int> ans;
deque<int> dq;
for(int i=0;i<nums.size();i++){
while(!dq.empty()&&nums[i]>nums[dq.back()]) dq.pop_back();//单调性
if(!dq.empty()&&dq.front()==i-k) dq.pop_front();//弹出过期元素
dq.push_back(i);
if(i>=k-1) ans.push_back(nums[dq.front()]);//单调性使得队列头部始终为窗口最大值
}
return ans;
}
};
215. 数组中的第K个最大元素
给定整数数组 nums
和整数 k
,请返回数组中第 **k**
个最大的元素。
请注意,你需要找的是数组排序后的第 k
个最大的元素,而不是第 k
个不同的元素。
你必须设计并实现时间复杂度为 O(n)
的算法解决此问题。
示例 1:
输入: [3,2,1,5,6,4], k = 2
输出: 5
示例 2:
输入: [3,2,3,1,2,4,5,5,6], k = 4
输出: 4
构建堆
https://www.cnblogs.com/hello-shf/p/11393655.html#_label3
最小堆中所有父节点值<子节点值,最大堆相反,可以在O(1)时间返回堆头部元素:堆中最大/最小元素
用数组模拟二叉树,手动构建堆,以下两个操作:
- 添加元素&上浮:在数组结尾添加新元素,然后不断和其父节点比较并交换,直到无法上浮或者下标为0(到达堆顶)
- 删除元素&下沉:只能删除堆顶元素,将堆顶元素(下标为0)与堆底元素(数组最后的元素)交换,然后数组大小(最大下标)-1模拟删除(这里也可选物理删除pop_back()),然后不断将换上来的那个元素与两个子元素比较,交换并下沉
注意上浮时候,当前节点的父节点下标为 (i-1)/2
而非 i/2
class Solution {
public:
vector<int> heap;
void sinkdown(int i){
int left,right,swappos;
while(1){
left = i*2+1,right = i*2+2;//左右两个子元素
swappos = i;
//先判断左右儿子不超过堆长度,再判断是否下沉
if(left<heap.size()&&heap[left]<heap[swappos]) swappos = left;
if(right<heap.size()&&heap[right]<heap[swappos]) swappos = right;
//如果可以下沉,就把下沉后的下标作为新的基点进入下一轮下沉循环,否则退出
if(swappos!=i){
swap(heap[swappos],heap[i]);
i = swappos;
}
else break;
}
}
void sinkdown(int i,int heapsize){
int left = i*2+1,right = i*2+2;
int swappos = i;
if(left<heapsize&&heap[left]>heap[swappos]) swappos = left;
if(right<heapsize&&heap[right]>heap[swappos]) swappos = right;
if(swappos!=i){
swap(heap[swappos],heap[i]);
sinkdown(swappos,heapsize);
}
}
void floatup(int i){
while(i>0&&heap[i]<heap[(i-1)/2]){//注意上浮时候,当前节点的父节点下标为 `(i-1)/2` 而非 `i/2`
swap(heap[i],heap[(i-1)/2]);
i = (i-1)/2;
}
}
//构建大小为k的小根堆,将所有元素判断与堆顶大小->是否加入堆,最后堆顶元素即为第k大的元素
int findKthLargest(vector<int>& nums, int k) {
for(int num:nums){
if(heap.size()<k){//数组底部加入元素,然后将该元素上浮直到停下
heap.push_back(num);
floatup(heap.size()-1);
}
else if(num>heap[0]){//堆顶部与数组底部元素交换,在数组底部删除(物理或者手动控制堆大小数值),将堆顶元素不断下沉
heap.push_back(num);
swap(heap[0],heap.back());
heap.pop_back();
sinkdown(0);
}
}
return heap[0];
}
//在原数组上构建大根堆,然后删除k-1个元素后得到堆顶为第k大的元素
int findKthLargest(vector<int>& nums, int k) {
heap = nums;
int heapsize = heap.size();
for(int i = heap.size()/2;i>=0;i--){//原数组上构建大根堆,注意从heap.size()/2开始,逆序遍历直到头部,每个元素都下沉
sinkdown(i,heapsize);
}
for(;k>1;k--){//删除k-1个堆顶元素,这里没有物理删除,而是用heapsize标记堆大小进行删除
swap(heap[0],heap[--heapsize]);
sinkdown(0,heapsize);
}
return heap[0];
}
};
347. 前 K 个高频元素
给你一个整数数组 nums
和一个整数 k
,请你返回其中出现频率前 k
高的元素。你可以按 任意顺序 返回答案。
示例 1:
输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]
示例 2:
输入: nums = [1], k = 1
输出: [1]
提示:
1 <= nums.length <= 105
k
的取值范围是[1, 数组中不相同的元素的个数]
- 题目数据保证答案唯一,换句话说,数组中前
k
个高频元素的集合是唯一的
**进阶:**你所设计算法的时间复杂度 必须 优于 O(n log n)
,其中 n
是数组大小。
优先队列/小根堆
排序是O(nlogn),用哈希表统计数字频次,维护大小为k的小根堆,每次插入新元素的时间复杂度是O(logk),因此总的 时间复杂度为O(nlogk)
为什么是小根堆?虽然找的是前k大,但是如果是大根堆,需要将所有元素都插入一遍
而小根堆O(1)返回队列中最小值,因此将小于该值的元素直接略过,不用插入
class Solution {
public:
struct cmp{//结构体定义仿函数,注意>号定义了小根堆,<为大根堆
bool operator()(const pair<int,int> &a,const pair<int,int> &b){
return a.second>b.second;
}
};
vector<int> topKFrequent(vector<int>& nums, int k) {
unordered_map<int,int> mp;
priority_queue<pair<int,int>,vector<pair<int,int> >,cmp> pq;//底层容器为数组
for(int num:nums) mp[num]++;
for(auto &p:mp){
if(pq.size()<k) pq.push(p);
else if(p.second>pq.top().second){
pq.push(p);
pq.pop();
}
}
vector<int> ans;
while(!pq.empty()){
ans.push_back(pq.top().first);
pq.pop();
}
return ans;
}
};
桶排序
说人话就是用哈希表统计频次后,将频次作为下标,对应数字作为值,填入一个拉链数组/桶,然后倒序遍历取k个元素就是频次最多的k个元素
怎么确定拉链数组的下标范围?统计频数时候用一个变量maxcount记录最大频数作为数组上界
class Solution {
public:
vector<int> topKFrequent(vector<int>& nums, int k) {
unordered_map<int,int> mp;
int maxcount = INT_MIN;//统计最大频数作为反数组(桶)的最大下标
for(int num:nums){
mp[num]++;
maxcount = max(maxcount,mp[num]);
}
vector<vector<int>> bucket(maxcount+1);//构建拉链vector<vector<int>>数组/桶
for(auto &a:mp){
bucket[a.second].push_back(a.first);
}
vector<int> ans;
for(int i = maxcount;k>0;i--){//倒序遍历桶的后k个下标元素就是答案了
for(int j:bucket[i]){
ans.push_back(j);
k--;
}
}
return ans;
}
};
1696. 跳跃游戏 VI
给你一个下标从 0 开始的整数数组 nums
和一个整数 k
。
一开始你在下标 0
处。每一步,你最多可以往前跳 k
步,但你不能跳出数组的边界。也就是说,你可以从下标 i
跳到 [i + 1, min(n - 1, i + k)]
包含 两个端点的任意位置。
你的目标是到达数组最后一个位置(下标为 n - 1
),你的 得分 为经过的所有数字之和。
请你返回你能得到的 最大得分 。
示例 1:
输入:nums = [1,-1,-2,4,-7,3], k = 2
输出:7
解释:你可以选择子序列 [1,-1,4,3] (上面加粗的数字),和为 7 。
示例 2:
输入:nums = [10,-5,-2,4,0,3], k = 3
输出:17
解释:你可以选择子序列 [10,4,3] (上面加粗数字),和为 17 。
递归dp
一、启发思考:寻找子问题
看示例 2,nums=[10,−5,−2,4,0,3], k=3
我们要解决的问题是:从下标 0 跳到下标 n−1=5,经过的所有数字之和最大是多少?
思考「最后一步」发生了什么,有 3 种选择:
从 4 跳到 5,我们需要知道:从 0 跳到 4,经过的所有数字之和最大是多少?
从 3 跳到 5,我们需要知道:从 0 跳到 3,经过的所有数字之和最大是多少?
从 2 跳到 5,我们需要知道:从 0 跳到 2,经过的所有数字之和最大是多少?
由于这 3 种选择,都把原问题变为和原问题相似的、规模更小的子问题,所以可以用递归解决。
注 1:从右往左倒着思考,主要是为了方便把递归翻译成递推。从左往右思考也是可以的。
注 2:动态规划有「选或不选」和「枚举选哪个」两种基本思考方式。在做题时,可根据题目要求,选择适合题目的一种来思考。本题用到的是「枚举选哪个」。
二、递归怎么写:状态定义与状态转移方程
因为要解决的问题都形如「从 0 跳到 i,经过的所有数字之和最大是多少」,所以定义 dfs(i) 表示从 0 跳到 i,经过的所有数字之和的最大值。
如果从 j 跳过来,那么有 dfs(i)=dfs(j)+nums[i],其中 max(i−k,0)≤j≤i−1
枚举 j,取转移来源的最大值,即
递归边界:dfs(0)=nums[0]。
递归入口:dfs(n−1),也就是答案。
class Solution {
public:
int maxResult(vector<int>& nums, int k) {
function<int(int)> dfs = [&](int i)->int{
if(i==0) return nums[0];
int mx = INT_MIN;
for(int j=max(0,i-k);j<i;j++){//在上一次跳过来的区间中遍历,往前递归
int z = dfs(j);
mx = max(mx,z);//选取最大值
}
return mx+nums[i];
};
return dfs(nums.size()-1);
}
};
递推
class Solution {
public:
int maxResult(vector<int>& nums, int k) {
vector<int> dp(nums.size());
dp[0] = nums[0];
for(int i=1;i<nums.size();i++){
int mx = INT_MIN;
for(int j = max(0,i-k);j<i;j++){
mx = max(mx,dp[j]);
}
dp[i] = nums[i]+mx;
}
return dp[nums.size()-1];
}
};
单调队列优化递推
递推过程中,每轮循环都要对 i 找 [i-k,i-1] 中的dp子问题最大值,这个求最值过程可以优化成单调队列
递推过程中对长度为 k 的窗口维护单调递减队列
每次取队列头部元素即为窗口内最大值
class Solution {
public:
int maxResult(vector<int>& nums, int k) {
vector<int> dp(nums.size());
deque<int> dqueue;
dp[0] = nums[0];
for(int i=1;i<nums.size();i++){
while(!dqueue.empty()&&dp[i-1]>=dp[dqueue.back()]) dqueue.pop_back();
dqueue.push_back(i-1);
if(dqueue.front()<i-k) dqueue.pop_front();//弹出窗口外的元素
dp[i] = nums[i]+dp[dqueue.front()];//队列头即为最大值
}
return dp[nums.size()-1];
}
};
2788. 按分隔符拆分字符串
给你一个字符串数组 words
和一个字符 separator
,请你按 separator
拆分 words
中的每个字符串。
返回一个由拆分后的新字符串组成的字符串数组,不包括空字符串 。
注意
separator
用于决定拆分发生的位置,但它不包含在结果字符串中。- 拆分可能形成两个以上的字符串。
- 结果字符串必须保持初始相同的先后顺序。
示例 1:
输入:words = ["one.two.three","four.five","six"], separator = "."
输出:["one","two","three","four","five","six"]
解释:在本示例中,我们进行下述拆分:
"one.two.three" 拆分为 "one", "two", "three"
"four.five" 拆分为 "four", "five"
"six" 拆分为 "six"
因此,结果数组为 ["one","two","three","four","five","six"] 。
示例 2:
输入:words = ["$easy$","$problem$"], separator = "$"
输出:["easy","problem"]
解释:在本示例中,我们进行下述拆分:
"$easy$" 拆分为 "easy"(不包括空字符串)
"$problem$" 拆分为 "problem"(不包括空字符串)
因此,结果数组为 ["easy","problem"] 。
示例 3:
输入:words = ["|||"], separator = "|"
输出:[]
解释:在本示例中,"|||" 的拆分结果将只包含一些空字符串,所以我们返回一个空数组 [] 。
stringstream 和 getline 实现 split
c++ string没有实现split方法,可以用stringstream字符流和getline实现该功能
getline()的原型是istream& getline ( istream &is , string &str , char delim );
其中 istream &is 表示一个输入流,
例如,可使用cin;
string str ; getline(cin ,str)
也可以使用 stringstream
stringstream ss("test#") ; getline(ss,str)
char delim表示遇到这个字符停止读入,通常系统默认该字符为’\n’,也可以自定义字符
//自己实现的split方法
vector<string> split(string str, char del) {
stringstream ss(str);//先对待分割字符串创建流
string temp;
vector<string> ret;
while (getline(ss, temp, del)) {//第一个参数作为字符流,第二个参数作为接收的字符串,第三个参数作为读入即停止的字符
ret.push_back(temp);//while循环直到getline读完即可,全自动,不用管ss尾部没有结束字符,系统自动停止
}
return ret;
}
//本题
class Solution {
public:
vector<string> splitWordsBySeparator(vector<string>& words, char separator) {
vector<string> res;
for (string &word : words) {
stringstream ss(word);
string sub;
while (getline(ss, sub, separator)) {
if (!sub.empty()) {
res.push_back(sub);
}
}
}
return res;
}
};
普通模拟
class Solution {
public:
vector<string> splitWordsBySeparator(vector<string>& words, char separator) {
vector<string> ans;
for (string &word : words) {
word += separator;//这一步省去了很多corner case的处理,让结尾也有分隔符,记住这个思想
string str = "";
for (char ch : word) {//注意到string可以看做char数组,因此char element可以range-based for loop
if (ch != separator) {
str += ch;
} else {
if (!str.empty()) {
ans.push_back(std::move(str));
str.clear();//clear()
}
}
}
}
return ans;
}
};
98. 验证二叉搜索树
给你一个二叉树的根节点 root
,判断其是否是一个有效的二叉搜索树。
中序遍历
二叉搜索树的中序遍历序列为一个严格递增数组
因此我们只需要用一个变量pre维护中序遍历中上一个节点的值,将当前节点值与其比较即可
class Solution {
public:
bool isValidBST(TreeNode* root) {
long pre = LONG_MIN;//用全局变量,维护访问序列中上一个节点的值
function<bool(TreeNode*)> check = [&](TreeNode* root)->bool{
if(!root) return true;
if(!check(root->left)||root->val<=pre) return false;//左,中
pre = root->val;//维护访问序列中上一个节点的值
return check(root->right);//右
};
return check(root);
}
};
后序遍历
注意将所有的变量设置为long
与前序遍历中将节点值范围往下传相反,自底向上将节点值范围往上传
看起来对于当前节点,只需要左边返回一个最大值,右边返回一个最小值,比较当前节点值即可
这是不对的,比如将上图的4改成6,此时5的左子树返回的是3而非6
因此左右子树需要返回取值范围,用范围来保证左子树的最大值和右子树的最小值的正确性
取到左右子树的范围后,只需比较当前节点值是否满足 > 左子树最大值,< 右子树最小值,符合以上的为二叉搜索树
递归到空节点时,返回{LONG_MAX,LONG_MIN}来保证父节点值进行比较时肯定为二叉搜索树,
如果当前树不为二叉搜索树,返回{LONG_MIN,LONG_MAX}保证父节点值进行比较时肯定不为二叉搜索树,也可作为结果的bool false返回
class Solution {
public:
pair<long,long> postorder(TreeNode* root){
if(!root) return pair(LONG_MAX,LONG_MIN);//保证叶子节点在验证左右节点时肯定是二叉搜索树
auto [left_min,left_max] = postorder(root->left);
auto [right_min,right_max] = postorder(root->right);
if(root->val<=left_max||root->val>=right_min) return pair(LONG_MIN,LONG_MAX);
return {min((long)root->val,left_min),max((long)root->val,right_max)};
}
bool isValidBST(TreeNode* root) {
auto [left,right] = postorder(root);
return left!=LONG_MIN;
}
};
543. 二叉树的直径
给你一棵二叉树的根节点,返回该树的 直径 。
二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 root
。
两节点之间路径的 长度 由它们之间边数表示。
示例 1:
输入:root = [1,2,3,4,5]
输出:3
解释:3 ,取路径 [4,2,1,3] 或 [5,2,1,3] 的长度。
示例 2:
输入:root = [1,2]
输出:1
递归/树dp
dp:如果能找到和原问题相似的子问题,并建立起他们的联系,那就可以用递归解决该问题
递归的部分与求二叉树最大深度做法一致
这题坑点在最长直径不一定过根节点,必须在所有内部节点中都比较:以该点作为折点,可能的最长直径
也就是 求最大深度的过程中逐节点更新直径,从 当前节点 拐弯 的最大链长就等于 左右子树的最大链长+2
我当时是这么想的:递归三部曲
- 返回值:当前子树的最大深度(链长),作为其父亲树的求直径组件
- 递归体内逻辑:
已经获得了左右子树的最大深度(链长),那么需要考虑两种全局最大直径的可能性
- 有可能以该节点为根节点的子树,左右子树最大链长+2 == 全局最大直径
- 也有可能将当前节点子树最大链长(max(左右子树最大链长)+1)转发给父亲节点,在父亲节点的递归体逻辑内判断是否最大直径,也即上面的返回值情况
- 比如已求得当前节点的左子树最大深度(链长),那么该数值要么联合右子树的数值并+2,要么+1转发给父亲节点
- 所以递归体内用1看能不能更新全局最大深度即可
- 其实每个节点就两种可能:直径在这里转弯,直径不在这里转弯
- 结束条件:叶子节点
class Solution {
public:
int dfs(TreeNode* root,int &ans){
if(!root) return -1;//叶节点的深度为0,因为考虑到后面要加上与其父节点的链长1,因此这里返回值设成-1
int left = dfs(root->left,ans);//左子树的最大链长
int right = dfs(root->right,ans);//右子树的最大链长
//在这一步,对于全局变量 最大直径来说,有两种可能,一种是以当前节点作为根节点,左右子树的最大链长连在一起形成的直径,或者将该节点作为普通节点,向上转发该子树的最大链长
ans = max(ans,left+right+2);
return max(left,right)+1;
}
int diameterOfBinaryTree(TreeNode* root) {
int ans = 0;
dfs(root,ans);
return ans;
}
};
124.二叉树中的最大路径和
二叉树中的 路径 被定义为一条节点序列,序列中每对相邻节点之间都存在一条边。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。
路径和 是路径中各节点值的总和。
给你一个二叉树的根节点 root
,返回其 最大路径和 。
示例 1:
输入:root = [1,2,3]
输出:6
解释:最优路径是 2 -> 1 -> 3 ,路径和为 2 + 1 + 3 = 6
示例 2:
输入:root = [-10,9,20,null,null,15,7]
输出:42
解释:最优路径是 15 -> 20 -> 7 ,路径和为 15 + 20 + 7 = 42
递归/树dp
和上面那题一样,在每个节点处判断两种可能:
-
以这个节点作为根节点的子树条件下,计算有可能的最大路径和,也就是递归三部曲中,当前递归层内要做的事情
-
把当前节点作为最大路径和中的一环,传递子树最大路径和给父节点,也就是返回值
-
注意用max(0,…)来消除子树路径和中可能的负数,也即不走路径和为负数的子树路径
-
在递归函数中,你计算了
left
和right
的和,并比较它们与0的大小,建议使用max(left, 0)
和max(right, 0)
来确保负值被忽略。
class Solution {
public:
int recursion(TreeNode* root,int &ans){
if(!root) return 0;
int left = max(0,recursion(root->left,ans));//注意这里,用max(0,...)来消除子树路径和中可能的负数
int right = max(0,recursion(root->right,ans));
ans = max(ans,left+right+root->val);
return max(left,right)+root->val;
}
int maxPathSum(TreeNode* root) {
int ans = root->val;
recursion(root,ans);
return ans;
}
};