Слишком много потребления памяти при создании магических квадратов в erlang - нужна помощь для оптимизации

для университета я должен реализовать алгоритм, который создает все возможные магические квадраты для заданной длины ребра и конкретной суммы. При n = 3 алгоритм работает как ожидалось. Но при создании всех магических квадратов для n = 4 через некоторое время у меня закончилась память. Эта проблема уже упоминалась в описании задачи. Я уже пытался оптимизировать код, но он все еще не работает должным образом. Поэтому я надеюсь, что кто-то может дать мне несколько советов.

Моя основная идея: сначала я генерирую все возможные строки, которые я могу использовать с указанными числами, а затем я пытаюсь объединить их так, чтобы ограничения магического квадрата были заполнены. Это происходит с помощью обратного отслеживания. Я думаю, что проблема заключается в функции makeRows, которая потребляет слишком много памяти после того, как для хранения всех строк.

Если вам нужно еще некоторое объяснение кода, который я могу дать!

magicSquare(N, Value) ->
    Squares = buildSquare(N, makeRows(N, N*N, Value, N)),
    io:fwrite("Squares ready"), io:fwrite("~n"),
    Result = lists:filter(fun(X) -> testsquare(X, N, Value) end, Squares),
    io:write(length(Result)),
    Result.

buildSquare(0, _) -> [[]];
buildSquare(Rows, AvailableRows) ->
    [ [X|L] || L <- buildSquare(Rows-1, AvailableRows), X <- AvailableRows, onlyUniqueNumbers(lists:flatten([X|L]))].

onlyUniqueNumbers(List) -> erlang:length(List) == sets:size(sets:from_list(List)).

%produces all possible rows with a dimension of Fields and the Numbers from 1 to Numbers and the right sum for each row
makeRows(0,_,_,_) -> [[]];
makeRows(Fields, Numbers, Value, TargetLength) ->
    [ [X|L] || X <- makeRows(Fields-1, Numbers, Value, TargetLength), L <- lists:seq(1,Numbers), checkRow([X|L], TargetLength, Value)].

checkRow(Row, Length, Value) when length(Row) < Length -> true;
checkRow(Row, Length, Value) ->
    Sum = lists:sum(Row),
    if Sum == Value -> true;
    true -> false
    end.

testsquare(Square, N, Value) -> checkAllDiagonal(Square, Value) andalso checkAllHorizontal(Square, Value) andalso checkAllVertical(Square, N, Value).

checkAllHorizontal([H|T], Value) ->
    case checkHorizontal(H, Value, 0) of
        true -> checkHorizontal(lists:nth(1, T), Value, 0);
        false -> false
    end;
checkAllHorizontal([], Value) -> true.

checkHorizontal([H|T], Value, Summe) -> checkHorizontal(T, Value, Summe + H);
checkHorizontal([], Value, Summe) when Summe == Value -> true;
checkHorizontal([], Value, Summe) -> false.

checkAllVertical(Square, N, Value) -> checkAllVertical(Square, N, Value, 1).
checkAllVertical(Square, N, Value, Column) ->
    if
        Column > N -> true;
        true ->
            case checkVertical(Square, Value, 0, Column) of
                true -> checkAllVertical(Square, N, Value, Column + 1);
                false -> false
            end
    end.

checkVertical([], Value, Summe, Column) when Summe == Value -> true;
checkVertical([], Value, Summe, Column) -> false;
checkVertical([H|T], Value, Summe, Column) -> checkVertical(T, Value, Summe + lists:nth(Column, H), Column).

checkAllDiagonal(Square, Value) ->
    case checkDiagonal(Square, Value, 0, 1,1) of
        true -> case checkDiagonal(Square, Value, 0, length(lists:nth(1, Square)),-1) of
                            true -> true;
                            false -> false
                        end;
        false -> false
    end.

checkDiagonal([H|T], Value, Summe, Position, Richtung) -> checkDiagonal(T, Value, Summe + lists:nth(Position, H), Position + Richtung, Richtung);
checkDiagonal([], Value, Summe, Position, Richtung) when Summe == Value -> true;
checkDiagonal([], Value, Summe, Position, Richtung) -> false.

Хорошо. Я попытался добавить проверки для строк и квадратов ранее в процессе расчета. Вот измененные функции.

buildSquare(0, _, _, _) -> [[]];
buildSquare(Rows, AvailableRows, RowLength, Value) ->
    [ [X|L] || L <- buildSquare(Rows-1, AvailableRows, RowLength, Value), X <- AvailableRows, validateSquare([X|L], RowLength, Value)].

checkOnlyUniqueNumbers(List) -> erlang:length(lists:flatten(List)) == sets:size(sets:from_list(lists:flatten(List))).

validateSquare(List, RowLength, Value) when length(List) == RowLength -> testsquare(List, RowLength, Value) andalso checkOnlyUniqueNumbers(List);
validateSquare(List, _,_) -> checkOnlyUniqueNumbers(List).

%produces all possible rows with a dimension of Fields and the Numbers from 1 to Numbers
makeRows(0,_,_,_) -> [[]];
makeRows(Fields, Numbers, Value, TargetLength) ->
    [ [X|L] || L <- makeRows(Fields-1, Numbers, Value, TargetLength), X <- lists:seq(1,Numbers), checkRow([X|L], TargetLength, Value)].

%Checks if the sum of the row is Value when the row has the needed length Length
checkRow(Row, Length, _) when length(Row) < Length -> checkOnlyUniqueNumbers(Row);
checkRow(Row, _, Value) ->
    Sum = lists:sum(Row),
    Sum == Value andalso checkOnlyUniqueNumbers(Row).

Ответ 1

Ну, erlang не ленив, поэтому

magicSquare(N, Value) ->
    Squares = buildSquare(N, makeRows(N, N*N, Value, N)),

пытается построить список всех 3121348608 возможных комбинаций из четырех строк, каждый из которых суммируется до 34, используя все числа от 1 до 16 (включительно) между ними при вызове с аргументами 4 и 34.

Даже если каждый квадрат занял всего 16 байт (по одному для каждой ячейки), для этого потребуется около 48 ГБ памяти, не считая служебных данных списка.

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

Вы можете проверить, могут ли вертикали и диагонали даже суммироваться с целевым значением уже в buildSquare, что может привести к тому, что объем памяти будет достаточно низким, чтобы он вписывался в память для 4 × 4 магического квадрата (но я меньше, чем убежден). Если вы создаете только сетки (N-1)×N и вычисляете последнюю строку из вертикальных сумм, это уменьшит размер Squares на другой коэффициент N!, который имеет больше шансов поместиться в память (для N == 4, на самом деле не для больших N) вместе с более ранним обрезкой.

Но вы должны перестроить свой алгоритм, чтобы использовать ограничения как можно раньше. Скажем, вы проверите первую строку 1 2 15 16. Тогда вы знаете, что три цифры ниже 1 в левом столбце, а три оставшихся номера на главной диагонали каждый должны суммироваться до 33. Таким образом, вам нужен набор из шести чисел, выбранных из { 3,4,5,6,7,8,9,10,11,12,13,14}, суммируя до 66. Там aren многие варианты этих шести чисел, так как шесть из них в сумме составляют только 69, поэтому вы

6, 10, 11, 12, 13, 14
7,  9, 11, 12, 13, 14
8,  9, 10, 12, 13, 14

только три возможности. Два числа в нижних углах также ограничены правой колонкой и главной северо-восточной диагональю. Рассмотрение этих ограничений вместе ограничивает пространство поиска.

Если вы рассматриваете возможные квадраты последовательно, одну верхнюю строку за другой и не сохраняете кандидатов [вы можете хранить магические 4 × 4 квадрата, их не так много), вы можете найти все магические квадраты в небольшой памяти, и если вы справляетесь с ограничениями в хорошем смысле, даже относительно быстро.

Ответ 2

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

Сначала я считаю, что эта проблема - NP-Complete, которая указывает, что вы собираетесь использовать экспоненциальную память или время, поскольку размер ввода увеличивается линейно.

В любом случае это был мой подход:

  • Если ваш магический квадрат включает числа 1..N, вы собираетесь создать все перестановки для этих N чисел. В конце концов, magicSquare (3,15) будет подмножеством всех возможных перестановок 1..15

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

  • Моя тестовая функция взяла параметр, указывающий длину строки (в этом случае 3) и смог проверить перестановку [8,1,6,3,5,7,4,9,2] и определить, что каждая строка (каждый подсписок 1-3, 4-6,7-9 суммируется до 15.
  • после получения перестановок, где по крайней мере каждая строка суммируется с Magic Number, затем фильтруйте остальные критерии MN.

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

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

Надеюсь, это поможет.