lcMonostack - 单调栈

单调栈

其基本特性:

  • 1 单调栈的极值性质:单调递减栈的第一个字符为目前最大的元素,单调递增栈则相反; 关于目前的解释,由于单调栈是遍历整个数组出栈入栈的过程,假设目前遍历到节点i,则arr[:i]为目前单调栈遍历过的元素们,单调栈递增递减栈的第一个数字分别为arr[:i]的最大最小值
  • 2 单调栈的单调性:单调栈内的元素严格单调

1 单调栈写法

一下为求每个元素左侧最大值的一个示例:

1
2
3
4
5
6
7
8
9
10
vector<int> normalOrderMono;
int n = height.size();
vector<int> leftMax(n);
for(int i = 0; i < n; ++i){
while(!normalOrderMono.empty() && height[normalOrderMono.back()] <= height[i]) {
normalOrderMono.pop_back();
}
leftMax[i] = normalOrderMono.empty() ? height[i] : height[normalOrderMono[0]];
normalOrderMono.emplace_back(i);
}

示例

0000_interview_1712trapWater 接雨水

1 题目:

https://leetcode-cn.com/problems/volume-of-histogram-lcci/

2 解题思路:

  • 1 参照提示的一句话:

    每个长方形的顶部都有水,水的高度应与左侧最高长方形和右侧最高长方形的较小值相匹配,也就是说,water_on_top[i] = min(tallest_ bar(0->i), tallest_bar(i, n))。

  • 2 使用单调栈计算当前节点左侧(右侧)最大值
    • 2.1 很简单: 考虑单调栈的性质,单调递减栈的第一个字符为目前最大的元素,单调递增栈则相反,为最小元素
    • 2.2 关于目前的解释,由于单调栈是遍历整个数组出栈入栈的过程,遍历到节点i,则arr[:i]为目前单调栈遍历过的元素们,单调栈递增递减栈的第一个数字分别为arr[:i]的最大最小值
    • 2.3 由于需要统计右侧最大值,则我们只需要逆序遍历数组即可,最大值可以使用单调递减获得
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Solution {
public:
int trap(vector<int>& height) {
// using descending mono stack to find maxValue in left or right
vector<int> normalOrderMono;
vector<int> reverseOrderMono;
int n = height.size();
vector<int> leftMax(n);
vector<int> rightMax(n);
for(int i = n - 1; i >= 0; --i){
while(!reverseOrderMono.empty() && height[reverseOrderMono.back()] <= height[i]) {
reverseOrderMono.pop_back();
}
rightMax[i] = reverseOrderMono.empty() ? height[i] : height[reverseOrderMono[0]];
reverseOrderMono.emplace_back(i);
}

for(int i = 0; i < n; ++i){
while(!normalOrderMono.empty() && height[normalOrderMono.back()] <= height[i]) {
normalOrderMono.pop_back();
}
leftMax[i] = normalOrderMono.empty() ? height[i] : height[normalOrderMono[0]];
normalOrderMono.emplace_back(i);
}

int res = 0;
for(int i = 0; i < n; i++) {
res += max(min(leftMax[i], rightMax[i]) - height[i], 0);
}

return res;
}
};

2030. 含特定字母的最小子序列 smallestSubsequence

1 题目:

https://leetcode-cn.com/problems/smallest-k-length-subsequence-with-occurrences-of-a-letter/

2 解题思路:

  • 1 考虑一个简单化的问题,选出长度为k的最小字典序的字符串,算法如下:
    • 1.1 采用单调栈维护一个递增栈,自然的保持了最小字典序
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      class Solution {
      public:
      string smallestSubsequence(string s, int k, char letter, int repetition) {
      // first, using monostack to get the min sub arr whose len = k
      // and check if there are rep's 'letter' in sub arr

      vector<int> mono; // abscending chars
      int n = s.size();
      int specialCharCnt = 0;
      int avaliableSpeChar = 0;
      for(auto c : s) {
      avaliableSpeChar += (c == letter);
      }


      for(int i = 0; i < n; ++i) {
      while(!mono.empty() && s[mono.back()] >= s[i] && mono.size() - 1 + n - i >= k) {
      specialCharCnt -= static_cast<int>(s[mono.back()] == letter);
      mono.pop_back();
      }

      mono.emplace_back(i);
      if(s[i] == letter) {
      --avaliableSpeChar;
      ++specialCharCnt;
      }
      }

      string res = "";
      for(auto i : mono){
      res += s[i];
      }

      return res;
      }
      };
  • 2 考虑另一个子问题,需要选出含有rep个特殊字符的子序列,可以使用一个队列存储特殊字符的下标,当队列长度达到rep个,则记为一个子序列
  • 3 将两个问题结合起来考虑就是是说:
    • 3.1 在递增栈构造的过程中,要保证当前位置后面剩余的特殊字符加上当前栈内的字符大于等于repetition,否则将不能出栈特殊字符(因为如果出栈则无法满足有repetition个特殊字符的要求
    • 3.2 经过3.1步骤,单调栈内含有我们的答案,但是一定有一些额外的字符存在,那么如下说明从栈内获得答案的方式:
      • 3.2.1 eg: 当aaabbbcccddd为输入,则单调栈为aaabbbcccddd,那么我们想要的结果为在至少有2个b的字符串,那么我们获得最终结果的方式为:在保证有大于repetition个letter的情况下,从尾部开始删除字符串直到单调栈内剩下k个字符即可
        1
        2
        3
        4
        "aaabbbcccddd"
        3
        "b"
        2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
class Solution {
public:
string smallestSubsequence(string s, int k, char letter, int repetition) {
// first, using monostack to get the min sub arr whose len = k
// and check if there are rep's 'letter' in sub arr

clock_t start,end;   //定义clock_t变量
start = clock();    //开始时间

string mono; // abscending chars
int n = s.size();
int specialCharCnt = 0;
int avaliableSpeChar = 0;
for(auto &c : s) {
avaliableSpeChar += (c == letter);
}


for(int i = 0; i < n; ++i) {
while(!mono.empty() && mono.back() > s[i] && mono.size() - 1 + n - i >= k) {
if(mono.back() == letter) {
// when not enough special letter, we do not pop special char
if(avaliableSpeChar <= repetition - specialCharCnt) {
break;
}
--specialCharCnt;
}
mono.pop_back();
}

mono.push_back(s[i]);
if(s[i] == letter) {
--avaliableSpeChar;
++specialCharCnt;
}
}

start = clock();    //开始时间
string res = "";
// eliminate some extra chars reversely
int delNum = mono.size() - k;
// cout << "letter Cnt: " << specialCharCnt << endl;
for(int i = mono.size() - 1; i >= 0; --i){
// make sure there are more than rep's 'letter'
if(delNum != 0 ) {
if(specialCharCnt > repetition) {
specialCharCnt -= (mono[i] == letter);
--delNum;
continue;
} else {
if(mono[i] != letter) {
--delNum;
continue;
}
}
}
// spend: 0.311s
// res = mono[i] + res; // this spend two much time, ocuppy nearly 100% time! so we change our policy
// spend: 0.000153s, 1000 times faster!
res.push_back(mono[i]);
}

reverse(res.begin(), res.end());
end = clock(); //结束时间
cout<<"time = "<<double(end-start)/CLOCKS_PER_SEC<<"s"<<endl; //输出
return res;
}
};


3 关于string的+和push_back

如果是一个字符一个字符的话,使用push_back会比+快1000倍!如上代码可以自己尝试统计

1
2
3
4
// spend: 0.311s
// res = mono[i] + res; // this spend two much time, ocuppy nearly 100% time! so we change our policy
// spend: 0.000153s, 1000 times faster!
res.push_back(mono[i]);

0321 拼接最大数

1 题目

https://leetcode-cn.com/problems/longest-duplicate-substring/

2 解题思路

  • 1 首先分解问题:
    • 1.1 从长度为m和n(假设m <= n)中的字符串里选出k个,然后这个字串要求最大,遍历的思路:
      • 1.2 首先一共要选k个,自然想到从m和n中各挑选几个?那就是遍历了,m中的挑选长度的起点为: max(0, k - n),最少一个不挑,然后从m中挑的个数身下还有k - m个一定能够从n中挑出,所以起点是从0到k-n,(为什么区最大值?因为当n,m均大于k的时候,k-n为负数),挑选终点:自然是k,或者没有那么多可以调k个,则挑m个,则min(k, m)
      • 1.3 那么已经知道所有从m,n中挑选出k个字符串的方法,那么对于每一个方法,如何获取最大字符串呢?其实就是分别从该方法的m和n串中各选出他们的最大字串,然后合并即可,于是问题转化为:从m中如何选出某个长度记为l的最大字串?
        • 1.3.1 我们考虑一个使用单调递减栈,因为它的栈顶总是当前字符串最大的值,然后后面都是递减的,这正是我们需要的,比如 9 1 2 5 8 3选择3个的时候,使用单调栈可以直接获得9,8,3,但是有个问题,比如从 9 1 2 5 8,单调栈遍历完则为9 5,这3个没选够,所以何时停止从单调栈里弹出呢?遍历位置以及后面剩余的元素刚好够挑选长度的时候,就不再弹出了(即使单调栈内的元素已经不单调了)
      • 1.4 在下一个问题,对于一个挑选方法,m中挑选l个,n中挑选k-l个,分别得到一个最大字串,如何合并成最终字串呢?
        • 1.4.1 其实很简单,两个字符串分别维护一个head叫ha,hb吧,若ha比hb大,那么就把ha的值压入最终结果,直到ha < hb,同理移动b即可,但是需要考虑ha == hb的情况,直接比较ha,hb对应的尾串即可,参考如下测试样例即可:
          1
          2
          3
          4
          5
          6
          7
          8
          9
          eg1: 
          [2,5,6,4,4,0]
          [7,3,8,0,6,5,7,6,2]
          15

          eg2:
          [6,7]
          [6,0,4]
          5
          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
          15
          16
          17
          18
          19
          20
          21
          22
          23
          24
          25
          26
          27
          28
          29
          30
          31
          32
          33
          34
          35
          36
          37
          38
          39
          40
          41
          42
          43
          44
          45
          46
          47
          48
          49
          50
          51
          52
          53
          54
          55
          56
          57
          58
          59
          60
          61
          62
          63
          64
          65
          66
          67
          68
          69
          70
          71
          72
          73
          74
          75
          76
          77
          78
          79
          80
          81
          class Solution {
          public:

          string getMaxConcat(vector<int>& longVec, vector<int>& shortVec, int lenInLonger, int lenInShorter) {
          string monoLong, monoShort;

          // choose biggest lenInLonger's subArr from longer vec
          for(int i = 0; i < longVec.size(); ++i) {
          while(monoLong.size() > 0 && monoLong.back() - '0' < longVec[i] && monoLong.size() + longVec.size() - i > lenInLonger) {
          // cout << "pop_long: " << monoLong.back() << " monoLong's std len: " << lenInLonger << " curback: " << monoLong.back() << endl;
          monoLong.pop_back();
          }
          if(monoLong.size() < lenInLonger) {
          // cout << "push_long: in " << static_cast<char>(longVec[i] + '0') << endl;
          monoLong.push_back(static_cast<char>(longVec[i] + '0'));
          }
          }
          for(int i = 0; i < shortVec.size(); ++i) {
          // while(monoLong.back() < longVec[i] && monoLong.size() <= lenInLonger && monoLong.size() + longVec.size() - i + 1 < lenInLonger) {
          while(!monoShort.empty() && monoShort.back() - '0' < shortVec[i] && monoShort.size() + shortVec.size() - i > lenInShorter) {
          // cout << "pop_short: " << monoShort.back() << " monoLong's std len: " << lenInShorter << " curback: " << monoShort.back() << endl;
          monoShort.pop_back();
          }
          if(monoShort.size() < lenInShorter) {
          // cout << "push_short: in " << static_cast<char>(longVec[i] + '0') << endl;
          monoShort.push_back(static_cast<char>(shortVec[i] + '0'));
          }
          }

          int j = 0;
          // merger the two biggest substr,
          string finalRes = "";
          // cout << "longMax and shortMax str: " << monoLong << " " << monoShort << endl;
          for(int i = 0; i < monoShort.size(); ++i) {
          while(j < monoLong.size() && monoLong[j] > monoShort[i]) {
          finalRes.push_back(monoLong[j++]);
          }
          // decided whether to use long str or short when the char compared is true
          if(monoLong[j] == monoShort[i]) {
          if(monoLong.substr(j) > monoShort.substr(i)) {
          finalRes.push_back(monoLong[j++]);
          i--;
          continue;
          }
          }
          finalRes.push_back(monoShort[i]);
          }
          finalRes += monoLong.substr(j);
          // cout << "finalRes string is: " << finalRes << endl;
          return finalRes;
          }

          vector<int> maxNumber(vector<int>& nums1, vector<int>& nums2, int k) {
          int m = nums1.size();
          int n = nums2.size();
          // cout << "m/n" << m << " " << n << endl;

          // let k split into nums1 and nums2
          string maxStr(k, '0');
          if(m <= n) {
          for(int lenInShorter = max(0, k - n); lenInShorter <= min(m, k); ++lenInShorter) {
          int lenInLonger = k - lenInShorter;
          // cout << "lenInLong/short" << lenInLonger << " " << lenInShorter << endl;
          string curMax = getMaxConcat(nums2, nums1, lenInLonger, lenInShorter);
          maxStr = maxStr > curMax ? maxStr : curMax;
          }
          } else {
          for(int lenInShorter = max(0, k - m); lenInShorter <= min(n, k); ++lenInShorter) {
          int lenInLonger = k - lenInShorter;
          string curMax = getMaxConcat(nums1, nums2, lenInLonger, lenInShorter);
          maxStr = maxStr > curMax ? maxStr : curMax;
          }
          }

          vector<int> res;
          for(auto& c : maxStr) {
          res.emplace_back(c - '0');
          }
          return res;
          }
          };

0768 最多能完成排序的块 II maxChunksToSroted

1 题目

https://leetcode-cn.com/problems/max-chunks-to-make-sorted-ii

2 解题思路

  • 1 普通思路:

    • 1.1 利用已经排好序的数组,和当前数组进行比较,得到分块方式,也就是题目的提示:

      Each k for which some permutation of arr[:k] is equal to sorted(arr)[:k] is where we should cut each chunk.

    • 1.2 具体算法:
      • 记原数组为arr,然后其排序为sortArr,而后遍历arr,如何确定一个下标k是否为chunk的分割点呢?
      • 使用hashA记录arr[:k]子数组中每个元素的出现次数,使用diffCount记录arr[:k]和sortArr[:k](在两个数组里出现次数不同的)元素个数
      • 当diffCount为0,就找到一个k,最后返回所有diffCount为0的地方即可
    • 1.3 普通解法:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      class Solution {
      public:

      int maxChunksToSorted(vector<int>& arr) {
      // monoStack: every ele in mono represent: the biggest value in a chunk
      unordered_map<int, int> hash;

      // diffCnt, the size of those numbers whose cnt are not equal in arr[:k] and sortArr[:k]
      int ans = 0, n = arr.size(), diffCnt = 0;
      vector<int> sortArr(arr);
      sort(sortArr.begin(), sortArr.end(), std::less<int>());
      for(int k = 0; k < n; ++k) {
      ++ hash[arr[k]];
      if(hash[arr[k]] == 1) {
      diffCnt++;
      }
      if(hash[arr[k]] == 0){
      diffCnt--;
      }

      -- hash[sortArr[k]];
      if(hash[sortArr[k]] == 0) {
      diffCnt--;
      }
      if(hash[sortArr[k]] == -1) { // sortArr[k] is redundant
      diffCnt++;
      }

      ans += diffCnt == 0;
      }

      return ans;
      }
      };
  • 2 单调栈:

    • 2.1 考虑例子: 1 4 3 7 5,很容易发现,就三个分块,然后arr的单调增栈为1 4 7,刚好为每个分块的最大值,所以有这么一个单调栈定义:单调栈里存入的数字为每个分块的最大值
    • 2.2 当然这也有问题,会涉及到单调栈需要合并分块的情况: 1 4 3 7 5 2,当没检测到2的时候,单调栈为3个分块,最大值分别为1,4,7,当检测到2的时候,我们需要先弹出所有比2大的分块的最大值,因为2在这些分块后面意味着必须将2和这些分块合并,这样才能保证最终从小到大的排序,然后压入合并的这些分块里的最大值,也就是遇到2之前单调栈的栈顶 7,单调栈变成了1,7
    • 2.3 具体看代码:单调栈解法:可视化参考
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      class Solution {
      public:

      int maxChunksToSorted(vector<int>& arr) {
      // monoStack: every ele in mono represent: the biggest value in a chunk
      vector<int> mono = {INT_MIN}; // from small to bigger
      int n = arr.size();
      for(int i = 0; i < n; ++i) {
      if(arr[i] >= mono.back()) {
      mono.emplace_back(arr[i]);
      } else { // arr[i] < mono.back()
      // merge the chunk untill arr[i] > mono.back()
      int curChunkMax = mono.back();
      while(mono.back() > arr[i]) {
      mono.pop_back();
      }
      mono.emplace_back(curChunkMax);
      }
      }

      return mono.size() - 1;
      }
      };

1793 好子数组的最大分数 maximumScore

1 题目:

https://leetcode-cn.com/problems/maximum-score-of-a-good-subarray/

类似题目:

2 解题思路:

  • 1 单调栈:
    • 1.1 想法很简单,遍历上述的柱状图的一列,栈顶总是最大,栈底最小,一旦有小于等于前栈的值,那么栈里面那些小于等于当前栈顶的值都不可能再在遍历的后续位置发生作用了,因为已经有小于等于它的值出现了,所以我们把这些值弹出然后算弹出的这一部分的体积,那么栈里面剩下的就是任然能够为后续遍历的位置贡献面积的值,所以就是这样,具体的看代码吧。
    • 1.2 计算过程中,我们只有当矩形的两边分别位于k的两边才会更新结果
  • 2 强调单调栈的几个特性(以递增单调栈为例子)
    • 2.1 栈底一定是整个数组最小的
    • 2.2 弹出当前元素记其下标为j后,当前栈顶元素下标记为i,那么i是第一个下标满足: i < j && arr[i] <= arr[j]
    • 2.3 **TIPS: **注意单调栈会将所有元素都入栈,但并不会都出栈,很多时候我们要求arr中的每个元素都出栈,那么常见操作为在arr末尾加一个比所有元素都要小的值即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Solution {
public:
int maximumScore(vector<int>& nums, int k) {
// k split nums into left and right
vector<int> lPart(nums.begin(), nums.begin() + k + 1);

// construct the monostack, abscending
vector<int> mono;

int res = nums[k];
nums.emplace_back(0); // make sure all the heights are used for nums
for(int r = 0; r < nums.size(); ++r) {
while(!mono.empty() && nums[mono.back()] >= nums[r]) {
int h = nums[mono.back()];
// cout << "h: " << h << endl;
mono.pop_back();
int l = mono.empty() ? -1 : mono.back();
int w = r - l - 1;
cout << "w/r/l/h: " << w << "/" << r << "/" << l << "/" << h << endl;
if(r - 1 >= k && l + 1 <= k) {
res = max(res, w * h);
}
}
mono.emplace_back(r);
}

return res;
}
}