• home > theory > algorithm > leetcode >

    单调栈进阶: 去除重复字母/拼接最大数/最大矩形

    Author:zhoulujun Date:

    《单调栈模板:下一个更大元素 每日温度 移掉K位数字 接雨水》讲解的单调栈的原理与套路模板,本篇是单调leetcode困难题

    上一篇 单调栈模板:下一个更大元素/每日温度/移掉K位数字/接雨水,本篇再来几题

    去除重复字母

    给你一个字符串 s ,请你去除字符串中重复的字母,使得每个字母只出现一次。需保证 返回结果的字典序最小(要求不能打乱其他字符的相对位置)。

    示例 1:输入:s = "bcabc"          输出:"abc"

    示例 2:输入:s = "cbacdcbc"     输出:"acdb"


    这个首先想到是 哈希表去重,但是此题目的三个要求:去重不能打乱其他字符顺序字典序最小

    1. 去重:可以通过 「使用哈希表存储字母出现次数」 的方式,将每个字母出现的次数统计起来,再遍历一遍,去除重复的字母。

    2. 不能打乱其他字符顺序:按顺序遍历,将非重复的字母存储到答案数组或者栈中,最后再拼接起来,就能保证不打乱其他字符顺序。

    3. 字典序最小:意味着字典序小的字母应该尽可能放在前面。

      1. 对于第 i 个字符 s[i] 而言,如果第 0 ~ i - 1 之间的某个字符 s[j] 在 s[i] 之后不再出现了,那么 s[j] 必须放到 s[i] 之前。

      2. 而如果 s[j] 在之后还会出现,并且 s[j] 的字典序大于 s[i],我们则可以先舍弃 s[j],把 s[i] 尽可能的放到前面。后边再考虑使用 s[j] 所对应的字符。


    这个可以参考 《单调栈模板:下一个更大元素/每日温度/移掉K位数字/接雨水》中的 移掉K位数字,只是这道题没有一个全局的删除次数 k。而是对于每一个在字符串 str 中出现的字母 letter都有一个 k 值。这个 k 是 字母letter 出现次数 - 1。

    核心都是需要如何判断相同长度的字符串哪个的字典序更大

    与402.移掉K位数字 题目不同的是,这里需要满足同一种字符都需要出现,且只能出现一次。那怎么才能满足这些条件呢?

    因此,需要额外考虑以下两个问题:

    1. 在考虑字符 s[i] 时,如果它已经存在于栈中,则不能加入字符 s[i]到单调栈中。为此,需要记录每个字符是否出现在栈中。

    2. 在弹出栈顶字符时,如果字符串在后面的位置上再也没有这一字符,则不能弹出栈顶字符。为此,需要记录每个字符的剩余数量,当这个值为 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

    leetcode 最大矩形

    思路:84题的变种,从第一行到第n行形成的柱状图可以利用84题求解,然后循环每一行,计算以这一行为底的柱状图最大面积,然后更新最大矩形面积

    具体参考官方题解:https://leetcode.cn/problems/maximal-rectangle/solutions/535672/zui-da-ju-xing-by-leetcode-solution-bjlu/

    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