5

对于智能手机,有一款名为Ruzzle的游戏。
Ruzzle 板
这是一个找词游戏。

快速说明:
游戏板是一个 4x4 的字母网格。
您从任何单元格开始,并尝试通过向上、向下、向左、向右或对角线拖动来拼写单词。
板不换行,您不能重复使用已选择的字母。

平均而言,我和我的朋友会找到大约 40 个单词,并且在回合结束时,游戏会告诉您您可以获得多少个可能的单词。这个数字通常约为 250 - 350。
我们想知道哪个板会产生最多的可能单词。

我将如何寻找最佳板?
我用 C 语言编写了一个程序,它需要 16 个字符并输出所有适当的单词。
测试超过 80,000 个单词,大约需要一秒钟的时间来处理。

问题:
游戏板排列的数量是 26^16。
那是 43608742899428874059776(43 六分之一)。

我需要某种启发式方法。
我是否应该跳过所有具有zqx等的,因为它们应该没有那么多的单词?我不想在不确定的情况下排除一个字母。
每个板也有 4 个副本,因为旋转板仍然会产生相同的结果。
但即使有这些限制,我认为我一生中没有足够的时间来找到答案。

也许董事会一代不是答案。
有没有更快的方法来查看单词列表来找到答案?

4

2 回答 2

8

tldr;

S   E   R   O
P   I   T   S
L   A   N   E
S   E   R   G

或其任何反映。

该板包含 1212 个单词(事实证明,您可以排除“z”、“q”和“x”)。

首先,事实证明您使用的是错误的字典。在没有与 Ruzzle 的字数完全匹配后,我查看了它,似乎 Ruzzle 使用了一本名为 TWL06 的字典,它有大约 180,000 个单词。不要问我它代表什么,但它可以在 txt 中免费获得。

我还编写了代码来查找给定 16 个字符板的所有可能单词,如下所示。它将字典构建成树形结构,然后在找到单词时几乎只是递归地四处走动。它按长度顺序打印它们。唯一性由 STL 集合结构维护。

#include <cstdlib>
#include <ctime>
#include <map>
#include <string>
#include <set>
#include <algorithm>
#include <fstream>
#include <iostream>

using namespace std;

struct TreeDict {

    bool existing;
    map<char, TreeDict> sub;

    TreeDict() {
        existing = false;
    }

    TreeDict& operator=(TreeDict &a) {

        existing = a.existing;
        sub = a.sub;
        return *this;
    }

    void insert(string s) {

        if(s.size() == 0) {

            existing = true;
            return;
        }

        sub[s[0]].insert(s.substr(1));
    }

    bool exists(string s = "") {

        if(s.size() == 0)
            return existing;

        if(sub.find(s[0]) == sub.end())
            return false;

        return sub[s[0]].exists(s.substr(1));
    }

    TreeDict* operator[](char alpha) {

        if(sub.find(alpha) == sub.end())
            return NULL;

        return &sub[alpha];
    }
};

TreeDict DICTIONARY;

set<string> boggle_h(const string board, string word, int index, int mask, TreeDict *dict) {

    if(index < 0 || index >= 16 || (mask & (1 << index)))
        return set<string>();

    word += board[index];
    mask |= 1 << index;
    dict = (*dict)[board[index]];

    if(dict == NULL)
        return set<string>();

    set<string> rt;

    if((*dict).exists())
        rt.insert(word);

    if((*dict).sub.empty())
        return rt;

    if(index % 4 != 0) {

        set<string> a = boggle_h(board, word, index - 4 - 1, mask, dict);
        set<string> b = boggle_h(board, word, index - 1, mask, dict);
        set<string> c = boggle_h(board, word, index + 4 - 1, mask, dict);

        rt.insert(a.begin(), a.end());
        rt.insert(b.begin(), b.end());
        rt.insert(c.begin(), c.end());
    }

    if(index % 4 != 3) {

        set<string> a = boggle_h(board, word, index - 4 + 1, mask, dict);
        set<string> b = boggle_h(board, word, index + 1, mask, dict);
        set<string> c = boggle_h(board, word, index + 4 + 1, mask, dict);

        rt.insert(a.begin(), a.end());
        rt.insert(b.begin(), b.end());
        rt.insert(c.begin(), c.end());
    }

    set<string> a = boggle_h(board, word, index + 4, mask, dict);
    set<string> b = boggle_h(board, word, index - 4, mask, dict);

    rt.insert(a.begin(), a.end());
    rt.insert(b.begin(), b.end());

    return rt;
}

set<string> boggle(string board) {

    set<string> words;
    for(int i = 0; i < 16; i++) {

        set<string> a = boggle_h(board, "", i, 0, &DICTIONARY);
        words.insert(a.begin(), a.end());
    }
    return words;
}

void buildDict(string file, TreeDict &dict = DICTIONARY) {

    ifstream fstr(file.c_str());
    string s;

    if(fstr.is_open()) {
        while(fstr.good()) {

            fstr >> s;
            dict.insert(s);
        }
        fstr.close();
    }
}

struct lencmp {
    bool operator()(const string &a, const string &b) {

        if(a.size() != b.size())
            return a.size() > b.size();
        return a < b;
    }
};

int main() {

    srand(time(NULL));

    buildDict("/Users/XXX/Desktop/TWL06.txt");

    set<string> a = boggle("SEROPITSLANESERG");
    set<string, lencmp> words;
    words.insert(a.begin(), a.end());
    set<string>::iterator it;
    for(it = words.begin(); it != words.end(); it++)
        cout << *it << endl;
    cout << words.size() << " words." << endl;
}

随机生成板并针对它们进行测试并没有变得太有效,预期,我并没有真正为运行它而烦恼,但如果它们超过 200 个字,我会感到惊讶。相反,我更改了板的生成以生成与它们在 TWL06 中的频率成比例分布的字母的板,通过快速累积频率(频率降低了 100 倍)实现,如下所示。

string randomBoard() {

    string board = "";
    for(int i = 0; i < 16; i++)
        board += (char)('A' + rand() % 26);
    return board;
}

char distLetter() {

    int x = rand() % 15833;
    if(x < 1209) return 'A';
    if(x < 1510) return 'B';
    if(x < 2151) return 'C';
    if(x < 2699) return 'D';
    if(x < 4526) return 'E';
    if(x < 4726) return 'F';
    if(x < 5161) return 'G';
    if(x < 5528) return 'H';
    if(x < 6931) return 'I';
    if(x < 6957) return 'J';
    if(x < 7101) return 'K';
    if(x < 7947) return 'L';
    if(x < 8395) return 'M';
    if(x < 9462) return 'N';
    if(x < 10496) return 'O';
    if(x < 10962) return 'P';
    if(x < 10987) return 'Q';
    if(x < 12111) return 'R';
    if(x < 13613) return 'S';
    if(x < 14653) return 'T';
    if(x < 15174) return 'U';
    if(x < 15328) return 'V';
    if(x < 15452) return 'W';
    if(x < 15499) return 'X';
    if(x < 15757) return 'Y';
    if(x < 15833) return 'Z';
}

string distBoard() {

    string board = "";
    for(int i = 0; i < 16; i++)
        board += distLetter();
    return board;
}

这明显更有效,很容易实现 400+ 字板。我让它运行(比我预期的要长),在检查了超过一百万块板之后,发现的最高值是 650 字左右。这仍然本质上是随机生成,并且有其局限性。

相反,我选择了一种贪婪的最大化策略,在这种策略中,我会选择一块板并对其进行小幅更改,然后仅在它增加字数时才提交更改。

string changeLetter(string x) {

    int y = rand() % 16;
    x[y] = distLetter();
    return x;
}

string swapLetter(string x) {

    int y = rand() % 16;
    int z = rand() % 16;
    char w = x[y];
    x[y] = x[z];
    x[z] = w;
    return x;
}

string change(string x) {

    if(rand() % 2)
        return changeLetter(x);
    return swapLetter(x);
}

int main() {

    srand(time(NULL));

    buildDict("/Users/XXX/Desktop/TWL06.txt");

    string board = "SEROPITSLANESERG";
    int locmax = boggle(board).size();
    for(int j = 0; j < 5000; j++) {

        int changes = 1;

        string board2 = board;
        for(int k = 0; k < changes; k++)
            board2 = change(board);

        int loc = boggle(board2).size();
        if(loc >= locmax && board != board2) {
            j = 0;
            board = board2;
            locmax = loc;
        }
    }
}

尽管起点是随机的,但这很快就让我得到了 1000 多个单词板,它们的字母模式大致相似。是什么让我相信给出的棋盘是最好的棋盘是它,或者它的各种反射之一,在最大化随机棋盘的前 100 次尝试中反复出现。

怀疑的最大原因是这个算法的贪婪,这会导致算法错过更好的板。所做的小改动在其结果上是相当灵活的——也就是说,它们有能力将网格从其(随机)起始位置完全转换。可能的更改次数,新字母为 26*16,字母交换为 16*15,均明显小于 5000,即允许的连续丢弃更改次数。

程序能够在前 100 奇次内重复此板输出的事实意味着局部最大值的数量相对较少,并且存在未发现的最大值的概率较低。

尽管贪婪在直觉上似乎是正确的——通过随机板的增量变化达到给定网格的可能性并不低——而且两个可能的变化,一个交换和一个新的字母似乎确实包含了所有可能的改进,我更改了程序,以允许它在检查增加之前进行更多更改。这再次返回相同的板,重复。

int main() {

    srand(time(NULL));

    buildDict("/Users/XXX/Desktop/TWL06.txt");

    int glomax = 0;
    int i = 0;
    while(true) {

        string board = distBoard();
        int locmax = boggle(board).size();
        for(int j = 0; j < 500; j++) {

            string board2 = board;
            for(int k = 0; k < 2; k++)
                board2 = change(board);

            int loc = boggle(board2).size();
            if(loc >= locmax && board != board2) {
                j = 0;
                board = board2;
                locmax = loc;
            }
        }

        if(glomax <= locmax) {
            glomax = locmax;
            cout << board << " " << glomax << " words." << endl;
        }

        if(++i % 10 == 0)
            cout << i << endl;
    }
}

在这个循环上迭代了大约 1000 次,这个特定的板配置显示了大约 10 次,我非常有信心这是目前具有最独特单词的 Ruzzle 板,直到英语发生变化。

于 2013-06-28T19:43:26.820 回答
3

有趣的问题。我看到(至少,但主要是)两种方法

  1. 一种是尝试根据字典尽可能多地粘贴可单词的字母(在各个方向)。正如你所说,有很多可能的组合,而这条路线需要一个精心设计和复杂的算法才能达到有形的东西

  2. 还有另一个我更喜欢的基于概率的“松散”解决方案。您建议去除一些外观低的字母以最大化电路板良率。对此的扩展可能是在字典中使用更多的高外观字母。

进一步的步骤可能是:

  • 根据 80k 字典D,您可以找出 26 个字母的 L 集合中的每个l1字母的字母l2在l1之前或之后的概率。这是一个L x L概率数组,非常小,因此您甚至可以扩展到L x L x L,即考虑l1l2什么概率适合l3 。如果算法想要估计准确的概率,这有点复杂,因为概率和取决于 3 个字母的相对位置,例如在“三角形”配置中(例如位置 (3,3)、(3,4 ) 和 (3,5)) 结果可能比字母对齐时的产量要低[只是一个假设]。为什么不去L x L x L x L,这将需要一些优化...

  • 然后你在板上随机分布一些高外观字母(比如 4~6)(在 8 个可能方向中的至少 5 个周围至少有 1 个空白单元格),然后使用你的L x L [xL]probas 数组来完成 - 含义基于现有的字母,下一个单元格填充给定配置的概率高的字母[再次,按概率降序排序的字母,如果两个字母紧密联系,则使用随机性]。

例如,仅采用水平配置,具有以下字母,我们希望找到介于两者之间的最佳 2ERTO

  ...ER??TO...

使用L x L,这样的循环(l1 和 l2 是我们缺少的两个字母)。找到绝对更好的字母 - 但bestchoicebestproba可以改为数组并保留 - 比如说 - 10 个最佳选择。注意:在这种情况下,无需将概率保持在 [0,1] 范围内,我们可以总结概率(不给出概率 - 但数字很重要。数学概率可能类似于 p = ( p(l0,l1) + p(l2,l3) ) / 2, l0 和 l3 是我们示例中的RTL x L )

  bestproba = 0
  bestchoice = (none, none)
  for letter l1 in L
    for letter l2 in L
      p = proba('R',l1) + proba(l2,'T')
      if ( p > bestproba ) 
        bestproba = p
        bestchoice = (l1, l2)
      fi
    rof
  rof

该算法可以考虑更多的因素,还需要考虑垂直和对角线。使用L x L x L,会考虑更多方向上的更多字母,例如ER?, R??, ??T, ?TO- 这需要对算法进行更多思考 - 也许从开始L x L可以了解该算法的相关性。

请注意,其中很多可能是预先计算的,L x L数组当然是其中之一。

于 2013-01-19T10:35:50.810 回答