십자말 풀이를 생성하는 알고리즘


123

단어 목록이 주어 졌을 때 십자말 풀이 격자로 정렬하는 방법은 무엇입니까?

대칭 적이거나 그와 비슷한 "적절한"십자말 풀이처럼 될 필요는 없습니다. 기본적으로 각 단어의 시작 위치와 방향을 출력합니다.

답변:


62

나는 아마도 가장 효율적이지는 않지만 충분히 잘 작동하는 솔루션을 생각해 냈습니다. 원래:

  1. 모든 단어를 길이별로 내림차순으로 정렬합니다.
  2. 첫 번째 단어를 가져 와서 칠판에 놓습니다.
  3. 다음 단어를 가져 가십시오.
  4. 보드에 이미있는 모든 단어를 검색하고이 단어와 교차 할 수있는 교차점 (일반적인 문자)이 있는지 확인합니다.
  5. 이 단어에 대한 가능한 위치가 있으면 칠판에있는 모든 단어를 반복하고 새 단어가 간섭하는지 확인합니다.
  6. 이 단어가 보드를 깨지 않으면 거기에 놓고 3 단계로 이동하고 그렇지 않으면 계속해서 장소를 검색합니다 (4 단계).
  7. 모든 단어가 배치되거나 배치 될 수 없을 때까지이 루프를 계속하십시오.

이것은 작동하지만 종종 매우 좋지 않은 십자말 풀이를 만듭니다. 더 나은 결과를 얻기 위해 위의 기본 레시피를 여러 가지 변경했습니다.

  • 십자말 풀이 생성이 끝나면 배치 된 단어 수 (더 많을수록 좋음), 보드 크기 (작을수록 좋음), 높이와 너비 사이의 비율 (가까울수록 좋음)에 따라 점수를 부여합니다. 1로 더 좋음). 여러 십자말 풀이를 생성 한 다음 점수를 비교하고 가장 좋은 것을 선택하십시오.
    • 임의의 수의 반복을 실행하는 대신 임의의 시간 내에 가능한 한 많은 십자말 풀이를 만들기로 결정했습니다. 작은 단어 목록 만 있으면 5 초 안에 수십 개의 가능한 십자말 풀이를 얻을 수 있습니다. 더 큰 십자말 풀이는 5-6 개의 가능성 중에서 선택 될 수 있습니다.
  • 새 단어를 배치 할 때 허용 가능한 위치를 찾는 즉시 배치하는 대신 그리드의 크기를 늘리는 정도와 교차점 수에 따라 해당 단어 위치에 점수를 부여하십시오 (이상적으로는 각 단어가 2-3 개의 다른 단어로 교차). 모든 위치와 점수를 기록한 다음 가장 좋은 것을 선택하십시오.

7
나는 우리가 말하는대로이 프로그램을 작성하고 있으며 이것은 내가 선택한 것과 동일한 알고리즘입니다. 적은 수의 단어 (10 개 이하)의 경우 프로그램은 가능한 모든 솔루션을 밀리 초 단위로 계산하는 데 문제가 없습니다. 알고리즘은 기하 급수적입니다. 쉬운 부분은 가능한 모든 조합을 통해 무차별 대입하는 기본 알고리즘을 작성하는 것입니다. 어려운 부분은 프로그램이 모든 막 다른 솔루션을 시도하지 못하도록 방지하는 데 필요한 12 개 정도의 '바로 가기'입니다.
user31586

2
"5. ... 그리고 새로운 단어가 간섭하는지 확인하십시오."새 단어가 기존 단어와 함께 위치하여 인접한 사각형이있는 위치에서 횡설수설을 생성하는 상황을 어떻게 설명합니까? 예 : LEMON ERASE "LE", "ER"및 "MA"등이 목록에있는 단어가 아니면 이것은 잘못된 것입니다. 반면에 이러한 인접성을 완전히 거부하면 다음과 같은 정말 좋은 그리드가 사라질 수 있습니다. W LEMON ERASE NEXUS TT
George Armhold

4
@Kaffeine, 예, 무슨 뜻인지 압니다.이 옵션은 정말 좋은 그리드를 만들 수 있지만 확인하기가 너무 어렵 기 때문에 버려야했습니다 (읽기 : 귀찮게 할 수 없음) , 어차피 간섭 일 가능성이 있습니다. .
nickf

4
위의 권장 사항과 몇 가지를 사용하여 jQuery / Javascript로 구축되었습니다. mlewiscs.com/crossword
MLewisCodeSolutions

@MLewisCodeSolutions 멋지네요. 이것을 오픈 소싱하고 있습니까?
GKS

23

나는 최근에 파이썬으로 직접 작성했습니다. http://bryanhelmig.com/python-crossword-puzzle-generator/에서 찾을 수 있습니다 . 조밀 한 NYT 스타일의 십자말 풀이는 아니지만 어린이의 퍼즐 책에서 찾을 수있는 십자말 풀이 스타일입니다.

몇 가지 제안한 단어를 배치하는 무작위 무차별 대입 방법을 구현 한 몇 가지 알고리즘과 달리, 저는 단어 배치에서 약간 더 똑똑한 무차별 대입 접근 방식을 구현하려고했습니다. 내 프로세스는 다음과 같습니다.

  1. 크기에 관계없이 그리드와 단어 목록을 만듭니다.
  2. 단어 목록을 섞은 다음 가장 긴 단어부터 가장 짧은 단어 순으로 정렬합니다.
  3. 첫 번째와 가장 긴 단어를 가장 왼쪽 위의 위치 인 1,1 (세로 또는 가로)에 배치합니다.
  4. 다음 단어로 이동하여 단어의 각 문자와 격자의 각 셀을 반복하여 문자 대 문자 일치를 찾습니다.
  5. 일치하는 항목이 발견되면 해당 단어에 대해 제안 된 좌표 목록에 해당 위치를 추가하면됩니다.
  6. 제안 된 좌표 목록을 반복하고 교차하는 다른 단어 수에 따라 단어 배치를 "점수"합니다. 0 점은 잘못된 배치 (기존 단어에 인접) 또는 교차 단어가 없음을 나타냅니다.
  7. 단어 목록이 고갈 될 때까지 4 단계로 돌아갑니다. 선택적 두 번째 패스.
  8. 이제 십자말 풀이가 있어야하지만 일부 무작위 배치로 인해 품질이 떨어질 수 있습니다. 따라서이 십자말 풀이를 버퍼링하고 2 단계로 돌아갑니다. 다음 십자말 풀이에 보드에 더 많은 단어가 배치되면 버퍼의 십자말 풀이를 대체합니다. 이것은 시간 제한이 있습니다 (x 초 안에 최상의 크로스 워드 찾기).

결국, 당신은 괜찮은 십자말 풀이 또는 단어 검색 퍼즐이 거의 같습니다. 다소 잘 돌아가는 경향이 있지만 개선에 대한 제안이 있으면 알려주십시오. 더 큰 그리드는 기하 급수적으로 느리게 실행됩니다. 더 큰 단어는 선형으로 나열됩니다. 더 큰 단어 목록은 더 나은 단어 배치 번호를 얻을 가능성이 훨씬 더 높습니다.


@Neil N : 아마도 다른 단어에 대해 더 나은 문자 일치 가능성이 있습니다. 단어 당 포함 된 개별 문자 수를 기준으로 정렬하는 방법 일 수도 있으며, 이는 대부분 동일한 결과로 이어집니다.
Karl Adler

@NeilN Python array.sort(key=f)은 안정적입니다. 즉, (예를 들어) 단순히 알파벳순 단어 목록을 길이별로 정렬하면 모든 8 글자 단어가 알파벳순으로 정렬됩니다.
Lynn

4
@Bryan, 귀하의 웹 사이트 링크가 저에게 작동하지 않으며 기본 도메인이 Twitter로 리디렉션됩니다. 코드에 대한 업데이트 된 링크가 있습니까?
Michael A

2
다음은 (분명히) Bryan의 생성기 복제품입니다 : github.com/jeremy886/crossword_helmig
lvictorino

20

나는 실제로 십자말 풀이 생성 프로그램을 약 10 년 전에 썼습니다 (이는 수수께끼 였지만 일반적인 십자말 풀이에도 동일한 규칙이 적용될 것입니다).

파일에 저장된 단어 목록 (및 관련 단서)이 현재까지 내림차순으로 정렬되어있어 덜 사용되는 단어가 파일 맨 위에 표시됩니다. 기본적으로 검은 색 사각형과 자유 사각형을 나타내는 비트 마스크 인 템플릿은 클라이언트가 제공 한 풀에서 무작위로 선택되었습니다.

그런 다음 퍼즐의 완성되지 않은 각 단어에 대해 (기본적으로 첫 번째 빈 사각형을 찾고 오른쪽에있는 단어 (교차 단어) 또는 아래에있는 단어 (아래 단어)도 비어 있는지 확인) 검색이 수행되었습니다. 해당 단어에 이미있는 문자를 고려하여 맞는 첫 번째 단어를 찾는 파일. 적합 할 수있는 단어가 없다면 전체 단어를 불완전한 것으로 표시하고 계속 진행했습니다.

마지막에는 컴파일러가 채워야하는 완료되지 않은 단어가있을 것입니다 (원하는 경우 파일에 단어와 단서를 추가). 그들이 가지고 올 수없는 경우 모든 아이디어, 그들은 제약 조건을 변경하거나 전체 재생성을 요청 수동으로 크로스 워드 퍼즐, 편집 할 수 있습니다.

단어 / 단서 파일이 특정 크기에 도달하면 (이 클라이언트에 대해 하루에 50-100 개의 단서를 추가 함) 각 십자말 풀이에 대해 수행해야하는 수동 수정 작업이 두세 번 이상 발생하는 경우는 거의 없었습니다. .


내 상황에서 실제로 도움이되지는 않습니다. 약 6 ~ 12 개의 단어 목록 만 가지고 있기 때문입니다. Mine은 단어 퍼즐 이라기보다는 사용자를위한 학습 연습에 가깝습니다. 어쨌든 흥미로운 알고리즘에 +1!
nickf 2009-06-03

1
좋은 설명입니다. 나는 이것에 대해 과거에 몇 번 생각했지만 결코 시도하지 않았습니다. 이제 마법의 질문이 있습니다. 얼마나 잘 작동 했나요? 희소 퍼즐을위한 것입니까, 아니면 밀도가 높은 퍼즐 (종이와 같은)을위한 것입니까? 그리고 조밀 한 퍼즐에 필요한 단서는 몇 개입니까?
dmckee --- ex-moderator kitten 새끼 고양이

1
@dmckee, 오래 전 이었지만 기억에서 보면 빽빽한 퍼즐조차도 꽤 좋았습니다. 많은 부분이 개입없이 완료되었지만 여전히 1 ~ 2 개의 추가 단어가 필요한 5 분의 1을 얻을 수 있습니다. 그리고 우리는 파일에있는 수천 개의 단어에 대해 이야기하고 있습니다. 역 추적이 도움이 될 수는 있었지만 클라이언트가 추가 단서를 찾는 것에 대해 걱정하는 것보다 5 개의 미완성 단어로 하나를 거부하는 것이 더 쉬웠습니다. 5는 내가 미완성 단어로 본 외부 한계에 관한 것입니다.
paxdiablo

16

이 알고리즘은 60 초 내에 50 개의 조밀 한 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)

11

이것은 오래된 질문이지만 내가 한 비슷한 작업을 기반으로 답변을 시도합니다.

제약 문제를 해결하는 방법에는 여러 가지가 있습니다 (일반 레이아웃은 NPC 복잡성 클래스에 있음).

이것은 조합 최적화 및 제약 프로그래밍과 관련이 있습니다. 이 경우 제약 조건은 그리드의 기하학적 구조와 단어가 고유해야한다는 요구 사항 등입니다.

임의 화 / 어닐링 접근 방식도 작동 할 수 있습니다 (적절한 설정 내에서).

효율적인 단순함이 궁극적 인 지혜 일 수 있습니다!

요구 사항은 다소 완전한 크로스 워드 컴파일러 및 (시각적 WYSIWYG) 빌더에 대한 것이 었습니다.

WYSIWYG 빌더 부분을 제쳐두고 컴파일러 개요는 다음과 같습니다.

  1. 사용 가능한 단어 목록로드 (단어 길이별로 정렬, 즉 2,3, .., 20)

  2. 사용자가 구성한 그리드에서 단어 슬롯 (예 : 그리드 단어)을 찾습니다 (예 : 길이가 L, 수평 또는 수직 인 x, y의 단어) (복잡도 O (N))

  3. 채워야하는 그리드 단어의 교차점 계산 (복잡도 O (N ^ 2))

  4. 사용 된 알파벳의 다양한 문자와 단어 목록의 단어의 교차점을 계산합니다 (이를 통해 템플릿을 사용하여 일치하는 단어를 검색 할 수 있습니다 (예 : cwc에서 사용하는 Sik Cambon 논문 ). 복잡도 O (WL * AL)).

.3 및 .4 단계에서이 작업을 수행 할 수 있습니다.

ㅏ. 그리드 단어와 그 자체의 교차점은이 그리드 단어에 대해 사용 가능한 단어의 관련 단어 목록에서 일치하는 단어를 찾기위한 "템플릿"을 만들 수 있습니다 (이미 특정 단어에 이미 채워진이 단어와 교차하는 다른 단어의 문자를 사용하여 알고리즘 단계)

비. 단어 목록의 단어와 알파벳의 교차점을 통해 주어진 "템플릿"과 일치하는 일치하는 (후보) 단어를 찾을 수 있습니다 (예 : 'A'는 1 위, 'B'는 3 위 등).

따라서 이러한 데이터 구조를 구현하면 사용 된 알고리즘은 다음과 같습니다.

참고 : 그리드와 단어 데이터베이스가 일정하면 이전 단계를 한 번만 수행 할 수 있습니다.

  1. 알고리즘의 첫 번째 단계는 빈 단어 슬롯 (그리드 단어)을 무작위로 선택하고 연관된 단어 목록에서 후보 단어로 채우는 것입니다 (무작위 화는 알고리즘의 연속 실행에서 다른 솔루션을 생성 할 수 있음) (복잡도 O (1) 또는 O ( N))

  2. 여전히 비어있는 각 단어 슬롯 (이미 채워진 단어 슬롯과 교차가 있음)에 대해 제약 비율을 계산하고 (이것은 다를 수 있으며, sth simple은 해당 단계에서 사용 가능한 솔루션의 수입니다)이 비율에 따라 빈 단어 슬롯을 정렬합니다 (복잡도 O (NlogN ) 또는 O (N))

  3. 이전 단계에서 계산 된 빈 단어 슬롯을 반복하고 각각에 대해 여러 후보 솔루션을 시도합니다 ( "arc-consistency가 유지됨"확인, 즉이 단어가 사용되는 경우이 단계 이후에 그리드에 솔루션이 있음). 다음 단계에 대한 최대 가용성 (즉,이 단어가 해당 장소에서 해당 시간에 사용되는 경우 다음 단계에는 가능한 최대 솔루션이 있습니다.) (복잡도 O (N * MaxCandidatesUsed))

  4. 해당 단어를 채우십시오 (채워진 것으로 표시하고 2 단계로 이동).

  5. 단계 .3의 기준을 충족하는 단어가 발견되지 않으면 이전 단계의 다른 후보 솔루션으로 역 추적 해보십시오 (기준은 여기에서 다를 수 있음) (복잡도 O (N)).

  6. 역 추적이 발견되면 대체를 사용하고 선택적으로 재설정이 필요할 수있는 이미 채워진 단어를 재설정합니다 (다시 채워지지 않은 것으로 표시) (복잡도 O (N))

  7. 역 추적을 찾지 못하면 솔루션을 찾을 수 없습니다 (적어도이 구성, 초기 시드 등).

  8. 그렇지 않으면 모든 워드 롯이 채워지면 하나의 솔루션이 있습니다.

이 알고리즘은 문제의 솔루션 트리를 무작위로 일관되게 진행합니다. 어떤 지점에서 막 다른 골목이 있으면 이전 노드로 역 추적하고 다른 경로를 따릅니다. 솔루션이 발견되거나 다양한 노드에 대한 후보 수가 소진 될 때까지.

일관성 부분은 발견 된 솔루션이 실제로 솔루션인지 확인하고 임의 부분은 다른 실행에서 다른 솔루션을 생성 할 수 있으며 평균적으로 더 나은 성능을 제공합니다.

추신. 이 모든 것 (및 기타)은 순수 JavaScript (병렬 처리 및 WYSIWYG 포함) 기능으로 구현되었습니다.

PS2. 알고리즘은 동시에 하나 이상의 (다른) 솔루션을 생성하기 위해 쉽게 병렬화 될 수 있습니다.

도움이 되었기를 바랍니다


1
조밀 한 레이아웃 (NY Times like) 또는 희소 레이아웃을 만들기위한 것입니까?
Jim

1
@Jim, 이것은 대부분 조밀 한 레이아웃을위한 것이지만 희박한 경우에도 조정할 수 있습니다. 차이점은 조밀 한 레이아웃 (예 : 클래식, 스칸디 나비 크 등)에 있습니다. 하나는 그리드가 있고 단어를 검색하는 반면, 자유형 레이아웃 (희소)의 경우에는 단어가 있고 그리드를 검색합니다.
Nikos M.

1
위의 단계를 구현하는 어딘가에 샘플 소스를 사용할 수 있습니까? 예를 들어, 나는 대부분의 경우 당신과 함께 있지만 (그리고 이미 대부분 독립적으로 구현했습니다) "제약 비율 계산 ..."에 관해서는 당신이 나를 잃었다는 것을 인정해야합니다. "STH 비율"과 같은 것을 웹 검색하는 것도 나에게 큰 도움이되지 않습니다. 구현 의 문제 는 채울 단어를 찾는 것이 매우 비효율적이며 너무 오래 걸리는 것입니다.
Jim

1
@Jim, 나는 이것이 이미 사용되었지만 이것은 내가 가진 일에 대해 구체적으로 수행되었습니다. 더 많은 도움이 필요하면 저에게 연락하십시오 (실제로 ps) 내 오픈 소스 프로젝트에 경량 버전을 게시 할 수 있습니다 알고리즘은 내가 게시 된 경우가 있습니다 ) 너무 오래 걸리지 만 평균이 나던
니 코스 M.에게

1
@Jim,이 십자말 풀이 사이트 (아직 진행 중) istavrolexo.gr (그리스어)에서 유사한 알고리즘 ( 큰 스칸디 나비 크 십자말 )에 의해 생성 된 다양한 (밀도) 십자말 풀이 (예 : 스칸디 나비 크 , 클래식, 스도쿠)를 살펴보십시오.
Nikos M.

9

무작위 확률 적 접근 방식을 사용하여 시작하지 않는 이유는 무엇입니까? 단어로 시작한 다음 무작위 단어를 반복해서 골라 크기 등에 대한 제약을 깨지 않고 퍼즐의 현재 상태에 맞추려고합니다. 실패하면 다시 시작하십시오.

이와 같은 Monte Carlo 접근 방식이 얼마나 자주 작동하는지 놀랄 것입니다.


2
예, 이것이 제가 선택한 접근 방식입니다. 당신은 매우 영리하려고 노력할 필요가 없습니다. 가장 긴 단어에서 가장 짧은 단어로 정렬합니다. 루프에서 임의의 셀 (열 및 행 좌표)을 선택하고 보드 테스트에 단어를 배치하여 끝에서 벗어나거나 다른 단어를 방해하는지 확인합니다 (그리드에 단어를 쓰기 전에 각 셀이 비어 있거나 문자가있는 경우 해당 문자는 작성하려는 문자와 일치합니다). 경계와 물건을 확인하는 다른 논리가 있습니다. 나는 무차별 대입으로 더 작고 작은 그리드를 생성 한 다음 교차하는 단어를 기반으로 가장 작은 그리드의 순위를 매 깁니다.
Max Hodges 2012

6

다음은 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();
}

단어 객체 스키마가없는, wordArray 제공하십시오

문자 그대로 [ 'apple', 'orange', 'pear']와 같은 단어 배열
FascistDonut

안녕, 참고로 내 편집은 많은 코드를 변경하지 않았으며 형식을 지정했습니다. '인라인'으로 볼 때 너무 지저분 해 보이지만 코드 의 실제 변경 사항을 보려면 'side-by-side-markdown'을 클릭하십시오. 글쎄 ... 편집 설명에 "형식화 된 코드"를 작성 했어야했는데 meh.
2

어떻게 작동합니까? 이 자바 스크립트를 포함하는 html 파일을 제공 할 수 있습니까?
GKS

5

길이와 스크래블 점수의 두 가지 숫자를 생성합니다. 스크래블 점수가 낮 으면 참여하기가 더 쉽다는 것을 의미한다고 가정합니다 (낮은 점수 = 많은 공통 문자). 목록을 내림차순으로 정렬하고 스크래블 점수를 오름차순으로 정렬합니다.

다음으로 목록 아래로 이동하십시오. 단어가 기존 단어와 교차하지 않는 경우 (각 단어의 길이와 스크래블 점수로 각 단어를 확인) 대기열에 넣고 다음 단어를 확인합니다.

헹구고 반복하면 십자말 풀이가 생성됩니다.

물론, 이것이 O (n!)이고 십자말 풀이를 완성하는 것이 보장되지는 않지만 누군가가 그것을 향상시킬 수 있다고 확신합니다.


3

이 문제에 대해 생각하고 있습니다. 제 생각에는 정말 조밀 한 십자말 풀이를 만들려면 제한된 단어 목록이 충분할 것이라고 기대할 수 없습니다. 따라서 사전을 가져 와서 "trie"데이터 구조에 배치 할 수 있습니다. 이렇게하면 남은 공백을 채우는 단어를 쉽게 찾을 수 있습니다. trie에서는 "c? t"형식의 모든 단어를 제공하는 순회를 구현하는 것이 상당히 효율적입니다.

그래서, 제 일반적인 생각은 : 여기에 설명 된 것과 같이 비교적 무차별 한 접근 방식을 만들어 저밀도 크로스를 만들고 공백을 사전 단어로 채 웁니다.

다른 사람이이 접근 방식을 취했다면 알려주세요.


3

나는 십자말 풀이 생성기 엔진을 가지고 놀고 있었고 이것이 가장 중요하다는 것을 알았습니다.

0.!/usr/bin/python

  1. ㅏ. allwords.sort(key=len, reverse=True)

    비. 나중에 임의의 선택으로 반복하지 않는 한 쉬운 방향을 위해 매트릭스 주위를 돌아 다니는 커서와 같은 항목 / 객체를 만드십시오.

  2. 첫 번째는 첫 번째 쌍을 집어 0,0에서 가로 질러 놓는다. 첫 번째 단어를 현재 크로스 워드 '리더'로 저장합니다.

  3. 대각선 확률이 더 큰 다음 빈 셀로 커서를 대각선 또는 무작위 순서로 이동

  4. 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 )

  5. 내가 사용한 여유 공간과 단어를 비교하려면 다음을 사용하십시오.

    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
    
  6. 성공적으로 사용한 각 단어 후에 방향을 변경하십시오. 모든 셀이 채워지는 동안 반복하거나 단어가 부족하거나 반복 제한에 의해 다음을 수행하십시오.

# CHANGE ALL WORDS LIST inexOf1stWord = allwords.index( leading_w ) allwords = allwords[:inexOf1stWord+1][:] + allwords[inexOf1stWord+1:][:]

... 새 낱말 맞추기를 다시 반복합니다.

  1. 채우기의 용이성과 일부 추정 계산으로 채점 시스템을 만드십시오. 현재 십자말 풀이에 점수를 부여하고 점수 시스템이 점수를 만족하는 경우 만든 십자말 풀이 목록에 추가하여 나중에 선택 범위를 좁 힙니다.

  2. 첫 번째 반복 세션 후 만들어진 십자말 풀이 목록에서 다시 반복하여 작업을 완료합니다.

더 많은 매개 변수를 사용하면 속도가 크게 향상 될 수 있습니다.


2

가능한 십자가를 알기 위해 각 단어가 사용하는 각 문자의 색인을 얻습니다. 그런 다음 가장 큰 단어를 선택하여 기본으로 사용합니다. 다음 큰 것을 선택하고 교차하십시오. 헹구고 반복하십시오. 아마도 NP 문제 일 것입니다.

또 다른 아이디어는 강도 메트릭이 그리드에 얼마나 많은 단어를 넣을 수 있는지에 대한 유전 알고리즘을 만드는 것입니다.

내가 찾는 어려운 부분은 특정 목록을 넘을 수 없다는 것을 알 때입니다.


1
나는 또한 유전 알고리즘을 생각하고있었습니다. 피트니스 함수는 단어가 그리드에 얼마나 밀집되어 있는지를 나타냅니다.
Adrian McCarthy

2

이것은 AI CS50 과정 의 프로젝트로 나타납니다. Harvard . 이 아이디어는 십자말 풀이 문제를 제약 만족 문제로 공식화하고 검색 공간을 줄이기 위해 다른 휴리스틱으로 역 추적하여 해결하는 것입니다.

시작하려면 몇 가지 입력 파일이 필요합니다.

  1. 십자말 풀이의 구조 (다음과 유사합니다. 예를 들어 '#'는 채워지지 않는 문자를 나타내고 '_'는 채워질 문자를 나타냄)

`

###_####_#
____####_#
_##_#_____
_##_#_##_#
______####
#_###_####
#_##______
#_###_##_#
_____###_#
#_######_#
##_______#    

`

  1. 후보 단어가 선택 될 입력 어휘 (단어 목록 / 사전) (다음에 표시된 것과 같음).

    a abandon ability able abortion about above abroad absence absolute absolutely ...

이제 CSP가 정의되고 다음과 같이 해결됩니다.

  1. 변수는 입력으로 제공된 단어 (어휘) 목록의 값 (즉, 도메인)을 갖도록 정의됩니다.
  2. 각 변수는 3 개의 튜플 (grid_coordinate, direction, length)로 표시됩니다. 여기서 좌표는 해당 단어의 시작을 나타내고 방향은 수평 또는 수직 중 하나 일 수 있으며 길이는 변수가 될 단어의 길이로 정의됩니다. 할당.
  3. 제약 조건은 제공된 구조 입력에 의해 정의됩니다. 예를 들어 수평 및 수직 변수에 공통 문자가있는 경우 중첩 (아크) 제약 조건으로 표시됩니다.
  4. 이제 노드 일관성 및 AC3 아크 일관성 알고리즘을 사용하여 도메인을 줄일 수 있습니다.
  5. 그런 다음 역 추적하여 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█

`

다음 애니메이션은 역 추적 단계를 보여줍니다.

여기에 이미지 설명 입력

다음은 벵골어 (벵골어) 단어 목록이있는 또 다른 단어입니다.

여기에 이미지 설명 입력


정말 흥미로운 설명은 +1. 하지만 여기서 해결하려는 문제의 맥락은 모두 사용해야하는 작은 단어 집합이 있다는 것이었고, 레이아웃으로 시작하여 단어를 찾기보다는 십자말 풀이에 대한 최적의 레이아웃을 찾으려고 노력했습니다. 맞는.
nickf

1

jQuery 크로스 워드 퍼즐 생성기 및 게임

이 문제에 대한 JavaScript / jQuery 솔루션을 코딩했습니다.

샘플 데모 : http://www.earthfluent.com/crossword-puzzle-demo.html

소스 코드 : https://github.com/HoldOffHunger/jquery-crossword-puzzle-generator

내가 사용한 알고리즘의 의도 :

  1. 그리드에서 사용할 수없는 사각형의 수를 가능한 최소화하십시오.
  2. 가능한 한 많은 단어가 섞여 있습니다.
  3. 매우 빠른 시간에 컴퓨팅하십시오.

생성 된 십자말 풀이의 시연.

내가 사용한 알고리즘을 설명하겠습니다.

  1. 공통 문자를 공유하는 단어에 따라 단어를 그룹화하십시오.

  2. 이러한 그룹에서 새로운 데이터 구조 ( "단어 블록")의 집합을 작성합니다.이 단어는 기본 단어 (다른 모든 단어를 통해 실행 됨)와 다른 단어 (기본 단어를 통해 실행 됨) 인 것입니다.

  3. 십자말 풀이의 맨 위 왼쪽 위치에있는 첫 번째 단어 블록으로 십자말 풀이를 시작하십시오.

  4. 나머지 단어 블록의 경우 십자말 풀이의 가장 오른쪽 아래에서 시작하여 채울 수있는 슬롯이 더 이상 없을 때까지 위쪽과 왼쪽으로 이동합니다. 왼쪽보다 위쪽에 빈 열이 더 많은 경우 위쪽으로 이동하고 그 반대의 경우도 마찬가지입니다.


@holdoffhunger 십자말 풀이 키를 표시하는 방법이 있습니까? 글자가 채워진 상자?
Jon Glazer 19

@Jon Glazer : 일반적으로 십자말 풀이 키를 함수 자체에 보내지 만 십자말 풀이를 문자의 2d 배열로 기록 할 수 있습니다 var crosswords = generateCrosswordBlockSources(puzzlewords);. 이 값을 콘솔에 기록하십시오. 잊지 마세요. 게임에는 "치트 모드"가 있습니다. 여기서 "Reveal Answer"를 클릭하면 즉시 값을 얻을 수 있습니다.
HoldOffHunger

이것은 인접한 "아래"상자가있는 장소에서 의미없는 "횡단"단어로 퍼즐을 생성하며 그 반대의 경우도 마찬가지입니다. 그것이 비록 표준 십자말 풀이는 다음과 같이 작동하지 않는 밀도를 극대화 할 수 있습니다.
Beejor
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.