[algorithm] 십자말 풀이를 생성하는 알고리즘
단어 목록이 주어 졌을 때 십자말 풀이 격자로 정렬하는 방법은 무엇입니까?
대칭 적이거나 그와 비슷한 “적절한”십자말 풀이처럼 될 필요는 없습니다. 기본적으로 각 단어의 시작 위치와 방향을 출력합니다.
답변
나는 아마도 가장 효율적이지는 않지만 충분히 잘 작동하는 솔루션을 생각해 냈습니다. 원래:
- 모든 단어를 길이별로 내림차순으로 정렬합니다.
- 첫 번째 단어를 가져 와서 칠판에 놓습니다.
- 다음 단어를 가져 가십시오.
- 보드에 이미있는 모든 단어를 검색하고이 단어와 교차 할 수있는 교차점 (일반적인 문자)이 있는지 확인합니다.
- 이 단어에 대한 가능한 위치가 있으면 칠판에있는 모든 단어를 반복하고 새 단어가 간섭하는지 확인합니다.
- 이 단어가 보드를 깨지 않으면 거기에 놓고 3 단계로 이동하고 그렇지 않으면 계속해서 장소를 검색합니다 (4 단계).
- 모든 단어가 배치되거나 배치 될 수 없을 때까지이 루프를 계속하십시오.
이것은 작동하지만 종종 매우 좋지 않은 십자말 풀이를 만듭니다. 더 나은 결과를 얻기 위해 위의 기본 레시피를 여러 가지 변경했습니다.
- 십자말 풀이 생성이 끝나면 배치 된 단어 수 (더 많을수록 좋음), 보드 크기 (작을수록 좋음), 높이와 너비 사이의 비율 (가까울수록 좋음)에 따라 점수를 부여합니다. 1로 더 좋음). 여러 십자말 풀이를 생성 한 다음 점수를 비교하고 가장 좋은 것을 선택하십시오.
- 임의의 수의 반복을 실행하는 대신 임의의 시간 내에 가능한 한 많은 십자말 풀이를 만들기로 결정했습니다. 작은 단어 목록 만 있으면 5 초 안에 수십 개의 가능한 십자말 풀이를 얻을 수 있습니다. 더 큰 십자말 풀이는 5-6 개의 가능성 중에서 선택 될 수 있습니다.
- 새 단어를 배치 할 때 허용 가능한 위치를 찾는 즉시 배치하는 대신 그리드의 크기를 늘리는 정도와 교차점 수에 따라 해당 단어 위치에 점수를 부여하십시오 (이상적으로는 각 단어가 2-3 개의 다른 단어로 교차). 모든 위치와 점수를 기록한 다음 가장 좋은 것을 선택하십시오.
답변
나는 최근에 파이썬으로 직접 작성했습니다. http://bryanhelmig.com/python-crossword-puzzle-generator/에서 찾을 수 있습니다 . 조밀 한 NYT 스타일의 십자말 풀이는 아니지만 어린이의 퍼즐 책에서 찾을 수있는 십자말 풀이 스타일입니다.
몇 가지 제안한 단어를 배치하는 무작위 무차별 대입 방법을 구현 한 몇 가지 알고리즘과 달리, 저는 단어 배치에서 약간 더 똑똑한 무차별 대입 접근 방식을 구현하려고했습니다. 내 프로세스는 다음과 같습니다.
- 크기에 관계없이 그리드와 단어 목록을 만듭니다.
- 단어 목록을 섞은 다음 가장 긴 단어부터 가장 짧은 단어 순으로 정렬합니다.
- 첫 번째와 가장 긴 단어를 가장 왼쪽 위의 위치 인 1,1 (세로 또는 가로)에 배치합니다.
- 다음 단어로 이동하여 단어의 각 문자와 격자의 각 셀을 반복하여 문자 대 문자 일치를 찾습니다.
- 일치하는 항목이 발견되면 해당 단어에 대해 제안 된 좌표 목록에 해당 위치를 추가하면됩니다.
- 제안 된 좌표 목록을 반복하고 교차하는 다른 단어 수에 따라 단어 배치를 “점수”합니다. 0 점은 잘못된 배치 (기존 단어에 인접) 또는 교차 단어가 없음을 나타냅니다.
- 단어 목록이 고갈 될 때까지 4 단계로 돌아갑니다. 선택적 두 번째 패스.
- 이제 십자말 풀이가 있어야하지만 일부 무작위 배치로 인해 품질이 떨어질 수 있습니다. 따라서이 십자말 풀이를 버퍼링하고 2 단계로 돌아갑니다. 다음 십자말 풀이에 보드에 더 많은 단어가 배치되면 버퍼의 십자말 풀이를 대체합니다. 이것은 시간 제한이 있습니다 (x 초 안에 최상의 크로스 워드 찾기).
결국, 당신은 괜찮은 십자말 풀이 또는 단어 검색 퍼즐이 거의 같습니다. 다소 잘 돌아가는 경향이 있지만 개선에 대한 제안이 있으면 알려주십시오. 더 큰 그리드는 기하 급수적으로 느리게 실행됩니다. 더 큰 단어는 선형으로 나열됩니다. 더 큰 단어 목록은 더 나은 단어 배치 번호를 얻을 가능성이 훨씬 더 높습니다.
답변
나는 실제로 십자말 풀이 생성 프로그램을 약 10 년 전에 썼습니다 (이는 수수께끼 였지만 일반적인 십자말 풀이에도 동일한 규칙이 적용될 것입니다).
파일에 저장된 단어 목록 (및 관련 단서)이 현재까지 내림차순으로 정렬되어있어 덜 사용되는 단어가 파일 맨 위에 표시됩니다. 기본적으로 검은 색 사각형과 자유 사각형을 나타내는 비트 마스크 인 템플릿은 클라이언트가 제공 한 풀에서 무작위로 선택되었습니다.
그런 다음 퍼즐의 완성되지 않은 각 단어에 대해 (기본적으로 첫 번째 빈 사각형을 찾고 오른쪽에있는 단어 (교차 단어) 또는 아래에있는 단어 (아래 단어)도 비어 있는지 확인) 검색이 수행되었습니다. 해당 단어에 이미있는 문자를 고려하여 맞는 첫 번째 단어를 찾는 파일. 적합 할 수있는 단어가 없다면 전체 단어를 불완전한 것으로 표시하고 계속 진행했습니다.
마지막에는 컴파일러가 채워야하는 완료되지 않은 단어가있을 것입니다 (원하는 경우 파일에 단어와 단서를 추가). 그들이 가지고 올 수없는 경우 모든 아이디어, 그들은 제약 조건을 변경하거나 전체 재생성을 요청 수동으로 크로스 워드 퍼즐, 편집 할 수 있습니다.
단어 / 단서 파일이 특정 크기에 도달하면 (이 클라이언트에 대해 하루에 50-100 개의 단서를 추가 함) 각 십자말 풀이에 대해 수행해야하는 수동 수정 작업이 두세 번 이상 발생하는 경우는 거의 없었습니다. .
답변
이 알고리즘은 60 초 내에 50 개의 조밀 한 6×9 화살표 십자말 풀이 를 만듭니다 . 단어 데이터베이스 (단어 + 팁 포함)와 보드 데이터베이스 (사전 구성된 보드 포함)를 사용합니다.
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
더 큰 단어 데이터베이스는 생성 시간을 상당히 줄여주고 어떤 종류의 보드는 채우기가 더 어렵습니다! 보드가 클수록 올바르게 채우려면 더 많은 시간이 필요합니다!
예:
사전 구성된 6×9 보드 :
(#은 하나의 셀에 하나의 팁을 의미하고 %는 하나의 셀에 두 개의 팁을 의미하며 화살표는 표시되지 않음)
# - # # - % # - #
- - - - - - - - -
# - - - - - # - -
% - - # - # - - -
% - - - - - % - -
- - - - - - - - -
6×9 보드 생성 :
# 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)
답변
이것은 오래된 질문이지만 내가 한 비슷한 작업을 기반으로 답변을 시도합니다.
제약 문제를 해결하는 방법에는 여러 가지가 있습니다 (일반 레이아웃은 NPC 복잡성 클래스에 있음).
이것은 조합 최적화 및 제약 프로그래밍과 관련이 있습니다. 이 경우 제약 조건은 그리드의 기하학적 구조와 단어가 고유해야한다는 요구 사항 등입니다.
임의 화 / 어닐링 접근 방식도 작동 할 수 있습니다 (적절한 설정 내에서).
효율적인 단순함이 궁극적 인 지혜 일 수 있습니다!
요구 사항은 다소 완전한 크로스 워드 컴파일러 및 (시각적 WYSIWYG) 빌더에 대한 것이 었습니다.
WYSIWYG 빌더 부분을 제쳐두고 컴파일러 개요는 다음과 같습니다.
-
사용 가능한 단어 목록로드 (단어 길이별로 정렬, 즉 2,3, .., 20)
-
사용자가 구성한 그리드에서 단어 슬롯 (예 : 그리드 단어)을 찾습니다 (예 : 길이가 L, 수평 또는 수직 인 x, y의 단어) (복잡도 O (N))
-
채워야하는 그리드 단어의 교차점 계산 (복잡도 O (N ^ 2))
-
사용 된 알파벳의 다양한 문자와 단어 목록의 단어의 교차점을 계산합니다 (이를 통해 템플릿을 사용하여 일치하는 단어를 검색 할 수 있습니다 (예 : cwc에서 사용하는 Sik Cambon 논문 ). 복잡도 O (WL * AL)).
.3 및 .4 단계에서이 작업을 수행 할 수 있습니다.
ㅏ. 그리드 단어와 그 자체의 교차점은이 그리드 단어에 대해 사용 가능한 단어의 관련 단어 목록에서 일치하는 단어를 찾기위한 “템플릿”을 만들 수 있습니다 (이미 특정 단어에 이미 채워진이 단어와 교차하는 다른 단어의 문자를 사용하여 알고리즘 단계)
비. 단어 목록의 단어와 알파벳의 교차점을 통해 주어진 “템플릿”과 일치하는 일치하는 (후보) 단어를 찾을 수 있습니다 (예 : ‘A’는 1 위, ‘B’는 3 위 등).
따라서 이러한 데이터 구조를 구현하면 사용 된 알고리즘은 다음과 같습니다.
참고 : 그리드와 단어 데이터베이스가 일정하면 이전 단계를 한 번만 수행 할 수 있습니다.
-
알고리즘의 첫 번째 단계는 빈 단어 슬롯 (그리드 단어)을 무작위로 선택하고 연관된 단어 목록에서 후보 단어로 채우는 것입니다 (무작위 화는 알고리즘의 연속 실행에서 다른 솔루션을 생성 할 수 있음) (복잡도 O (1) 또는 O ( N))
-
여전히 비어있는 각 단어 슬롯 (이미 채워진 단어 슬롯과 교차가 있음)에 대해 제약 비율을 계산하고 (이것은 다를 수 있으며, sth simple은 해당 단계에서 사용 가능한 솔루션의 수입니다)이 비율에 따라 빈 단어 슬롯을 정렬합니다 (복잡도 O (NlogN ) 또는 O (N))
-
이전 단계에서 계산 된 빈 단어 슬롯을 반복하고 각각에 대해 여러 후보 솔루션을 시도합니다 ( “arc-consistency가 유지됨”확인, 즉이 단어가 사용되는 경우이 단계 이후에 그리드에 솔루션이 있음). 다음 단계에 대한 최대 가용성 (즉,이 단어가 해당 장소에서 해당 시간에 사용되는 경우 다음 단계에는 가능한 최대 솔루션이 있습니다.) (복잡도 O (N * MaxCandidatesUsed))
-
해당 단어를 채우십시오 (채워진 것으로 표시하고 2 단계로 이동).
-
단계 .3의 기준을 충족하는 단어가 발견되지 않으면 이전 단계의 다른 후보 솔루션으로 역 추적 해보십시오 (기준은 여기에서 다를 수 있음) (복잡도 O (N)).
-
역 추적이 발견되면 대체를 사용하고 선택적으로 재설정이 필요할 수있는 이미 채워진 단어를 재설정합니다 (다시 채워지지 않은 것으로 표시) (복잡도 O (N))
-
역 추적을 찾지 못하면 솔루션을 찾을 수 없습니다 (적어도이 구성, 초기 시드 등).
-
그렇지 않으면 모든 워드 롯이 채워지면 하나의 솔루션이 있습니다.
이 알고리즘은 문제의 솔루션 트리를 무작위로 일관되게 진행합니다. 어떤 지점에서 막 다른 골목이 있으면 이전 노드로 역 추적하고 다른 경로를 따릅니다. 솔루션이 발견되거나 다양한 노드에 대한 후보 수가 소진 될 때까지.
일관성 부분은 발견 된 솔루션이 실제로 솔루션인지 확인하고 임의 부분은 다른 실행에서 다른 솔루션을 생성 할 수 있으며 평균적으로 더 나은 성능을 제공합니다.
추신. 이 모든 것 (및 기타)은 순수 JavaScript (병렬 처리 및 WYSIWYG 포함) 기능으로 구현되었습니다.
PS2. 알고리즘은 동시에 하나 이상의 (다른) 솔루션을 생성하기 위해 쉽게 병렬화 될 수 있습니다.
도움이 되었기를 바랍니다
답변
무작위 확률 적 접근 방식을 사용하여 시작하지 않는 이유는 무엇입니까? 단어로 시작한 다음 무작위 단어를 반복해서 골라 크기 등에 대한 제약을 깨지 않고 퍼즐의 현재 상태에 맞추려고합니다. 실패하면 다시 시작하십시오.
이와 같은 Monte Carlo 접근 방식이 얼마나 자주 작동하는지 놀랄 것입니다.
답변
다음은 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();
}