在Erlang中生成幻方时内存消耗过大-需要优化帮助

10
为了完成大学的任务,我需要实现一个算法,用于生成给定边长和特定总和的所有可能魔方阵。 当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).

嗯,我想在代码里向下滚动就可以了。 - soupdiver
我们能在这里添加一些并发吗?Erlang高手们,这里有什么可以并行化的地方? - Muzaaya Joshua
我也考虑过这个问题,但目前如果算法能够在一般情况下(即使用少于48GB的内存)工作,我会很高兴。我认为构建正方形的过程可以并行化,但我不确定如何避免重复计算。 - soupdiver
2个回答

9

好的,erlang不是惰性的,所以

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

尝试构建所有可能的四行组合列表,每行总和为34,使用从1到16(包括)之间的所有数字,在参数4和34的情况下,共有3121348608种组合方式。即使每个格子只需要16字节(每个单元格一个),也需要约48GB的内存,不包括列表开销。在惰性语言中,您的算法可以工作-尽管非常缓慢。但是在非惰性语言中,您需要更早地修剪更多的部分,或者选择完全不同的算法。您可以在buildSquare中测试垂直和对角线是否已经具有达到目标值的机会,这可能会将内存要求降低到足以适合4x4魔方阵的内存大小(但我并不确定)。如果您仅构建(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。由于它们中最大的六个数字仅总和为69,因此这六个数字的选择并不多。
6, 10, 11, 12, 13, 14
7,  9, 11, 12, 13, 14
8,  9, 10, 12, 13, 14

只有三种可能。底部角落的两个数字也受到右侧列和主对角线的限制。将这些约束条件考虑在一起可以进一步限制搜索空间。

如果您按顺序逐个考虑可能的正方形,一个接一个地考虑顶部行,而不存储候选项[您可以存储魔术4×4正方形,它们并不太多],您可以在小内存中找到所有魔术正方形,并且如果您以良好的方式处理约束条件,甚至可以相对较快地完成。


好的,所以我必须将buildSquare和makeRows结合在一个函数中,这样我就可以更早地检查正方形是否合理,对吧? - soupdiver
不,你可以将makeRows保持原样分离(尽管更有效的方法是将其传递给合格数字列表,以便从一开始就不重复使用任何数字),但是在buildSquare中需要更多和更早的检查。顺便提一下,在makeRows中有一个地方混淆了XL - Daniel Fischer
你从对makeRows的递归调用中得到了X,从seq(1,n)中得到了L,但在结果中你却把它们放在了[X|L]里,这样列表中的数字就在前面了(静态类型检查真是太好了,如果是编译时就能发现)。在这两个地方中的一个,你应该交换XL - Daniel Fischer
好的,我在我的帖子中添加了一些修改。你认为这样行得通吗? - soupdiver
仅出于兴趣,我相信一般的幻方问题是NP完全问题,这意味着(粗略地说),所有针对一般情况的算法解决方案都需要随着问题输入规模的增加而指数级增加内存或时间(或两者兼备)。 - Jr0

0

我有一个可能会有帮助的方向。我已经差不多让它工作了,但接下来的几天将无法花时间在它上面。

首先,我认为这个问题是NP-完全的,这意味着随着输入大小线性增加,您将使用指数级的内存或时间。

无论如何,这是我的方法:

  1. 如果你的幻方涉及到1..N的数字,那么你将为这N个数字创建所有的排列组合。毕竟,magicSquare(3,15)将是1..15所有可能排列组合的子集。

  2. 关键在于在生成每个排列组合时,如果它所代表的所有行的总和不等于魔数,则立即将其删除。通过这种方式,您不会存储所有的排列组合,只会存储那些非常有前途的排列组合,从而避免指数级的内存消耗(但不能避免指数级的时间复杂度)。换句话说,在生成每个排列组合时,只有当它有可能成为一个幻方时才保存它。我使用了列表推导式来创建带有限定符的排列组合,该限定符基于一个生成器进行测试,以确保所有行都正确求和。

  3. 我的测试函数接受一个参数,表示行长度(在本例中为3),并能够检查[8,1,6,3,5,7,4,9,2]的排列组合,并确定每行(每个子列表1-3、4-6、7-9)的总和为15。
  4. 在获取至少每行总和等于魔数的排列组合后,然后过滤出其余的MN条件。

顺便提一下,确保你创建排列的算法是尾递归的。

再次强调,这对我来说似乎有效(除了一些例外情况),但我会离开电脑几天。

希望这能有所帮助。


我认为我已经做到了这一点... makeRows(字段,数字,值,目标长度)-> [ [X | L] || L <- makeRows(字段-1,数字,值,目标长度),X <- lists:seq(1,数字),checkRow([X | L],目标长度,值)]。 已经检查每个新生成的行是否可以使用。问题在于buildSquare,我不能及早地检测出该正方形是否可以构成有效的一个。 - soupdiver

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接