Головоломка: Найдите самый большой прямоугольник (задача о максимальном прямоугольнике)

Какой самый эффективный алгоритм найти прямоугольник с наибольшей площадью, который поместится в пустом пространстве?

Скажем, экран выглядит так (символ # обозначает заполненную область):

....................
..............######
##..................
.................###
.................###
#####...............
#####...............
#####...............

Возможное решение:

....................
..............######
##...++++++++++++...
.....++++++++++++###
.....++++++++++++###
#####++++++++++++...
#####++++++++++++...
#####++++++++++++...

Обычно мне нравится находить решение. Хотя на этот раз я бы не стал тратить время на поиски в одиночестве, поскольку это имеет практическое применение для проекта, над которым я работаю. Есть известное решение?

Shog9 написал:

Является ли ваш ввод массивом (как подразумевается другими ответами) или списком окклюзий в форме прямоугольников произвольного размера (как это может иметь место в оконной системе при работе с позициями окон)?

Да, у меня есть структура, которая отслеживает набор окон, размещенных на экране. У меня также есть сетка, которая отслеживает все области между каждым краем, являются ли они пустыми или заполненными, а также положение в пикселях их левого или верхнего края. Я думаю, что есть какая-то измененная форма, которая могла бы использовать это свойство. Вы что-нибудь знаете?

Ответов (7)

Решение

@lassevk

Я нашел указанную статью из DDJ: The Maximal Rectangle Problem

Я автор статьи доктора Добба, и меня иногда спрашивают о реализации. Вот простой на C:

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>

typedef struct {
  int one;
  int two;
} Pair;

Pair best_ll = { 0, 0 };
Pair best_ur = { -1, -1 };
int best_area = 0;

int *c; /* Cache */
Pair *s; /* Stack */
int top = 0; /* Top of stack */

void push(int a, int b) {
  s[top].one = a;
  s[top].two = b;
  ++top;
}

void pop(int *a, int *b) {
  --top;
  *a = s[top].one;
  *b = s[top].two;
}


int M, N; /* Dimension of input; M is length of a row. */

void update_cache() {
  int m;
  char b;
  for (m = 0; m!=M; ++m) {
    scanf(" %c", &b);
    fprintf(stderr, " %c", b);
    if (b=='0') {
      c[m] = 0;
    } else { ++c[m]; }
  }
  fprintf(stderr, "\n");
}


int main() {
  int m, n;
  scanf("%d %d", &M, &N);
  fprintf(stderr, "Reading %dx%d array (1 row == %d elements)\n", M, N, M);
  c = (int*)malloc((M+1)*sizeof(int));
  s = (Pair*)malloc((M+1)*sizeof(Pair));
  for (m = 0; m!=M+1; ++m) { c[m] = s[m].one = s[m].two = 0; }
  /* Main algorithm: */
  for (n = 0; n!=N; ++n) {
    int open_width = 0;
    update_cache();
    for (m = 0; m!=M+1; ++m) {
      if (c[m]>open_width) { /* Open new rectangle? */
        push(m, open_width);
        open_width = c[m];
      } else /* "else" optional here */
      if (c[m]<open_width) { /* Close rectangle(s)? */
        int m0, w0, area;
        do {
          pop(&m0, &w0);
          area = open_width*(m-m0);
          if (area>best_area) {
            best_area = area;
            best_ll.one = m0; best_ll.two = n;
            best_ur.one = m-1; best_ur.two = n-open_width+1;
          }
          open_width = w0;
        } while (c[m]<open_width);
        open_width = c[m];
        if (open_width!=0) {
          push(m0, w0);
        }
      }
    }
  }
  fprintf(stderr, "The maximal rectangle has area %d.\n", best_area);
  fprintf(stderr, "Location: [col=%d, row=%d] to [col=%d, row=%d]\n",
                  best_ll.one+1, best_ll.two+1, best_ur.one+1, best_ur.two+1);
  return 0;
}

Он принимает данные с консоли. Вы можете, например, передать ему этот файл:

16 12
0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 1 1 0 0 0 0 0 0 0 0 1 0 0 0
0 0 0 1 1 0 0 1 0 0 0 1 1 0 1 0
0 0 0 1 1 0 1 1 1 0 1 1 1 0 1 0
0 0 0 0 1 1 * * * * * * 0 0 1 0
0 0 0 0 0 0 * * * * * * 0 0 1 0
0 0 0 0 0 0 1 1 0 1 1 1 1 1 1 0
0 0 1 0 0 0 0 1 0 0 1 1 1 0 1 0 
0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 
0 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0
0 0 0 0 1 1 0 0 0 0 1 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0

И после печати его ввода он выведет:

The maximal rectangle has area 12.
Location: [col=7, row=6] to [col=12, row=5]

В приведенной выше реализации, конечно, нет ничего необычного, но она очень близка к объяснению в статье доктора Добба и должна быть легко переведена на все необходимое.

Я реализовал решение Доббса на Java.

Никаких гарантий ни на что.

package com.test;

import java.util.Stack;

public class Test {

    public static void main(String[] args) {
        boolean[][] test2 = new boolean[][] { new boolean[] { false, true, true, false },
                new boolean[] { false, true, true, false }, new boolean[] { false, true, true, false },
                new boolean[] { false, true, false, false } };
        solution(test2);
    }

    private static class Point {
        public Point(int x, int y) {
            this.x = x;
            this.y = y;
        }

        public int x;
        public int y;
    }

    public static int[] updateCache(int[] cache, boolean[] matrixRow, int MaxX) {
        for (int m = 0; m < MaxX; m++) {
            if (!matrixRow[m]) {
                cache[m] = 0;
            } else {
                cache[m]++;
            }
        }
        return cache;
    }

    public static void solution(boolean[][] matrix) {
        Point best_ll = new Point(0, 0);
        Point best_ur = new Point(-1, -1);
        int best_area = 0;

        final int MaxX = matrix[0].length;
        final int MaxY = matrix.length;

        Stack<Point> stack = new Stack<Point>();
        int[] cache = new int[MaxX + 1];

        for (int m = 0; m != MaxX + 1; m++) {
            cache[m] = 0;
        }

        for (int n = 0; n != MaxY; n++) {
            int openWidth = 0;
            cache = updateCache(cache, matrix[n], MaxX);
            for (int m = 0; m != MaxX + 1; m++) {
                if (cache[m] > openWidth) {
                    stack.push(new Point(m, openWidth));
                    openWidth = cache[m];
                } else if (cache[m] < openWidth) {
                    int area;
                    Point p;
                    do {
                        p = stack.pop();
                        area = openWidth * (m - p.x);
                        if (area > best_area) {
                            best_area = area;
                            best_ll.x = p.x;
                            best_ll.y = n;
                            best_ur.x = m - 1;
                            best_ur.y = n - openWidth + 1;
                        }
                        openWidth = p.y;
                    } while (cache[m] < openWidth);
                    openWidth = cache[m];
                    if (openWidth != 0) {
                        stack.push(p);
                    }
                }
            }
        }

        System.out.printf("The maximal rectangle has area %d.\n", best_area);
        System.out.printf("Location: [col=%d, row=%d] to [col=%d, row=%d]\n", best_ll.x + 1, best_ll.y + 1,
                best_ur.x + 1, best_ur.y + 1);
    }

}

После стольких усилий я написал этот алгоритм ... Посмотрите код ...

Вы это понимаете. Этот код говорит об этом !!

import java.util.Scanner;
import java.util.Stack;

/**
 * Created by BK on 05-08-2017.
 */
public class LargestRectangleUnderHistogram {
    public static void main(String... args) {
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt();
        int[] input = new int[n];
        for (int j = 0; j < n; j++) {
            input[j] = scanner.nextInt();
        }



        /*
        *   This is the procedure used for solving :
        *
        *   Travel from first element to last element of the array
        *
        *   If stack is empty add current element to stack
        *
        *   If stack is not empty check for the top element of stack if
        *   it is smaller than the current element push into stack
        *
        *   If it is larger than the current element pop the stack until we get an
        *   element smaller than the current element or until stack becomes empty
        *
        *   After popping each element check if the stack is empty or not..
        *
        *   If stack is empty it means that this is the smallest element encountered till now
        *
        *   So we can multiply i with this element to get a big rectangle which is contributed by
        *
        *   this
        *
        *   If stack is not empty then check for individual areas(Not just one bar individual area means largest rectangle by this) (i-top)*input[top]
        *
        *
        * */

        /*
        * Initializing the maxarea as we check each area with maxarea
        */

        int maxarea = -1;
        int i = 0;
        Stack<Integer> stack = new Stack<>();
        for (i = 0; i < input.length; i++) {

            /*
            *   Pushing the element if stack is empty
            * */


            if (stack.isEmpty()) {
                stack.push(i);
            } else {

                /*
                *   If stack top element is less than current element push
                * */

                if (input[stack.peek()] < input[i]) {
                    stack.push(i);
                } else {

                    /*
                    *   Else pop until we encounter an element smaller than this in stack or stack becomes empty
                    *   
                    * */


                    while (!stack.isEmpty() && input[stack.peek()] > input[i]) {

                        int top = stack.pop();

                        /*
                        *   If stack is empty means this is the smallest element encountered so far
                        *   
                        *   So we can multiply this with i
                        * */

                        if (stack.isEmpty()) {
                            maxarea = maxarea < (input[top] * i) ? (input[top] * i) : maxarea;
                        }

                        /*
                         *  If stack is not empty we find areas of each individual rectangle
                         *  Remember we are in while loop
                         */

                        else {
                            maxarea = maxarea < (input[top] * (i - top)) ? (input[top] * (i - top)) : maxarea;
                        }
                    }
                    /*
                    *   Finally pushing the current element to stack
                    * */

                    stack.push(i);
                }
            }
        }

        /*
        *  This is for checking if stack is not empty after looping the last element of input
        *  
        *  This happens if input is like this 4 5 6 1 2 3 4 5
        *  
        *  Here 2 3 4 5 remains in stack as they are always increasing and we never got 
        *  
        *  a chance to pop them from stack by above process
        *  
        * */


        while (!stack.isEmpty()) {

            int top = stack.pop();

            maxarea = maxarea < (i - top) * input[top] ? (i - top) * input[top] : maxarea;
        }

        System.out.println(maxarea);
    }
}

Я являюсь автором решения максимального прямоугольника на LeetCode, на котором основан этот ответ.

Поскольку решение на основе стека уже обсуждалось в других ответах, я хотел бы представить оптимальное O(NM) решение для динамического программирования, которое исходит от пользователя morrischen2008 .

Интуиция

Представьте себе алгоритм, в котором для каждой точки мы вычисляем прямоугольник, выполнив следующие действия:

  • Нахождение максимальной высоты прямоугольника путем итерации вверх, пока не будет достигнута заполненная область

  • Нахождение максимальной ширины прямоугольника путем итераций влево и вправо наружу до тех пор, пока высота не будет соответствовать максимальной высоте прямоугольника

Например, найти прямоугольник, обозначенный желтой точкой: введите описание изображения здесь

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

Для каждой точки мы определяем несколько переменных:

h - высота прямоугольника, определяемого этой точкой

l - левая граница прямоугольника, определяемого этой точкой

r - правая граница прямоугольника, определяемого этой точкой

Эти три переменные однозначно определяют прямоугольник в этой точке. Мы можем вычислить площадь этого прямоугольника с помощью h * (r - l) . Наш результат - это глобальный максимум по всем этим направлениям.

Использование динамического программирования, мы можем использовать h, l и r каждую точку в предыдущей строке , чтобы вычислить h, l и r для каждой точки в следующей строке в линейное время.

Алгоритм

Учитывая ряд matrix[i], мы отслеживаем из h, l и r каждой точки в строке, определяя три массива - height, left и right .

height[j] будет соответствовать высоте matrix[i][j] и т. д. с другими массивами.

Теперь возникает вопрос, как обновить каждый массив.

height

h определяется как количество непрерывных незаполненных пробелов в строке от нашей точки. Мы увеличиваем, если есть новое пространство, и устанавливаем его на ноль, если пространство заполнено (мы используем «1», чтобы указать пустое пространство, и «0» как заполненное).

new_height[j] = old_height[j] + 1 if row[j] == '1' else 0

left:

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

В результате мы можем определить:

new_left[j] = max(old_left[j], cur_left)

cur_left на единицу больше, чем крайнее правое заполненное пространство, с которым мы столкнулись. Когда мы «расширяем» прямоугольник влево, мы знаем, что он не может расшириться за эту точку, иначе он столкнется с заполненным пространством.

right:

Здесь мы можем повторно использовать наши рассуждения left и определить:

new_right[j] = min(old_right[j], cur_right)

cur_right это крайнее левое вхождение заполненного пространства, с которым мы столкнулись.

Реализация

def maximalRectangle(matrix):
    if not matrix: return 0

    m = len(matrix)
    n = len(matrix[0])

    left = [0] * n # initialize left as the leftmost boundary possible
    right = [n] * n # initialize right as the rightmost boundary possible
    height = [0] * n

    maxarea = 0

    for i in range(m):

        cur_left, cur_right = 0, n
        # update height
        for j in range(n):
            if matrix[i][j] == '1': height[j] += 1
            else: height[j] = 0
        # update left
        for j in range(n):
            if matrix[i][j] == '1': left[j] = max(left[j], cur_left)
            else:
                left[j] = 0
                cur_left = j + 1
        # update right
        for j in range(n-1, -1, -1):
            if matrix[i][j] == '1': right[j] = min(right[j], cur_right)
            else:
                right[j] = n
                cur_right = j
        # update the area
        for j in range(n):
            maxarea = max(maxarea, height[j] * (right[j] - left[j]))

    return maxarea

Реализация стекового алгоритма на простом Javascript (с линейной временной сложностью):

function maxRectangle(mask) {
  var best = {area: 0}
  const width = mask[0].length
  const depth = Array(width).fill(0)
  for (var y = 0; y < mask.length; y++) {
    const ranges = Array()
    for (var x = 0; x < width; x++) {
      const d = depth[x] = mask[y][x] ? depth[x] + 1 : 0
      if (!ranges.length || ranges[ranges.length - 1].height < d) {
        ranges.push({left: x, height: d})
      } else {
        for (var j = ranges.length - 1; j >= 0 && ranges[j].height >= d; j--) {
          const {left, height} = ranges[j]
          const area = (x - left) * height
          if (area > best.area) {
            best = {area, left, top: y + 1 - height, right: x, bottom: y + 1}
          }
        }
        ranges.splice(j+2)
        ranges[j+1].height = d
      }
    }
  }
  return best;
}

var example = [
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0],
[0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0],
[0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]]

console.log(maxRectangle(example))

@lassevk

    // 4. Outer double-for-loop to consider all possible positions 
    //    for topleft corner. 
    for (int i=0; i < M; i++) {
      for (int j=0; j < N; j++) {

        // 2.1 With (i,j) as topleft, consider all possible bottom-right corners. 

        for (int a=i; a < M; a++) {
          for (int b=j; b < N; b++) {

ХАХА ... О (m2 n2) .. Это наверное то, что я придумал.

Я вижу, что они продолжают развивать оптимизацию ... выглядит хорошо, я буду читать.