Увеличьте прямоугольную область под гистограммой

У меня есть гистограмма с целыми высотами и постоянной шириной 1. Я хочу максимизировать прямоугольную область под гистограммой. например:.

 _
| |
| |_ 
|   |
|   |_
|     |

Ответ для этого будет 6, 3 * 2, используя col1 и col2.

O (n ^ 2) грубая сила мне понятна, мне нужен алгоритм O (n log n). Я пытаюсь думать о динамическом программировании вдоль линий максимальной возрастающей подпоследовательности O (n log n) algo, но не буду дальше. Должен ли я использовать алгоритм разделения и покорения?

PS: людям с достаточной репутацией требуется удалить тег divide-and-завоевание, если такого решения нет.

После комментариев mho: Я имею в виду область наибольшего прямоугольника, которая полностью подходит. (Спасибо j_random_hacker за разъяснение:)).

Ответ 1

Существует три способа решения этой проблемы в дополнение к подходу грубой силы. Я запишу их все. Коды java прошли тесты на онлайн-сайте судьи под названием leetcode: http://www.leetcode.com/onlinejudge#question_84. поэтому я уверен, что коды верны.

Решение 1: динамическое программирование + n * n матрица как кеш

время: O (n ^ 2), пространство: O (n ^ 2)

Основная идея: используйте n * n matrix dp [i] [j] для кеширования минимальной высоты между баром [i] и bar [j]. Начните заполнять матрицу из прямоугольников шириной 1.

public int solution1(int[] height) {

    int n = height.length;
    if(n == 0) return 0;
    int[][] dp = new int[n][n];        
    int max = Integer.MIN_VALUE;

    for(int width = 1; width <= n; width++){

        for(int l = 0; l+width-1 < n; l++){

            int r = l + width - 1;

            if(width == 1){
                dp[l][l] = height[l];
                max = Math.max(max, dp[l][l]);
            } else {                    
                dp[l][r] = Math.min(dp[l][r-1], height[r]);
                max = Math.max(max, dp[l][r] * width);
            }                
        }
    }

    return max;
}

Решение 2: динамическое программирование + 2 массива в кеше.

время: O (n ^ 2), пробел: O (n)

Основная идея: это решение похоже на решение 1, но экономит пространство. Идея состоит в том, что в решении 1 мы строим матрицу из строки 1 в строку n. Но в каждой итерации только предыдущая строка способствует построению текущей строки. Таким образом, мы используем два массива в качестве предыдущей строки и текущей строки по очереди.

public int Solution2(int[] height) {

    int n = height.length;
    if(n == 0) return 0;

    int max = Integer.MIN_VALUE;

    // dp[0] and dp[1] take turns to be the "previous" line.
    int[][] dp = new int[2][n];      

    for(int width = 1; width <= n; width++){

        for(int l = 0; l+width-1 < n; l++){

            if(width == 1){
                dp[width%2][l] = height[l];
            } else {
                dp[width%2][l] = Math.min(dp[1-width%2][l], height[l+width-1]);                     
            }
            max = Math.max(max, dp[width%2][l] * width);   
        }
    }        
    return max;
}

Решение 3: использовать стек.

время: O (n), пробел: O (n)

Это решение сложно, и я узнал, как это сделать из объяснение без графиков и с графиками. Я предлагаю вам прочитать две ссылки, прежде чем читать мои объяснения ниже. Трудно объяснить без графов, поэтому мои объяснения могут быть трудными.

Ниже приводятся мои объяснения:

  • Для каждого бара мы должны иметь возможность найти самый большой прямоугольник, содержащий этот бар. Таким образом, самый большой из этих n прямоугольников - это то, что мы хотим.

  • Чтобы получить самый большой прямоугольник для определенного бара (скажем, bar [i], (i + 1) -й бар), нам просто нужно узнать самый большой интервал который содержит этот бар. Мы знаем, что все полосы в этом интервале должны быть по крайней мере одинаковой высоты с баром [i]. Поэтому, если мы выясним, сколько последовательные столбцы с одинаковой высотой или выше находятся в непосредственной близости от бара [i], и сколько последовательных строк с одинаковой высотой или более высоким значением находится непосредственно справа от панели [i], мы будет знать длину интервала, который является шириной самого большого прямоугольника для bar [i].

  • Чтобы подсчитать количество последовательных столбцов с одинаковой высотой или выше в непосредственной близости от бара [i], нам нужно всего лишь найти ближайшую полосу слева, которая короче чем панель [i], так как все полоски между этой полосой и баром [i] будут последовательными столбцами с одинаковой высотой или более высоким уровнем.

  • Мы используем стек, чтобы динамически отслеживать все левые столбцы, которые короче определенного бара. Другими словами, если мы итерации с первого бара на бар [i], когда мы просто доходим до бара [i] и не обновляем стек, стек должен хранить все полосы, которые не превышают бар [i-1], включая сам бар [i-1]. Мы сравниваем высоту bar [i] с каждым баром в стеке, пока не найдем ту, которая короче, чем bar [i], которая является самым коротким коротким баром. Если полоса [i] выше, чем все столбцы в стеке, это означает, что все столбцы слева от бара [i] выше бар [i].

  • Мы можем сделать то же самое с правой стороны i-го бара. Тогда для bar [i] мы знаем, сколько баров находится в интервале.

    public int solution3(int[] height) {
    
        int n = height.length;
        if(n == 0) return 0;
    
        Stack<Integer> left = new Stack<Integer>();
        Stack<Integer> right = new Stack<Integer>();
    
        int[] width = new int[n];// widths of intervals.
        Arrays.fill(width, 1);// all intervals should at least be 1 unit wide.
    
        for(int i = 0; i < n; i++){
            // count # of consecutive higher bars on the left of the (i+1)th bar
            while(!left.isEmpty() && height[i] <= height[left.peek()]){
                // while there are bars stored in the stack, we check the bar on the top of the stack.
                left.pop();                
            }
    
            if(left.isEmpty()){
                // all elements on the left are larger than height[i].
                width[i] += i;
            } else {
                // bar[left.peek()] is the closest shorter bar.
                width[i] += i - left.peek() - 1;
            }
            left.push(i);
        }
    
        for (int i = n-1; i >=0; i--) {
    
            while(!right.isEmpty() && height[i] <= height[right.peek()]){                
                right.pop();                
            }
    
            if(right.isEmpty()){
                // all elements to the right are larger than height[i]
                width[i] += n - 1 - i;
            } else {
                width[i] += right.peek() - i - 1;
            }
            right.push(i);
        }
    
        int max = Integer.MIN_VALUE;
        for(int i = 0; i < n; i++){
            // find the maximum value of all rectangle areas.
            max = Math.max(max, width[i] * height[i]);
        }
    
        return max;
    }
    

Ответ 2

Вышеупомянутые ответы дали лучшее решение O (n) в коде, однако их объяснения довольно сложно понять. Алгоритм O (n), использующий стек, сначала казался мне волшебным, но сейчас это имеет для меня все смысл. Хорошо, позвольте мне объяснить это.

Первое наблюдение:

Чтобы найти максимальный прямоугольник, если для каждого бара x мы знаем первую меньшую полосу на каждой стороне, скажем l и r, мы уверены, что height[x] * (r - l - 1) - лучший снимок можно получить с помощью высоты бара x. На рисунке ниже 1 и 2 являются первыми меньшими из 5.

ОК, допустим, что мы можем сделать это в O (1) раз для каждого бара, тогда мы можем решить эту проблему в O (n)! путем сканирования каждого столбца.

введите описание изображения здесь

Затем возникает вопрос: для каждого бара мы можем действительно найти первую меньшую панель слева и справа от нее в O (1) раз? Это кажется невозможным?... Возможно, используя увеличивающийся стек.

Зачем использовать увеличивающийся стек, чтобы отслеживать первые меньше слева и справа?

Возможно, рассказывая вам, что увеличение стека может выполнить эту работу, не является убедительным вообще, поэтому я проведу вас через это.

Во-первых, чтобы увеличить стек, нам нужна одна операция:

while x < stack.top():
    stack.pop()
stack.push(x)

Затем вы можете проверить, что в увеличивающемся стеке (как показано ниже) для stack[x], stack[x-1] является первым меньшим слева от него, тогда новый элемент, который может выскочить stack[x], является первым меньшим по его право.

введите описание изображения здесь

Все еще не могу поверить, что стек [x-1] является первым меньшим слева в стеке [x]?

Я докажу это противоречием.

Прежде всего, stack[x-1] < stack[x] - это точно. Но пусть предположим, что stack[x-1] не является первым меньшим слева от stack[x].

Итак, где первый меньше fs?

If fs < stack[x-1]:
    stack[x-1] will be popped out by fs,
else fs >= stack[x-1]:
    fs shall be pushed into stack,
Either case will result fs lie between stack[x-1] and stack[x], which is contradicting to the fact that there is no item between stack[x-1] and stack[x].

Поэтому стек [x-1] должен быть первым меньшим.

Резюме:

Увеличение стека может отслеживать первое меньшее по левому и правому значению для каждого элемента. Используя это свойство, максимальный прямоугольник в гистограмме можно решить, используя стек в O (n).

Поздравляем! Это действительно сложная проблема, я рад, что мое прозаическое объяснение не помешало вам закончить. Прикреплено мое доказанное решение как ваша награда:)

def largestRectangleArea(A):
    ans = 0
    A = [-1] + A
    A.append(-1)
    n = len(A)
    stack = [0]  # store index

    for i in range(n):
        while A[i] < A[stack[-1]]:
            h = A[stack.pop()]
            area = h*(i-stack[-1]-1)
            ans = max(ans, area)
        stack.append(i)
    return ans

Ответ 3

Реализация на Python ответ @IVlad Решение O (n):

from collections import namedtuple

Info = namedtuple('Info', 'start height')

def max_rectangle_area(histogram):
    """Find the area of the largest rectangle that fits entirely under
    the histogram.

    """
    stack = []
    top = lambda: stack[-1]
    max_area = 0
    pos = 0 # current position in the histogram
    for pos, height in enumerate(histogram):
        start = pos # position where rectangle starts
        while True:
            if not stack or height > top().height:
                stack.append(Info(start, height)) # push
            elif stack and height < top().height:
                max_area = max(max_area, top().height*(pos-top().start))
                start, _ = stack.pop()
                continue
            break # height == top().height goes here

    pos += 1
    for start, height in stack:
        max_area = max(max_area, height*(pos-start))

    return max_area

Пример:

>>> f = max_rectangle_area
>>> f([5,3,1])
6
>>> f([1,3,5])
6
>>> f([3,1,5])
5
>>> f([4,8,3,2,0])
9
>>> f([4,8,3,1,1,0])
9

Линейный поиск с использованием стека неполных подзадач

Описание алгоритма копирования-вставки (в случае, если страница опускается):

Мы обрабатываем элементы в слева направо и поддерживать стек информации о запуске, но но недостроенные субгистограммы. Всякий раз, когда приходит новый элемент, он подвергается к следующим правилам. Если стек пусто, мы открываем новую подзадачу посредством нажав элемент на стек. В противном случае мы сравним его с элементом поверх стека. Если новый больше мы снова подталкиваем его. Если новый один равен, мы пропустим его. Во всех этих случаев, мы продолжим следующий элемент. Если новый меньше, мы завершите верхнюю подзадачу обновление максимальной площади w.r.t. элемент в верхней части стека. Затем, мы отбрасываем элемент сверху, и повторите процедуру, поддерживающую текущий новый элемент. Таким образом, все подзадачи завершаются до тех пор, пока стек становится пустым или его вершина элемент меньше или равен новый элемент, приводящий к действиям описано выше. Если все элементы имеют обрабатывается, а стек не но пустые, мы заканчиваем оставшиеся подзадач путем обновления максимального значения площадь w.r.t. к элементам на сверху.

Для обновления w.r.t. элемент, мы найти самый большой прямоугольник, который включает этот элемент. Заметим, что обновляется максимальная площадь для всех элементов, за исключением тех пропускаются. Если элемент пропускается, однако он имеет тот же самый самый большой прямоугольник как элемент поверх стек в это время, который будет обновлено позже. Высота наибольший прямоугольник - это, конечно, значение элемента. В момент обновление, мы знаем, как далеко наибольший прямоугольник простирается вправо элемента, потому что тогда для первый раз, новый элемент с меньшим высота прибывший. Информация, как далеко самый большой прямоугольник распространяется на слева от элемента, имеется если мы сохраним его в стеке.

Поэтому мы пересматриваем процедуру описано выше. Если новый элемент немедленно, либо потому, что стек пуст или он больше, чем верхний элемент стека, наибольший прямоугольник, содержащий его простирается налево не дальше, чем текущий элемент. Если он толкается после того, как несколько элементов были выскочил со стека, потому что он меньше этих элементов, самый большой прямоугольник, содержащий его, распространяется на осталось до самого недавно появившийся элемент.

Каждый элемент выталкивается и наиболее раз и на каждом шагу процедура, по крайней мере, одного элемента толкнул или выскочил. Поскольку количество работать над решениями и обновлять является постоянным, сложность алгоритмом является O (n) путем амортизации анализ.

Ответ 4

Самое простое решение в O (N)

long long getMaxArea(long long hist[], long long n)
{

    stack<long long> s;

    long long max_area = 0; 
    long long tp;  
    long long area_with_top; 

    long long i = 0;
    while (i < n)
    {
        if (s.empty() || hist[s.top()] <= hist[i])
            s.push(i++);
       else
        {
            tp = s.top();  // store the top index
            s.pop();  // pop the top
            area_with_top = hist[tp] * (s.empty() ? i : i - s.top() - 1);
            if (max_area < area_with_top)
            {
                max_area = area_with_top;
            }
        }
    }

   while (!s.empty())
    {
        tp = s.top();
        s.pop();
        area_with_top = hist[tp] * (s.empty() ? i : i - s.top() - 1);

        if (max_area < area_with_top)
            max_area = area_with_top;
    }

    return max_area;
}

Ответ 5

Существует также другое решение, использующее Divide и Conquer. Алгоритм для этого:

1) Разделите массив на 2 части с наименьшей высотой в качестве точки прерывания

2) Максимальная площадь - это максимум: a) Наименьшая высота * размера массива b) Максимальный прямоугольник в левой половине массива c) Максимальный прямоугольник в правой половине массива

Сложность времени приходит к O (nlogn)

Ответ 6

Я не понимаю другие записи, но я думаю, что знаю, как это сделать в O (n) следующим образом.

A) для каждого индекса найдите наибольший прямоугольник внутри гистограммы, заканчивающийся на том индексе, где столбец индекса касается верхней части прямоугольника и запоминает, где начинается прямоугольник. Это можно сделать в O (n) с использованием алгоритма на основе стека.

B) Аналогично для каждого индекса найдите самый большой прямоугольник, начинающийся с этого индекса, где столбец индекса касается верхней части прямоугольника и запоминает, где заканчивается прямоугольник. Также O (n) использует тот же метод, что и (A), но сканирует гистограмму назад.

C) Для каждого индекса объединяйте результаты (A) и (B), чтобы определить самый большой прямоугольник, где столбец с этим индексом касается верхней части прямоугольника. O (n), как (A).

D) Поскольку наибольший прямоугольник должен быть затронут некоторым столбцом гистограммы, наибольший прямоугольник является наибольшим прямоугольником, найденным на этапе (C).

Жесткая часть реализует (A) и (B), что, по моему мнению, может решить JF Себастьян, а не общая проблема.

Ответ 7

Я закодировал это и чувствовал себя немного лучше в том смысле:

import java.util.Stack;

     class StackItem{
       public int sup;
       public int height;
       public int sub;

       public StackItem(int a, int b, int c){
           sup = a;
           height = b;
           sub =c;
       }
       public int getArea(){
           return (sup - sub)* height;
       }


       @Override
       public String toString(){
       return "     from:"+sup+
              "     to:"+sub+
              "     height:"+height+              
              "     Area ="+getArea();
       }
    }   


public class MaxRectangleInHistogram {    
    Stack<StackItem> S;
    StackItem curr;
    StackItem maxRectangle;

    public StackItem getMaxRectangleInHistogram(int A[], int n){
        int i = 0;
        S = new Stack();        
        S.push(new StackItem(0,0,-1));
        maxRectangle = new StackItem(0,0,-1);

        while(i<n){

                curr = new StackItem(i,A[i],i);

                    if(curr.height > S.peek().height){
                            S.push(curr); 
                    }else if(curr.height == S.peek().height){                            
                            S.peek().sup = i+1;                         
                    }else if(curr.height < S.peek().height){                            

                            while((S.size()>1) && (curr.height<=S.peek().height)){
                                curr.sub = S.peek().sub;
                                S.peek().sup = i;
                                decideMaxRectangle(S.peek());
                                S.pop(); 
                            }                               
                        S.push(curr);                    
                    }
            i++;
        }

        while(S.size()>1){ 
            S.peek().sup = i;
            decideMaxRectangle(S.peek());
            S.pop();            
        }  

        return maxRectangle;
    }

    private void decideMaxRectangle(StackItem s){ 

        if(s.getArea() > maxRectangle.getArea() )
            maxRectangle = s;      
    }

}

Просто примечание:

Time Complexity: T(n) < O(2n) ~ O(n)
Space Complexity S(n) < O(n)

Ответ 8

Решение стека является одним из самых умных решений, которые я видел до настоящего времени. И может быть немного сложно понять, почему это работает.

Я попытался пояснить то же самое в деталях здесь.

Сводные пункты из сообщения: -

  • Общий способ, который думает наш мозг, - это:
    • Создайте каждую ситуацию и попытайтесь найти значение искажения, которое необходимо для решения проблемы.
    • И мы с радостью преобразуем его в код: - найдите значение contraint (min) для каждой ситуации (пара (i, j))

Умные решения пытаются перевернуть проблему. Для каждого значения constraint/min этой области, какие наилучшие возможные крайности слева и справа?

  • Итак, если мы пересекаем все возможные min в массиве. Каковы левые и правые крайности для каждого значения?

    • Маленькая мысль говорит, что первое значение слева меньше, чем current min и аналогично первое самое правое значение, которое меньше текущего min.
  • Итак, теперь нам нужно выяснить, можем ли мы найти умный способ найти первые левые и правые значения, меньшие, чем текущее значение.

  • Думать: если мы пройдем, массив частично скажет до min_i, как можно построить решение для min_i + 1?

  • Нам нужно, чтобы первое значение было меньше min_i слева.

  • Инвертирование оператора: нам нужно игнорировать все значения слева от min_i, которые больше min_i. Мы остановимся, когда найдем первое значение, меньшее min_i (i). Таким образом, прогибы на кривой становятся бесполезными, как только мы пересекли его. В гистограмме (2 4 3) = > , если 3 является min_i, 4 больше не представляет интереса.
  • Коррелятор: в диапазоне (i, j). j - значение min, которое мы рассматриваем.. все значения между j и его левым значением я бесполезны. Даже для дальнейших расчетов.
  • Любая гистограмма справа с минимальным значением, большим j, будет привязана к j. Значения, представляющие интерес слева, образуют монотонно возрастающую последовательность с наибольшим значением j. (Ценности представляют здесь возможные значения, которые могут представлять интерес для более позднего массива)
  • Так как мы движемся слева направо, для каждого значения min/current - мы не знаем, будет ли в правой части массива меньше элемента.
    • Поэтому мы должны хранить его в памяти, пока мы не узнаем, что это значение бесполезно. (поскольку найдено меньшее значение)
  • Все это приводит к использованию нашей собственной структуры stack.

    • Мы держимся за стек, пока мы не узнаем его бесполезность.
    • Мы удаляем из стека, как только мы знаем, что это дерьмо.
  • Итак, для каждого значения min, чтобы найти его меньшее меньшее значение, мы делаем следующее: -

    • поместите элементы, большие к нему (бесполезные значения)
    • Первым элементом, меньшим значения, является крайняя левая. я к нашему минимуму.
  • Мы можем сделать то же самое с правой стороны массива, и мы получим j к нашему минимуму.

Сложно это объяснить, но если это имеет смысл, я бы предложил прочитать здесь полную статью , поскольку она имеет больше идеи и подробности.

Ответ 9

Вы можете использовать метод O (n), который использует стек для вычисления максимальной площади под гистограммой.

long long histogramArea(vector<int> &histo){
   stack<int> s;
   long long maxArea=0;
   long long area= 0;
   int i =0;
   for (i = 0; i < histo.size();) {
    if(s.empty() || histo[s.top()] <= histo[i]){
        s.push(i++);
    }
    else{
        int top = s.top(); s.pop();
        area= histo[top]* (s.empty()?i:i-s.top()-1);
        if(area >maxArea)
            maxArea= area;
    }
  }
  while(!s.empty()){
    int top = s.top();s.pop();
    area= histo[top]* (s.empty()?i:i-s.top()-1);
    if(area >maxArea)
        maxArea= area;
 }
 return maxArea;
}

Для объяснения вы можете прочитать здесь http://www.geeksforgeeks.org/largest-rectangle-under-histogram/