Bonjour,
Pour un programme perso de solveur de mots-croisés, j'aimerais bien pouvoir mettre un score sur la capacité des mots d'un dictionnaire à bien "croiser".
Pour un premier essai, je prends comme score du dictionnaire la somme du nombre de croisements entre mots pris 2 à 2.
Par exemple pour "HELLO", "CROSS" et "WORLD", le résultat donne 6 car il y a 6 combinaisons de croisements entre mots pris 2 par 2 :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6 |C| |W| |W| |W| |W| |W| |R| |O| |O| |H|E|L|L|O| |O| |C|R|O|S|S| |H|E|L|L|O| |R| |R| |R| |C|R|O|S|S| |R| |S| |H|E|L|L|O| |H|E|L|L|O| |L| |L| |L| |S| |D| |D| |D| |D| |D|
Le code en question :
Code java : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138 package com.gitlab.super7ramp.croiseur.dictionary.tools; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.IntStream; import java.util.stream.Stream; /** * Computes a <em>crossability</em> score for the words of a given dictionary. * <p> * This score corresponds to the cumulated sum of the number of crossings between words taken 2 by * 2. * <p> * Example: "HELLO", "WORLD" will return 3 because there are three ways to cross these words: * <pre> * |W| |W| |W| * |O| |O| |H|E|L|L|O| * |R| |R| |R| * |H|E|L|L|O| |H|E|L|L|O| |L| * |D| |D| |D| * </pre> */ public final class Scorer implements Callable<Long> { /** The number of words to process between each progress print. */ private static final int WORDS_BETWEEN_EACH_PRINT_PROGRESS = 1_000; /** The words to compute for which to compute the number of crossings. */ private final List<String> words; /** * A shared counter incremented each time the crossings of a word with all the other words have * been computed. */ private final AtomicInteger counter; /** * Constructs an instance. * * @param wordsArg the words for which to compute the number of crossings */ Scorer(final List<String> wordsArg) { words = wordsArg; counter = new AtomicInteger(); } @Override public Long call() { return IntStream.range(0, words.size()) .parallel() .mapToLong(i -> IntStream.range(i + 1, words.size()) .parallel() .mapToLong( j -> crossings(words.get(i), words.get(j))) .sum()) .peek(i -> printProgress()) .sum(); } /** * Computes the number of crossings between two given words. * * @param a a word * @param b another word * @return the number of crossings between the two given words */ private static int crossings(final String a, final String b) { int numberOfCrossings = 0; for (int i = 0; i < a.length(); i++) { for (int j = 0; j < b.length(); j++) { numberOfCrossings += eq(a.charAt(i), b.charAt(j)); } } return numberOfCrossings; } /** * Returns 1 if given characters are equal, 0 otherwise. * * @param a left operand * @param b right operand * @return 1 if given characters are equal, 0 otherwise */ private static int eq(final char a, final char b) { return a == b ? 1 : 0; } /** * Prints progress on standard output every {@value WORDS_BETWEEN_EACH_PRINT_PROGRESS} words * processed. */ private void printProgress() { final int current = counter.incrementAndGet(); if (current % WORDS_BETWEEN_EACH_PRINT_PROGRESS == 0) { final long completionPercentage = (long) ((current * 100.0) / words.size()); final String progress = String.format("[%d] %d / %d [%d %%]", Thread.currentThread().getId(), current, words.size(), completionPercentage); System.err.println(progress); } } /** * Usage: {@code scorer path/to/wordlist.txt}. * * @param args arguments * @throws ExecutionException if computation fails * @throws InterruptedException if interrupted while computing */ public static void main(final String[] args) throws ExecutionException, InterruptedException { if (args.length != 1) { System.err.println("Usage: scorer path/to/wordlist.txt"); System.exit(1); } final Path wordListPath = Path.of(args[0]); List<String> words = null; try (final Stream<String> lines = Files.lines(wordListPath)) { words = lines.distinct().toList(); } catch (final IOException e) { System.err.println("Failed to read " + wordListPath + ": " + e.getMessage()); System.exit(2); } final ForkJoinPool pool = new ForkJoinPool(16); final Long result = pool.submit(new Scorer(words)).get(); System.out.println("score = " + result); } }
Ça marche, mais c'est lent pour un gros dictionnaire. Ça m'a pris ~2h pour UKACD (250 000 mots).
Je fais la somme des croisements entre un mot et tous les mots suivants dans la liste, pour chacun des mots de la liste. Ça revient à une complexité en O(n²) si je ne m'abuse. Je me demande s'il n'y a pas moyen de faire mieux.
Peut-être en faisant une première passe pour décomposer les mots en leur nombre de lettres et ensuite en sommant le nombre de combinaisons apportées par chaque lettre comme ceci :
Si chaque lettre est unique dans un mot, c'est rapide : le nombre de combinaisons apportées par une lettre vaut alors (2 parmi m) où m est le nombre de mots où la lettre apparaît. Par exemple pour la lettre O dans l'exemple au-dessus, ça fait (2 parmi 3) ce que l'on peut calculer directement et qui vaut 3.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6 HELLO -> 1E + 1H + 2L + 1O CROSS -> 1C + 1O + 1R + 2S WORLD -> 1D + + 1L + 1O + 1R + 1W --------------------------------------------------- 2 + 3 + 1
Mais dès que les lettres se répètent, je ne vois pas comment faire un calcul rapide.
Est-ce que vous voyez un meilleur algorithme ?
Partager