Как разработать алгоритм для определения всех возможных перестановок элементов?

Я пытаюсь создать игру Судоку с буквами вместо цифр. Там будет пустое поле, подобное следующему:

Каждая маленькая ячейка будет заполнена буквой так, что все горизонтальные и все вертикальные буквы образуют слова. Чтобы помочь пользователю, я дам ему 6 слов, которые будут работать, но им нужно придумать, как расположить эти 6 слов. Пример правильно заполненной коробки:

В этой версии судоку, созданной для игроков в скрэббл, допустимым словом является oxo (я использую текстовый файл для списка допустимых слов).

У меня есть проблема: как программа может определить все слова из трех букв, которые подходят друг другу по горизонтали и вертикали? (Я выберу 6 слов из этого подмножества для вывода пользователю).

Текстовый файл хранится в наборе, поэтому каждое слово является строковым элементом. Я подумал о том, чтобы перебрать все слова в наборе и разбить каждое слово на массив символов. Но это кажется далекой перспективой. Есть ли у вас какие-либо идеи о том, как сгенерировать все возможные решения?

После выбора первого слова вам нужно будет найти слова, в которых 1 или более букв уже определены. Вероятно, вам нужна структура данных, которая позволяет это сделать. Может, Дерево?

DarkMatter 23.12.2020 11:32

Можно ли использовать одно и то же слово более одного раза? И вы планируете найти возможную комбинацию во время выполнения или предварительно составить список возможных комбинаций?

DarkMatter 23.12.2020 18:19

@DarkMatter Мне нельзя использовать одно и то же слово дважды. Я пытаюсь найти его во время выполнения.

Samurai 23.12.2020 22:38

Уникальность слов должна легко обеспечиваться. Но найти во время выполнения может и не быть. Моя лучшая попытка найти все возможные комбинации занимает около 1 минуты и дает 4,2 миллиона комбинаций. (Я не знаю, правильный ли результат.)

DarkMatter 23.12.2020 22:54

Это не обязательно должно быть во время выполнения, я полагаю, я мог бы предварительно скомпилировать и сохранить такой список в файле. @Темная материя

Samurai 23.12.2020 22:56

@DarkMatter Не могли бы вы показать код, который вы использовали для вычисления 4,2 миллиона комбинаций?

Samurai 23.12.2020 23:13

Я думал об использовании структуры данных Trie, но я все еще новичок в Java, и это кажется мне продвинутым.

Samurai 23.12.2020 23:54

Пожалуйста, не искажайте вопросы. Особенно, если есть полезные ответы, подумайте дважды, прежде чем удалять один.

greybeard 04.01.2021 06:58
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
0
8
243
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

В соответствии с просьбой, вот решение, которое я придумал после нескольких часов возни. Он состоит из трех основных частей: основного класса, выполняющего работу, класса Word и класса помощи словаря. (Есть также загрузчик для чтения списка слов из файла.)

Загрузчик:

public class Loader {

    public List<String> load(String fileName) throws IOException {
        List<String> wordList = new ArrayList<>();
        InputStream is = getClass().getClassLoader().getResourceAsStream(fileName);
        BufferedReader br = new BufferedReader(new InputStreamReader(is));

        String currentLine = br.readLine();
        while (currentLine != null) {
            if (!"".equals(currentLine)) {
                for (String word : currentLine.split(" ")) {
                    wordList.add(word);
                }
            }
            currentLine = br.readLine();
        }

        br.close();

        return wordList;
    }
}

Слово:

public class Word {
    private final String word;
    private final Character[] characters;

    public Word(String word) {
        this.word = word;
        characters = new Character[3];
        characters[0] = word.charAt(0);
        characters[1] = word.charAt(1);
        characters[2] = word.charAt(2);
    }

    public String getWord() {
        return word;
    }

    public Character getCharacter(int index) {
        return characters[index];
    }

    @Override
    public String toString() {
        return getWord();
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Word word1 = (Word) o;

        if (word != null ? !word.equals(word1.word) : word1.word != null) return false;
        // Probably incorrect - comparing Object[] arrays with Arrays.equals
        return Arrays.equals(characters, word1.characters);
    }

    @Override
    public int hashCode() {
        int result = word != null ? word.hashCode() : 0;
        result = 31 * result + Arrays.hashCode(characters);
        return result;
    }
}

Основной причиной для класса Word является предварительное вычисление составляющих символов.

Словарь:

public class Dictionary {
    private final List<Word> wordList;
    private final Set<Word> wordSet;
    private final Map<Character, List<Word>> firstLetterMap;
    private final Map<Character, Word[]> firstLetterArrayMap;


    public Dictionary(List<String> wordList) {
        this.wordList = wordList.stream().map(Word::new).collect(toList());
        this.wordSet = new HashSet<>();
        wordSet.addAll(this.wordList);

        this.firstLetterMap = this.wordList.stream()
                .collect(groupingBy(
                        w -> w.getCharacter(0)
                ));
        this.firstLetterArrayMap = new ConcurrentHashMap<>();
        for (Map.Entry<Character, List<Word>> entry : firstLetterMap.entrySet()) {
            Word[] ws = new Word[entry.getValue().size()];
            entry.getValue().toArray(ws);
            firstLetterArrayMap.put(entry.getKey(), ws);
        }
    }

    public List<Word> getWords() {
        return new ArrayList<>(wordList);
    }

    public Word[] getWordsArray(Character c) {
        return Optional.ofNullable(firstLetterArrayMap.get(c)).orElseGet(() -> new Word[0]);
    }

    public boolean isValidWord(Word word) {
        return wordSet.contains(word);
    }
}

Здесь особо нечего объяснять. Он содержит список слов и карту для поиска слов по их первой букве. Он также имеет метод для проверки слова.

Основная программа:

public class Tester {

    private final Loader loader;

    public Tester() {
        loader = new Loader();
    }

    public static void main(String[] args) {
        new Tester().run2();
    }

    public void run2() {

        Map<Set<Word>, String> validBoards = new ConcurrentHashMap<>();
        try {
            List<String> words = loader.load("scrabble-3-letter.txt");
            Dictionary dictionary = new Dictionary(words);
            List<Word> allWords = dictionary.getWords();
            Map<Word, String> checked = new ConcurrentHashMap<>();


            System.out.println("Start search...");
            long start = System.currentTimeMillis();

            allWords.parallelStream().forEach(w -> {
                checked.put(w, "");
                Word[] list1 = dictionary.getWordsArray(w.getCharacter(0));
                Word[] list2 = dictionary.getWordsArray(w.getCharacter(1));
                Word[] list3 = dictionary.getWordsArray(w.getCharacter(2));
                for (int i = 0; i < list1.length; ++i) {
                    Word w1 = list1[i];
                    if (checked.get(w1) != null) {
                        continue;
                    }
                    for (int j = 0; j < list2.length; ++j) {
                        Word w2 = list2[j];
                        for (int k = 0; k < list3.length; ++k) {
                            Word w3 = list3[k];
                            if (evaluate(w1, w2, w3, dictionary)) {
                                validBoards.put(new HashSet<>(Arrays.asList(w1, w2, w3)), "");
                            }
                        }
                    }
                }
            });
            long end = System.currentTimeMillis();

            System.out.println("Found " + validBoards.size() + " unique boards in " + (end - start) + " ms");

            String forPrint = validBoards.keySet().stream()
                    .map(ArrayList::new)
                    .map(this::boardToString)
                    .collect(Collectors.joining("\n"));
            writeToFile(forPrint, ".\\scrabble-sudoku-output.txt");

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void writeToFile(String data, String fileName) throws IOException {
        BufferedWriter writer = new BufferedWriter(new FileWriter(fileName));
        writer.write(data);

        writer.close();

    }

    private boolean evaluate(Word first, Word second, Word third, Dictionary dictionary) {
        Word firstV = buildVerticalWord(first, second, third, 0);
        Word secondV = buildVerticalWord(first, second, third, 1);
        Word thirdV = buildVerticalWord(first, second, third, 2);

        return dictionary.isValidWord(first) && dictionary.isValidWord(second) && dictionary.isValidWord(third)
                && dictionary.isValidWord(firstV) && dictionary.isValidWord(secondV) && dictionary.isValidWord(thirdV)
                && boardUniqueness(first, second, third, firstV, secondV, thirdV);
    }

    private boolean boardUniqueness(Word w1, Word w2, Word w3, Word w4, Word w5, Word w6) {
        Set<Word> checkDup = new HashSet<>();
        checkDup.add(w1);
        checkDup.add(w2);
        checkDup.add(w3);
        checkDup.add(w4);
        checkDup.add(w5);
        checkDup.add(w6);
        return checkDup.size() == 6;
    }

    private static Word buildVerticalWord(Word first, Word second, Word third, int index) {
        return new Word("" + first.getCharacter(index) + second.getCharacter(index) + third.getCharacter(index));
    }

    private String boardToString(List<Word> board) {
        StringBuilder sb = new StringBuilder();
        List<Word> sortedWords = new ArrayList<>(board);
        sortedWords.add(buildVerticalWord(board.get(0), board.get(1), board.get(2), 0));
        sortedWords.add(buildVerticalWord(board.get(0), board.get(1), board.get(2), 1));
        sortedWords.add(buildVerticalWord(board.get(0), board.get(1), board.get(2), 2));
        sortedWords.sort(Comparator.comparing(Word::getWord));
        sb.append(sortedWords.stream().map(Word::getWord).collect(Collectors.joining(", ")));
        return sb.toString();
    }
}

Это вторая попытка (отсюда run2()). Первым был грубый перебор, выбирающий по одному слову в строке из полного списка и затем проверяющий, допустимы ли вертикальные слова. Излишне говорить, что такой подход был не очень эффективным. (Мой список содержит 1347 слов, так что нужно проверить 2474829630 комбинаций.)

Второй подход, направленный на сокращение количества комбинаций, заключается в проверке только тех комбинаций строк, которые имеют шанс быть действительными.

Вот как это работает: Мы перебираем полный список слов. В каждой итерации выбранное слово «w» является первым столбцом. Затем мы отфильтровываем три списка возможных строк на основе первой, второй и третьей буквы w.

Эти сокращенные списки намного меньше полного списка, и мы проводим исчерпывающий поиск по этим спискам. Для каждой из этих комбинаций первый столбец остается неизменным.

Оценка проверяет, что каждое из 6 слов допустимо и что действительно есть 6 уникальных слов. Любая допустимая комбинация сохраняется как набор на карте. Набор должен перезаписывать любые дубликаты, а карта необходима для параллелизма.

Последний шаг — запись в файл. Кажется, это не работает блестяще, поэтому я хотел бы изменить это.

В цикле мы отслеживаем, какие слова w мы использовали. Если w1 совпадает с w, мы пропускаем его. Это потому, что каждая (допустимая) комбинация из 6 слов имеет две возможные формы.

A A A
B B B
C C C

такой же как

A B C
A B C
A B C

РЕДАКТИРОВАТЬ

Поиск всех решений требует времени, но найти какое-то решение можно быстро. Это требует двух дополнительных строк кода.

В конструкторе словаря добавьте перетасовку в основной список слов после того, как он был сопоставлен:

// ....
public Dictionary(List<String> wordList) {
    this.wordList = wordList.stream().map(Word::new).collect(toList());
    Collections.shuffle(this.wordList); // Shuffle here
    this.wordSet = new HashSet<>();
    wordSet.addAll(this.wordList);
// ....

Это сделает все последующие списки слов и коллекции перетасованными, что сделает каждый новый прогон уникальным.

В основной цикл программы, в run2(), добавьте оператор return в самом внутреннем цикле при нахождении допустимой доски:

                    if (evaluate(w1, w2, w3, dictionary)) {
                        validBoards.put(new HashSet<>(Arrays.asList(w1, w2, w3)), "");
                        return; // Return here to end search
                    }

Причина, по которой это возврат, а не какой-то другой разрыв, заключается в том, что мы находимся внутри лямбда (внешний цикл - это Stream.forEach()), поэтому это приведет к выходу из лямбда-выражения.

Я ожидал найти одну (1) действующую доску с этим изменением, однако по какой-то причине вместо этого я получил около 1310 (точное число варьируется). Кажется, он завершит полный внешний цикл один раз перед остановкой.

Счастливых праздников!

Большое спасибо!! Я пытаюсь протестировать вашу автономную программу, а затем использовать ее со своим кодом. Сколько времени заняла ваша программа? Я позволяю ему работать уже около 10 минут, и он все еще ищет.

Samurai 24.12.2020 01:53

На моей машине это занимает около 65-80 секунд. Кажется, в параллельном потоке работает 8 потоков (вероятно, зависит от количества ядер процессора). Вы можете запустить его в сокращенном списке слов, чтобы увидеть, правильно ли он работает. Если вы хотите увидеть прогресс, вы можете добавить распечатку в самый внешний цикл.

DarkMatter 24.12.2020 09:52

Я запустил программу (с тем же списком слов) на другом компьютере, и она завершилась за 15-17 секунд. Эта машина имеет 8-ядерный/16-поточный Ryzen с тактовой частотой 3,6 ГГц, а другая – 4-ядерный/8-поточный i7 с тактовой частотой 2,0 ГГц. Аппаратное обеспечение, кажется, является основным фактором здесь. Однако я думаю, что если бы вы могли быстро перетасовать список слов перед запуском алгоритма, а затем выйти на первой подходящей доске, вы могли бы найти ее во время выполнения за разумное время.

DarkMatter 24.12.2020 10:57

Некоторые общие предложения: * Используйте try-with-resources. Доступно с Java 7. * Используйте BufferedReader.lines(). Доступно с Java 8.

Johannes Kuhn 24.12.2020 13:37

@DarkMatter Эй, по какой-то причине я просто не могу заставить ваше решение работать :( Я очень ценю всю проделанную вами работу и не хочу, чтобы она пропала даром. Не могли бы вы загрузить видео с кодом и выводом, показывающим его работающий?

Samurai 26.12.2020 08:54

Я посмотрю, смогу ли я что-то сделать. Вот две вещи, которые вы можете попробовать в первую очередь: Используйте список слов только из 6 слов, которые являются правильным решением (ваш пример работает). Поместите печать «w» прямо в поток forEach(). Посмотрите, что выйдет.

DarkMatter 26.12.2020 11:12

Я немного доработал код и могу выложить его на github. У меня есть способ связаться с вами, чтобы дать вам ссылку? (Только если вы заинтересованы.)

DarkMatter 27.12.2020 15:28

Другие вопросы по теме