web-dev-qa-db-fra.com

Résoudre les mots croisés

J'ai un mot croisé et une liste de mots qui peuvent être utilisés pour le résoudre (les mots peuvent être placés plusieurs fois ou même pas une fois ). Il y a toujours une solution pour les mots croisés et la liste de mots donnés.

J'ai cherché des indices sur la façon de résoudre ce problème et j'ai découvert qu'il s'agissait de NP-Complete. Ma taille maximale de mots croisés est de 250 sur 250, la longueur maximale de la liste (nombre de mots pouvant être utilisés pour le résoudre) est de 200. Mon objectif est de résoudre les mots croisés de cette taille par la force brute/le retour en arrière, ce qui devrait être possible quelques secondes (c'est une estimation approximative de ma part, corrigez-moi si je me trompe).

Exemple:

Une liste de mots donnés pouvant être utilisés pour résoudre les mots croisés:

  • pouvez
  • la musique
  • thon
  • salut

Les mots croisés vides donnés (X sont des champs qui ne peuvent pas être remplis, les champs vides doivent être remplis):

 An empty crossword which needs to be solved

La solution:

 The solution of the problem above

Maintenant, mon approche actuelle consiste à représenter les mots croisés sous forme de tableau à deux dimensions et à rechercher des espaces vides (deux itérations sur les mots croisés). Ensuite, je fais correspondre les mots aux espaces vides en fonction de leur longueur, puis j'essaie toutes les combinaisons de mots pour créer des espaces vides de même longueur. Cette approche est devenue très compliquée très rapidement, je me suis perdue en essayant de la mettre en œuvre, existe-t-il une solution plus élégante?

9
Anna Vopureta

L'idée de base que vous avez est assez sensée:

  1. Identifiez les emplacements sur le tableau.
  2. Essayez chaque emplacement avec chaque mot qui convient.
  3. Si tous les créneaux peuvent être remplis sans conflit, le problème est résolu.

C'est un excellent plan. L'étape suivante consiste à le transformer en un dessin. Pour de petits programmes comme celui-ci, nous pouvons passer directement au pseudo-code. En résumé: expliqué par d’autres réponses, est récursion :

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.

Vous trouverez ci-dessous un exemple court mais complet que je vous ai concocté à partir de vos exigences.

Il existe plusieurs façons de traiter un chat. Mon code a permuté les étapes 1 et 2 et combine les étapes 4 à 6 dans une boucle de remplissage.

Points clés:

  • Utilisez un formateur pour adapter le code à votre style.
  • Le tableau 2D est stocké dans un tableau de caractères linéaire dans rang-majeur .
  • Cela permet à la carte d'être sauvegardée par clone() et restaurée par arraycopy .
  • Lors de la création, la carte est analysée pour rechercher des créneaux en deux passes dans deux directions.
  • Les deux listes de créneaux sont résolues par la même boucle et diffèrent principalement par la façon dont les créneaux sont remplis.
  • Le processus récurrent est affiché pour que vous puissiez voir comment cela fonctionne.
  • Beaucoup d'hypothèses sont faites. Pas de fente d'une seule lettre, tous les mots dans le même cas, le tableau est correct, etc.
  • Sois patient. Apprenez ce qui est nouveau et donnez-vous le temps de l'absorber.

La source:

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;
   }
}

Exercice: Vous pouvez réécrire une récursivité à une itération; plus rapide et peut supporter de plus grandes planches. Une fois cela fait, il peut être converti en multi-thread et courir encore plus vite.

4
Sheepy

Pour rendre ce problème plus facile à résoudre, je vais le décomposer en problèmes plus petits et plus faciles. Notez que je n’inclus pas le code/les algorithmes, car je pense que cela n’aidera pas ici (si nous voulions le meilleur code, il y aurait des index, des bases de données et une magie noire qui ferait exploser votre tête en le voyant). Au lieu de cela, cette réponse tente de répondre à la question en parlant de méthodes de pensée qui aideront le PO à s’attaquer à ce problème (et à ceux à venir) en utilisant la méthode qui convient le mieux au lecteur.

Que souhaitez-vous savoir

Cette réponse suppose que vous savez comment procéder comme suit

  • Créer et utiliser des objets ayant des propriétés et des fonctions
  • Choisissez une structure de données qui fonctionne (pas nécessairement bien) pour ce que vous voulez faire avec son contenu.

Modéliser votre espace

Ainsi, il est assez facile de charger vos mots croisés dans une matrice n par m (tableau 2D, ci-après «grille»), mais il est très clair que cela fonctionne avec pragmatisme. Commençons donc par analyser vos mots croisés d’une grille à un objet légitime.

Pour autant que votre programme ait besoin de le savoir, chaque entrée dans les mots croisés a 4 propriétés. 

  1. Une coordonnée X-Y dans la grille pour la première lettre
  2. Une direction (en bas ou en travers)
  3. Longueur du mot
  4. Valeur du mot
  5. Carte des index liés
    • Key: Index de Word partagé avec une autre entrée
    • Valeur: entrée avec laquelle cet index est partagé
    • (Vous pouvez en faire un tuple et inclure l'index partagé de l'autre entrée pour faciliter la comparaison)

Vous pouvez les trouver dans la grille en fonction de ces règles lors de l'analyse.

  1. Si Row_1_up est fermé et que Row_1_down est ouvert, il s'agit de l'index de début d'un mot vers le bas. (recherche d'une longueur. Pour les index liés, un espace gauche ou droit sera ouvert. scannez à gauche pour obtenir les coordonnées de l'entrée liée)
  2. Identique à 1 mais pivoté pour plusieurs mots (vous pouvez le faire en même temps que l'analyse pour 1)

Dans votre objet de mots croisés, vous pouvez stocker les entrées en utilisant la coordonnée + direction comme clé pour faciliter la référence et la conversion facile vers/à partir du formulaire de grille de texte.

Utiliser votre modèle

Vous devriez maintenant avoir un objet contenant une collection d'entrées de mots croisés, contenant leurs liaisons d'index pertinentes. Vous devez maintenant trouver un ensemble de valeurs qui satisferont toutes vos entrées.

Vos objets d'entrée doivent avoir des méthodes d'assistance telles que isValidEntry(str) qui vérifie la valeur donnée et l'état actuel des mots croisés, puis-je mettre ce mot ici? En rendant chaque objet de votre modèle responsable de son propre niveau de logique, le code correspondant au problème posé par la pensée peut simplement appeler la logique sans se soucier de son implémentation (dans cet exemple, votre solveur n'a pas à s'inquiéter de la logique. of est une valeur valide, il peut simplement demander isValidEntry pour cela)

Si vous avez fait ce qui est dit ci-dessus, résoudre le problème est alors une simple question d’itération sur tous les mots pour que toutes les entrées trouvent une solution.

Liste de sous problèmes

Pour référence, voici ma liste de sous problèmes que vous devez écrire quelque chose à résoudre.

  • Comment puis-je idéalement modéliser mon espace de travail avec lequel il est facile de travailler?
  • Que doit-il savoir pour chaque pièce de mon modèle? Quelle logique peut-il gérer pour moi?
  • Comment puis-je transformer ma saisie de texte en un objet de modèle utilisable?
  • Comment résoudre mon problème en utilisant mes objets de modèle? (Pour vous, il s'agit d'itérer tous les mots/toutes les entrées pour trouver un ensemble valide. Peut-être en utilisant la récursivité)
1
Tezra

Vous avez raison, le problème est NP- complet. Donc, votre meilleure chance est de le résoudre par la force brute (si vous trouvez un algorithme polynomial, dites-le-moi, nous pouvons tous deux être riches =)).

Ce que je vous suggère est de regarderbacktracking. Cela vous permettra d’écrire une solution élégante (et néanmoins lente compte tenu de la taille de votre saisie) au problème des mots croisés.

Si vous avez besoin de matériel plus stimulant, jetez un coup d'œil à ce solveur qui utilise le retour en arrière comme méthode de navigation dans l'arborescence de la solution.

Notez qu'il existe des algorithmes qui pourraient en pratique fonctionner mieux qu'une force brute pure (même si leur complexité est toujours exponentielle). De plus, une recherche rapide sur scholar révèle un bon nombre d'articles. sur le sujet que vous voudrez peut-être examiner, tels que les suivants:

1
Davide Spataro

Un mot croisé est un problème de satisfaction de contrainte qui est généralement un NP-Complet, mais de nombreux solveurs appliqueront les algorithmes les plus efficaces à un problème de contrainte que vous spécifiez. Le solutionneur Z3 SMT peut résoudre ces problèmes très facilement et à grande échelle. Tout ce que vous avez à faire est d’écrire un programme Java qui transforme le jeu de mots croisés en un problème SMT que le solutionneur peut comprendre puis le donne au solutionneur pour le résoudre. Z3 a des liaisons Java donc ça devrait être assez simple. J'ai écrit le code Z3 pour résoudre le premier exemple ci-dessous. Il ne devrait pas être difficile pour vous de suivre le modèle de votre programme Java pour spécifier des énigmes de croisement arbitrairement grandes. 

; 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)

Je recommande le solveur Z3 SMT, mais il existe de nombreux autres solveurs de contraintes. Vous n'avez pas besoin non plus d'implémenter votre propre algorithme de résolution de contraintes, mais également d'implémenter votre propre algorithme de tri. 

0
Adam