Как наиболее эффективно найти прямоугольные области с одинаковыми значениями заданного размера в матрице? - PullRequest
9 голосов
/ 11 января 2011

Моя проблема очень проста, но я еще не нашел эффективную реализацию.

Предположим, что есть матрица A, подобная этой:

0 0 0 0 0 0 0
4 4 2 2 2 0 0
4 4 2 2 2 0 0
0 0 2 2 2 1 1
0 0 0 0 0 1 1

Теперь я хочу найти все начальные позиции прямоугольных областей в этой матрице, которые имеют заданный размер. Область - это подмножество A, где все числа одинаковы.

Допустим, ширина = 2 и высота = 3. Есть 3 области, которые имеют этот размер:

2 2   2 2   0 0
2 2   2 2   0 0
2 2   2 2   0 0

Результатом вызова функции будет список начальных позиций (x, y, начинающихся с 0) этих областей.

List((2,1),(3,1),(5,0))

Ниже приведена моя текущая реализация. «Области» здесь называются «поверхностями».

case class Dimension2D(width: Int, height: Int)
case class Position2D(x: Int, y: Int)

def findFlatSurfaces(matrix: Array[Array[Int]], surfaceSize: Dimension2D): List[Position2D] = {

    val matrixWidth = matrix.length
    val matrixHeight = matrix(0).length
    var resultPositions: List[Position2D] = Nil

    for (y <- 0 to matrixHeight - surfaceSize.height) {
        var x = 0
        while (x <= matrixWidth - surfaceSize.width) {
            val topLeft = matrix(x)(y)
            val topRight = matrix(x + surfaceSize.width - 1)(y)
            val bottomLeft = matrix(x)(y + surfaceSize.height - 1)
            val bottomRight = matrix(x + surfaceSize.width - 1)(y + surfaceSize.height - 1)
            // investigate further if corners are equal
            if (topLeft == bottomLeft && topLeft == topRight && topLeft == bottomRight) {
                breakable {
                    for (sx <- x until x + surfaceSize.width;
                         sy <- y until y + surfaceSize.height) {
                        if (matrix(sx)(sy) != topLeft) {
                            x = if (x == sx) sx + 1 else sx 
                            break
                        }
                    }
                    // found one!       
                    resultPositions ::= Position2D(x, y)
                    x += 1
                }
            } else if (topRight != bottomRight) {
                // can skip x a bit as there won't be a valid match in current row in this area
                x += surfaceSize.width 
            } else {
                x += 1
            }
        }   
    }
    return resultPositions
}

Я уже пытался включить в него некоторые оптимизации, но я уверен, что есть гораздо лучшие решения. Существует ли для него функция matlab, которую я мог бы портировать? Мне также интересно, имеет ли эта проблема свое собственное имя, поскольку я точно не знал, для чего Google.

Спасибо, что подумали об этом! Я рад видеть ваши предложения или решения:)

РЕДАКТИРОВАТЬ: Размеры матрицы в моем приложении примерно от 300x300 до 3000x3000. Кроме того, алгоритм будет вызываться один раз для той же матрицы. Причина в том, что матрица всегда будет изменяться впоследствии (примерно 1-20% от нее).

РЕЗУЛЬТАТЫ

Я реализовал алгоритмы Кевина, Никиты и Даниэля и сравнил их в своей прикладной среде, то есть здесь не было ни одного изолированного синтетического теста, но особое внимание было уделено интеграции всех алгоритмов в их наиболее эффективном виде, что было особенно важно для подхода Кевина, так как он использует дженерики (см. ниже).

Сначала необработанные результаты с использованием Scala 2.8 и jdk 1.6.0_23. Алгоритмы были выполнены несколько сотен раз как часть решения конкретной задачи. «Длительность» обозначает общее время, необходимое для завершения работы алгоритма приложения (конечно, без запуска jvm и т. Д.). Моя машина - 2,8 ГГц Core 2 Duo с 2 ядрами и 2 гигабайтами памяти, JVM были переданы -Xmx800M.

ВАЖНОЕ ПРИМЕЧАНИЕ: Я думаю, что мои настройки эталонных тестов не совсем справедливы для распараллеленных алгоритмов, таких как от Daniel. Это потому, что приложение уже вычисляет многопоточность. Таким образом, результаты здесь, вероятно, показывают только эквивалент однопоточной скорости.

Размер матрицы 233x587:

                  duration | JVM memory | avg CPU utilization
original O(n^4) | 3000s      30M          100%  
original/-server| 840s       270M         100%
Nikita O(n^2)   | 5-6s       34M          70-80%
Nikita/-server  | 1-2s       300M         100%
Kevin/-server   | 7400s      800M         96-98%
Kevin/-server** | 4900s      800M         96-99%
Daniel/-server  | 240s       360M         96-99%

** с @specialized, чтобы сделать генерики быстрее , избегая стирания типа

Размер матрицы 2000x3000:

                  duration | JVM memory | avg CPU utilization
original O(n^4) | too long   100M         100%  
Nikita O(n^2)   | 150s       760M         70%
Nikita/-server  | 295s (!)   780M         100%
Kevin/-server   | too long, didn't try

Сначала небольшая заметка о памяти. Опция -server JVM использует значительно больше памяти с преимуществом большего количества оптимизаций и в целом более быстрого выполнения. Как вы можете видеть из 2-й таблицы, алгоритм Никиты медленнее с опцией -server, которая, очевидно, связана с превышением лимита памяти. Я предполагаю, что это также замедляет алгоритм Кевина даже для небольшой матрицы, поскольку функциональный подход в любом случае использует гораздо больше памяти. Чтобы устранить фактор памяти, я также попробовал один раз с матрицей 50x50, а затем Кевину понадобилось 5 сек, а Никите 0 сек (ну, почти 0). Так что в любом случае это все еще медленнее, и не только из-за памяти.

Как вы можете видеть из чисел, я, очевидно, буду использовать алгоритм Никиты, потому что он чертовски быстр, и это абсолютно необходимо в моем случае. Это также может быть легко распараллелено, как указал Даниэль. Единственным недостатком является то, что это не скала.

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

С целью прямого преобразования алгоритма Никиты в функциональный стиль Даниэль придумал решение, которое уже достаточно быстрое и, как он говорит, было бы еще быстрее, если бы он мог использовать scanRight (см. Последние комментарии в своем ответе).

Что дальше?

На технологической стороне: ожидание Scala 2.9, ScalaCL и выполнение синтетических тестов для получения необработанных скоростей.

Моя цель во всем этом - иметь функциональный код, НО только если он не жертвует слишком большой скоростью.

Выбор ответа:

Что касается выбора ответа, я бы хотел отметить алгоритмы Никиты и Даниэля как ответы, но я должен выбрать один. Название моего вопроса включало «наиболее эффективно», и один из них является самым быстрым в императивном, а другой - в функциональном стиле. Хотя этот вопрос помечен как Scala, я выбрал императивный алгоритм Никиты, поскольку 2 с против 240 с по-прежнему слишком большая разница, чтобы я мог с этим согласиться. Я уверен, что разницу все еще можно немного уменьшить, есть идеи?

Итак, большое спасибо всем вам! Хотя я пока не буду использовать функциональные алгоритмы , у меня появилось много нового понимания Scala, и я думаю, что постепенно понимаю все функциональное безумие и его потенциал. (конечно, даже не занимаясь большим количеством функционального программирования, Scala гораздо приятнее, чем Java ... это еще одна причина для изучения)

Ответы [ 3 ]

8 голосов
/ 11 января 2011

Сначала пара вспомогательных функций:

//count the number of elements matching the head
def runLength[T](xs:List[T]) = xs.takeWhile(_ == xs.head).size

//pair each element with the number of subsequent occurrences
def runLengths[T](row:List[T]) : List[(T,Int)] = row match {
  case Nil => Nil
  case h :: t => (h, runLength(row)) :: runLengths(t)
}
//should be optimised for tail-call, but easier to understand this way

//sample input: 1,1,2,2,2,3,4,4,4,4,5,5,6
//output: (1,2), (1,1), (2,3), (2,2), (2,1), (3,1), (4,4), (4,3), (4,2), (4,1), (5,2), (5,1), (6,1)

Это может быть использовано против каждой строки в сетке:

val grid = List(
  List(0,0,0,0),
  List(0,1,1,0),
  List(0,1,1,0),
  List(0,0,0,0))

val stage1 = grid map runLengths
// returns stage1: List[List[(Int, Int)]] =
// 0,4  0,3  0,2  0,1
// 0,1  1,2  1,1  0,1
// 0,1  1,2  1,1  0,1
// 0,4  0,3  0,2  0,1

Затем, выполнив горизонтальные строки, теперь мы выполняем точно такую ​​же операцию со столбцами. При этом используется метод transpose, доступный в стандартной библиотеке коллекций Scala, для обмена строками <-> столбцов согласно операции с математической матрицей с тем же именем. Мы также переносим обратно, как только это будет сделано.

val stage2 = (stage1.transpose map runLengths).transpose
// returns stage2: List[List[((Int, Int), Int)]] =
// (0,4),1  (0,3),1  (0,2),1  (0,1),4
// (0,1),2  (1,2),2  (1,1),2  (0,1),3
// (0,1),1  (1,2),1  (1,1),1  (0,1),2
// (0,4),1  (0,3),1  (0,2),1  (0,1),1

Что это значит? Взяв один элемент: (1,2),2, это означает, что эта ячейка содержит значение 1, и сканирование вправо, что в строке есть 2 соседние ячейки, содержащие 1. При сканировании вниз есть две соседние ячейки с одинаковым свойством, содержащие значение 1 и имеющие одинаковое количество равных значений справа.

После уборки становится немного понятнее, конвертируя вложенные кортежи вида ((a,b),c) в (a,(b,c)):

val stage3 = stage2 map { _.map {case ((a,b),c) => a->(b,c) } }
//returns stage3: List[List[(Int, (Int, Int))]] =
//  0,(4,1)  0,(3,1)  0,(2,1)  0,(1,4)
//  0,(1,2)  1,(2,2)  1,(1,2)  0,(1,3)
//  0,(1,1)  1,(2,1)  1,(1,1)  0,(1,2)
//  0,(4,1)  0,(3,1)  0,(2,1)  0,(1,1)

Где 1,(2,2) относится к ячейке, содержащей значение 1 и находящейся в верхнем левом углу прямоугольника 2x2 с аналогичными по значению ячейками.

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

ОБНОВЛЕНИЕ: Как написано, алгоритм не работает хорошо для случаев, таких как ячейка в (0,0), которая принадлежит двум разным прямоугольникам (1x4 и 4x1). При необходимости это также можно решить, используя те же методы. (выполните один проход с помощью map / transpose / map / transpose, а второй - с transpose / map / transpose / map, а затем сожмите и сгладьте результаты) .

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

0 0 0 0 0 0 0 0
0 0 1 1 1 0 0 0
0 0 1 1 1 0 0 0
0 0 1 1 1 1 1 0
0 0 1 1 1 1 1 0
0 0 1 1 1 1 1 0
0 0 0 0 0 0 0 0

Собираем все вместе и убираем немного:

type Grid[T] = List[List[T]]

def runLengths[T](row:List[T]) : List[(T,Int)] = row match {
  case Nil => Nil
  case h :: t => (h -> row.takeWhile(_ == h).size) :: runLengths(t)
}

def findRectangles[T](grid: Grid[T]) = {
  val step1 = (grid map runLengths)
  val step2 = (step1.transpose map runLengths).transpose
  step2 map { _ map { case ((a,b),c) => (a,(b,c)) } }
}

UPDATE2

Держись за свои шляпы, это большая ...

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

import annotation.tailrec

class RowOps[T](row: List[T]) {
  def withRunLengths[U](func: (T,Int)=>U) : List[U] = {
    @tailrec def recurse(row:List[T], acc:List[U]): List[U] = row match {
      case Nil => acc
      case head :: tail =>
        recurse(
          tail,
          func(head, row.takeWhile(head==).size) :: acc)
    }
    recurse(row, Nil).reverse
  }

  def mapRange(start: Int, len: Int)(func: T=>T) =
    row.splitAt(start) match {
      case (l,r) => l ::: r.take(len).map(func) ::: r.drop(len)
    }
}

implicit def rowToOps[T](row: List[T]) = new RowOps(row)

Это добавляет withRunLengths метод к спискам. Одно заметное отличие состоит в том, что вместо возврата списка из (value, length) пар метод принимает в качестве параметра функцию для создания выходного значения для каждой такой пары. Это пригодится позже ...

type Grid[T] = List[List[T]]

class GridOps[T](grid: Grid[T]) {
  def deepZip[U](other: Grid[U]) = (grid zip other) map { case (g,o) => g zip o}
  def deepMap[U](f: (T)=>U) = grid map { _ map f}
  def mapCols[U](f: List[T]=>List[U]) = (grid.transpose map f).transpose
  def height = grid.size
  def width = grid.head.size
  def coords = List.tabulate(height,width){ case (y,x) => (x,y) }
  def zipWithCoords = deepZip(coords)
  def deepMapRange(x: Int, y: Int, w: Int, h: Int)(func: T=>T) =
    grid mapRange (y,h){ _.mapRange(x,w)(func) }
}

implicit def gridToOps[T](grid: Grid[T]) = new GridOps(grid)

Здесь не должно быть никаких сюрпризов. Методы deepXXX позволяют избежать написания конструкций вида list map { _ map { ... } }. tabulate также может быть новым для вас, но, как мы надеемся, его назначение очевидно из использования.

Используя их, становится тривиально определить функции для нахождения длин по горизонтали и вертикали по всей сетке:

def withRowRunLengths[T,U](grid: Grid[T])(func: (T,Int)=>U) =
  grid map { _.withRunLengths(func) }

def withColRunLengths[T,U](grid: Grid[T])(func: (T,Int)=>U) =
  grid mapCols { _.withRunLengths(func) }

Почему 2 блока параметров, а не один? Я скоро объясню.

Я мог бы определить их как методы в GridOps, но они не подходят для общего использования. Не стесняйтесь не соглашаться со мной здесь:)

Затем определите некоторый вход ...

def parseIntGrid(rows: String*): Grid[Int] =
  rows.toList map { _ map {_.toString.toInt} toList }

val input: Grid[Int] = parseIntGrid("0000","0110","0110","0000")

... еще один полезный тип помощника ...

case class Rect(w: Int, h: Int)
object Rect { def empty = Rect(0,0) }

как альтернатива кортежу, это действительно помогает при отладке. Глубоко вложенные скобки не легко для глаз. (извините фанатов Lisp!)

... и используйте функции, определенные выше:

val stage1w = withRowRunLengths(input) {
  case (cell,width) => (cell,width)
}

val stage2w = withColRunLengths(stage1w) {
  case ((cell,width),height) => Rect(width,height)
}


val stage1t = withColRunLengths(input) {
 case (cell,height) => (cell,height)
}

val stage2t = withRowRunLengths(stage1t) {
  case ((cell,height),width) => Rect(width,height)
}

Все вышеперечисленные блоки должны быть однострочными, но я переформатировал для StackOverflow.

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

Помните, я объяснял, что RowOps#withRunLengths принимает функцию в качестве параметра?Ну, это где он используется.case (cell,width) => (cell,width) на самом деле является функцией, которая принимает значение ячейки и длину выполнения (вызывая их cell и width), а затем возвращает (cell,width) кортеж.

Именно поэтому я использовал два блока параметров вопределяя функции, поэтому второй параметр может быть передан в {фигурные скобки} и делает все это очень красивым и похожим на DSL.

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

def withRowRunLengths[T,U](grid: Grid[T])(func: (T,Int)=>U)

Тип T будет определяться поставляемой сеткой.Затем компилятор знает тип ввода для функции, предоставленной в качестве второго аргумента, - Int в этом случае, поскольку первый параметр был Grid[Int] - поэтому я смог написать case (cell,width) => (cell,width) и не нужно явно указывать где-либо, что cell и width являются целыми числами.При втором использовании предоставляется Grid[(Int,Int)], это соответствует замыканию case ((cell,width),height) => Rect(width,height) и снова оно просто работает.

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

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

case class Cell[T](
  value: T,
  coords: (Int,Int) = (0,0),
  widest: Rect = Rect.empty,
  tallest: Rect = Rect.empty
)

Ничего особенного здесь, только класс case с именованными / параметрами по умолчанию.Я также рад, что у меня была дальновидность, чтобы определить Rect.empty выше:)

Теперь смешайте 4 набора данных (входные значения, координаты, самые широкие линии, самые высокие линии), постепенно складывайте в ячейки, осторожно перемешивайте,и обслуживаем:

val cellsWithCoords = input.zipWithCoords deepMap {
  case (v,(x,y)) => Cell(value=v, coords=(x,y))
}

val cellsWithWidest = cellsWithCoords deepZip stage2w deepMap {
  case (cell,rect) => cell.copy(widest=rect)
}

val cellsWithWidestAndTallest = cellsWithWidest deepZip stage2t deepMap {
  case (cell,rect) => cell.copy(tallest=rect)
}

val results = (cellsWithWidestAndTallest deepMap {
  case Cell(value, coords, widest, tallest) =>
    List((widest, value, coords), (tallest, value, coords))
  }
).flatten.flatten

Последний этап интересен: он конвертирует каждую ячейку в список кортежей размера 2 (прямоугольник, значение, координаты) (размер 2, поскольку у нас есть как самые широкие, так исамые высокие ректы для каждой ячейки) .При повторном вызове flatten полученный List[List[List[_]]] возвращается к одному List.Больше нет необходимости сохранять 2D-структуру, поскольку необходимые координаты уже включены в результаты.

Вуаля!

Теперь вам решать, что вы теперь будете делать с этим списком.Следующим этапом, вероятно, является некоторая форма сортировки и удаления дубликатов ...

5 голосов
/ 15 января 2011

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

Во-первых, вычисление последовательных ячеек с тем же номером под ячейкой. Я внес небольшое изменение, вернув все значения плюс одно по сравнению с предложенным Никитой выводом, чтобы избежать - 1 в какой-то другой части кода.

def computeHeights(column: Array[Int]) = (
    column
    .reverse
    .sliding(2)
    .map(pair => pair(0) == pair(1))
    .foldLeft(List(1)) ( 
        (list, flag) => (if (flag) list.head + 1 else 1) :: list
    )
)

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

Теперь все столбцы одновременно:

import scala.actors.Futures.future

def getGridHeights(grid: Array[Array[Int]]) = (
    grid
    .transpose
    .map(column => future(computeHeights(column)))
    .map(_())
    .toList
    .transpose
)

Я не уверен, окупятся ли издержки распараллеливания здесь или нет, но это первый алгоритм в переполнении стека, где он действительно имеет шанс, учитывая нетривиальные усилия при вычислении столбца. Вот еще один способ написать это, используя предстоящие функции 2.9 (он может работать на Scala 2.8.1 - не уверен, что:

def getGridHeights(grid: Array[Array[Int]]) = (
    grid
    .transpose
    .toParSeq
    .map(computeHeights)
    .toList
    .transpose
)

Теперь алгоритм Никиты:

def computeWidths(height: Int, row: Array[Int], heightRow: List[Int]) = (
    row
    .sliding(2)
    .zip(heightRow.iterator)
    .toSeq
    .reverse
    .foldLeft(List(1)) { case (widths @ (currWidth :: _), (Array(prev, curr), currHeight)) =>
        (
            if (currHeight >= height && currWidth > 0 && prev == curr) currWidth + 1
            else 1
        ) :: widths
    }
    .toArray
)

В этом фрагменте кода я использую сопоставление с образцом, хотя меня беспокоит его скорость, потому что со всеми скольжением, защелкиванием и складыванием здесь происходит много разных манипуляций. И, говоря о производительности, я использую Array вместо IndexedSeq, потому что Array - единственный тип в JVM, который не стирается, что приводит к гораздо лучшей производительности с Int. И, кроме того, есть .toSeq, который меня не особо радует, из-за локальности памяти и пропускной способности.

Кроме того, я делаю это справа налево вместо Никиты слева направо, просто чтобы найти верхний левый угол.

Однако это то же самое, что и код в ответе Никиты, за исключением того факта, что я все еще добавляю единицу к текущей ширине по сравнению с его кодом, а не печатаю результат прямо здесь. Здесь есть куча вещей без явного происхождения - row, heightRow, height ... Давайте посмотрим на этот код в контексте - и распараллелим! - чтобы получить общую картину.

def getGridWidths(height: Int, grid: Array[Array[Int]]) = (
    grid
    .zip(getGridHeights(grid))
    .map { case (row, heightsRow) => future(computeWidths(height, row, heightsRow)) }
    .map(_())
)

И версия 2.9:

def getGridWidths(height: Int, grid: Array[Array[Int]]) = (
    grid
    .toParSeq
    .zip(getGridHeights(grid))
    .map { case (row, heightsRow) => computeWidths(height, row, heightsRow) }
)

А для гран-финала

def findRectangles(height: Int, width: Int, grid: Array[Array[Int]]) = {
    val gridWidths = getGridWidths(height, grid)
    for {
        y <- gridWidths.indices
        x <- gridWidths(y).indices
        if gridWidths(y)(x) >= width
    } yield (x, y)
}

Итак ... Я не сомневаюсь, что императивная версия алгоритма Никиты быстрее - он использует только Array, что намного быстрее с примитивами, чем любой другой тип, и позволяет избежать массового создания временных коллекций - хотя Scala мог бы добиться большего успеха здесь. Кроме того, нет замыканий - насколько они помогают, они на медленнее, чем код без замыканий. По крайней мере, до тех пор, пока JVM не вырастит что-то, чтобы помочь им.

Кроме того, код Никиты можно легко распараллелить с потоками - из всех вещей! - без особых проблем.

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

EDIT

Итак, я решил сделать эффективную функциональную версию. Это не совсем функционально, потому что я использую Iterator, который изменчив, но достаточно близок. К сожалению, он не будет работать на Scala 2.8.1, потому что ему не хватает scanLeft на Iterator.

Здесь есть еще две неудачные вещи. Во-первых, я отказался от распараллеливания высот сетки, так как не мог обойтись, имея хотя бы один transpose, со всем копированием коллекции, которое влечет за собой. Тем не менее, есть еще хотя бы одно копирование данных (см. toArray вызов, чтобы понять, где).

Кроме того, поскольку я работаю с Iterable, я теряю возможность использовать параллельные коллекции. Мне интересно, нельзя ли сделать код лучше, если бы grid был параллельной коллекцией параллельных коллекций с самого начала.

Понятия не имею, эффективнее ли это по сравнению с предыдущей версией. Это интересный вопрос ...

def getGridHeights(grid: Array[Array[Int]]) = (
    grid
    .sliding(2)
    .scanLeft(Array.fill(grid.head.size)(1)) { case (currHeightArray, Array(prevRow, nextRow)) =>
        (prevRow, nextRow, currHeightArray)
        .zipped
        .map { case (x, y, currHeight) =>  if (x == y) currHeight + 1 else 1 }
    }
)

def computeWidths(height: Int, row: Array[Int], heightRow: Array[Int]) = (
    row
    .sliding(2)
    .map { case Array(x, y) => x == y }
    .zip(heightRow.iterator)
    .scanLeft(1) { case (currWidth , (isConsecutive, currHeight)) =>
        if (currHeight >= height && currWidth > 0 && isConsecutive) currWidth + 1
        else 1
    }
    .toArray
)

import scala.actors.Futures.future

def getGridWidths(height: Int, grid: Array[Array[Int]]) = (
    grid
    .iterator
    .zip(getGridHeights(grid))
    .map { case (row, heightsRow) => future(computeWidths(height, row, heightsRow)) }
    .map(_())
    .toArray
)

def findRectangles(height: Int, width: Int, grid: Array[Array[Int]]) = {
    val gridWidths = getGridWidths(height, grid)
    for {
        y <- gridWidths.indices
        x <- gridWidths(y).indices
        if gridWidths(y)(x) >= width
    } yield (x - width + 1, y - height + 1)
}
5 голосов
/ 11 января 2011

Вы можете сделать это за O(n^2) сравнительно легко.
Во-первых, некоторые предварительные расчеты.Для каждой ячейки в матрице подсчитайте, сколько последовательных ячеек под ней имеют одинаковое число.
Для вашего примера, результирующая матрица a (не может придумать лучшего названия: /) будет выглядеть так*

Его можно легко создать в O(n^2).

Теперь для каждой строки i найдем все прямоугольники с верхней стороной в ряду i (и нижней стороной в ряду i + height - 1).
Вот иллюстрация для i = 1

0 0 0 0 0 0 0
-------------
4 4 2 2 2 0 0
4 4 2 2 2 0 0
0 0 2 2 2 1 1
-------------
0 0 0 0 0 1 1

Теперь основная идея

int current_width = 0;
for (int j = 0; j < matrix.width; ++j) {
    if (a[i][j] < height - 1) {
        // this column has different numbers in it, no game
        current_width = 0;
        continue;
    }

    if (current_width > 0) {
        // this column should consist of the same numbers as the one before
        if (matrix[i][j] != matrix[i][j - 1]) {
            current_width = 1; // start streak anew, from the current column
            continue;
        }
    }

    ++current_width;
    if (current_width >= width) {
        // we've found a rectangle!
    }
}

В приведенном выше примере (i = 1) current_width после каждой итерациибудет 0, 0, 1, 2, 3, 0, 0.

Теперь нам нужно перебрать все возможные i, и у нас есть решение.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...