Algorithmic design and analysis of double-ended monotonic queues (sliding window problem) and monotonic stacks

Posted by padma on Thu, 03 Feb 2022 18:46:27 +0100

Dual-ended monotonic queue (sliding window problem)

  • Basic concepts: windows (on the basis of arrays)
    (1) There is a left boundary for L and a right boundary for R, initially all on the leftmost side of the array
    (2) When moving: L or R can only move to the right
    (3) L must always be on the left side of R
    (4) R moves elements right into the window, L moves elements right out of the window
  • Requirements: At a very low cost (less than traversing a window), the maximum or minimum value of a window is obtained each time the user changes the window
  • Solution: Double-ended queue
    (1) Subscripts of arrays are stored in a double-ended queue (through which elements can be accessed and their position in the array can be determined, making it easy to manipulate in real-time code)
    (2) Assuming maximum, head->tail (maximum->minimum)
    (3) R moves, the element enters the queue from the end of the queue, and judges whether the monotonic relationship between big and small is satisfied before joining the queue. If the monotonic relationship between big and small is satisfied, if not, the <=value of the element is all queued (and there is no need to add it again)
    (4) L move, just see if the head node is currently in the window, if not, queue directly from the head, if in, no operation is required
    (5) Dual-ended queue maintenance information: always provides maximum window value on the opposite side
  • Time Complexity Analysis: A double-ended queue maintains an entire traversed array of O(N), a moving window is O(N)/ N = O(1), and N is the length of the array
  • Code implementation:
    public static class WindowMax{
        //Window Class
        private int L; //Left and right boundary
        private int R;
        private int[] arr; //Original Array
        private LinkedList<Integer> queue; //Double-ended Queue

        public WindowMax(int[] arr){
            this.arr = arr;
            L = -1;
            R = -1;
            queue = new LinkedList<>();
        }

        public void addNum(){
            //R Move
            if (R == arr.length - 1){
                return;
            }
            R++;
            while (!queue.isEmpty() && arr[queue.peekLast()] <= arr[R]){
                //Monotonic queue not satisfied, queue end queued
                queue.pollLast();
            }
            //Entry
            queue.addLast(R);
        }

        public void removeNum(){
            //L Move
            if (L >= R - 1){
                //L always needs to be on the right side of R
                return;
            }
            L++;
            if (queue.peekFirst() == L){
                //Determine whether the opponent needs to leave the team
                queue.pollFirst();
            }
        }

        public Integer getMax(){
            //Get the maximum
            if (!queue.isEmpty()){
                //Double-ended queue is not empty, return directly to the opposite end
                return arr[queue.peekFirst()];
            }
            return null;
        }
    }
  • Example:
    public static int[] getWindowMax(int[] arr, int w){
        if (arr == null || w < 1 || arr.length < w){
            return null;
        }
        LinkedList<Integer> queue = new LinkedList<Integer>(); //Double-ended Queue
        int[] res = new int[arr.length - w + 1];
        int index = 0; //Position in res array
        for (int i = 0; i < arr.length; i++){
            //foreach
            //i is the right boundary
            //i - w is the left boundary
            while (!queue.isEmpty() && arr[queue.pollLast()] <= arr[i]){
                //Queues that do not satisfy the end of a double-ended monotonic queue are maintained when the right boundary is moved
                queue.pollLast();
            }
            queue.addLast(i); //Right Boundary Move
            if (queue.peekFirst() == i - w){
                //Unsatisfied Team Leader Exit
                //Left Boundary Move
                queue.pollFirst();
            }
            if (i >= w - 1){
                //Start collecting maximum values after the window is formed
                res[index++] = arr[queue.pollFirst()];
            }
        }
        return res;
    }

Monotonic stack

  • Description of the problem: On an array, it is required that the nearest left and right sides of each location be larger or smaller than the value of that location.
  • The general idea is to traverse left and right to find a location with O (N ^ 2) time complexity.
  • Goal: Optimize to O(N) time complexity
  • Solution: Monotonic stack
    (1) Subscripts of arrays are stored in the monotonic stack, (through which elements can be accessed and their position in the array can be determined, making it easy to manipulate in real-time code)
    (2) Assuming that the maximum value is calculated, the minimum only needs to be changed to the bottom of the stack - > the top of the stack (maximum - > minimum), and the minimum only needs to be changed to the top of the stack (minimum - > maximum)
    (3) traverse the array to satisfy the direct access stack of the monotonic stack, if not, the unsatisfied elements will pop up in turn
    (4) The maximum information of the pop-up element can be obtained: the left side is larger, and the element immediately below it in the original stack (the current top element of the stack); The larger element on the right that currently stacks the element, if none is none
    (5) After traversing the array, the stack is processed, and all the stack elements are out of the stack. The left side of the stack element is larger, and the elements immediately below the element in the original stack are processed. None of the larger ones on the right
  • Special Processing: If the same element appears, the same element can be pushed into the same space in the stack: Stack<List<Integer> that is, the list will be a space on the stack, and when added, the same element will definitely meet at the top of the stack. List of elements directly on the top of the stack can be added, in which the same element is used to determine the location of the nearest larger value, which is the last one in the list.
  • Time Complexity Analysis: Traverse once to get, time complexity O(N)
  • Code implementation:
    public static int[][] getNearMore_NoRepeat(int[] arr){
        //Get larger values on both sides of each character without repetition
        int[][] res = new int[arr.length][2]; //Save only the corresponding subscript [][0]: left, [][]1:right
        Stack<Integer> stack = new Stack<>(); //Monotonic stack
        for (int i = 0; i < arr.length; i++){
            //foreach
            while (!stack.isEmpty() && arr[stack.peek()] < arr[i]){
                //Conditions for determining whether to leave the stack
                int temp = stack.pop();
                //The left side is larger, if the stack is empty: none, return-1; If you return to the current top of the stack
                int leftMoreIndex = stack.isEmpty() ? -1 : stack.peek();
                res[temp][0] = leftMoreIndex;
                res[temp][1] = i;
            }
            stack.push(i);
        }
        while (!stack.isEmpty()){
            //End of array traversal, stack handling
            int temp = stack.pop();
            int leftMoreIndex = stack.isEmpty() ? -1 : stack.peek();
            res[temp][0] = leftMoreIndex;
            //None on the right, return -1
            res[temp][1] = -1;
        }
        return res;
    }
    public static int[][] getNearMore(int[] arr){
        //Get larger values on both sides of each character with repetition
        int[][] res = new int[arr.length][2]; //Save only the corresponding subscript [][0]: left, [][]1:right
        Stack<List<Integer>> stack = new Stack<>(); //Monotonic stack
        for (int i = 0; i < arr.length; i++){
            //foreach
            while (!stack.isEmpty() && arr[stack.peek().get(0)] < arr[i]){
                //Conditions for determining whether to leave the stack
                List<Integer> tempList = stack.pop();
                //The left side is larger, if the stack is empty: none, return-1; If there is one last queue returning to the current top of the stack
                int leftMoreIndex = stack.isEmpty() ? -1 : stack.peek().get(stack.peek().size() - 1);
                for (Integer temp : tempList){
                    res[temp][0] = leftMoreIndex;
                    res[temp][1] = i;
                }
            }
            if (!stack.isEmpty() && arr[stack.peek().get(0)] == arr[i]) {
                //Same elements in the same queue on the stack
                stack.peek().add(i);
            } else {
                ArrayList<Integer> list = new ArrayList<>();
                list.add(i);
                stack.push(list);
            }
        }
        while (!stack.isEmpty()){
            //End of array traversal, stack handling
            List<Integer> tempList = stack.pop();
            int leftMoreIndex = stack.isEmpty() ? -1 : stack.peek().get(stack.peek().size() - 1);
            for (Integer temp : tempList){
                res[temp][0] = leftMoreIndex;
                res[temp][1] = -1;
            }
        }
        return res;
    }
    public static int[][] getNearLess_NoRepeat(int[] arr){
        //Get smaller values on both sides of each character without duplication
        int[][] res = new int[arr.length][2]; //Save only the corresponding subscript [][0]: left, [][]1:right
        Stack<Integer> stack = new Stack<>(); //Monotonic stack
        for (int i = 0; i < arr.length; i++){
            //foreach
            while (!stack.isEmpty() && arr[stack.peek()] > arr[i]){
                //Conditions for determining whether to leave the stack
                int temp = stack.pop();
                //The left side is larger, if the stack is empty: none, return-1; If you return to the current top of the stack
                int leftMoreIndex = stack.isEmpty() ? -1 : stack.peek();
                res[temp][0] = leftMoreIndex;
                res[temp][1] = i;
            }
            stack.push(i);
        }
        while (!stack.isEmpty()){
            //End of array traversal, stack handling
            int temp = stack.pop();
            int leftMoreIndex = stack.isEmpty() ? -1 : stack.peek();
            res[temp][0] = leftMoreIndex;
            //None on the right, return -1
            res[temp][1] = -1;
        }
        return res;
    }
    public static int[][] getNearLess_NoRepeat(int[] arr){
        //Get smaller values on both sides of each character without duplication
        int[][] res = new int[arr.length][2]; //Save only the corresponding subscript [][0]: left, [][]1:right
        Stack<Integer> stack = new Stack<>(); //Monotonic stack
        for (int i = 0; i < arr.length; i++){
            //foreach
            while (!stack.isEmpty() && arr[stack.peek()] > arr[i]){
                //Conditions for determining whether to leave the stack
                int temp = stack.pop();
                //The left side is larger, if the stack is empty: none, return-1; If you return to the current top of the stack
                int leftMoreIndex = stack.isEmpty() ? -1 : stack.peek();
                res[temp][0] = leftMoreIndex;
                res[temp][1] = i;
            }
            stack.push(i);
        }
        while (!stack.isEmpty()){
            //End of array traversal, stack handling
            int temp = stack.pop();
            int leftMoreIndex = stack.isEmpty() ? -1 : stack.peek();
            res[temp][0] = leftMoreIndex;
            //None on the right, return -1
            res[temp][1] = -1;
        }
        return res;
    }

    public static int[][] getNearLess(int[] arr){
        //Get smaller values on both sides of each character with repetition
        int[][] res = new int[arr.length][2]; //Save only the corresponding subscript [][0]: left, [][]1:right
        Stack<List<Integer>> stack = new Stack<>(); //Monotonic stack
        for (int i = 0; i < arr.length; i++){
            //foreach
            while (!stack.isEmpty() && arr[stack.peek().get(0)] > arr[i]){
                //Conditions for determining whether to leave the stack
                List<Integer> tempList = stack.pop();
                //The left side is larger, if the stack is empty: none, return-1; If there is one last queue returning to the current top of the stack
                int leftMoreIndex = stack.isEmpty() ? -1 : stack.peek().get(stack.peek().size() - 1);
                for (Integer temp : tempList){
                    res[temp][0] = leftMoreIndex;
                    res[temp][1] = i;
                }
            }
            if (!stack.isEmpty() && arr[stack.peek().get(0)] == arr[i]) {
                //Same elements in the same queue on the stack
                stack.peek().add(i);
            } else {
                ArrayList<Integer> list = new ArrayList<>();
                list.add(i);
                stack.push(list);
            }
        }
        while (!stack.isEmpty()){
            //End of array traversal, stack handling
            List<Integer> tempList = stack.pop();
            int leftMoreIndex = stack.isEmpty() ? -1 : stack.peek().get(stack.peek().size() - 1);
            for (Integer temp : tempList){
                res[temp][0] = leftMoreIndex;
                res[temp][1] = -1;
            }
        }
        return res;
    }
  • Example:

    Example: [5, 3, 2, 4, 6, 1, 7, 9, 8]
    If the array determined by choosing 5 is [5]
    If select 2 to determine the array as [2], [3, 2], [5, 3, 2], [2, 4], [2, 4, 6]... [5, 3, 2, 4, 6]
    If select 1 to determine the array as [1], [6,1], [4,6,1]... [5, 3, 2, 4, 6, 1, 7, 9, 8]
  • Difficulty point: too many subarrays using the general idea (enumeration) and very high time complexity
  • Analysis: This topic seems to be unrelated to monotonic stack, in fact: each number as a minimum, its corresponding subarray is the longest time to find A. In fact, you can use the method of monotonic stack to find the minimum on both sides, to determine each number as the minimum value, quickly determine its corresponding longest subarray
  • General idea: enumeration (violent solution), traverse all subarrays, get the sum of all subarrays, minimum value, find the corresponding A
    public static int getMaxA(int[] arr){
        int maxA = Integer.MIN_VALUE;
        for (int i = 0; i < arr.length; i++){
            for (int j = i; j < arr.length; j++){
                //Traverse through all subarrays [i...j]
                int minNum = Integer.MAX_VALUE; //Minimum value of subarray
                int sum = 0; //Sum of Subarrays
                for (int index = i; index <= j; index++){
                    //Minimum sum in subarray
                    sum += arr[index];
                    minNum = Math.min(minNum, arr[index]);
                }
                maxA = Math.max(maxA, sum * minNum);
            }
        }
        return maxA;
    }

Time Complexity: O(N^3)

  • The idea of using monotonic stack: optimize on the basis of the general idea, find the smallest on both sides, determine the longest subarray corresponding to each number as the minimum value, and save a lot of time to find the subarray
    (1) First find the sum from the leftmost to the right, and save it in sums[]
    (2) Build monotonic stack, bottom of stack - > top of stack (minimum - > maximum)
    (3) Direct stacking when monotonic stack conditions are met
    (4) If the stack is not satisfied, the A value of the stack element can be directly calculated at this time. The minimum value is: the current stack element corresponds to the element in the array, sum: Make the element out of the stack smaller than it can provide R, and its element on the top of the current stack can also provide L compared to the small element; If the stack is empty, L is the leftmost end of the array
    (5) When the array is traversed and the stack is not empty, then R is the rightmost end of the array, and L is still searched by the top of the stack method
    (6) Tip: Sums[] Array exists, sum[L, R] = sums [R] - sums [L], no need to traverse subarrays to find sum
    public static int getMaxA(int[] arr){
        int size = arr.length;
        int[] sums = new int[size];
        sums[0] = arr[0];
        for (int i = 1; i < size; i++) {
            //Record the sum for each location from left to right
            sums[i] = sums[i - 1] + arr[i];
        }
        int max = Integer.MIN_VALUE;
        Stack<Integer> stack = new Stack<Integer>(); //Monotonic stack
        for (int i = 0; i < size; i++) {
            //foreach
            while (!stack.isEmpty() && arr[stack.peek()] >= arr[i]) {
                //Satisfies the stack condition, this element is the minimum value, find its corresponding longest subarray
                int minNumIndex = stack.pop();
                //sum[L,R] = sums[R] - sum[L]
                //Stack empty, L = 0
                int sum = stack.isEmpty() ? sums[i - 1] : (sums[i - 1] - sums[stack.peek()]);
                max = Math.max(max, sum * minNumIndex);
            }
            stack.push(i);
        }
        while (!stack.isEmpty()) {
            //End of array traversal, remaining in processing stack
            int minNumIndex = stack.pop();
            //R is the rightmost end, R = size - 1
            int sum = stack.isEmpty() ? sums[size - 1] : (sums[size - 1] - sums[stack.peek()]);
            max = Math.max(max, sum * minNumIndex);
        }
        return max;
    }

Time Complexity O(N)

summary

  • Dual-Ended Monotonic Queue: Can be used to find the maximum value of a sliding window
  • Monotonic stack: The longest subarray of each number in an array that can be used to find the minimum (the nearest left and right sides to a location are smaller than the value at that location) or the maximum (the nearest left and right sides to a location are larger than the value at that location).

Topics: Java Algorithm