【问题标题】:Scrabble tile checking拼字游戏瓷砖检查
【发布时间】:2011-01-31 18:17:06
【问题描述】:

对于拼字游戏中的拼贴检查,您需要制作四个 5x5 的字母网格,总共 100 个拼贴。我想制作一个所有 40 个水平和垂直单词都有效的单词。可用的图块集包含:

  • 12 x E
  • 9 x A,我
  • 8 x O
  • 6 x N、R、T
  • 4 x D、L、S、U
  • 3 x G
  • 2 x B、C、F、H、M、P、V、W、Y,空白图块(通配符)
  • 1 x K、J、Q、X、Z

有效词词典可用here (700KB)。大约有 12,000 个有效的 5 个字母单词。

这是一个所有 20 个横向单词都有效的示例:

Z O W I E|P I N O T
Y O G I N|O C t A D   <= blank being used as 't'
X E B E C|N A L E D
W A I T E|M E R L E
V I N E R|L U T E A
---------+---------
U S N E A|K N O S P
T A V E R|J O L E D
S O F T A|I A M B I
R I D G Y|H A I T h   <= blank being used as 'h'
Q U R S H|G R O U F

我想创建一个所有垂直的也都有效的。你能帮我解决这个问题吗?这不是家庭作业。这是一个朋友向我寻求帮助的问题。

【问题讨论】:

  • 我的第一个倾向是,您可以根据垂直前缀的有效单词数量来评估潜在的水平移动。
  • Null Set 所说的,如果你能证明没有可能的词可以适合顶部的特定水平词集的垂直槽,那么你可以消除整个搜索子集。
  • 您可能还需要额外的工作来寻找有效的地方来放置难以使用的字母。
  • 花了我几次尝试来解析你的第一句话。你的意思是“检查你有一套完整的拼字游戏”吗?我很确定,但为了清楚起见,我还不够确定。
  • 如果有人好奇的话,我最终计算出有 2,693,056,976 个有效的 5x5 块。要么是那个,要么是 6,988,024,272,因为它可能已经溢出,但我认为这不太可能。

标签: string algorithm data-structures scrabble


【解决方案1】:

最终编辑:已解决!这是一个解决方案。

GNAWN|jOULE
RACHE|EUROS
IDIOT|STEAN
PINOT|TRAvE
TRIPY|SOLES
-----+-----
HOWFF|ZEBRA
AGILE|EQUID
CIVIL|BUXOM
EVENT|RIOJA
KEDGY|ADMAN

这是用我的拼字游戏组构建的它的照片。 http://twitpic.com/3wn7iu

一旦我找到了正确的方法,这个很容易找到,所以我打赌你可以通过这种方式找到更多。方法见下文。


从每行和每列的 5 个字母单词的字典中构造一个前缀树。递归地,如果给定的瓦片放置为其列和行形成有效的前缀,并且该瓦片可用,并且下一个瓦片放置有效,则该瓦片放置是有效的。基本情况是,如果没有剩余的瓷砖可以放置,则它是有效的。

像 Glenn 所说的那样,只找到所有有效的 5x5 板,然后看看它们中的任何四个是否可以组合起来,这可能是有意义的。递归到 100 的深度听起来并不有趣。

编辑:这是我的代码的第 2 版。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>

typedef union node node;
union node {
    node* child[26];
    char string[6];
};

typedef struct snap snap;
struct snap {
    node* rows[5];
    node* cols[5];
    char tiles[27];
    snap* next;
};

node* root;
node* vtrie[5];
node* htrie[5];
snap* head;

char bag[27] = {9,2,2,4,12,2,3,2,9,1,1,4,2,6,8,2,1,6,4,6,4,2,2,1,2,1,2};
const char full_bag[27] = {9,2,2,4,12,2,3,2,9,1,1,4,2,6,8,2,1,6,4,6,4,2,2,1,2,1,2};
const char order[26] = {16,23,9,25,21,22,5,10,1,6,7,12,15,2,24,3,20,13,19,11,8,17,14,0,18,4};

void insert(char* string){
    node* place = root;
    int i;
    for(i=0;i<5;i++){
        if(place->child[string[i] - 'A'] == NULL){
            int j;
            place->child[string[i] - 'A'] = malloc(sizeof(node));
            for(j=0;j<26;j++){
                place->child[string[i] - 'A']->child[j] = NULL;
            }
        }
        place = place->child[string[i] - 'A'];
    }
    memcpy(place->string, string, 6);
}

void check_four(){
    snap *a, *b, *c, *d;
    char two_total[27];
    char three_total[27];
    int i;
    bool match;
    a = head;
    for(b = a->next; b != NULL; b = b->next){
        for(i=0;i<27; i++)
            two_total[i] = a->tiles[i] + b->tiles[i];
        for(c = b->next; c != NULL; c = c->next){
            for(i=0;i<27; i++)
                three_total[i] = two_total[i] + c->tiles[i];
            for(d = c->next; d != NULL; d = d->next){
                match = true;
                for(i=0; i<27; i++){
                    if(three_total[i] + d->tiles[i] != full_bag[i]){
                        match = false;
                        break;
                    }
                }
                if(match){
                    printf("\nBoard Found!\n\n");
                    for(i=0;i<5;i++){
                        printf("%s\n", a->rows[i]->string);
                    }
                    printf("\n");
                    for(i=0;i<5;i++){
                        printf("%s\n", b->rows[i]->string);
                    }
                    printf("\n");
                    for(i=0;i<5;i++){
                        printf("%s\n", c->rows[i]->string);
                    }
                    printf("\n");
                    for(i=0;i<5;i++){
                        printf("%s\n", d->rows[i]->string);
                    }
                    exit(0);
                }
            }
        }
    }
}

void snapshot(){
    snap* shot = malloc(sizeof(snap));
    int i;
    for(i=0;i<5;i++){
        printf("%s\n", htrie[i]->string);
        shot->rows[i] = htrie[i];
        shot->cols[i] = vtrie[i];
    }
    printf("\n");
    for(i=0;i<27;i++){
        shot->tiles[i] = full_bag[i] - bag[i];
    }
    bool transpose = false;
    snap* place = head;
    while(place != NULL && !transpose){
        transpose = true;
        for(i=0;i<5;i++){
            if(shot->rows[i] != place->cols[i]){
                transpose = false;
                break;
            }
        }
        place = place->next;
    }
    if(transpose){
        free(shot);
    }
    else {
        shot->next = head;
        head = shot;
        check_four();

    }
}

void pick(x, y){
    if(y==5){
        snapshot();
        return;
    }
    int i, tile,nextx, nexty, nextz;
    node* oldv = vtrie[x];
    node* oldh = htrie[y];
    if(x+1==5){
        nexty = y+1;
        nextx = 0;
    } else {
        nextx = x+1;
        nexty = y;
    }
    for(i=0;i<26;i++){
        if(vtrie[x]->child[order[i]]!=NULL &&
           htrie[y]->child[order[i]]!=NULL &&
           (tile = bag[i] ? i : bag[26] ? 26 : -1) + 1) {
                vtrie[x] = vtrie[x]->child[order[i]];
                htrie[y] = htrie[y]->child[order[i]];
                bag[tile]--;

                pick(nextx, nexty);

                vtrie[x] = oldv;
                htrie[y] = oldh;
                bag[tile]++;
           }
    }
}

int main(int argc, char** argv){
    root = malloc(sizeof(node));
    FILE* wordlist = fopen("sowpods5letters.txt", "r");
    head = NULL;
    int i;
    for(i=0;i<26;i++){
        root->child[i] = NULL;
    }
    for(i=0;i<5;i++){
        vtrie[i] = root;
        htrie[i] = root;
    }

    char* string = malloc(sizeof(char)*6);
    while(fscanf(wordlist, "%s", string) != EOF){
        insert(string);
    }
    free(string);
    fclose(wordlist);
    pick(0,0);

    return 0;
}

这首先尝试不常见的字母,我不再确定这是一个好主意。它在从 x 开始脱离棋盘之前就开始陷入困境。在看到有多少个 5x5 块后,我更改了代码以列出所有有效的 5x5 块。我现在有一个 150 MB 的文本文件,其中包含所有 4,430,974 个 5x5 解决方案。

我也尝试过,只递归遍历完整的 100 个图块,并且仍在运行。

编辑 2:这是我生成的所有有效 5x5 块的列表。 http://web.cs.sunyit.edu/~levyt/solutions.rar

编辑 3:嗯,我的磁贴使用跟踪似乎有一个错误,因为我刚刚在我的输出文件中发现了一个使用 5 Zs 的块。

COSTE
ORCIN
SCUZZ
TIZZY
ENZYM

编辑 4:这是最终产品。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>

typedef union node node;
union node {
    node* child[26];
    char string[6];
};

node* root;
node* vtrie[5];
node* htrie[5];
int score;
int max_score;

char block_1[27] = {4,2,0,2, 2,0,0,0,2,1,0,0,2,1,2,0,1,2,0,0,2,0,0,1,0,1,0};//ZEBRA EQUID BUXOM RIOJA ADMAN
char block_2[27] = {1,0,1,1, 4,2,2,1,3,0,1,2,0,1,1,0,0,0,0,1,0,2,1,0,1,0,0};//HOWFF AGILE CIVIL EVENT KEDGY
char block_3[27] = {2,0,1,1, 1,0,1,1,4,0,0,0,0,3,2,2,0,2,0,3,0,0,1,0,1,0,0};//GNAWN RACHE IDIOT PINOT TRIPY
                                                                            //JOULE EUROS STEAN TRAVE SOLES
char bag[27] =     {9,2,2,4,12,2,3,2,9,1,1,4,2,6,8,2,1,6,4,6,4,2,2,1,2,1,2};
const char full_bag[27] = {9,2,2,4,12,2,3,2,9,1,1,4,2,6,8,2,1,6,4,6,4,2,2,1,2,1,2};
const char order[26] = {16,23,9,25,21,22,5,10,1,6,7,12,15,2,24,3,20,13,19,11,8,17,14,0,18,4};
const int value[27] = {244,862,678,564,226,1309,844,765,363,4656,909,414,691,463,333,687,11998,329,218,423,536,1944,1244,4673,639,3363,0};

void insert(char* string){
    node* place = root;
    int i;
    for(i=0;i<5;i++){
        if(place->child[string[i] - 'A'] == NULL){
            int j;
            place->child[string[i] - 'A'] = malloc(sizeof(node));
            for(j=0;j<26;j++){
                place->child[string[i] - 'A']->child[j] = NULL;
            }
        }
        place = place->child[string[i] - 'A'];
    }
    memcpy(place->string, string, 6);
}

void snapshot(){
    static int count = 0;
    int i;
    for(i=0;i<5;i++){
        printf("%s\n", htrie[i]->string);
    }
    for(i=0;i<27;i++){
            printf("%c%d ", 'A'+i, bag[i]);
    }
    printf("\n");
    if(++count>=1000){
        exit(0);
    }
}


void pick(x, y){
    if(y==5){
        if(score>max_score){
            snapshot();
            max_score = score;
        }
        return;
    }
    int i, tile,nextx, nexty;
    node* oldv = vtrie[x];
    node* oldh = htrie[y];
    if(x+1==5){
        nextx = 0;
        nexty = y+1;
    } else {
        nextx = x+1;
        nexty = y;
    }
    for(i=0;i<26;i++){
        if(vtrie[x]->child[order[i]]!=NULL &&
           htrie[y]->child[order[i]]!=NULL &&
           (tile = bag[order[i]] ? order[i] : bag[26] ? 26 : -1) + 1) {
                vtrie[x] = vtrie[x]->child[order[i]];
                htrie[y] = htrie[y]->child[order[i]];
                bag[tile]--;
                score+=value[tile];

                pick(nextx, nexty);

                vtrie[x] = oldv;
                htrie[y] = oldh;
                bag[tile]++;
                score-=value[tile];
           }
    }
}

int main(int argc, char** argv){
    root = malloc(sizeof(node));
    FILE* wordlist = fopen("sowpods5letters.txt", "r");
    score = 0;
    max_score = 0;
    int i;
    for(i=0;i<26;i++){
        root->child[i] = NULL;
    }
    for(i=0;i<5;i++){
        vtrie[i] = root;
        htrie[i] = root;
    }
    for(i=0;i<27;i++){
        bag[i] = bag[i] - block_1[i];
        bag[i] = bag[i] - block_2[i];
        bag[i] = bag[i] - block_3[i];

        printf("%c%d ", 'A'+i, bag[i]);
    }

    char* string = malloc(sizeof(char)*6);
    while(fscanf(wordlist, "%s", string) != EOF){
        insert(string);
    }
    free(string);
    fclose(wordlist);
    pick(0,0);

    return 0;
}

在找出有多少块(近 20 亿个并且还在计数)之后,我转而尝试寻找某些类型的块,尤其是那些使用不常见字母难以构建的块。我的希望是,如果我最终在最后一个块中输入了一组足够温和的字母,那么有效块的巨大空间可能会有一个用于该组字母。

我为每个图块分配了一个与它出现的 5 个字母单词的数量成反比的值。然后,当我找到一个有效的块时,我会对图块值求和,如果分数是我见过的最好的,我会打印出块。

对于第一个块,我删除了空白图块,认为最后一个块最需要这种灵活性。让它运行直到我有一段时间没有看到更好的积木出现后,我选择了最好的积木,然后从袋子里取出里面的瓷砖,再次运行程序,得到第二个积木。我在第三个街区重复了这一点。然后对于最后一个块,我添加了空白,并使用了它找到的第一个有效块。

【讨论】:

  • 我修改了这个算法,使其更喜欢作为许多单词前缀的图块,我得到了一个魔方。 :)
  • 因为 4 个 5x5 块的排列不影响正确性,递归“全部 100 次”实际上会做 4*3*2*1=12 倍的工作,尝试所有的排列一组 4 个块。您可以通过在您尝试的每个解决方案中对 4 个块执行命令来解决此问题,例如左上角的块,被视为 25 个字符的字符串,在字典上必须小于右上,右上必须小于左下等。
  • 另一个可能有帮助的想法是为每个不同的使用字母向量保留 1 个具有代表性的 5x5 块。例如。如果 3 个不同的 5x5 块都使用相同数量的每个字母,则您只需要保留其中 1 个块 - 哪个块都没有关系 - 因为所有 3 个块在将 4 个块装入一个完整的 10x10 正方形方面是等效的. (稍后,如果您想列举所有可能的解决方案,您仍然可以返回所有 4,430,974 个块的列表来找到它们。)
  • 我的代码产生了超过 400 万个有效的 5x5 块。到目前为止,我有 2300 万,这只是以 A 开头的块。当然,我可能有一个错误。
  • 查看我的答案,我从更简单的地方开始——2x2 而不是 5x5。
【解决方案2】:

这就是我要尝试的方法。首先构造一个前缀树。

选择一个单词并将其水平放置在顶部。选择一个单词并将其垂直放置。交替使用它们,直到用尽选项。通过交替,您开始修复第一个字母并消除许多不匹配的单词。如果你真的找到了这样的方块,那就检查一下它们是否可以用这些碎片制成。

对于 5x5 方格:经过一番思考,随机文本单词不会比 O(12000!/11990!) 差。但是想多了一点。每次你修正一个字母(在普通文本中),你消除了大约 90%(乐观的猜测)你的话。这意味着经过 3 次迭代后,您将获得 12 个单词。所以实际速度是

O(n * n/10 * n/10 * n/100 * n/100 * n/1000 * n/1000 ...
which for 12000 elements acts something like n^4 algorithm

这还不错。

也许有人可以更好地分析问题。但是单词的搜索应该还是会很快收敛的。

通过滥用不常用的字母可以完成更多的消除。基本上找到所有不常用字母的单词。尝试为每个字母做一个匹配的位置。为每个位置构造一组有效字母。

例如,假设我们有四个单词,其中包含字母 Q。

 AQFED, ZQABE, EDQDE, ELQUO

 this means there are two valid positionings of those:

 xZxxx
 AQFED
 xAxxx   ---> this limits our search for words that contain [ABDEFZ] as the second letter
 xBxxx
 xExxx

 same for the other

 EDQDE   ---> this limits our search for words that contain [EDLU] as the third letter
 ELQUO

 all appropriate words are in union of those two conditions

因此,基本上,如果我们有多个单词在位置 N 的单词 S 中包含不常见的字母 X,则意味着该矩阵中的其他单词必须在位置 n 的位置具有也在 S 中的字母。

公式:

  • 查找在位置 1 包含不常见字母 X 的所有单词(下一次迭代 2、3...)
  • 将这些单词中的字母组成一组 A
  • 仅保留字典中在位置 1 中包含集合 A 中的字母的单词
  • 尝试将它们放入矩阵中(使用第一种方法)
  • 重复位置 2

【讨论】:

    【解决方案3】:

    我会以悲观的态度来解决问题(当然是天真地)。我会尝试证明不存在 5x5 解决方案,因此肯定不是四个 5x5 解决方案。为了证明不存在 5x5 解决方案,我会尝试从所有可能性中构建一个。如果我的猜想失败并且我能够构建一个 5x5 解决方案,那么,我将有一种构建 5x5 解决方案的方法,并且我会尝试构建所有(独立的)5x5 解决方案。如果至少有 4 个,那么我将确定某个组合是否满足字母数限制。

    [编辑] Null Set 已确定有“4,430,974 个 5x5 解决方案”。这些有效吗? 我的意思是我们对可以使用的字母数量有限制。这个限制可以表示为一个边界向量 BV = [9, 2, 2, 4, ...] 对应于 A、B、C 等的限制(你可以在 Null Set 的代码中看到这个向量)。如果其字母计数向量的每个项都小于 BV 中的相应项,则 5x5 解决方案是有效的。很容易检查 5x5 解决方案在创建时是否有效。也许可以减少 4,430,974 这个数字,比如 N。

    无论如何,我们可以将问题描述为:在 N 中找到四个字母计数向量,其和等于 BV。有 (N, 4) 个可能的总和(“N 选择 4”)。 N 等于 400 万,这仍然是 10^25 的数量级——这不是一个令人鼓舞的数字。也许您可以搜索第一项总和为 9 的四个,如果是,则检查它们的第二项总和为 2,等等。

    我要说的是,在从 N 中选择 4 之后,计算是独立的,所以如果你有一台多核机器,你可以使用并行解决方案来加快速度。

    [Edit2] 不过,并行化可能不会有太大的不同。在这一点上,我可能会持乐观态度:5x5 的解决方案肯定比我预期的要多,所以最终解决方案也可能比预期的要多。也许您可能不必走太远 10^25 就能击中一个。

    【讨论】:

    • 天真地构建它们将需要 O(26^25) 次操作。这是一个庞大的数字,而且速度太慢了。
    • Knuth 第 4A 卷中肯定有一个 5x5 的解决方案——不止一个。他使用了一个不同的、更小的字典并且不限制可用的图块——所以如果没有解决方案,可能是因为图块集而不是字典。不过,我希望仍然有解决方案。
    • @Darius 我刚刚实现了我的解决方案,我的天哪,有一大堆 5x5 字块。当我的输出文件是整个 SOWPODS 文件大小的两倍时,我停止打印它们。
    • @marcog 将其编辑到我的答案中。
    • 如果我们改为查找 3 个方块,剩余的瓦片集与另一个方块使用的相同,并使用哈希表进行此测试,我们可以显着加快速度。天真地这样做仍然不够快。
    【解决方案4】:

    我从更简单的开始。

    以下是目前的一些结果:

       3736 2x2 solutions
    8812672 3x3 solutions
    
    The 1000th 4x4 solution is
       A A H S
       A C A I
       L A I R
       S I R E
    
    The 1000th 5x5 solution is
       A A H E D
       A B U N A
       H U R S T
       E N S U E
       D A T E D
    
    The 1000th 2x4x4 solution is
       A A H S | A A H S
       A B A C | A B A C
       H A I R | L E K U
       S C R Y | S T E D
       --------+--------
       D E E D | D E E M
       E I N E | I N T I
       E N O L | O V E R
       T E L T | L Y N E
    

    请注意,转置“A”和用作“A”的空白应被视为相同的解决方案。但是将行与列转置应该被视为不同的解决方案。我希望这是有道理的。

    【讨论】:

      【解决方案5】:

      这里有很多预先计算的 5x5。留给读者作为练习,以找到 4 个兼容的 :-)

      http://www.gtoal.com/wordgames/wordsquare/all5

      【讨论】:

      • 这是否使用所需的字典?
      • 它没有。例如,“AARON”不在 sowpods.txt 中。
      • 没错,但我认为它是 SOWPODS 的超集,因此您应该能够消除任何包含您不想要的单词的方块...
      猜你喜欢
      • 2013-01-26
      • 2013-01-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-01-11
      • 1970-01-01
      • 2018-05-28
      相关资源
      最近更新 更多