单调栈进阶: 去除重复字母/拼接最大数/最大矩形
Author:zhoulujun Date:
上一篇 《单调栈模板:下一个更大元素/每日温度/移掉K位数字/接雨水》,本篇再来几题
去除重复字母
给你一个字符串 s ,请你去除字符串中重复的字母,使得每个字母只出现一次。需保证 返回结果的字典序最小(要求不能打乱其他字符的相对位置)。
示例 1:输入:s = "bcabc" 输出:"abc"
示例 2:输入:s = "cbacdcbc" 输出:"acdb"
这个首先想到是 哈希表去重,但是此题目的三个要求:去重、不能打乱其他字符顺序、字典序最小。
去重:可以通过 「使用哈希表存储字母出现次数」 的方式,将每个字母出现的次数统计起来,再遍历一遍,去除重复的字母。
不能打乱其他字符顺序:按顺序遍历,将非重复的字母存储到答案数组或者栈中,最后再拼接起来,就能保证不打乱其他字符顺序。
字典序最小:意味着字典序小的字母应该尽可能放在前面。
对于第 i 个字符 s[i] 而言,如果第 0 ~ i - 1 之间的某个字符 s[j] 在 s[i] 之后不再出现了,那么 s[j] 必须放到 s[i] 之前。
而如果 s[j] 在之后还会出现,并且 s[j] 的字典序大于 s[i],我们则可以先舍弃 s[j],把 s[i] 尽可能的放到前面。后边再考虑使用 s[j] 所对应的字符。
这个可以参考 《单调栈模板:下一个更大元素/每日温度/移掉K位数字/接雨水》中的 移掉K位数字,只是这道题没有一个全局的删除次数 k。而是对于每一个在字符串 str 中出现的字母 letter都有一个 k 值。这个 k 是 字母letter 出现次数 - 1。
核心都是需要如何判断相同长度的字符串哪个的字典序更大。
与402.移掉K位数字 题目不同的是,这里需要满足同一种字符都需要出现,且只能出现一次。那怎么才能满足这些条件呢?
因此,需要额外考虑以下两个问题:
在考虑字符 s[i] 时,如果它已经存在于栈中,则不能加入字符 s[i]到单调栈中。为此,需要记录每个字符是否出现在栈中。
在弹出栈顶字符时,如果字符串在后面的位置上再也没有这一字符,则不能弹出栈顶字符。为此,需要记录每个字符的剩余数量,当这个值为 0 时,就不能弹出栈顶字符了。
function removeDuplicateLetters(s) { const seen = new Map(); // 哈希表记录每个字符出现的次数 const stack = []; // 单调栈 for (let i = 0; i < s.length; i++) { const ch = s[i]; if (!seen.has(ch)) { while (stack.length > 0 && stack[stack.length - 1] > ch && seen.get(stack[stack.length - 1]) > 1) { stack.pop(); } stack.push(ch); seen.set(ch, 1); } else { seen.set(ch, seen.get(ch) + 1); } } let result = ""; while (stack.length > 0) { result = stack.pop() + result; } return result; }
拼接最大数
给你两个整数数组 nums1 和 nums2,它们的长度分别为 m 和 n。数组 nums1 和 nums2 分别代表两个数各位上的数字。同时你也会得到一个整数 k。
请你利用这两个数组中的数字中创建一个长度为 k <= m + n 的最大数,在这个必须保留来自同一数组的数字的相对顺序。
返回代表答案的长度为 k 的数组。
示例 1: 输入:nums1 = [3,4,6,5], nums2 = [9,1,2,5,8,3], k = 5 输出:[9,8,6,5,3]
示例 2: 输入:nums1 = [6,7], nums2 = [6,0,4], k = 5 输出:[6,7,6,0,4]
示例 3: 输入:nums1 = [3,9], nums2 = [8,9], k = 3 输出:[9,8,9]
最大数可以是:
最大数全部在 nums1 中
最大数全部在 nums2 中
最大数部分在 nums1 中,部分在 nums2 中; 所以此处需要一个枚举;
在 nums1 中找 i 个最大值并在 nums2 找 k−i 个最大值组成的所有可能的最大值即位题目答案
很显然这可以用单调栈来优化,整个算法就分成三步:
分别计算 num1 和 nums2 的最大子序列
按照字典序降序合并子序列
保存对应的整数较大的子序列
function maxNumber(nums1, nums2, k) { const maxSubsequence = (nums, k) => { const stack = []; let drop = nums.length - k; for (let num of nums) { while (drop > 0 && stack.length > 0 && stack[stack.length - 1] < num) { stack.pop(); drop--; } stack.push(num); } return stack.slice(0, k); }; const merge = (nums1, nums2) => { const mergeArray = (arr1, arr2) => { const merged = []; while (arr1.length + arr2.length > 0) { let bigger = arr1 > arr2 ? arr1 : arr2; merged.push(bigger.shift()); } return merged; }; const result = []; while (nums1.length > 0 && nums2.length > 0) { if (nums1 > nums2) { result.push(nums1.shift()); } else { result.push(nums2.shift()); } } result.push(...nums1, ...nums2); return result; }; let maxResult = []; for (let i = Math.max(0, k - nums2.length); i <= Math.min(k, nums1.length); i++) { const candidate = merge(maxSubsequence(nums1, i), maxSubsequence(nums2, k - i)); if (candidate > maxResult) { maxResult = candidate; } } return maxResult; }
柱状图中最大的矩形
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
示例 1: 输入:heights = [2,1,5,6,2,3] 输出:10 解释:最大的矩形为图中红色区域,面积为 10
本题最好的解释:https://www.cnblogs.com/GarrettWale/p/15796780.html
暴力法-超时
这道问题的暴力解法比「接雨水」那道题要其实好想得多:可以枚举以每个柱形为高度的最大矩形的面积。
要想找到第 i 位置最大面积是什么?
是以 i 为中心,向左找第一个小于 heights[i] 的位置 left_i;向右找第一个小于于 heights[i] 的位置 right_i,即最大面积为 heights[i] * (right_i - left_i -1),如下图所示:
所以,我们的问题就变成如何找right_i 和 left_i?最简单的思路就是,就是暴力法,直接分别在 i 左右移动。
具体来说是:依次遍历柱形的高度,对于每一个高度分别向两边扩散,求出以当前高度为矩形的最大宽度多少。
为此,我们需要:
左边看一下,看最多能向左延伸多长,找到大于等于当前柱形高度的最左边元素的下标;
右边看一下,看最多能向右延伸多长;找到大于等于当前柱形高度的最右边元素的下标。
对于每一个位置,我们都这样操作,得到一个矩形面积,求出它们的最大值。
function largestRectangleArea(heights) { const n = heights.length; let maxArea = -Infinity; for (let i = 0; i < n; i++) { const currentHeight = heights[i]; let left = i; while (left >= 0 && heights[left] >= currentHeight) { left--; } let right = i; while (right < n && heights[right] >= currentHeight) { right++; } maxArea = Math.max(maxArea, (right - left - 1) * currentHeight); } return maxArea; }
两种暴力方法的时间复杂度均为 O(N^2 ),会超出时间限制,我们必须要进行优化。
单调栈
如何快速计算 left 和 right?这可以用单调栈求出。
var largestRectangleArea = function(heights) { const n = heights.length; // 获取柱状图的高度数组长度 const leftArr = Array(n).fill(-1);// 用于存储每个柱形左侧第一个比它矮的柱形的索引;初始化-1,表示没有更矮的柱形在左侧 const stack = []; for (let i = 0; i < n; i++) {//初始化为n(表示没有更矮的柱形在右侧) const x = heights[i]; // 当前柱形的高度 //当栈不为空且当前柱子高度小于等于栈顶柱子的高度时,出栈 while (stack.length && x <= heights[stack[stack.length - 1]]) { stack.pop(); } if (stack.length) { // 如果栈不为空,说明找到了左边第一个比当前柱子矮的柱子,将其索引存入leftArr leftArr[i] = stack[stack.length - 1]; } stack.push(i); // 将当前柱形索引压入栈中 } const rightArr = Array(n).fill(n);//用于存储每个柱子右边第一个比它矮的柱子的索引,初始化为n(表示没有更矮的柱形在右侧) stack.length = 0; // 清空栈,重新利用栈来计算rightArr for (let i = n - 1; i >= 0; i--) { const x = heights[i]; while (stack.length && x <= heights[stack[stack.length - 1]]) {// 同上,但方向相反 stack.pop(); } if (stack.length) { rightArr[i] = stack[stack.length - 1]; } stack.push(i); } let ans = 0; // 初始化最大矩形面积为0 for (let i = 0; i < n; i++) { // 遍历每个柱形,计算以其高度为高的最大矩形面积,并更新最大面积 ans = Math.max(ans, heights[i] * (rightArr[i] - leftArr[i] - 1)); } return ans; };
单调栈+哨兵机制
考虑到枚举「宽」的方法使用了两重循环,本身就已经需要 O(N ^2) 的时间复杂度,不容易优化,因此我们可以考虑优化只使用了一重循环的枚举「高」的方法。
var largestRectangleArea = function(heights) { let maxArea = 0; const stack = [];// 维护一个单调递增栈 heights = [0,...heights,0]; // 数组头部加入元素0 数组尾部加入元素0 ——为了避免最后队列中还存留一些元素 // 只用考虑情况一 当前遍历的元素heights[i]小于栈顶元素heights[stack[stack.length-1]]]的情况 for(let i = 0; i < heights.length; i++){ while(heights[i] < heights[stack[stack.length-1]]){// 当前bar比栈顶bar矮 const stackTopIndex = stack.pop();// 栈顶元素出栈,并保存栈顶bar的索引 let w = i - stack[stack.length -1] - 1; let h = heights[stackTopIndex] maxArea = Math.max(maxArea, w * h); // 计算面积,并取最大面积 } stack.push(i);// 当前bar比栈顶bar高了,入栈 } return maxArea; };
最大矩形
给定一个仅包含 0 和 1 、大小为 rows x cols 的二维二进制矩阵,找出只包含 1 的最大矩形,并返回其面积。
示例 1: 输入:matrix = [["1","0","1","0","0"],["1","0","1","1","1"],["1","1","1","1","1"],["1","0","0","1","0"]] 输出:6
思路:84题的变种,从第一行到第n行形成的柱状图可以利用84题求解,然后循环每一行,计算以这一行为底的柱状图最大面积,然后更新最大矩形面积
var maximalRectangle = function(matrix) { const m = matrix.length; if (m === 0) { return 0; } const n = matrix[0].length; const left = new Array(m).fill(0).map(() => new Array(n).fill(0)); for (let i = 0; i < m; i++) { for (let j = 0; j < n; j++) { if (matrix[i][j] === '1') { left[i][j] = (j === 0 ? 0 : left[i][j - 1]) + 1; } } } let ret = 0; for (let j = 0; j < n; j++) { // 对于每一列,使用基于柱状图的方法 const up = new Array(m).fill(0); const down = new Array(m).fill(0); let stack = new Array(); for (let i = 0; i < m; i++) { while (stack.length && left[stack[stack.length - 1]][j] >= left[i][j]) { stack.pop(); } up[i] = stack.length === 0 ? -1 : stack[stack.length - 1]; stack.push(i); } stack = []; for (let i = m - 1; i >= 0; i--) { while (stack.length && left[stack[stack.length - 1]][j] >= left[i][j]) { stack.pop(); } down[i] = stack.length === 0 ? m : stack[stack.length - 1]; stack.push(i); } for (let i = 0; i < m; i++) { const height = down[i] - up[i] - 1; const area = height * left[i][j]; ret = Math.max(ret, area); } } return ret; };
其实最简单还是动态规划来做,具体还是看《 岛屿算法(求岛屿数量与岛屿面积)》
参考文章:
用javascript分类刷leetcode13.单调栈(图文视频讲解) https://cloud.tencent.com/developer/article/2205708
[路飞]_321. 拼接最大数 https://juejin.cn/post/7071584649142599693
321. 拼接最大数 #https://howieyuen.github.io/docs/leetcode/0321/
321. 拼接最大数 java贪心题解 https://blog.csdn.net/qq_38409944/article/details/84678140
转载本站文章《单调栈进阶: 去除重复字母/拼接最大数/最大矩形》,
请注明出处:https://www.zhoulujun.cn/html/theory/algorithm/leetcode/9087.html