如何从单词列表创建一个Boggle Board? (反向Boggle求解器!)

时间:2014-02-06 04:20:33

标签: python algorithm boggle

我正在尝试解决反向Boggle问题。简单地说,给定一个单词列表,得出一个4x4字母的字母,其中列表中的多个单词可以在相邻字母的序列中找到(字母正交和对角相邻)。

我不想拿一块已知的板子来解决它。这是一个简单的TRIE问题,已经在人们的CS项目中讨论/解决了死亡。

单词列表示例:

margays, jaguars, cougars, tomcats, margay, jaguar, cougar, pumas, puma, toms

解决方案:

ATJY
CTSA
OMGS
PUAR

这个问题很难(对我来说)。我到目前为止的算法:

  1. 对于输入中的每个单词,列出其可以合法地在单板上显示的所有可能方式。
  2. 尝试在这些板上放置#2字的所有可能组合,并保留没有冲突的那些。
  3. 重复直到列表末尾。
  4. ...
  5. 利润!!! (对于那些阅读/.)
  6. 的人

    显然,有实施细节。首先从最长的单词开始。忽略作为其他单词的子串的单词。

    我可以在大约0.4秒内为7个字符的单词生成所有68k可能的板。然后,当我添加一个额外的7个字符板时,我需要比较68k x 68k板x 7比较。解决时间变得冰冷。

    必须有更好的方法来做到这一点!!!!

    一些代码:

    BOARD_SIDE_LENGTH = 4
    
    class Board:
        def __init__(self):
            pass
    
        def setup(self, word, start_position):
            self.word = word
            self.indexSequence = [start_position,]
            self.letters_left_over = word[1:]
            self.overlay = []
            # set up template for overlay.  When we compare boards, we will add to this if the board fits
            for i in range(BOARD_SIDE_LENGTH*BOARD_SIDE_LENGTH):
                self.overlay.append('')
            self.overlay[start_position] = word[0]
            self.overlay_count = 0
    
        @classmethod
        def copy(boardClass, board):
            newBoard = boardClass()
            newBoard.word = board.word
            newBoard.indexSequence = board.indexSequence[:]
            newBoard.letters_left_over = board.letters_left_over
            newBoard.overlay = board.overlay[:]
            newBoard.overlay_count = board.overlay_count
            return newBoard
    
        # need to check if otherboard will fit into existing board (allowed to use blank spaces!)
        # otherBoard will always be just a single word
        @classmethod
        def testOverlay(self, this_board, otherBoard):
            for pos in otherBoard.indexSequence:
                this_board_letter = this_board.overlay[pos]
                other_board_letter = otherBoard.overlay[pos]
                if this_board_letter == '' or other_board_letter == '':
                    continue
                elif this_board_letter == other_board_letter:
                    continue
                else:
                    return False
            return True
    
        @classmethod
        def doOverlay(self, this_board, otherBoard):
            # otherBoard will always be just a single word
            for pos in otherBoard.indexSequence:
                this_board.overlay[pos] = otherBoard.overlay[pos]
            this_board.overlay_count = this_board.overlay_count + 1
    
        @classmethod
        def newFromBoard(boardClass, board, next_position):
            newBoard = boardClass()
            newBoard.indexSequence = board.indexSequence + [next_position]
            newBoard.word = board.word
            newBoard.overlay = board.overlay[:]
            newBoard.overlay[next_position] = board.letters_left_over[0]    
            newBoard.letters_left_over = board.letters_left_over[1:]
            newBoard.overlay_count = board.overlay_count
            return newBoard
    
        def getValidCoordinates(self, board, position):
            row = position / 4
            column = position % 4
            for r in range(row - 1, row + 2):
                for c in range(column - 1, column + 2):
                    if r >= 0 and r < BOARD_SIDE_LENGTH and c >= 0 and c < BOARD_SIDE_LENGTH:
                        if (r*BOARD_SIDE_LENGTH+c not in board.indexSequence):
                            yield r, c
    
    class boardgen:
        def __init__(self):
            self.boards = []
    
        def createAll(self, board):
            # get the next letter
            if len(board.letters_left_over) == 0:
                self.boards.append(board)
                return
            next_letter = board.letters_left_over[0]    
            last_position = board.indexSequence[-1]
            for row, column in board.getValidCoordinates(board, last_position):
                new_board = Board.newFromBoard(board, row*BOARD_SIDE_LENGTH+column)
                self.createAll(new_board)
    

    并像这样使用它:

    words = ['margays', 'jaguars', 'cougars', 'tomcats', 'margay', 'jaguar', 'cougar', 'pumas', 'puma']
    words.sort(key=len)
    
    first_word = words.pop()
    
    # generate all boards for the first word
    overlaid_boards = []
    for i in range(BOARD_SIDE_LENGTH*BOARD_SIDE_LENGTH):
        test_board = Board()
        test_board.setup(first_word, i)
        generator = boardgen()
        generator.createAll(test_board)
        overlaid_boards += generator.boards
    

2 个答案:

答案 0 :(得分:1)

这是一个有趣的问题。我无法提出完整的优化解决方案,但您可以尝试一些想法。

如果你不能适应所有单词,那么困难的部分是找到最佳子集的要求。这将增加复杂性。首先要消除明显不起作用的单词组合。用> 16个字母剪切任何单词。计算所需的唯一字母数。一定要考虑在同一个单词中重复出现的字母。例如,如果列表中包含“eagle”,我认为您不允许对该单词的两端使用相同的“e”。如果您需要的字母列表> 16,则必须删除一些字词。找出首先要切割的是一个有趣的子问题......我从含有最少使用字母的单词开始。按分数排序所有子列表可能会有所帮助。

然后你可以完成字长总和<16的微不足道的情况。之后,您将从完整的单词列表开始,看看是否有解决方案。如果没有,请找出要删除的单词,然后重试。

根据单词列表,核心算法是找到包含的网格(如果存在) 所有这些话。

愚蠢的蛮力方式是用您需要的字母迭代所有可能的网格,并测试每个网格以查看您的所有单词是否合适。但它非常苛刻:中间案例是16! = 2x10exp13板。 n个独特字母的确切公式是......(16!)/(16-n)! x pow(n,16-n)。这给出了3x10exp16范围内的最坏情况。不太可管理。 即使您可以避免旋转和翻转,也只能节省1/16的搜索空间。

一种更聪明的贪婪算法是按照某些标准对单词进行排序,例如难度或长度。递归解决方案是将列表中的顶部单词保留,并尝试将其放在网格上。然后使用该网格和剩余的单词列表进行递归。如果在用完单词之前填满网格,那么你必须回溯并尝试另一种放置单词的方法。更贪婪的方法是尝试首先重复使用最多字母的展示位置。 你可以做一些修剪。如果在任何时候网格中剩余的空格数小于所需的剩余唯一字母数,那么您可以消除这些子树。还有一些其他情况显然没有可以削减的解决方案,特别是当剩余的网格空间<1时。最后一个字的长度。 搜索空间取决于字长和重复使用的字母数。我敢肯定它比蛮力好,但我不知道这是否足以使问题合理。

聪明的方法是使用某种形式的动态编程。我无法完全看到完整的算法。一个想法是有一个字母的树或图形,将每个字母连接到单词列表中的“相邻”字母。然后从最连接的字母开始,尝试将树映射到网格上。始终放置完成单词列表大部分的字母。必须有一些方法来处理网格中多个相同字母的情况。而且我不确定如何订购,因此您不必搜索所有组合。

最好的方法是拥有一个动态算法,该算法还包括所有子词列表。因此,如果列表中有“雾”和“狐狸”,并且狐狸不适合雾,那么就可以处理它,而无需在列表的两个版本上运行整个事情。这增加了复杂性,因为现在你必须按照分数对每个解决方案进行排名。但是在所有单词都不适合的情况下,它会节省很多时间。

祝你好运。

答案 1 :(得分:0)

有一些关于加速回溯搜索的一般想法,你可以尝试:

1)提前检查。它通常有助于丢弃尽可能无法尽早工作的部分解决方案,即使以更多工作为代价也是如此。考虑通过砍掉你试图适合的单词而产生的所有双字符序列 - 例如PUMAS贡献PU,UM,MA和AS。这些必须全部出现在最终答案中。如果部分解决方案没有足够的重叠双字符空间可以包含它还没有的所有重叠的双字符序列,那么它就无法扩展到最终答案。

2)对称性。我认为如果你试图证明没有解决方案,这可能是最有用的。给定一种填充板的方法,您可以旋转并反映该解决方案,以找到填充板的其他方式。如果你有68K起点而且一个起点是另一个起点的旋转或反射,你不需要同时尝试两者,因为如果你能够(或者可以)从一个起点解决问题,你可以得到答案通过旋转或反射板的另一个起点。因此,您可以将需要尝试的起点数除以某个整数。

这个问题不是每个阶段都有大量替代品的问题。这也影响了旅行商问题。如果你不能保证你会找到绝对的最佳答案,那么你可以尝试不跟进这些68k选择中最不可预测的。您需要某种分数来决定保留哪些分数 - 您可能希望保留尽可能多的字母使用的分数。一些针对旅行商问题的程序很早就丢弃了节点之间没有希望的链接。丢弃部分解决方案而不是进行全深度优先搜索或分支绑定的更一般方法是有限差异搜索 - 例如参见http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.34.2426

当然,TSP丢弃树搜索的一些方法完全支持某种爬山方法。您可以从填充的boggle方块开始,并反复尝试在其中找到您的单词,修改几个字符以强制它们进入,尝试找到连续增加可在广场中找到的单词数量的步骤。最简单的登山形式是从多个随机开始重复简单的爬山。另一种方法是通过随机化一小部分解决方案来重新开始爬山 - 因为你不知道随机化的部分的最佳大小你可能决定选择随机化随机化的部分大小,以便在至少有一小部分时间你正在随机化正确的区域大小以产生一个新的方块开始。遗传算法和模拟退火在这里非常流行。关于一个新想法,即延迟接受爬坡的论文,也描述了它的一些竞争对手 - http://www.cs.nott.ac.uk/~yxb/LAHC/LAHC-TR.pdf