提问人:nickf 提问时间:6/3/2009 最后编辑:nickf 更新时间:4/20/2020 访问量:111098
生成填字游戏的算法 [已关闭]
Algorithm to generate a crossword [closed]
问:
给定一个单词列表,您将如何将它们排列成填字游戏网格?
它不必像一个“适当的”填字游戏,它是对称的或类似的东西:基本上只是输出每个单词的起始位置和方向。
答:
我会得到每个单词使用的每个字母的索引,以了解可能的交叉。然后我会选择最大的单词并使用它作为基础。选择下一个大的并越过它。冲洗并重复。这可能是一个NP问题。
另一个想法是创建一种遗传算法,其中强度指标是您可以在网格中输入多少个单词。
我发现最困难的部分是什么时候知道某个列表不可能被跨越。
评论
实际上,大约十年前,我写了一个填字游戏生成程序(它很神秘,但同样的规则也适用于普通的填字游戏)。
它有一个单词列表(和相关线索),存储在一个文件中,按迄今为止的用法降序排序(因此较少使用的单词位于文件的顶部)。一个模板,基本上是代表黑色和自由方块的位掩码,是从客户端提供的池中随机选择的。
然后,对于拼图中的每个不完整的单词(基本上找到第一个空白方块,看看右边的方块(横字)或下面的方块(下字)是否也是空白的),对文件进行搜索,寻找第一个合适的单词,同时考虑到该单词中已有的字母。如果没有合适的单词,您只需将整个单词标记为不完整并继续前进。
最后是一些未完成的单词,编译器必须填写这些单词(如果需要,还可以将单词和线索添加到文件中)。如果他们想不出任何想法,他们可以手动编辑填字游戏以更改约束,或者只是要求完全重新生成。
一旦单词/线索文件达到一定大小(并且每天为该客户添加 50-100 条线索),很少有必须为每个填字游戏完成两到三个以上的手动修复的情况。
评论
我会生成两个数字:长度和拼字游戏分数。假设拼字游戏分数低意味着它更容易加入(低分数 = 大量常用字母)。按长度降序和拼字游戏分数升序对列表进行排序。
接下来,只需向下看列表。如果单词没有与现有单词交叉(分别按长度和拼字游戏分数检查每个单词),则将其放入队列中,并检查下一个单词。
冲洗并重复,这应该会生成一个填字游戏。
当然,我很确定这是 O(n!),它不能保证为您完成填字游戏,但也许有人可以改进它。
我想出了一个解决方案,它可能不是最有效的,但它足够有效。基本上:
- 按长度对所有单词进行降序排序。
- 把第一个字放在黑板上。
- 拿下一个词。
- 搜索板上已有的所有单词,看看是否有任何可能的交叉点(任何常用字母)与该单词。
- 如果该单词可能存在位置,请遍历板上的所有单词,并检查新单词是否干扰。
- 如果这个词没有破坏板子,那么把它放在那里并转到步骤 3,否则,继续搜索一个地方(步骤 4)。
- 继续此循环,直到所有单词都已放置或无法放置。
这使得填字游戏有效,但通常很差。我对上面的基本配方进行了一些更改,以得出更好的结果。
- 在生成填字游戏结束时,根据放置的单词数量(越多越好)、棋盘的大小(越小越好)以及高度和宽度之间的比率(越接近 1 越好)给它打分。生成一些填字游戏,然后比较它们的分数并选择最好的填字游戏。
- 我决定在任意的时间内创建尽可能多的填字游戏,而不是运行任意数量的迭代。如果你只有一个小单词列表,那么你会在 5 秒内得到几十个可能的填字游戏。较大的填字游戏可能只能从 5-6 种可能性中选择。
- 放置一个新单词时,不要在找到可接受的位置后立即放置它,而是根据它增加网格大小的程度和交叉点的数量给该单词位置打分(理想情况下,您希望每个单词被 2-3 个其他单词交叉)。跟踪所有位置及其分数,然后选择最佳位置。
评论
为什么不直接使用随机概率方法开始呢?从一个单词开始,然后反复随机选择一个单词,并尝试将其融入拼图的当前状态,而不会破坏对大小等的限制。如果你失败了,就重新开始。
您会惊讶于像这样的蒙特卡洛方法的有效频率。
评论
我最近刚刚用 Python 编写了自己的内容。你可以在这里找到它:http://bryanhelmig.com/python-crossword-puzzle-generator/。它不会创建密集的纽约时报风格的填字游戏,而是您可能在儿童益智书中找到的填字游戏风格。
与我发现的一些算法不同,这些算法实现了一些随机的蛮力方法来放置单词,就像一些人建议的那样,我试图在单词放置时实现一种稍微聪明的蛮力方法。这是我的过程:
- 创建任意大小的网格和单词列表。
- 随机排列单词列表,然后按最长到最短的顺序对单词进行排序。
- 将第一个也是最长的单词放在左上角最上角的位置 1,1(垂直或水平)。
- 移动到下一个单词,循环遍历单词中的每个字母和网格中的每个单元格,寻找字母到字母的匹配。
- 找到匹配项后,只需将该位置添加到该单词的建议坐标列表中即可。
- 循环使用建议的坐标列表,并根据单词交叉的其他单词数量对单词位置进行“评分”。分数为 0 表示位置不正确(与现有单词相邻)或没有单词交叉。
- 返回步骤#4,直到单词列表用尽。可选的第二道关口。
- 我们现在应该有一个填字游戏,但由于一些随机放置,质量可能会被击中或错过。因此,我们缓冲此填字游戏并返回步骤#2。如果下一个填字游戏在黑板上放置了更多单词,它将替换缓冲区中的填字游戏。这是有时间限制的(在 x 秒内找到最好的填字游戏)。
到最后,你有一个像样的填字游戏或单词搜索游戏,因为它们差不多。它往往运行得相当好,但如果您有任何改进建议,请告诉我。更大的网格运行速度呈指数级增长;线性更大的单词列表。单词列表越大,获得更好单词位置数字的机会也要高得多。
评论
array.sort(key=f)
我一直在思考这个问题。我的感觉是,要创建一个真正密集的填字游戏,你不能指望你有限的单词列表就足够了。因此,您可能希望获取字典并将其放入“trie”数据结构中。这将使您能够轻松找到填充剩余空格的单词。在尝试中,实现一个遍历是相当有效的,比如说,给你所有形式的“c?t”的单词。
所以,我的总体想法是:创建某种相对蛮力的方法,就像这里描述的那样,创建一个低密度的十字架,并用字典单词填补空白。
如果其他人采取了这种方法,请告诉我。
该算法可在 50 秒内创建 60 个密集的 6x9 箭头填字游戏。它使用一个单词数据库(带有单词+提示)和一个板数据库(带有预配置的板)。
1) Search for all starting cells (the ones with an arrow), store their size and directions
2) Loop through all starting cells
2.1) Search a word
2.1.1) Check if it was not already used
2.1.2) Check if it fits
2.2) Add the word to the board
3) Check if all cells were filled
更大的单词数据库大大缩短了生成时间,并且某些类型的板更难填充!较大的板子需要更多时间才能正确填充!
例:
预配置的 6x9 板:
(# 表示一个单元格中的一个尖端,% 表示一个单元格中有两个尖端,箭头未显示)
# - # # - % # - #
- - - - - - - - -
# - - - - - # - -
% - - # - # - - -
% - - - - - % - -
- - - - - - - - -
生成的 6x9 板:
# C # # P % # O #
S A T E L L I T E
# N I N E S # T A
% A B # A # G A S
% D E N S E % W E
C A T H E D R A L
提示 [行,列]:
[1,0] SATELLITE: Used for weather forecast
[5,0] CATHEDRAL: The principal church of a city
[0,1] CANADA: Country on USA's northern border
[0,4] PLEASE: A polite way to ask things
[0,7] OTTAWA: Canada's capital
[1,2] TIBET: Dalai Lama's region
[1,8] EASEL: A tripod used to put a painting
[2,1] NINES: Dressed up to (?)
[4,1] DENSE: Thick; impenetrable
[3,6] GAS: Type of fuel
[1,5] LS: Lori Singer, american actress
[2,7] TA: Teaching assistant (abbr.)
[3,1] AB: A blood type
[4,3] NH: New Hampshire (abbr.)
[4,5] ED: (?) Harris, american actor
[4,7] WE: The first person of plural (Grammar)
以下是一些基于 nickf 的答案和 Bryan 的 Python 代码的 JavaScript 代码。只是在 js 中发布它以防其他人需要它。
function board(cols, rows) { //instantiator object for making gameboards
this.cols = cols;
this.rows = rows;
var activeWordList = []; //keeps array of words actually placed in board
var acrossCount = 0;
var downCount = 0;
var grid = new Array(cols); //create 2 dimensional array for letter grid
for (var i = 0; i < rows; i++) {
grid[i] = new Array(rows);
}
for (var x = 0; x < cols; x++) {
for (var y = 0; y < rows; y++) {
grid[x][y] = {};
grid[x][y].targetChar = EMPTYCHAR; //target character, hidden
grid[x][y].indexDisplay = ''; //used to display index number of word start
grid[x][y].value = '-'; //actual current letter shown on board
}
}
function suggestCoords(word) { //search for potential cross placement locations
var c = '';
coordCount = [];
coordCount = 0;
for (i = 0; i < word.length; i++) { //cycle through each character of the word
for (x = 0; x < GRID_HEIGHT; x++) {
for (y = 0; y < GRID_WIDTH; y++) {
c = word[i];
if (grid[x][y].targetChar == c) { //check for letter match in cell
if (x - i + 1> 0 && x - i + word.length-1 < GRID_HEIGHT) { //would fit vertically?
coordList[coordCount] = {};
coordList[coordCount].x = x - i;
coordList[coordCount].y = y;
coordList[coordCount].score = 0;
coordList[coordCount].vertical = true;
coordCount++;
}
if (y - i + 1 > 0 && y - i + word.length-1 < GRID_WIDTH) { //would fit horizontally?
coordList[coordCount] = {};
coordList[coordCount].x = x;
coordList[coordCount].y = y - i;
coordList[coordCount].score = 0;
coordList[coordCount].vertical = false;
coordCount++;
}
}
}
}
}
}
function checkFitScore(word, x, y, vertical) {
var fitScore = 1; //default is 1, 2+ has crosses, 0 is invalid due to collision
if (vertical) { //vertical checking
for (i = 0; i < word.length; i++) {
if (i == 0 && x > 0) { //check for empty space preceeding first character of word if not on edge
if (grid[x - 1][y].targetChar != EMPTYCHAR) { //adjacent letter collision
fitScore = 0;
break;
}
} else if (i == word.length && x < GRID_HEIGHT) { //check for empty space after last character of word if not on edge
if (grid[x+i+1][y].targetChar != EMPTYCHAR) { //adjacent letter collision
fitScore = 0;
break;
}
}
if (x + i < GRID_HEIGHT) {
if (grid[x + i][y].targetChar == word[i]) { //letter match - aka cross point
fitScore += 1;
} else if (grid[x + i][y].targetChar != EMPTYCHAR) { //letter doesn't match and it isn't empty so there is a collision
fitScore = 0;
break;
} else { //verify that there aren't letters on either side of placement if it isn't a crosspoint
if (y < GRID_WIDTH - 1) { //check right side if it isn't on the edge
if (grid[x + i][y + 1].targetChar != EMPTYCHAR) { //adjacent letter collision
fitScore = 0;
break;
}
}
if (y > 0) { //check left side if it isn't on the edge
if (grid[x + i][y - 1].targetChar != EMPTYCHAR) { //adjacent letter collision
fitScore = 0;
break;
}
}
}
}
}
} else { //horizontal checking
for (i = 0; i < word.length; i++) {
if (i == 0 && y > 0) { //check for empty space preceeding first character of word if not on edge
if (grid[x][y-1].targetChar != EMPTYCHAR) { //adjacent letter collision
fitScore = 0;
break;
}
} else if (i == word.length - 1 && y + i < GRID_WIDTH -1) { //check for empty space after last character of word if not on edge
if (grid[x][y + i + 1].targetChar != EMPTYCHAR) { //adjacent letter collision
fitScore = 0;
break;
}
}
if (y + i < GRID_WIDTH) {
if (grid[x][y + i].targetChar == word[i]) { //letter match - aka cross point
fitScore += 1;
} else if (grid[x][y + i].targetChar != EMPTYCHAR) { //letter doesn't match and it isn't empty so there is a collision
fitScore = 0;
break;
} else { //verify that there aren't letters on either side of placement if it isn't a crosspoint
if (x < GRID_HEIGHT) { //check top side if it isn't on the edge
if (grid[x + 1][y + i].targetChar != EMPTYCHAR) { //adjacent letter collision
fitScore = 0;
break;
}
}
if (x > 0) { //check bottom side if it isn't on the edge
if (grid[x - 1][y + i].targetChar != EMPTYCHAR) { //adjacent letter collision
fitScore = 0;
break;
}
}
}
}
}
}
return fitScore;
}
function placeWord(word, clue, x, y, vertical) { //places a new active word on the board
var wordPlaced = false;
if (vertical) {
if (word.length + x < GRID_HEIGHT) {
for (i = 0; i < word.length; i++) {
grid[x + i][y].targetChar = word[i];
}
wordPlaced = true;
}
} else {
if (word.length + y < GRID_WIDTH) {
for (i = 0; i < word.length; i++) {
grid[x][y + i].targetChar = word[i];
}
wordPlaced = true;
}
}
if (wordPlaced) {
var currentIndex = activeWordList.length;
activeWordList[currentIndex] = {};
activeWordList[currentIndex].word = word;
activeWordList[currentIndex].clue = clue;
activeWordList[currentIndex].x = x;
activeWordList[currentIndex].y = y;
activeWordList[currentIndex].vertical = vertical;
if (activeWordList[currentIndex].vertical) {
downCount++;
activeWordList[currentIndex].number = downCount;
} else {
acrossCount++;
activeWordList[currentIndex].number = acrossCount;
}
}
}
function isActiveWord(word) {
if (activeWordList.length > 0) {
for (var w = 0; w < activeWordList.length; w++) {
if (word == activeWordList[w].word) {
//console.log(word + ' in activeWordList');
return true;
}
}
}
return false;
}
this.displayGrid = function displayGrid() {
var rowStr = "";
for (var x = 0; x < cols; x++) {
for (var y = 0; y < rows; y++) {
rowStr += "<td>" + grid[x][y].targetChar + "</td>";
}
$('#tempTable').append("<tr>" + rowStr + "</tr>");
rowStr = "";
}
console.log('across ' + acrossCount);
console.log('down ' + downCount);
}
//for each word in the source array we test where it can fit on the board and then test those locations for validity against other already placed words
this.generateBoard = function generateBoard(seed = 0) {
var bestScoreIndex = 0;
var top = 0;
var fitScore = 0;
var startTime;
//manually place the longest word horizontally at 0,0, try others if the generated board is too weak
placeWord(wordArray[seed].word, wordArray[seed].displayWord, wordArray[seed].clue, 0, 0, false);
//attempt to fill the rest of the board
for (var iy = 0; iy < FIT_ATTEMPTS; iy++) { //usually 2 times is enough for max fill potential
for (var ix = 1; ix < wordArray.length; ix++) {
if (!isActiveWord(wordArray[ix].word)) { //only add if not already in the active word list
topScore = 0;
bestScoreIndex = 0;
suggestCoords(wordArray[ix].word); //fills coordList and coordCount
coordList = shuffleArray(coordList); //adds some randomization
if (coordList[0]) {
for (c = 0; c < coordList.length; c++) { //get the best fit score from the list of possible valid coordinates
fitScore = checkFitScore(wordArray[ix].word, coordList[c].x, coordList[c].y, coordList[c].vertical);
if (fitScore > topScore) {
topScore = fitScore;
bestScoreIndex = c;
}
}
}
if (topScore > 1) { //only place a word if it has a fitscore of 2 or higher
placeWord(wordArray[ix].word, wordArray[ix].clue, coordList[bestScoreIndex].x, coordList[bestScoreIndex].y, coordList[bestScoreIndex].vertical);
}
}
}
}
if(activeWordList.length < wordArray.length/2) { //regenerate board if if less than half the words were placed
seed++;
generateBoard(seed);
}
}
}
function seedBoard() {
gameboard = new board(GRID_WIDTH, GRID_HEIGHT);
gameboard.generateBoard();
gameboard.displayGrid();
}
评论
虽然这是一个较老的问题,但会根据我所做的类似工作尝试答案。
有许多方法可以解决约束问题(一般属于 NPC 复杂度类)。
这与组合优化和约束规划有关。在这种情况下,约束是网格的几何形状和单词唯一的要求等。
随机化/退火方法也可以工作(尽管在适当的设置范围内)。
高效简单可能就是终极智慧!
要求是或多或少完整的填字游戏编译器和(可视化所见即所得)构建器。
撇开所见即所得的构建器部分不谈,编译器大纲是这样的:
加载可用的单词列表(按单词长度排序,即 2,3,..,20)
在用户构造的网格上找到单词(即网格单词)(例如,长度为L的x,y处的单词,水平或垂直)(复杂度O(N) )
计算网格字的相交点(需要填充)(复杂度 O(N^2) )
计算单词列表中单词与所用字母表中各种字母的交集(这允许使用模板搜索匹配的单词,例如。cwc 使用的 Sik Cambon 论文 )( 复杂度 O(WL*AL) )
步骤 .3 和 .4 允许执行此任务:
一个。网格词与自身的交集可以创建一个“模板”,用于尝试在该网格词的可用词的关联词列表中查找匹配项(通过使用其他与该词相交的单词的字母,这些字母已经在算法的某个步骤中填充)
b.单词列表中的单词与字母表的交集可以找到与给定“模板”匹配的匹配(候选)单词(例如,第一位的“A”和第三位的“B”等)
因此,在实现这些数据结构后,使用的算法如下所示:
注意:如果网格和单词数据库是常量,则前面的步骤只需执行一次。
该算法的第一步是随机选择一个空的字槽(网格字),并从其相关的词列表中填充一个候选词(随机化可以在算法的连续执行中产生不同的解子)(复杂度 O(1) 或 O(N) )
对于每个仍然为空的字槽(与已填充的字槽有交集),计算一个约束比(这可能会有所不同,简单是该步骤中可用解的数量)并按此比率对空字槽进行排序(复杂度 O(NlogN) 或 O(N) ) )
遍历上一步计算的空字段,并为每个空位尝试一些候选解决方案(确保“弧一致性保留”,即如果使用此词,网格在此步骤之后有一个解决方案)并根据下一步的最大可用性对它们进行排序(即,如果该词在那个时候在那个地方使用,则下一步具有最大可能的解决方案, 等。( 复杂度 O(N*MaxCandidatesUsed) )
填写该单词(将其标记为已填写并转到步骤 2)
如果没有找到满足步骤 .3 条件的单词,请尝试回溯到前一步的另一个候选解决方案(此处的标准可能会有所不同)(复杂度 O(N) ) )
如果找到回溯,请使用替代方法,并选择性地重置任何可能需要重置的已填充单词(再次将它们标记为未填充)(复杂度 O(N) )
如果未找到回溯,则找不到解决方案(至少使用此配置、初始种子等)
否则,当所有字数都填满时,您就有一个解决方案
该算法对问题的解决方案树进行随机一致的游走。如果在某个时候有一个死胡同,它会回溯到前一个节点并遵循另一条路线。直到找到解决方案或各个节点的候选节点数量耗尽。
一致性部分确保找到的解决方案确实是解决方案,随机部分能够在不同的执行中产生不同的解决方案,并且平均而言具有更好的性能。
所有这些(以及其他)都是在纯JavaScript(具有并行处理和所见即所得)功能中实现的
PS2的。该算法可以很容易地并行化,以便同时生成多个(不同的)解决方案
希望这会有所帮助
评论
我正在玩填字游戏生成器引擎,我发现这是最重要的:
0.!/usr/bin/python
一个。
allwords.sort(key=len, reverse=True)
b. 制作一些像光标这样的项目/对象,它将在矩阵中移动以便于定位 除非您以后想通过随机选择进行迭代。
第一个,拿起第一对,把它们放在 0,0 的对面和下面; 将第一个存储为我们当前的填字游戏“领导者”。
按对角线或随机顺序移动光标,对角线概率更大,移动到下一个空单元格
遍历单词 like 并使用可用空格长度来定义最大单词长度:
temp=[] for w_size in range( len( w_space ), 2, -1 ) : # t for w in [ word for word in allwords if len(word) == w_size ] : # if w not in temp and putTheWord( w, w_space ) : # temp.append( w )
将单词与我使用的可用空间进行比较,即:
w_space=['c','.','a','.','.','.'] # whereas dots are blank cells # CONVERT MULTIPLE '.' INTO '.*' FOR REGEX pattern = r''.join( [ x.letter for x in w_space ] ) pattern = pattern.strip('.') +'.*' if pattern[-1] == '.' else pattern prog = re.compile( pattern, re.U | re.I ) if prog.match( w ) : # if prog.match( w ).group() == w : # return True
每个成功使用的单词后,改变方向。 当所有单元格都填满时循环,或者你用完了单词,或者通过迭代的限制,然后:
# CHANGE ALL WORDS LIST
inexOf1stWord = allwords.index( leading_w )
allwords = allwords[:inexOf1stWord+1][:] + allwords[inexOf1stWord+1:][:]
...并再次迭代新的填字游戏。
通过易于填写和一些估计计算来制作评分系统。 为当前填字游戏打分,并通过将其附加到 如果您的评分系统满足分数,则填字游戏列表。
在第一次迭代会话之后,再次从制作的填字游戏列表中迭代以完成工作。
通过使用更多的参数,速度可以提高一个巨大的因素。
我已经为这个问题编写了一个JavaScript / jQuery解决方案:
示例演示:http://www.earthfluent.com/crossword-puzzle-demo.html
源代码: https://github.com/HoldOffHunger/jquery-crossword-puzzle-generator
我使用的算法的意图:
- 尽可能减少网格中不可用的方块的数量。
- 尽可能多地混合单词。
- 在极快的时间内进行计算。
我将描述我使用的算法:
根据共享共同字母的单词将单词组合在一起。
从这些组中,构建新数据结构(“词块”)的集合,该结构是主要单词(贯穿所有其他单词),然后是其他单词(贯穿主要单词)。
从填字游戏左上角的第一个单词块开始填字游戏。
对于其余的单词块,从填字游戏的最右下角开始,向上和向左移动,直到没有更多可用插槽可以填充。如果向上的空列多于向左的空列,则向上移动,反之亦然。
评论
var crosswords = generateCrosswordBlockSources(puzzlewords);
这个项目出现在哈佛大学的 AI CS50 课程中。这个想法是将填字游戏生成问题表述为约束满足问题,并通过使用不同的启发式回溯来解决它,以减少搜索空间。
首先,我们需要几个输入文件:
- 填字游戏的结构(如下图所示,例如,“#”表示要填充的字符,“_”表示要填充的字符)
`
###_####_#
____####_#
_##_#_____
_##_#_##_#
______####
#_###_####
#_##______
#_###_##_#
_____###_#
#_######_#
##_______#
`
输入词汇表(单词列表/词典),从中选择候选单词(如下所示)。
a abandon ability able abortion about above abroad absence absolute absolutely ...
现在,CSP 已定义并按如下方式求解:
- 变量被定义为具有作为输入提供的单词(词汇表)列表中的值(即它们的域)。
- 每个变量由一个 3 元组表示:(grid_coordinate, direction, length),其中坐标表示相应单词的开头,direction 可以是水平或垂直的,长度定义为变量将被分配到的单词的长度。
- 约束由提供的结构输入定义:例如,如果水平变量和垂直变量具有共同特征,则它将表示为重叠(圆弧)约束。
- 现在,可以使用节点一致性和 AC3 弧一致性算法来减少域。
- 然后回溯以获得具有 MRV(最小剩余值)、度等的 CSP 解决方案(如果存在),启发式方法可用于选择下一个未分配的变量,而 LCV(最小约束值)等启发式方法可用于域排序,以使搜索算法更快。
下面显示了使用 CSP 求解算法的实现获得的输出:
`
███S████D█
MUCH████E█
E██A█AGENT
S██R█N██Y█
SUPPLY████
█N███O████
█I██INSIDE
█Q███E██A█
SUGAR███N█
█E██████C█
██OFFENSE█
`
以下动画显示了回溯步骤:
这是另一个带有孟加拉语(孟加拉语)单词列表的单词:
评论
AI CS50 course
评论