/* Squaring Words by John Walker -- January 1998 http://www.fourmilab.ch/ This program is in the public domain. This program searches for solutions to the "squaring words" puzzle posed in the conclusion of Chapter XVIII, "Picking Locks and Deciphering", of Charles Babbage's autobiography, "Passages from the Life of a Philosopher". For example, squaring the word "dean" is to fill in the missing letters in the grid: d e a n e . . . a . . . n . . . so that the tableau reads the same across and down. One solution for "dean" is the following, given by Babbage. d e a n e a s e a s k s n e s t This program, used in conjunction with a spelling checker dictionary consisting of an ASCII text file with one word per line (like a Unix /usr/dict/words file), searches for ways to square the word given as its sole command line argument, printing all solutions on standard output. If your system doesn't have a suitable dictionary, you can download the Moby Words dictionary from: http://www.clres.com/dict.html or by FTP from: ftp://ftp.dcs.shef.ac.uk/share/ilash/Moby/ Get the Moby Words file, mwords.tar.Z (many of the other files in this directory are also interesting), uncompress and un-tar, and consult the readme.txt file for a description of the contents. The list of 354,984 single words is an excellent one to use with this program. If you are looking for more common words in the square, try the list of 113,809 words from the Scrabble[tm] dictionary, or the list of 74,550 common dictionary words. */ #include <stdio.h> #include <string.h> #include <ctype.h> #include <stdlib.h> #define DICT "moby.txt" /* Dictionary file */ #define NW 5000 /* Maximum candidate words per initial letter */ #define MAXWORD 128 /* Longest target word */ #define IX(x) ((x) - 'a') /* Map lcased initial letter to array index */ #define FALSE 0 #define TRUE 1 static char *words[IX('z') + 1][NW]; static int nwl[IX('z') + 1]; static char *target; static int ltarget; static char *cand[128]; /* LCASE -- Translate a string in place to all lower case. If the string contains any non-alphabetic character FALSE is returned, otherwise TRUE. */ static int lcase(char *cp) { int allalpha = TRUE; while (*cp) { if (!isalpha(*cp)) { allalpha = FALSE; } if (isupper(*cp)) { *cp = tolower(*cp); } cp++; } return allalpha; } /* ISSOL -- Test whether the words assembled so far constitute a valid solution. */ static int issol(int n) { int i, j; for (i = 1; i <= n; i++) { for (j = 1; j < n; j++) { if (cand[i][j] != cand[j][i]) { return FALSE; } } } return TRUE; } /* TESTSOL -- Test for a solution for a given letter. Upon finding a solution at that level, this function plugs the candidate word into the cand[] array and calls itself recursively to search for solutions for the next letter. If a solution is found for the last letter, print the complete square and return. */ static void testsol(int letter) { int k, l, m; for (k = 0; k < nwl[IX(target[letter])]; k++) { char *w = words[IX(target[letter])][k]; /* printf("L = %d w = %s cand = %s\n", letter, cand[letter - 1], w); /* Show candidate tests */ if (cand[letter - 1][letter] == w[letter - 1]) { cand[letter] = w; if (letter == (ltarget - 1)) { if (issol(letter)) { printf("Solution:\n"); for (l = 0; l <= letter; l++) { printf(" "); for (m = 0; m < (int) strlen(cand[l]); m++) { printf(" %c", cand[l][m]); } printf("\n"); } } } else { if (issol(letter)) { testsol(letter + 1); } } } } } /* Main program */ int main(int argc, char *argv[]) { FILE *fp; char s[132]; if (argc == 2) { target = argv[1]; } else { fprintf(stderr, "usage: %s word\n", argv[0]); return 2; } /* Validate and prepare target word. */ if (!lcase(target)) { fprintf(stderr, "%s: cannot square word with non-alphabetic character.\n", argv[0]); return 2; } ltarget = strlen(target); if (ltarget > MAXWORD) { fprintf(stderr, "Length of target word (%d) exceeds the maximum of %d.\n", ltarget, MAXWORD); return 2; } /* Load the portion of dictionary that we need into memory. */ memset(nwl, 0, sizeof nwl); fp = fopen(DICT, "r"); if (fp == NULL) { fprintf(stderr, "Cannot open dictionary file: %s\n", DICT); } while (fgets(s, sizeof s, fp)) { /* Chop EOL this way because some dictionaries have MS-DOS CR/LF sequences. */ while (isspace(s[strlen(s) - 1])) { s[strlen(s) - 1] = 0; } /* Discard any dictionary entry with a non-alphabetic character in it. This isn't strictly necessary, but the problem was posed as a crossword puzzle, and the chance of finding solutions for words with non-alphabetic characters (for example, "we're") is vanishingly small. Note: if you're thinking of loosening this restriction, note that the indexing of the radix sort by first letter assumes only characters between 'a' and 'z'. If you permit other initial characters, you'll have to expand the words[] and nwl[] arrays and modify the IX() macro accordingly. */ if (lcase(s)) { /* Filter for words which begin with letters of the target and are short enough to be candidates. */ if (strchr(target + 1, s[0]) != NULL && strlen(s) == ltarget) { int x = IX(s[0]); if (nwl[x] >= NW) { fprintf(stderr, "Boom!!! More than %d words starting with \"%c\".\n", NW, s[0]); fprintf(stderr, " Rebuild with a larger setting for #define NW\n"); exit(1); } words[x][nwl[x]++] = strdup(s); /*printf("%s\n", s); /* Print candidate words from dictionary */ } } } fclose(fp); #ifdef DIAGNOSTICS /* Print summary of candidate words. */ for (i = 'a'; i <= 'z'; i++) { if (nwl[IX(i)] > 0) { fprintf(stderr, "%c %5d\n", i, nwl[IX(i)]); } } #endif /* Search for solutions. */ cand[0] = target; /* Set first level candidate as target word */ testsol(1); /* Invoke first level solution seeker */ return 0; }