如何为Puzzle Number 9制作一个有效的解算器

时间:2015-10-09 10:07:59

标签: python algorithm puzzle

这个游戏在iOSAndriod上称为第9号拼图(我与创作者没有任何关系)。您从3x3网格开始,其中数字1到9随机放置在电路板上。然后组合相邻的数字(跟踪路径)以加起来9.路径中的最后一个节点变为9,所有其他数字增加1.您将相同的9的倍数组合在一起,其中结束节点变为数字的两倍并且起始节点返回到一个节点。例如,如果您从

开始
1 2 3
5 4 6
7 8 9 

你可以从2-3-4开始,最后以

结束
1 3 4
5 9 6
7 8 9

然后结合两个9的

1 3 4
5 1 6
7 8 18

游戏的目标是达到1152.基本上它就像2048但没有随机元素。当你用完总数为9的数字时游戏结束,例如

8 7 6
5 5 5
9 1 72

我在python上写了一个简单的深度优先搜索,它适用于一些谜题,但我的内存却用于其他谜题:

import sys
import Queue

conf = "213 547 689"
grid1 = []
for y in conf.split():
    for x in y:
        grid1.append(int(x))

a = []
explored = set()
sol = Queue.LifoQueue()

def tostr(node):
    s = ''
    for i in range(0,9):
        s += str(node[i]) + ' '
    return s

def printsol():
    while not sol.empty():
        print sol.get()        

def same(x, y, grid):
    for n in neighbors(y):
        ng = grid[:]
        if grid[n] == x and n != y:
            if x == 576:
                printsol()
                sys.exit()
            ng[n] = 2*x
            ng[y] = 1
            sol.put(tostr(ng))
            same(2*x, n, ng)
            solve(ng, grid)
            sol.get()
            ng[n] = 1
            ng[y] = 2*x
            sol.put(tostr(ng))
            same(2*x, y, ng)
            solve(ng, grid)
            sol.get()

##succeeding functions are edited versions of Boggle solver from http://stackoverflow.com/questions/746082/how-to-find-list-of-possible-words-from-a-letter-matrix-boggle-solver
def solve(grid2, grid1):
    for i in range(0,9):
        if grid2[i] < 9 and tostr(grid2) not in explored:
            for result in extending(grid2[i], (i,), grid2):
                newgrid = grid2[:]
                y = len(result) - 1
                for j in range(0, y):
                    newgrid[result[j]] += 1
                newgrid[result[y]] = 9
                sol.put(tostr(newgrid))
                if tostr(newgrid) not in explored:
                    same(9, result[y], newgrid)
                    solve(newgrid, grid2)
                sol.get()
    explored.add(tostr(grid2))

def extending(add, path, grid2):
    if add == 9:
        yield path
    for n in neighbors(path[-1]):
        if n not in path:
            add1 = add + grid2[n]
            if add1 <= 9:
                for result in extending(add1, path + (n,), grid2):
                    yield result

def neighbors(n):
    for nx in range(max(0, n%3-1), min(n%3+2, 3)):
        for ny in range(max(0, n/3-1), min(n/3+2, 3)):
            yield ny*3+nx

sol.put(tostr(grid1))
solve(grid1, grid1)

如何提高效率?如果不是这样,对于知情搜索来说什么是一个很好的启发式?我想考虑从一定数量的板上数字的平均值的绝对差异,但什么是一个好数字?

4 个答案:

答案 0 :(得分:1)

  

游戏的目标是达到1152

我设法做到了!以下是搜索的示例输出。在第一行,三个数字是:状态的得分,图的深度和矩阵中的最大元素。接下来的三个是矩阵本身:

...
-577 246 576
  36  288    9 
   1    1  576 
 144   36   72 

-577 245 576
  36  288   18 
  18    1  576 
 144    9   72 

-577 249 576
   1  288    9 
   1  288  576 
   1    1    1 

-577 245 576
  36  288    1 
  18   18  576 
 144    9   72 

-577 250 576
   1    1    9 
   1  576  576 
   1    1    1 

-1153 251 1152
   1    1    9 
   1    1 1152 
   1    1    1 

-1153 251 1152
   1    1    9 
   1 1152    1 
   1    1    1 

-577 250 576
   1  576    9 
   1    1  576 
   1    1    1 

-1153 251 1152
   1    1    9 
   1    1 1152 
   1    1    1 

-1153 251 1152
   1 1152    9 
   1    1    1 
   1    1    1 
...

正如你所看到的,为了得分 1152 ,你必须深入探索。还要考虑到分支因子非常大,您可以看到问题具有挑战性。您必须执行〜 250 移动才能获得 1152 的分数。假设每次移动需要 10 秒,你将以 40分钟来玩游戏!!!

我所做的是将每个节点/状态的分数关联起来,在探索过程中,我只保留了顶部 1000 节点/状态。这确保我们仅探索相关节点。 所以剩下的就是设计得分函数/启发式算法。这是我尝试过的得分函数:

  • 矩阵中的最大元素
  • 节点的深度
  • 矩阵中元素的总和
  • 分数,告诉我有多少元素被9整除,相邻元素也可以被9整除
  • 矩阵中不同元素的数量
  • 9以上矩阵中不同元素的数量
  • 矩阵中不同元素的数量2
  • 分数,告诉我有多少元素被9整除,是相邻元素,也可以被9整除,两倍小

使用简单的启发式方法可以构建更复杂的方法。我最终得到的是启发式,它只是上述列表中第一个和最后一个的总和。采用更严格的启发式方法会使搜索变得混乱。这是代码(python 3):

import random
from queue import PriorityQueue


dx = [0,  0, -1, -1, -1,  1, 1, 1]
dy = [-1, 1, -1,  0,  1, -1, 0, 1]


class State:
    def __init__(self, a, depth):
        self.depth = depth
        if len(a) == 9:
            self.a = (tuple(a[:3]), tuple(a[3:6]), tuple(a[6:]))
        if len(a) == 3:
            self.a = tuple(map(tuple, a))

    @staticmethod
    def check(i):
        return 0 <= i < 3

    def get_9_moves(self):
        ans = []
        for i in range(3):
            for j in range(3):
                if self.a[i][j] % 9 == 0:

                    for k in range(len(dx)):
                        ni, nj = i + dx[k], j + dy[k]
                        if State.check(ni) and State.check(nj):
                            if self.a[ni][nj] % 9 == 0 and \
                               self.a[i][j] == self.a[ni][nj]:
                                ans.append(((i, j), (ni, nj)))
        return ans

    def get_sum_moves_rec(self, i, j, cur_sum, cur_cells, ans):
        if cur_sum > 9:
            return
        if cur_sum == 9 and len(cur_cells) > 1:
            ans.append(tuple(cur_cells))
        for k in range(len(dx)):
            ni, nj = i + dx[k], j + dy[k]
            pair = (ni, nj)
            if self.check(ni) and self.check(nj) and pair not in cur_cells:
                cur_cells.append(pair)
                self.get_sum_moves_rec(ni, nj, cur_sum + self.a[ni][nj],  cur_cells, ans)
                cur_cells.pop()

    def get_sum_moves(self):
        ans = []
        for i in range(3):
            for j in range(3):
                self.get_sum_moves_rec(i, j, self.a[i][j], [(i, j)], ans)
        return ans

    def get_neighbours(self):
        neighbours = []
        moves = self.get_sum_moves()
        for move in moves:
            a = list(map(list, self.a))
            for i in range(len(move)):
                x, y = move[i]
                if i == len(move)-1:
                    a[x][y] = 9
                else:
                    a[x][y] += 1
            neighbours.append(State(a, self.depth+1))
        moves = self.get_9_moves()
        for move in moves:
            a = list(map(list, self.a))
            a[move[0][0]][move[0][1]] = 1
            a[move[1][0]][move[1][1]] *= 2
            neighbours.append(State(a, self.depth+1))
        return neighbours

    def get_value(self):
        return max(map(max, self.a))

    # 576
    def get_score0(self):
        return -self.get_value()

    def get_score1(self):
        ans = 0
        for i in range(3):
            for j in range(3):
                if self.a[i][j] % 9 == 0:
                    ans += 1
        return ans

    # 36
    def get_score2(self):
        ans = 0
        for i in range(3):
            for j in range(3):
                if self.a[i][j] < 9:
                    ans += 1
        return ans

    # achieves 1152 but rather slow
    def get_score3(self):
        return -self.depth

    # 36
    def get_score4(self):
        ans = 0
        for i in range(3):
            for j in range(3):
                if self.a[i][j] == 1:
                    ans += 1
        return ans

    # 288
    def get_score5(self):
        return -sum(map(sum, self.a))

    def get_score6(self):
        t = []
        for i in range(3):
            for j in range(3):
                if self.a[i][j] % 9 == 0:
                    t.append((self.a[i][j], (i, j)))
        t.sort(reverse=True)
        ans = 0
        for i in range(len(t) - 1):
            pairi = t[i][1]
            pairj = t[i+1][1]

            if abs(pairi[0]-pairj[0]) <= 1 and abs(pairi[1]-pairj[1]) <= 1:
                ans += 1
            break
        return -ans

    def get_score7(self):
        t = []
        for i in range(3):
            for j in range(3):
                if self.a[i][j] % 9 == 0:
                    t.append(self.a[i][j])
        t.sort(reverse=True)
        ans = 0
        for i in range(len(t) - 1):

            if t[i] // t[i+1] == 2:
                ans += 1
            break
        return -ans

    def get_score8(self):
        t = []
        for e in self.a:
            t.extend(e)
        return len(list(filter(lambda x: x >= 9,t)))

    def get_score9(self):
        t = []
        for e in self.a:
            t.extend(e)
        return len(list(filter(lambda x: x <= 2,t)))

    def get_score10(self):
        t = []
        for e in self.a:
            t.extend(e)
        return len(set(filter(lambda x: x >= 9,t)))



    def get_score_mix(self):
        # achieves 1152
        return self.get_score0() + self.get_score6()

    def __repr__(self):
        ans = ''
        for i in range(3):
            for j in range(3):
                ans += '{0:4d} '.format(self.a[i][j])
            ans += '\n'
        return ans

    def __hash__(self):
        return  hash(self.a)

    def __lt__(self, other):
        # hash(self) < hash(other)
        pass


class Solver:

    @staticmethod
    def strategy1(s):
        visited = set()
        while True:
            # print(s)
            neighbours = s.get_neighbours()
            if len(neighbours) == 0:
                break
            ns = None
            for i in range(30):
                ns = random.choice(neighbours)
                if not ns in visited:
                    s = ns
                    break
            if ns is None:
                break
        print(s.get_value())

    @staticmethod
    def strategy2(s):
        visited = set()
        max_depth = 6
        max_value = 0
        calls = 0

        def dfs(state, depth):
            # print(state)
            nonlocal max_value, calls

            calls += 1
            if depth > max_depth:
                return
            if state in visited:
                return
            visited.add(state)
            max_value = max(max_value, s.get_value())

            for new_state in state.get_neighbours():
                dfs(new_state, depth + 1)

        dfs(s, 0)
        print(max_value)
        print(calls)

    @staticmethod
    def strategy3(s):

        visited = set()
        pq = PriorityQueue(maxsize=1000)
        pq.put((0, s))
        visited.add(s)
        max_value = 0

        while not pq.empty():
            score, state = pq.get()
            # print(' score0  score1  score2  score3  score4  score5  score6  score7  score8  score9 score10')
            # print('{0:7d} {1:7d} {2:7d} {3:7d} {4:7d} {5:7d} {6:7d} {7:7d} {8:7d} {9:7d} {10:7d}'.format(state.get_score0(), state.get_score1(), state.get_score2(),
            #                                                                                              state.get_score3(), state.get_score4(), state.get_score5(),
            #                                                                                              state.get_score6(), state.get_score7(), state.get_score8(),
            #                                                                                              state.get_score9(), state.get_score10()))
            print(score, state.depth, state.get_value())
            print(state)
            max_value = max(max_value, state.get_value())

            for new_state in state.get_neighbours():
                # print(random.randint(0, 10))
                if new_state not in visited:
                    visited.add(new_state)
                    pq._put((new_state.get_score_mix(), new_state))
        print(max_value)


start = list(range(1, 10))
random.shuffle(start)
s = State(start, 0)
Solver.strategy3(s)
# s  = State([1,    1,   18, 18,   18,   36, 18 ,  18,    2  ], 0)
# print(s)
#
# for n in s.get_neighbours():
#     print(n)

下一步是什么?

由于程序性能不是很好(找到答案需要大约1分钟),因此可以尝试找到对搜索有用的更好的启发式方法。似乎他们应该非常宽松地试图模拟要求,否则他们会搞乱搜索。

答案 1 :(得分:0)

我前段时间写了一些内容并在此发表:http://sourceforge.net/p/puzzlenumber9

它是一种蛮力搜索,但我对每个深度返回的选项进行排序,并限制深度以使其更快。我现在不记得我是如何排序和限制深度的,LOL。 (我将看看代码,稍后当我有时间时添加...我认为代码只是在最后一次运行具有最高总和的每个深度处设置少量结果。)

我确实记得在看到程序返回解决方案感到满意后,在设备上逐个移动路径似乎很乏味:)

我对这段代码的喜爱是,它基本上是“做好工作”,而不是努力寻找最简洁有效的方法。我特别喜欢长case ix of列表,以破坏对象的方式进行功能简化。

Haskell代码:

{-# OPTIONS_GHC -O2 #-}
import Data.List (sortBy,nubBy)
import Data.Ord (compare)
import System.Time

main = do  
    putStrLn "Puzzle Number 9 Solver Copyright May 2015 alhambra1"
    putStrLn "\nEnter 'e' at any time to exit"
    putStrLn "\nEnter target number"
    target <- getLine  
    if null target  
        then main
        else if head target == 'e'    
                then return ()        
                else do 
                      putStrLn "Enter number of moves at each choice point (density, 3 to 6 recommended)"  
                      density <- getLine  
                      if null density  
                          then main
                          else if head density == 'e'    
                                  then return ()        
                                  else do 
                                        putStrLn "Enter board numbers separated by spaces"  
                                        board <- getLine  
                                        if null board  
                                            then main
                                            else if head board == 'e'    
                                                    then return ()        
                                                    else do 
                                                        putStrLn ""
                                                        time1 <- getClockTime 
                                                        let b = map (\x -> read x :: Int) (take 9 $ words board)
                                                            t = read (head (words target)) :: Int
                                                            d = read (head (words density)) :: Int
                                                        print (map reverse $ reverse $ head $ take 1 $ f t b [] d)
                                                        time2 <- getClockTime
                                                        putStrLn ""
                                                        print (timeDiffToString $ diffClockTimes time2 time1)
                                                        putStrLn ""
                                                        exit

exit = do
     putStrLn "Enter 'a' to start again or 'e' to exit"
     line <- getLine
     if null line 
        then exit
             else if head line == 'a'
                     then do putStrLn ""
                             main
                          else if head line == 'e'
                                  then return ()
                                  else exit

f target board paths toTake
  | not (null hasTarget) = [(((\(x,y,z)-> z) . head $ hasTarget):paths)]
  | null ms              = []
  | otherwise            = do (s,bd,idxs) <- take toTake (sortBy (\(x,y,z) (x',y',z') -> compare x' x) ms')
                              f target bd (idxs:paths) toTake
 where hasTarget = filter ((==target) . (\(x,y,z)-> x)) ms
       ms = moves board
       ms' = nubBy (\(x,y,z) (x',y',z') -> let a = drop 1 (init z)
                                               b = drop 1 (init z')
                                           in if not (null a) && not (null b)
                                                 then a == b
                                                 else False) ms

moves board = do j <- [1..9]
                 let num = board !! (j - 1)
                     board' = (take (j - 1) board) ++ [num + 1] ++ (drop j board)
                 moves' j board' num [j] 0 num
 where moves' ix board s idxs prev next
        | (s == 9 || multiple) && (length idxs > 1) = [(s,board',idxs)]
        | s > 9 && mod s 9 /= 0 = []
        | otherwise = case ix of
            1 -> if elem 2 idxs then [] else moves' 2 board' (s + b) (2:idxs) next b
                 ++ (if elem 4 idxs then [] else moves' 4 board' (s + d) (4:idxs) next d)
                 ++ (if elem 5 idxs then [] else moves' 5 board' (s + e) (5:idxs) next e)
            2 -> if elem 1 idxs then [] else moves' 1 board' (s + a) (1:idxs) next a
                 ++ (if elem 3 idxs then [] else moves' 3 board' (s + c) (3:idxs) next c)
                 ++ (if elem 4 idxs then [] else moves' 4 board' (s + d) (4:idxs) next d)
                 ++ (if elem 5 idxs then [] else moves' 5 board' (s + e) (5:idxs) next e)
                 ++ (if elem 6 idxs then [] else moves' 6 board' (s + f) (6:idxs) next f)
            3 -> if elem 2 idxs then [] else moves' 2 board' (s + b) (2:idxs) next b
                 ++ (if elem 5 idxs then [] else moves' 5 board' (s + e) (5:idxs) next e)
                 ++ (if elem 6 idxs then [] else moves' 6 board' (s + f) (6:idxs) next f)
            4 -> if elem 1 idxs then [] else moves' 1 board' (s + a) (1:idxs) next a
                 ++ (if elem 2 idxs then [] else moves' 2 board' (s + b) (2:idxs) next b)
                 ++ (if elem 5 idxs then [] else moves' 5 board' (s + e) (5:idxs) next e)
                 ++ (if elem 7 idxs then [] else moves' 7 board' (s + g) (7:idxs) next g)
                 ++ (if elem 8 idxs then [] else moves' 8 board' (s + h) (8:idxs) next h)
            5 -> if elem 1 idxs then [] else moves' 1 board' (s + a) (1:idxs) next a
                 ++ (if elem 2 idxs then [] else moves' 2 board' (s + b) (2:idxs) next b)
                 ++ (if elem 3 idxs then [] else moves' 3 board' (s + c) (3:idxs) next c)
                 ++ (if elem 4 idxs then [] else moves' 4 board' (s + d) (4:idxs) next d)
                 ++ (if elem 6 idxs then [] else moves' 6 board' (s + f) (6:idxs) next f)
                 ++ (if elem 7 idxs then [] else moves' 7 board' (s + g) (7:idxs) next g)
                 ++ (if elem 8 idxs then [] else moves' 8 board' (s + h) (8:idxs) next h)
                 ++ (if elem 9 idxs then [] else moves' 9 board' (s + i) (9:idxs) next i)
            6 -> if elem 2 idxs then [] else moves' 2 board' (s + b) (2:idxs) next b
                 ++ (if elem 3 idxs then [] else moves' 3 board' (s + c) (3:idxs) next c)
                 ++ (if elem 5 idxs then [] else moves' 5 board' (s + e) (5:idxs) next e)
                 ++ (if elem 8 idxs then [] else moves' 8 board' (s + h) (8:idxs) next h)
                 ++ (if elem 9 idxs then [] else moves' 9 board' (s + i) (9:idxs) next i)
            7 -> if elem 4 idxs then [] else moves' 4 board' (s + d) (4:idxs) next d
                 ++ (if elem 5 idxs then [] else moves' 5 board' (s + e) (5:idxs) next e)
                 ++ (if elem 8 idxs then [] else moves' 8 board' (s + h) (8:idxs) next h)
            8 -> if elem 4 idxs then [] else moves' 4 board' (s + d) (4:idxs) next d
                 ++ (if elem 5 idxs then [] else moves' 5 board' (s + e) (5:idxs) next e)
                 ++ (if elem 6 idxs then [] else moves' 6 board' (s + f) (6:idxs) next f)
                 ++ (if elem 7 idxs then [] else moves' 7 board' (s + g) (7:idxs) next g)
                 ++ (if elem 9 idxs then [] else moves' 9 board' (s + i) (9:idxs) next i)
            9 -> if elem 5 idxs then [] else moves' 5 board' (s + e) (5:idxs) next e
                 ++ (if elem 6 idxs then [] else moves' 6 board' (s + f) (6:idxs) next f)
                 ++ (if elem 8 idxs then [] else moves' 8 board' (s + h) (8:idxs) next h)
          where multiple = length idxs == 2 && prev == next && mod s 9 == 0
                [a,b,c,d,e,f,g,h,i] = board
                board' = if s == 9
                            then (take (headIdxs - 1) board) ++ [9] ++ (drop headIdxs board)
                            else if multiple
                                    then board''
                                    else (take (headIdxs - 1) board) ++ [next + 1] ++ (drop headIdxs board)
                board'' = map (\(x,y) -> if x == headIdxs then y * 2 else if x == last idxs then 1 else y) (zip [1..] board)
                headIdxs = head idxs

答案 2 :(得分:0)

  • 深度 - 首先评估更深的深度。大大减少内存使用并更快地解决问题。在第9号拼图中,解决所需的移动数量有一个上限,因此深度优先可以安全地用于此问题。
  • 所有小于9的数字的总和 - 更喜欢使用更多数字,以便最终不会处于全部状态。
  • 使用集合跟踪以前的状态 - 使用集合来跟踪已访问的状态。减少运行时间。

我制作了一个Python程序来解决这个问题,并且它尝试移动的顺序中唯一真正的智能就是这两件事。最糟糕的解决问题的时间似乎需要大约30秒。平均值更像是3。

深度优先搜索结合Python垃圾收集使内存使用量低于50MB。

答案 3 :(得分:-1)

问题非常简单:DFS将遍历一条路径,直到它结束。因此,您最终会得到一个巨大的discovered集合,其中包含可能永远不会被使用的高值。改为使用BFS或A *。