Как наиболее эффективно находить прямоугольные области одинакового значения заданного размера в матрице?

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

Предположим, что имеется такая матрица 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.

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

EDIT: Размеры матрицы в моем приложении варьируются от 300 x 300 до 3000 x 3000 примерно. Кроме того, алгоритм будет вызываться один раз для одной и той же матрицы. Причина в том, что матрица всегда будет потом меняться (примерно 1-20%).

ПОЛУЧЕННЫЕ РЕЗУЛЬТАТЫ

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

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

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

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

                  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, чтобы ускорить универсальные шаблоны, избегая стирания типов

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

                  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... это еще одна причина ее изучать)


person letmaik    schedule 11.01.2011    source источник
comment
Существует несколько алгоритмов поиска областей (как в программе Paint): заливка заливкой Извлечение области. Но они не навязывают прямоугольный шаблон. Ответ Кевина выглядит очень хорошо для этого варианта использования.   -  person rds    schedule 11.01.2011
comment
@neo, просто из любопытства, насколько велики матрицы, которые вам нужно обработать?   -  person The Archetypal Paul    schedule 11.01.2011
comment
@Paul в диапазоне от ок. От 300 x 300 до 3000 x 3000, поэтому мне нужен самый эффективный алгоритм. Меня тоже интересует ScalaCL, но, к сожалению, моя gfx-карта слишком стара для этого...   -  person letmaik    schedule 11.01.2011
comment
Я удалил свой ответ, потому что заметил серьезный недостаток в алгоритме. Я позволю Кевину взять это. :-)   -  person Daniel C. Sobral    schedule 12.01.2011
comment
@daniel Спасибо, я думаю... :)   -  person Kevin Wright    schedule 13.01.2011
comment
Как бы то ни было, решение Никиты легко распараллелить. Первое вычисление, последовательные ячейки, может быть выполнено независимо для каждого столбца. Второе вычисление, показанное в ответе, может быть вычислено независимо для каждой строки. Если у вас есть барьер памяти между двумя шагами, это все, что вам нужно для синхронизации для многопоточности.   -  person Daniel C. Sobral    schedule 15.01.2011
comment
@neo Если ваша матрица состоит из Int или Double, то одна ОГРОМНАЯ проблема скорости с кодом Кевина заключается в том, что он параметризован. Если вы пройдете его и замените T фактическим типом, который вы используете (и удалите объявления параметров типа [T]), вы получите гораздо более быстрый код. Вы можете сделать то же самое, посыпав код аннотациями @specialized в нужных местах, что сделает код более быстрым для подклассов AnyVal, но все же гибким.   -  person Daniel C. Sobral    schedule 15.01.2011
comment
@daniel, это должна быть специализация. В проходе столбца эти методы вызываются с T=(Int,Int)   -  person Kevin Wright    schedule 15.01.2011
comment
@Daniel Я не ожидал, что параметризация будет проблемой скорости, я прочитал эту тему сейчас на lamp.epfl.ch/~dragos/files/scala-spec.pdf Я обновлю результаты тестов алгоритма Кевина.   -  person letmaik    schedule 15.01.2011
comment
@Daniel Benchmark обновлен, честно говоря, я ожидал немного большего ... что может означать, что основная работа заключается не в распаковке / упаковке, а в создании множества временных списков.   -  person letmaik    schedule 16.01.2011


Ответы (3)


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

0 0 0 0 0 2 2
1 1 2 2 2 1 1
0 0 1 1 1 0 0
1 1 0 0 0 1 1
0 0 0 0 0 0 0

Его можно легко изготовить в 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, и у нас есть решение.

person Nikita Rybak    schedule 11.01.2011
comment
Арх... Джава! Нечистый, нечистый! - person Kevin Wright; 11.01.2011
comment
Это достаточно близко к тому, о чем я думал, поэтому я, вероятно, не буду беспокоиться. - person aaronasterling; 11.01.2011
comment
@Kevin Это также может быть C или C ++ или любой из их семейства языков :) В любом случае, его должно быть легко преобразовать в любой язык, единственная специфичная для C конструкция здесь - это цикл for. Просто считайте это псевдокодом. - person Nikita Rybak; 11.01.2011
comment
@Nikita Честно говоря, есть причина, по которой люди используют эти теги, задавая вопросы! По крайней мере, использование вами изменяемой переменной current_width полностью бросает вызов идиоматическому Scala и означает, что это решение нельзя масштабировать для использования параллельных функций, которые вскоре будут выпущены с версией 2.9. Навязывать такой последовательный и очень императивный алгоритм тому, кто хочет расширить свои знания функционального языка программирования, несколько неуместно. - person Kevin Wright; 11.01.2011
comment
@Kevin Мне жаль, если достойное O(n^2) решение не может быть реализовано в Scala из-за его «особенностей». И мне очень жаль, что на протяжении десятилетий в CS/алгоритмах используется почти исключительно императивный псевдокод. Надеюсь, ты когда-нибудь простишь меня. - person Nikita Rybak; 11.01.2011
comment
@Kevin Что касается «роста», OP не указал, в какой области он хочет «расти»: алгоритмы или Scala. Но оставаться с решением грубой силы O(n^4), конечно, не выглядит для меня ростом. - person Nikita Rybak; 11.01.2011
comment
@Nikita: У моего графического процессора 256 ядер ... Закон Мура говорит нам, что потребность в распараллеливании в программном обеспечении будет удваиваться каждые 18 месяцев. Отдельные ядра настроены на замедление из соображений тепловой/энергетической эффективности. Big-O по своей сути является измерением последовательной сложности и не учитывает этого, а также не распознает производительные атрибуты алгоритма, такие как когерентность кэша. Неизменяемое декларативное решение может обрабатывать каждую строку одновременно, а затем каждый столбец (одновременно), используя что-то вроде этого подключаемого модуля компилятора: code.google.com/p/scalacl/wiki/ScalaCLPlugin - person Kevin Wright; 11.01.2011
comment
Big-O по своей сути является измерением серийной сложности. Нет, это не так. Это измерение необходимого количества операций. При фиксированном количестве ядер алгоритм O (n ^ 4) по-прежнему равен O (n ^ 4). И неизменное, декларативное решение не может одновременно использовать каждую строку, если только количество ядер не больше или равно n, если я что-то не упустил - person The Archetypal Paul; 11.01.2011
comment
@Kevin Это напоминает мне еще один аргумент, который я слышал давно. Нам не нужно учиться считать: в будущем это сделают за нас компьютеры! :) И, кстати, этот алгоритм тоже можно распараллелить: обработка разных строк может выполняться одновременно. (В случае a вычисления - столбцы) - person Nikita Rybak; 11.01.2011
comment
@Никита, почему бы также не создать матрицу того, сколько ячеек справа имеют одинаковый номер? Затем поиск плиток просто проходит по двум матрицам, выполняя два сравнения по ширине и высоте (и этот бит можно сделать функционально :-P) - person The Archetypal Paul; 11.01.2011
comment
@Paul Конечно, можно, вы просто резюмировали алгоритм в моем ответе :) - person Kevin Wright; 11.01.2011
comment
@ Пол, я не знаю о «двух сравнениях». Таким образом можно проверить верхнюю и левую границы прямоугольника, но не внутреннюю часть (если я вас правильно понял). В любом случае, я верю, что это можно переписать как своего рода рекурсию в scala, только это будет чертовски уродливо :) - person Nikita Rybak; 11.01.2011
comment
@Nikita вздох, нет, я не думаю, что компьютеры для нас важны. Но я ожидаю, что мы перестанем сосредотачиваться на устаревших методах измерения сложности, которые не допускают определенного дублирования усилий для значительного улучшения параллелизма алгоритма. Соответствующим экспоненциальным измерением здесь является количество ядер в зависимости от времени. - person Kevin Wright; 11.01.2011
comment
@ Кевин, Big-O не устарел из-за параллелизма. Вы говорите о постоянном факторе, а не о поведении большого O, которое остается актуальным, даже если для некоторых измерений худшее большое O перевешивается лучшей константой. Это верно как для параллельных решений, так и для последовательных. - person The Archetypal Paul; 11.01.2011
comment
@Nikita, два сравнения, о которых говорил Пол, будут сравнением строк, а затем сравнением столбцов кортежей (значение ячейки, сравнение строк). Я знаю это, потому что это именно тот алгоритм, который я написал. Я использую один и тот же метод runLengths как для строк, так и для столбцов, этот метод является не уродливым примером какой-то рекурсии в Scala. - person Kevin Wright; 11.01.2011
comment
@ Кевин Я говорил не о подсчете, а о предсказании будущего. Со средним потребительским ноутбуком, имеющим 2-4 ядра (число, которое в последнее время, казалось, не соблюдает закон Мура), времена, когда разница между 10 ^ 6 операций и 10 ^ 12 операций не важна, не скоро наступят. Не могу поверить, что мне даже приходится спорить об этом. (Не говоря уже о том, что ничто не препятствует распараллеливанию в Java или C, как я уже отмечал выше.) - person Nikita Rybak; 11.01.2011
comment
@Kevin Приятно, что вы объясняете свою идею здесь, но вы наверняка получите больше внимания (и голосов), если сделаете это в своем посте (желательно с некоторым псевдокодом и без синтаксиса, специфичного для Scala). Scala здесь широко не используется. Горькая правда. - person Nikita Rybak; 11.01.2011
comment
@Nikita Когда вопрос помечен как scala, так и scala 2.8, вы хотите верить, что об этом говорят здесь ... Я предлагаю вам вернуться и прочитать мой пост, в котором я делаю описываю, как это работает, как вы могли оклеветать его как O(n^4), если вы даже не знаете, что он делает? - person Kevin Wright; 11.01.2011
comment
@Nikita, кстати, у среднего ноутбука гораздо больше двух ядер, вы забываете о графическом процессоре ... - person Kevin Wright; 11.01.2011
comment
@Kevin Вот это смешно. Я заметил, что код ОП имеет сложность O(n^4), и вы начали доказывать, что в O(n^4) нет ничего плохого. Я понятия не имею о сложности вашего решения, хотя я пытался его понять. - person Nikita Rybak; 11.01.2011
comment
@Kevin А как вы используете GPU или видеокарту для алгоритмических вычислений? Я действительно заинтересован. - person Nikita Rybak; 11.01.2011
comment
@ Кевин О чем именно мы спорим? - person Nikita Rybak; 11.01.2011
comment
@Nikita Я считаю, что мы спорим об относительной важности параллельной масштабируемости по сравнению с минимизацией последовательного времени выполнения алгоритма ... Кроме того, вот плагин компилятора для запуска (некоторого) кода Scala на графическом процессоре: code.google.com/p/scalacl/wiki/ScalaCLPlugin - person Kevin Wright; 11.01.2011
comment
@Nikita Спасибо за решение! Я только что проверил его, и он превосходит мой на мили. Объем памяти явно больше из-за создания матрицы A, но это приемлемо. Я еще не реализовал подход Кевина, поэтому этим займусь дальше. - person letmaik; 11.01.2011
comment
@Nikita lol.... Я только что сделал больший тест, и когда я использую ваш в своем приложении (несколько сотен раз), это занимает всего 6-7 секунд, а мое исходное решение занимает 3065 секунд: D Так что ваше в 400-500 раз быстрее ... неужели такое может быть?? Конечно, эти измерения ни в коем случае не являются научными, но все же дают представление - person letmaik; 11.01.2011
comment
@neo, 500 меньше 25 ^ 2. Таким образом, алгоритм O (N ^ 2) может быть в 500 раз быстрее, чем O (N ^ 4) для N всего 25, при прочих равных условиях. Эти силы быстро становятся большими. для N = 3000 (ваша большая матрица) это может быть 9 миллионов раз быстрее! - person The Archetypal Paul; 11.01.2011
comment
@Nikita Под n^2 вы имеете в виду матрицу n x n? - person Daniel C. Sobral; 15.01.2011
comment
@ Даниэль Да. Можно сказать O(W*H), но маловероятно, что у вас будут разные ограничения для W и H. - person Nikita Rybak; 15.01.2011
comment
@Nikita Я не знаю, как я мог следить за этим во время всего моего тестирования и т. Д., Но в логике вашего кода очень небольшая ошибка. Третий current_width = 0;, где он проверяет одинаковые числа, должен быть = 1 !!! В противном случае он всегда пропускает один хороший столбец (проверка того, достаточно ли велика его высота, произошла раньше, так что все в порядке - не нужно проверять снова и можно просто установить его равным 1) - person letmaik; 24.01.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, и просканировав вправо, вы увидите, что в строке, содержащей 1, есть 2 соседние ячейки. Просматривая вниз, есть две соседние ячейки с одинаковым свойством содержания значения 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)) } }
}

ОБНОВЛЕНИЕ2

Придержите свои шляпы, это большое дело...

Прежде чем написать одну строку новой функциональности, мы сначала немного рефакторим, перетащив некоторые методы в классы 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) }

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

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

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.

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

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-структуру, так как необходимые координаты уже встроены в результаты.

Вуаля!

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

person Community    schedule 11.01.2011
comment
Хммм.... не знаю, может это просто плагин Scala для eclipse, но он не позволяет мне его скомпилировать из-за этой ошибки: слишком много аргументов для метода map2dRowFirst: (grid: HelpersTest.this.Grid[T]) (func: (List[T]) =› List[T])List[List[T]] Я пока не понял, для чего нужен 2-й список параметров в map2dRowFirst, я имею в виду вообще, а не только в вашем коде . Можете ли вы пролить свет на это? - person letmaik; 11.01.2011
comment
@Kevin Вы написали, что ваш код потребуется изменить, если ввод содержит смежные прямоугольники. Поскольку это абсолютно возможно в моем случае, не могли бы вы дать мне подсказку по этому поводу? - person letmaik; 11.01.2011
comment
@neo моя ошибка, я скопировал несоответствующие версии функций из REPL. Теперь все должно быть хорошо. - person Kevin Wright; 11.01.2011
comment
Соседние прямоугольники @neo в основном сложны, потому что их можно воспринимать по-разному. Например, вы бы сказали, что образец, который я дал в своем ответе, представляет собой 2 перекрывающихся прямоугольника или 3, которые не перекрываются? - person Kevin Wright; 11.01.2011
comment
@Kevin теперь жалуется на несоответствие типов; найдено: (List[Nothing]) => List[(Nothing, Int)] required: (List[Any]) => List[Any] для параметра runLengths в последней строке, что кажется правильным, поскольку List[(T ,Int)] не является List[T] - person letmaik; 11.01.2011
comment
@Kevin относительно прилегающих прямоугольников, конечно, это зависит от искомого размера. Для моих целей мне также нужны перекрывающиеся прямоугольники. Итак, если ширина должна быть 3, а высота 2, то есть 8 прямоугольников. Или для ширины 3 и высоты 3 их 5. - person letmaik; 11.01.2011
comment
@neo все исправлено. Жаль, что я оставил сеанс repl на другом компьютере, я бы хотел вернуться и проверить, что я пропустил. - person Kevin Wright; 12.01.2011
comment
@ Кевин, еще один вопрос, я попытался реализовать два прохода и хотел заархивировать и сгладить полученные списки, но я действительно не знаю, как это сделать правильно ... Я попробовал это с помощью List.flatten(a zip b ), где a и b — окончательные списки. Я получаю для сглаживания: несоответствие типов; найдено: Список[(Список[(T, (Int, Int))], Список[(T, (Int, Int))])] требуется: Список[Список[?]] - person letmaik; 12.01.2011
comment
@Kevin Теперь я понял, почему алгоритм в его нынешнем виде не поддерживает перекрывающиеся прямоугольники. Видите ли вы какой-нибудь простой способ, или тогда было бы лучше использовать другой подход? - person letmaik; 12.01.2011
comment
@Neo, вам нужно запустить его дважды, поменяв местами шаг 1 и шаг 2 во второй раз. Это даст вам 2 прямоугольника для каждой ячейки: самый высокий и самый широкий. Я обновлю более полно позже сегодня. - person Kevin Wright; 12.01.2011
comment
@Нео Наслаждайся! Мне было бы интересно посмотреть, как это работает, рекомендую вам запустить последнюю JVM с флагом -server, чтобы включить анализ побега. - person Kevin Wright; 12.01.2011
comment
@Кевин Спасибо за ваши усилия! Я очень ценю это и постараюсь понять каждую строчку. Я почти интегрировал ваш алгоритм, но поскольку мне пришлось использовать список вместо массива, я столкнулся с проблемой, которая является единственным недостающим звеном, прежде чем я смогу провести тесты. Я надеюсь, что вы можете мне помочь (вероятно, одним или двумя строками). Цель: для выбранного прямоугольника, скажем, размером 3x4 при x=0, y=1 со значением=1. Теперь мне нужно добавить число, скажем, 10, ко всем ячейкам этого прямоугольника в сетке (значение впоследствии=1+10= 11). Раньше я использовал вложенный цикл x-y for из-за легкого доступа O (1) к массивам, но как это сделать для списков? Огромное спасибо :) - person letmaik; 13.01.2011
comment
@Kevin Я не знал, что флаг -server, кажется, действительно ускоряет все алгоритмы (мой оригинальный, тот, что от Никиты, и ваш, вероятно, тоже, когда я проверю его позже). Единственным недостатком является потребление памяти, что может быть проблемой для моей рабочей станции с 2 ГБ памяти, но посмотрим. - person letmaik; 13.01.2011
comment
@neo добавление таких значений не тривиально. Прямоугольник 4x5 будет содержать внутри 4 прямоугольника 3x4. Как бы вы хотели увеличить значения в таком случае? - person Kevin Wright; 13.01.2011
comment
@Kevin Я не всегда прямо имею в виду один из самых широких или самых высоких прямоугольников, найденных вашим алгоритмом, а скорее его конкретный подпрямоугольник (который может быть одним и тем же). И все ячейки в этом подпрямоугольнике имеют одинаковое значение V, которое следует изменить на V+Z. Смотрите здесь для визуализации: pastebin.com/fLRFgFER - person letmaik; 13.01.2011
comment
@Neo попробуйте использовать метод deepMapRange в GridOps, только что добавленный в мой ответ. например input.deepMapRange(2,3,3,3){ x=>5 } - person Kevin Wright; 13.01.2011
comment
@Кевин Спасибо! :) Я просто интегрирую его, я думаю, вы пропустили точку в deepMapRange между grid mapRange(y,h)...., по крайней мере, Eclipse жалуется без нее, но не знаю, почему здесь не должна работать инфиксная нотация, может из-за карри? - person letmaik; 13.01.2011
comment
Кевин, твой метод измерения длины цикла излишне усложняет. Простой row.init.foldRight((row.last, List(1))) { case (curr, (prev, acc @ (head :: _))) => (curr, (if (curr == prev) head + 1 else 1) :: acc) }._2 вычислил бы его намного эффективнее, хотя, конечно, было бы лучше сначала обратить row и использовать foldLeft. Я вижу элегантность takeWhile, но не тогда, когда ты продолжаешь считать вещи, которые уже были сосчитаны. Это проблема сложности, которая действительно должна быть исправлена. - person Daniel C. Sobral; 15.01.2011
comment
@daniel, никаких аргументов от меня. Я верну это обратно в ответ с небольшим объяснением того, как это работает. - person Kevin Wright; 15.01.2011
comment
@neo Как я уже писал, да. Я оставляю всю тяжелую работу по созданию параметризованной версии Кевину. :-) - person Daniel C. Sobral; 16.01.2011
comment
извините, только что удалил свой комментарий, подумал, что это неправильно, вот еще раз: @Daniel: Не будет ли ваша альтернативная версия принудительно использовать List[Int] вместо List[T]? - person letmaik; 16.01.2011
comment
Я буду придерживаться хвостовой рекурсии, чтобы убрать стоимость создания объекта этого замыкания, просто обновлю его, чтобы использовать ту же логику, что и сгиб Дэниела. Это явно узкое место в алгоритме. По иронии судьбы, это также единственная часть, которая по своей природе очень серийна. - person Kevin Wright; 16.01.2011
comment
На самом деле, я также получу там некоторую специализацию, чтобы сократить расходы на бокс. Ожидайте увидеть это на github в ближайшее время, это хороший пример, он будет более читабелен там, и я даже не начал какую-либо оптимизацию, кроме как сделать его распараллеливаемым. - person Kevin Wright; 16.01.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
)

Я не уверен, окупятся ли здесь накладные расходы на распараллеливание или нет, но это первый алгоритм в Stack Overflow, где у него действительно есть шанс, учитывая нетривиальные усилия по вычислению столбца. Вот еще один способ написать это, используя будущие функции 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 не вырастет что-нибудь, чтобы помочь с ними.

Кроме того, код Никиты можно легко распараллелить с помощью потоков — самое главное! -- с небольшими трудностями.

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

ИЗМЕНИТЬ

Итак, я решил попробовать сделать эффективную функциональную версию. На самом деле он не полностью функционален, потому что я использую 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)
}
person Daniel C. Sobral    schedule 15.01.2011
comment
Спасибо! Я всегда знал, что приличные алгоритмы могут быть реализованы в scala :) Я посмотрю на это больше, когда у меня будет свободное время, может быть, я даже начну понимать что-то в scala. - person Nikita Rybak; 15.01.2011
comment
@ Даниэль Вау, я этого не ожидал! Я нахожу, что мне очень интересно видеть все ваши идеи, и теперь даже с использованием фреймворка актера, до сих пор я только читал об этом тут и там. - person letmaik; 15.01.2011
comment
@Daniel Я только что пытался протестировать ваш код, но компилятору не нравится последний вызов транспонирования в getGridHeights: не удалось найти неявное значение для параметра asArray: (List[Int]) => Array[U] и недостаточно аргументов для метод транспонирования: (неявный asArray: (List[Int]) => Array[U])Array[Array[U]]. Параметр с неопределенным значением asArray. - person letmaik; 15.01.2011
comment
@neo Хорошо, я проверил. Я некоторое время играл с этим кодом, возвращаясь назад и вперед, выбирая, какие коллекции использовать. Первоначально я везде использовал Array, предполагая, что будет много индексированного доступа. Когда я, наконец, закончил код, оказалось, что это не так, поэтому я вернулся и попытался сократить преобразования коллекций. Очевидно, я забыл протестировать этот конкретный фрагмент кода (в основном я использовал версию 2.9N), которая теперь исправлена. - person Daniel C. Sobral; 16.01.2011
comment
@neo Кстати, я больше всего хочу получить четкий код в функциональном стиле, а не скорость. Я был немного обескуражен из-за двух проблем в библиотеке Scala (одна, возможно, ошибка), которые также ускорили бы код. Однако в циклах скорости while, реализующих показанный Никитой алгоритм, ничто не превзойдет. Вы можете выиграть от распараллеливания, но то же самое возможно и в коде Никиты с очень небольшими изменениями. - person Daniel C. Sobral; 16.01.2011
comment
@Daniel Я также предпочитаю четкий код, когда это возможно, поэтому меня также интересуют быстрые функциональные решения, такие как ваше. Я включу результаты тестов для вашего кода через секунду. - person letmaik; 16.01.2011
comment
ну я имел в виду не понятный код (ибо код Никиты тоже понятный), а код функционального стиля - person letmaik; 16.01.2011
comment
Попробуйте хвостовую рекурсию для этих методов вычислений, вы сможете выжать больше производительности. - person Kevin Wright; 16.01.2011
comment
@Kevin Я бы хотел использовать scanRight на Iterable. К сожалению, нет scanRight на Iterable, а scanRight все равно не реализовано правильно. - person Daniel C. Sobral; 16.01.2011
comment
@Daniel Вы пробовали использовать плагин компилятора ScalaCL? У него все еще есть некоторые шероховатости, но он должен быть в состоянии ускорить многие циклы на массивах, включая reduceLeft/scanLeft (нет необходимости в каком-либо оборудовании OpenCL, это просто перезапись циклов). Я предлагаю использовать последнюю версию sbaz (0.2.Beta7). - person zOlive; 27.01.2011
comment
@zOlive Нет, не видел. У меня не было цели достичь скорости. - person Daniel C. Sobral; 28.01.2011