解决填字游戏

时间:2017-06-17 16:40:00

标签: java algorithm backtracking brute-force

我有一个填字游戏和一个可以用来解决它的单词列表(单词可以多次或不一次)。对于给定的填字游戏和单词列表,始终存在解决方案。

我搜索了如何解决这个问题的线索,发现它是NP-Complete。我的最大填字游戏大小是250乘250,列表的最大长度(可以用来解决它的单词数量)是200.我的目标是通过强力/回溯来解决这个大小的填字游戏,这应该是可能的几秒钟(这是我的粗略估计,如果我错了,请纠正我。)

示例:

可用于解决填字游戏的给定单词列表:

  • 可以
  • 音乐
  • 金枪鱼

给定的空填字游戏(X是无法填写的字段,需要填充空字段):

An empty crossword which needs to be solved

解决方案:

The solution of the problem above

现在我的方法是将填字游戏表示为二维数组,并搜索空格(填字游戏上的2次迭代)。然后我根据它们的长度将单词与空格匹配,然后我尝试所有单词组合来清空具有相同长度的空格。这种方法变得非常混乱非常快,我迷失了尝试实现这个,是否有更优雅的解决方案?

5 个答案:

答案 0 :(得分:4)

你的基本想法非常明智:

  1. 识别黑板上的插槽。
  2. 使用适合的每个单词尝试每个插槽。
  3. 如果每个插槽都可以填充而不会发生冲突,则会解决。
  4. 这是一个很好的计划。 下一步是将其转化为设计。 对于像这样的小程序,我们可以直接使用伪代码。 正如其他答案所解释的那样,它的要点是recursion

    1  Draw a slot from the slot pool.
    2     If slot pool is empty (all slots filled), stop solving.
    3  For each word with correct length:
    4     If part of the slot is filled, check conflict.
    5        If the word does not fit, continue the loop to next word.
          // No conflict
    6     Fill the slot with the word.
          // Try next slot (down a level)
    7     Recur from step 1.
    8     If the recur found no solution, revert (take the word back) and try next.
       // None of them works
    9  If no words yield a solution, an upper level need to try another word.
       Revert (put the slot back) and go back.
    

    以下是我根据您的要求制作的简短但完整的示例。

      

    给猫皮肤涂抹的方法不止一种。   我的代码交换了第1步和第2步,并在一个填充循环中结合了第4步到第6步。

    关键点:

    • 使用格式化程序使代码符合您的风格。
    • 2D板存储在row-major order中的线性字符数组中。
    • 这样可以按clone()保存并按arraycopy恢复。
    • 创建时,将从两个方向扫描两个插槽中的插槽。
    • 两个插槽列表由同一个循环解决,主要区别在于如何填充插槽。
    • 显示重复过程,以便您可以看到它是如何工作的。
    • 做出了许多假设。没有单个字母插槽,所有单词在相同的情况下,电路板都是正确的等等。
    • 要有耐心。学习新的东西,给自己时间吸收它。

    来源:

    import java.awt.Point;
    import java.util.*;
    import java.util.function.BiFunction;
    import java.util.function.Supplier;
    import java.util.stream.Stream;
    
    public class Crossword {
    
       public static void main ( String[] args ) {
          new Crossword( Arrays.asList( "5 4 4\n#_#_#\n_____\n#_##_\n#_##_\ntuna\nmusic\ncan\nhi".split( "\n" ) ) );
          new Crossword( Arrays.asList( "6 6 4\n##_###\n#____#\n___#__\n#_##_#\n#____#\n##_###\nnice\npain\npal\nid".split( "\n" ) ) );
       }
    
       private final int height, width; // Board size
       private final char[] board; // Current board state.  _ is unfilled.  # is blocked.  other characters are filled.
       private final Set<String> words; // List of words
       private final Map<Point, Integer> vertical = new HashMap<>(), horizontal = new HashMap<>();  // Vertical and horizontal slots
    
       private String indent = ""; // For formatting log
       private void log ( String message, Object... args ) { System.out.println( indent + String.format( message, args ) ); }
    
       private Crossword ( List<String> lines ) {
          // Parse input data
          final int[] sizes = Stream.of( lines.get(0).split( "\\s+" ) ).mapToInt( Integer::parseInt ).toArray();
          width = sizes[0];  height = sizes[1];
          board = String.join( "", lines.subList( 1, height+1 ) ).toCharArray();
          words = new HashSet<>( lines.subList( height+1, lines.size() ) );
          // Find horizontal slots then vertical slots
          for ( int y = 0, size ; y < height ; y++ )
             for ( int x = 0 ; x < width-1 ; x++ )
                if ( isSpace( x, y ) && isSpace( x+1, y ) ) {
                   for ( size = 2 ; x+size < width && isSpace( x+size, y ) ; size++ ); // Find slot size
                   horizontal.put( new Point( x, y ), size );
                   x += size; // Skip past this horizontal slot
                }
          for ( int x = 0, size ; x < width ; x++ )
             for ( int y = 0 ; y < height-1 ; y++ )
                if ( isSpace( x, y ) && isSpace( x, y+1 ) ) {
                   for ( size = 2 ; y+size < height && isSpace( x, y+size ) ; size++ ); // Find slot size
                   vertical.put( new Point( x, y ), size );
                   y += size; // Skip past this vertical slot
                }
          log( "A " + width + "x" + height + " board, " + vertical.size() + " vertical, " + horizontal.size() + " horizontal." );
          // Solve the crossword, horizontal first then vertical
          final boolean solved = solveHorizontal();
          // Show board, either fully filled or totally empty.
          for ( int i = 0 ; i < board.length ; i++ ) {
             if ( i % width == 0 ) System.out.println();
             System.out.print( board[i] );
          }
          System.out.println( solved ? "\n" : "\nNo solution found\n" );
       }
    
       // Helper functions to check or set board cell
       private char get ( int x, int y ) { return board[ y * width + x ]; }
       private void set ( int x, int y, char character ) { board[ y * width + x ] = character; }
       private boolean isSpace ( int x, int y ) { return get( x, y ) == '_'; }
    
       // Fit all horizontal slots, when success move to solve vertical.
       private boolean solveHorizontal () {
          return solve( horizontal, this::fitHorizontal, "horizontally", this::solveVertical );
       }
       // Fit all vertical slots, report success when done
       private boolean solveVertical () {
          return solve( vertical, this::fitVertical, "vertically", () -> true );
       }
    
       // Recur each slot, try every word in a loop.  When all slots of this kind are filled successfully, run next stage.
       private boolean solve ( Map<Point, Integer> slot, BiFunction<Point, String, Boolean> fill, String dir, Supplier<Boolean> next ) {
          if ( slot.isEmpty() ) return next.get(); // If finished, move to next stage.
          final Point pos = slot.keySet().iterator().next();
          final int size = slot.remove( pos );
          final char[] state = board.clone();
          /* Try each word */                                                   indent += "  ";
          for ( String word : words ) {
             if ( word.length() != size ) continue;
             /* If the word fit, recur. If recur success, done! */              log( "Trying %s %s at %d,%d", word, dir, pos.x, pos.y );
             if ( fill.apply( pos, word ) && solve( slot, fill, dir, next ) )
                return true;
             /* Doesn't match. Restore board and try next word */               log( "%s failed %s at %d,%d", word, dir, pos.x, pos.y );
             System.arraycopy( state, 0, board, 0, board.length );
          }
          /* No match.  Restore slot and report failure */                      indent = indent.substring( 0, indent.length() - 2 );
          slot.put( pos, size );
          return false;
       }
    
       // Try fit a word to a slot.  Return false if there is a conflict.
       private boolean fitHorizontal ( Point pos, String word ) {
          final int x = pos.x, y = pos.y;
          for ( int i = 0 ; i < word.length() ; i++ ) {
             if ( ! isSpace( x+i, y ) && get( x+i, y ) != word.charAt( i ) ) return false; // Conflict
             set( x+i, y, word.charAt( i ) );
          }
          return true;
       }
       private boolean fitVertical ( Point pos, String word ) {
          final int x = pos.x, y = pos.y;
          for ( int i = 0 ; i < word.length() ; i++ ) {
             if ( ! isSpace( x, y+i ) && get( x, y+i ) != word.charAt( i ) ) return false; // Conflict
             set( x, y+i, word.charAt( i ) );
          }
          return true;
       }
    }
    
      

    练习:你可以rewrite递归到迭代;更快,可以支持更大的电路板。   一旦完成,它可以转换为多线程并运行得更快。

答案 1 :(得分:1)

你是对的问题是NP - 完成。所以你最好的机会就是用蛮力来解决它(如果你找到一个多项式算法,请告诉我,我们都可以富有=)。)

我建议你看一下 https://httpd.apache.org/docs/2.4/rewrite/remapping.html 。它将允许您为填字游戏问题编写一个优雅(但输入大小很慢)的解决方案。

如果您需要更多鼓舞人心的材料,请查看使用回溯作为导航解决方案树的方法的backtracking

请注意,有些算法在实践中可能比纯蛮力(即使仍具有指数复杂性)表现更好。 此外,快速搜索学者会发现有关该主题的大量论文,您可能需要查看这些论文,例如:

答案 2 :(得分:1)

为了使这个问题更容易解决,我将其分解为更小,更容易的问题。请注意,我不包括代码/算法,因为我认为这在这里没有帮助(如果我们想要最好的代码,那么会有索引和数据库以及黑魔法让你的头部爆炸只是看到它)。相反,这个答案试图通过讨论有助于OP解决这个问题(以及未来的问题)的思维方法来回答这个问题,使用最适合读者的方法。

您需要知道的事情

此答案假设您知道如何执行以下操作

  • 创建和使用具有属性和功能的对象
  • 选择一个适用于您的内容的数据结构(不一定好)。

建模空间

因此,将填字游戏加载到n×m矩阵(2D数组,特此是&#39; grid&#39;)中非常容易,但这是非常合理的。因此,我们首先将您的填字游戏从网格解析为合法对象。

就您的程序需要知道,填字游戏中的每个条目都有4个属性。

  1. 第一个字母的网格中的X-Y坐标
  2. 方向(向下或跨越)
  3. 字长
  4. 字数值
  5. 绑定索引的映射
    • 键:与其他条目共享的单词索引
    • 值:索引与
    • 共享的条目
    • (你可以把它作为一个元组并包含来自其他条目的共享索引以便于参考)
  6. 您可以在扫描时根据这些规则在网格中找到这些内容。

    1. 如果Row_1_up已关闭且Row_1_down已打开,则这是一个向下字的起始索引。 (向下扫描长度。对于绑定索引,左侧或右侧空间将打开。向左扫描以获取链接条目coord-id)
    2. 与1相同但在单词之间旋转(您可以在扫描1的同时执行此操作)
    3. 在填字游戏对象中,您可以使用坐标+方向存储条目作为键,以便于参考和轻松转换为文本网格形式。

      使用您的模型

      您现在应该有一个包含填字游戏条目集合的对象,其中包含相关的索引绑定。您现在需要找到一组满足所有条目的值。

      你的输入对象应该有isValidEntry(str)等辅助方法来检查给定的值,以及填字游戏的当前状态,我可以把这个词放在这里吗?通过使模型中的每个对象负责其自己的逻辑级别,一个思想层的问题代码可以调用逻辑而不用担心它的实现(在这个例子中,你的求解器不会必须担心值是有效的逻辑,它可以只问isValidEntry

      如果您已经完成了上述权利,那么解决问题就是迭代所有条目以查找解决方案的简单问题。

      子问题列表

      作为参考,这里是我要写一些要解决的问题的子问题列表。

      • 我如何理想地为我的工作空间建模,这对我来说很容易合作?
      • 对于我的每个模型,它需要知道什么?它可以为我处理什么逻辑?
      • 如何将文本输入转换为可用的模型对象?
      • 如何使用模型对象解决问题? (对你而言,迭代所有单词/所有条目以找到有效集合。也许使用递归)

答案 3 :(得分:0)

填字游戏是一种约束满足问题,通常是NP-Complete,但有许多求解器会将最有效的算法应用于您指定的约束问题。 Z3 SMT求解器可以非常容易地和大规模地解决这些问题。您所要做的就是编写一个Java程序,将填字游戏转换为解决者可以理解的SMT问题,然后将其提供给求解器来解决它。 Z3有Java绑定所以它应该非常简单。我已经编写了Z3代码来解决下面的第一个例子。您应该不难在Java程序中遵循模式来指定任意大的十字路口谜题。

; Declare each possible word as string literals
(define-const str1 String "tuna")
(define-const str2 String "music")
(define-const str3 String "can")
(define-const str4 String "hi")

; Define a function that returns true if the given String is equal to one of the possible words defined above.
(define-fun validString ((s String)) Bool 
    (or (= s str1) (or (= s str2) (or (= s str3) (= s str4)))))

; Declare the strings that need to be solved
(declare-const unknownStr1 String)
(declare-const unknownStr2 String)
(declare-const unknownStr3 String)
(declare-const unknownStr4 String)

; Assert the correct lengths for each of the unknown strings.
(assert (= (str.len unknownStr1) 4))
(assert (= (str.len unknownStr2) 5))
(assert (= (str.len unknownStr3) 3))
(assert (= (str.len unknownStr4) 2))

; Assert each of the unknown strings is one of the possible words.
(assert (validString unknownStr1))
(assert (validString unknownStr2))
(assert (validString unknownStr3))
(assert (validString unknownStr4))

; Where one word in the crossword puzzle intersects another assert that the characters at the intersection point are equal.
(assert (= (str.at unknownStr1 1) (str.at unknownStr2 1)))
(assert (= (str.at unknownStr2 3) (str.at unknownStr4 1)))
(assert (= (str.at unknownStr2 4) (str.at unknownStr3 0)))

; Solve the model
(check-sat)
(get-model)

我推荐使用Z3 SMT求解器,但还有很多其他约束求解器。除了需要实现自己的排序算法之外,您无需再实现自己的约束求解算法。

答案 4 :(得分:0)

我刚刚在Scala中实现了代码来解决此类难题。我只是使用递归来解决问题。简而言之,对于每个单词,我都会找到所有可能的空位,然后选择一个空位并用单词填充它,然后尝试通过递归来解决部分难题。如果难题无法用其余的单词填充,它将尝试另一个槽,以此类推,否则,难题就解决了。

这是我的代码的链接: https://github.com/mysilver/AMP/blob/master/Crossword.scala