6

問題: 5 行 4 列の正方形のグリッドがあります。これらの数値を使用してグリッドを埋める必要があります。1,2,3,4,5,6,7,8,9,10,12,18,20,21,24,27,30,35,36,40. 水平方向と垂直方向のすべての隣人が残りなく他のものを分割するように、グリッドを埋める必要があります。たとえば、12and3は隣人になることができます12 % 3 == 0が、5and 12 はそうではありません。グリッド 2x2 は となります10

セットのリストを使用して問題を解決しようとしました。各セットは、各グリッドの可能な値を表します。すべてのセットに要素が 1 つしかない場合、問題は解決されます。この問題を解決するために使用する関数を次に示します (念のためすべてを追加しましたが、私の問題は解決関数にあると思います)。

class CannotSolveError(Exception):
    pass

def suitable_neighbor(a,b):
    "return True if a and b can be neighbors."
    return (a > b) and (a % b == 0) or (b % a == 0)

def equalize_tables(table1, table2):
    "Make two tables equal, by changing first one in-place"
    for i in range(len(table1)):
        table1[i] = table2[i]


def remove_possibility(table, row, column, value):
    """Remove possibilities that can't be neighbors with value in rowxcolumn grid."""

    index = ((row - 1) * num_cols) + column - 1

    if len(table[index]) == 1:
        return # This is a solved grid, do nothing.

    remaining_possibilities = set(
        filter(lambda x: suitable_neighbor(x, value), table[index])
                                )

    if not remaining_possibilities:
        raise ValueError("Invalid move")

    if len(remaining_possibilities) == 1:
        "Only one possibility remains, try to put it!"
        copy_table = table[:]
        try:
            "Try it on copy"
            put(copy_table, row, column, remaining_possibilities.pop())
        except ValueError:
            "Cannot put, re-raise and do nothing.."
            raise
        else:
            "Putting is successfull, update original table"
            equalize_tables(table, copy_table)
    else:
        table[index] = remaining_possibilities


def put(table, row, column, value):
    """Put a value on a grid, modifies given table. use with care!"""

    index = ((row - 1) * num_cols) + column - 1

    "Is this move possible?"
    if value not in table[index]:
        raise ValueError("Cannot put %d on %dx%d" % (value, row, column))

    "Remove possibilities from left neighbor"
    if column > 1:
        remove_possibility(table, row, column - 1, value)

    "Remove possibilities from right neighbor"
    if column < num_cols:
        remove_possibility(table, row, column + 1, value)

    "Remove possibilities from upper neighbor"
    if row > 1:
        remove_possibility(table, row - 1, column, value)

    "Remove possibilities from lower neighbor"
    if row < num_rows:
        remove_possibility(table, row + 1, column, value)

    "Remove this value from every other set."
    for i in range(num_rows * num_cols):
        if i == index:
            continue

        table[i].discard(value)

    "Put one-item set in place. Have to do this last."
    table[index] = set([value])



def solve(table):
    "Try to solve the table by trying possible values on grids."

    to_try = [(i,len(table[i])) for i in range(num_rows * num_cols) if len(table[i]) > 1]

    "Grid with least remaining possibilities will be tried first."
    to_try.sort(key = lambda x: x[1])

    for index, _ in to_try:
        for value in table[index]:
            row = index / num_cols + 1
            column = index % num_cols + 1
            copy_table = table[:]
            put(copy_table, row, column, value)
            try:
                solve(copy_table)
                equalize_tables(table, copy_table)
                return
            except CannotSolveError:
                continue
            except ValueError:
                continue
    raise CannotSolveError

このアルゴリズムは問題を解決するはずだと思います。しかし、最大再帰深度を超えています。これを回避する方法、または Python でこの問題をより適切に解決するにはどうすればよいですか?

これは宿題の質問ではありません。私は自分でこれに取り組んでいます。

4

3 に答える 3

6

スタックの爆発を回避するためのより堅牢なアプローチは、部分的なソリューション (ボードに部分的に埋められている) のエンコーディングを考案し、バックトラッキングを自分で実装することです。これは、Python のスタックに依存するよりもはるかに少ないメモリで済みます。

Google の Peter Norvig は、このような手法を使用して効率的なバックトラッキング数独ソルバーを構築する方法について説明した、啓発的な記事を書いています。彼が「制約伝播」と呼んでいる手法を使用してオプションのスペースを制限し、ブルート フォース バックトラッキング検索 (つまり、考えられるすべての数字のグリッドをチェックすることなく、まだ可能性のある部分的なグリッドのみを追求することはありません)によって解決策をすばやく見つけることができるようにします。解決に導きます)。一般的なアイデアだけでなく、詳細にも非常に適用できると思います。あなたの問題は、アプローチしたように、数独ソルバーに非常に近いものです。

于 2012-08-29T12:42:08.877 に答える
2

Python の再帰制限 (デフォルトでは 1000) のカスタム値を設定する方法があります。

import sys
sys.setrecursionlimit(10000000)

再帰呼び出しの前にこれらの行を追加できます。問題が解決しない場合は、他のバグの可能性について実装を確認する必要があります。

于 2012-08-29T12:12:46.997 に答える
0

ここは雨の日なので、解決策を書きました。必要に応じて投稿できますが、自分で見つけたほうがよいでしょうか?

ここにいくつかのヒントがあります:

  • あなたのコードは (2,2) で 10 から始まっていないようです

  • 新しい値を試すときは、空のスペースに追加できます。試してみるのに最適なスペースは、多くの隣人がいるスペースです。これにより、悪い値をすばやくテストして拒否できるからです。

  • 上記の仮定、または同じことの別の言い方-私の検索は値を超えていました. だから私は「次の動き」の場所を選び、そこでそれぞれの値を試しました. 反対に、場所を検索する (「次の値」を選択し、各場所でその値を使用して検索する) こともできますが、これは効率的ではありません (上記を参照)。

  • バックトラックして再試行するときは、常に同じパターンの場所に従います。たとえば、(2,2) が 10 で、(2,3) が 40 の場合、(2,4) に適合するものは何もない可能性があります。したがって、バックトラックして 40 を削除し、(2,3) で別の数値を試します。しかし、2 番目に試みる数字 (10 と (2,2) の何かの後) は常に (2,3) です。このように注意しないと、多くの重複する組み合わせをテストすることになります。申し訳ありませんが、これは非常に明確です。基本的には、「パス」を選択して、検索してバックトラックするときにそれに固執します。このパスは、近隣の数を最大化するために選択されているため (上記のポイント)、先に進んだように構築しましたが、バックトラック時に使用したパスの場所のキャッシュを保持しました。これは、コードを示すことで説明しやすくなります...

  • テーブルの場合、配列の配列を使用しました。コピーするときに、変更されていない列を再利用しました。これにより、メモリ使用量が削減されるはずです(重要かどうかはわかりません)。

  • 検索は 40 回 (値ごとに 1 回) 再帰するだけでよいため、スタックは十分に大きくなります。

  • Python での単純な検索では、各値を順番に試し、失敗した場合はバックトラックし、ラップトップで約 4 分間実行されます (上記のヒントを使用すると仮定します) (わずかに変更されたバージョンを印刷しない場合、わずか 8 秒かかります)。

  • yieldグリッドと位置を指定すると、隣人の座標のリスト (まあ、ジェネレーター) を返す Python 関数があると便利であることがわかりました。これにより、移動がOKかどうかをテストする関数など、他の関数の記述がより簡単になりました。

とにかく、コードまたは解決策が必要な場合 (コードをすべて印刷するように変更し、1 つしかありませんでした) 質問して投稿します。もちろん、バグもあるかもしれません:o)

解決

私はこれをいくつか微調整し、(2,2)=10 ソリューションを出力してから、すべてのソリューションを検索します (これはまだ実行されています)。

#!/usr/bin/python3

nx, ny = 4, 5
values = [1,2,3,4,5,6,7,8,9,10,12,18,20,21,24,27,30,35,36,40]
# grid[x][y] so it is a list of columns (prints misleadingly!)
grid = [[0 for _ in range(ny)] for _ in range(nx)]
# cache these to avoid re-calculating
xy_moves = {}
debug = False

def neighbours(grid, x, y):
    'coordinates of vertical/horizontal neighbours'
    for (xx, yy) in [(x-1,y),(x+1,y),(x,y-1),(x,y+1)]:
        if xx > -1 and xx < nx and yy > -1 and yy < ny:
            yield xx, yy

def filled_neighbours(grid, x, y):
    'filter "neighbours" to give only filled cells'
    return filter(lambda xy: grid[xy[0]][xy[1]], neighbours(grid, x, y))

def count_neighbours(grid, x, y):
    'use this to find most-constrained location'
    return sum(1 for _ in filled_neighbours(grid, x, y))

def next_xy(grid, depth):
    '''given a certain depth in the search, where should we move next?  
       choose a place with lots of neighbours so that we have good 
       constraints (and so can reject bad moves)'''
    if depth not in xy_moves:
        best, x, y = 0, nx // 2, ny // 2 # default to centre
        for xx in range(nx):
            for yy in range(ny):
                if not grid[xx][yy]:
                    count = count_neighbours(grid, xx, yy)
                    if count > best:
                        best, x, y = count, xx, yy
        xy_moves[depth] = (x, y)
        if debug: print('next move for %d is %d,%d' % (depth, x, y))
    return xy_moves[depth]

def drop_value(value, values):
    'remove value from the values'
    return [v for v in values if v != value]

def copy_grid(grid, x, y, value):
    'copy grid, replacing the value at x,y'
    return [[value if j == y else grid[i][j] for j in range(ny)]
            if x == i else grid[i]
            for i in range(nx)]

def move_ok(grid, x, y, value):
    'are all neighbours multiples?'
    for (xx, yy) in filled_neighbours(grid, x, y):
        g = grid[xx][yy]
        if (g > value and g % value) or (g < value and value % g):
            if debug: 
                print('fail: %d at %d,%d in %s' % (value, x, y, grid))
            return False
    return True

def search(grid, values, depth=0):
    'search over all values, backtracking on failure'
    if values:
        (x, y) = next_xy(grid, depth)
        for value in values:
            if move_ok(grid, x, y, value):
                if debug: print('add %d to %d,%d' % (value, x, y))
                for result in search(copy_grid(grid, x, y, value),
                                     drop_value(value, values), 
                                     depth+1):
                    yield result
    else:
        yield grid


# run the search, knowing that (2,2) (which is (1,1) for zero-indexing)
# has the value 10.
for result in search(copy_grid(grid, 1, 1, 10), drop_value(10, values)):
    print(result)

# how many solutions in total?
#xy_moves = {} # reset cache
#for (n, solution) in enumerate(search(grid, values)):
#    print('%d: %s' % (n, solution))

を使用して次の数字を追加する正方形を選択することから始めnext_xy()ます。数値を効率的にテストして拒否できるように、できるだけ多くの既存の数値に近い場所を選択します (xy_movesバックトラックした場合に再検索する必要がないように、位置が保存されます)。値ごとに、その位置に値を置くことが を使用して機能するかどうかを確認しますmove_ok。その場合、新しいグリッド (値が追加されたもの) と新しい値のリスト (使用された値が削除されたもの) を計算し、再帰します。追加する値がなくなると、再帰は終了します。

そして、ここに結果があります(各内部リストはです):

> time ./grid.py 
[[4, 20, 5, 35, 7], [40, 10, 30, 1, 21], [8, 2, 6, 18, 3], [24, 12, 36, 9, 27]]
real    0m5.909s

[再帰とジェネレータに関する誤ったコメントを削除]

アップデート

グローバル検索を終了しました-最初に (2,2) を修正しない場合、合計で12のソリューションがあるようです(単純な対称性を無視すると、3つの異なるソリューション)。

更新 2

対称的な重複のないすべてのソリューションの検索を含む最終的なコード

于 2012-08-29T16:02:21.577 に答える