- I removed the Qt dependencies from the code here so that hopefully anyone can compile it if they want. I use the command
g++ -o solver -O3 -std=gnu++11 -Wall solver.cpp main.cpp. You will have to grabtrie.hfrom here and a word list from here on GitHub. - Just yesterday, I tried to optimize the code a little by using a
std::setwith a custom compare functor to store words and remove duplicates, but I ended up losing some solutions that way. - I figured out how to store words in a
std::setin a way that works now, although the functor seems pretty inefficient. It has good results though. On one hard puzzle, the old code produced 239,711 solutions. The new code produces 195,489. I verified that the lost solutions are duplicates. The time difference between the two versions is negligible. - Changed where I push back onto
mWord. On harder puzzles, this results in millions or billions fewer calls topush_backand no calls topop_back. Hardly any savings as far as runtime, but I'll take it.
I suppose I have the usual questions:
- Is the code readable and understandable?
- What improvements can I make todo a LOT of string concatenation in
findWordswith the code or algorithmsstatementmPrefix += board[n];and the results stand out in my profiling with callgrind. Isstd::stringas good as I can get here? - What can I do to improve performance besides attempting multi-threading?
- What improvements can I make to the code or algorithms in general?
#ifndef SOLVER_H
#define SOLVER_H
#include <algorithm>
#include <array>
#include <memory>
#include <set>
#include <string>
#include <vector>
#include "solutionentry.h"
#include "trie.h"
class Solver
{
private:
typedef std::vector<unsigned int> tIntVec;
typedef std::vector<SolutionEntry> tSolution;
enum { NullIndex = -1 };
enum { NullNeighbor = -1, MaxNeighbors = 64 };
public:
Solver();
Solver(const std::string &board, int size, const tIntVec &wordlengths,
const std::string &wordfile);
~Solver();
bool solve();
void setBoard(const std::string &board)
{
mBoard = board;
}
void setSize(int size)
{
mSize = size;
mSizeSquared = size * size;
}
void setWordLengths(const tIntVec &wordlengths)
{
mWordLengths = wordlengths;
}
void setWordFile(const std::string &wordfile)
{
if (mWordFile != wordfile) {
mWordFile = wordfile;
}
}
tIntVec getWordLengths() {
return mWordLengths;
}
private:
struct WordCompare {
bool operator()(const std::pair<std::string, tIntVec> &lhs,
const std::pair<std::string, tIntVec> &rhs) const {
auto leftVector = lhs.second;
auto rightVector = rhs.second;
std::sort(leftVector.begin(), leftVector.end());
std::sort(rightVector.begin(), rightVector.end());
return (lhs.first != rhs.first) || (leftVector != rightVector);
}
};
bool findSolutions(const std::string &board, const tIntVec &wordpath,
unsigned int depth = 0);
void findWords(const std::string &board, int index,
unsigned int wordlength, unsigned int depth = 0);
std::array<int, MaxNeighbors>
neighbors(const std::string &board, int index) const;
std::string removeWord(const std::string &board, tIntVec word) const;
bool initializeDictionary();
void cleanUp();
void printSolution() const;
// Board parameters
std::string mBoard;
unsigned int mSize;
tIntVec mWordLengths;
std::string mWordFile;
unsigned int mSizeSquared;
// Solver variables
tSolution mSolution;
tIntVec mWord;
std::string mPrefix;
std::set<std::pair<std::string, tIntVec>, WordCompare> mWordList;
std::unique_ptr<trie_t> mDictionary;
const int mNeighborDx[8];
const int mNeighborDy[8];
};
#endif // SOLVER_H
#include <algorithm>
#include <cctype>
#include <chrono>
#include <fstream>
#include <iostream>
#include <numeric>
#include <sstream>
#include <string>
#include <thread>
#include "solutionentry.h"
#include "trie.h"
#include "solver.h"
Solver::Solver()
: mDictionary(nullptr),
mNeighborDx{ -1, 0, 1, -1, 1, -1, 0, 1 },
mNeighborDy{ -1, -1, -1, 0, 0, 1, 1, 1 }
{ }
Solver::Solver(const std::string &board, int size, const tIntVec &wordlengths,
const std::string &wordfile)
: mBoard(board),
mSize(size),
mWordLengths(wordlengths),
mWordFile(wordfile),
mDictionary(nullptr),
mNeighborDx{ -1, 0, 1, -1, 1, -1, 0, 1 },
mNeighborDy{ -1, -1, -1, 0, 0, 1, 1, 1 }
{
mSizeSquared = mSize * mSize;
}
Solver::~Solver()
{
cleanUp();
}
bool Solver::solve()
{
if (!initializeDictionary()) {
std::cerr << "Unable to initialize the dictionary\n";
return false;
}
cleanUp();
std::chrono::time_point<std::chrono::system_clock> start, end;
start = std::chrono::system_clock::now();
findSolutions(mBoard, tIntVec());
end = std::chrono::system_clock::now();
std::chrono::duration<double> elapsed_seconds = end - start;
std::cout << elapsed_seconds.count() << '\n';
return true;
}
bool Solver::findSolutions(const std::string &board, const Solver::tIntVec &wordpath,
unsigned int depth)
{
// A complete solution has been found
if (depth == mWordLengths.size()) {
printSolution();
mSolution.pop_back();
return true;
}
// Remove current word from the board
std::string newBoard = removeWord(board, wordpath);
// Find any words that may exist on the new board
mWordList.clear();
findWords(newBoard, Solver::NullIndex, mWordLengths[depth]);
auto currentWords = mWordList;
for (const auto &word : currentWords) {
mSolution.push_back(SolutionEntry(word.second, newBoard, depth));
findSolutions(newBoard, word.second, depth + 1);
}
if (!mSolution.empty()) {
mSolution.pop_back();
}
return true;
}
void Solver::findWords(const std::string &board, int index,
unsigned int wordlength, unsigned int depth)
{
// We have a valid word
if (depth == wordlength && mDictionary->isWord(mPrefix)) {
mWordList.insert(std::make_pair(mPrefix, mWord));
mWord.pop_back();
mPrefix.pop_back();
return;
}
for (const auto n : neighbors(board, index)) {
// Visit a neighbor only if we haven't encountered it before
if (n != Solver::NullNeighbor &&
std::find(mWord.begin(), mWord.end(), n) == mWord.end()) {
mWord.push_back(n);
mPrefix += board[n];
// Recursive step
if (mDictionary->isPrefix(mPrefix)) {
// Recursive step
mWord.push_back(n);
findWords(board, n, wordlength, depth + 1);
} else {
mWord.pop_back();
mPrefix.pop_back();
}
}
}
if (!mWord.empty()) {
mWord.pop_back();
}
if (!mPrefix.empty()) {
mPrefix.pop_back();
}
}
std::array<int, Solver::MaxNeighbors>
Solver::neighbors(const std::string &board, int index) const
{
std::array<int, Solver::MaxNeighbors> n{};
n.fill(Solver::NullNeighbor);
// Return consecutive integers from 0 to mSizeSquared
if (index == Solver::NullIndex) {
std::iota(n.begin(), n.begin() + mSizeSquared, 0);
return n;
}
// Otherwise, return the actual neighbors of the letter at index
int x, y, pos;
int row = index / mSize;
int col = index % mSize;
for (int i = 0, j = 0; i < 8; ++i) {
x = row + mNeighborDx[i];
y = col + mNeighborDy[i];
pos = x * mSize + y;
if (x >= 0 && static_cast<unsigned int>(x) < mSize &&
y >= 0 && static_cast<unsigned int>(y) < mSize && board[pos] != ' ') {
n[j] = pos;
++j;
}
}
return n;
}
std::string Solver::removeWord(const std::string &board, Solver::tIntVec word) const
{
std::string newBoard(board);
std::sort(word.begin(), word.end());
for (const auto &n : word) {
// Remove the letter at index n
newBoard[n] = ' ';
// Move the letters above n down one position
int index = n - mSize;
while (index >= 0) {
if (newBoard[index] == ' ') {
index -= mSize;
} else {
char tmp = newBoard[index];
newBoard[index] = ' ';
newBoard[index + mSize] = tmp;
index -= mSize;
}
}
}
return newBoard;
}
bool Solver::initializeDictionary()
{
std::ifstream inf(mWordFile);
if (inf.is_open()) {
mDictionary.reset(new trie_t);
std::string curWord;
while (inf >> curWord) {
std::transform(curWord.begin(), curWord.end(), curWord.begin(),
[](unsigned char ch){ return std::tolower(ch); });
if (curWord.find_first_not_of("abcdefghijklmnopqrstuvwxyz") == std::string::npos) {
for (const auto &length : mWordLengths) {
// Only add words of the length we'll be working with
if (curWord.length() == length) {
mDictionary->addWord(curWord);
break;
}
}
}
}
inf.close();
return true;
} else {
std::cerr << "Unable to open file " << mWordFile;
return false;
}
}
void Solver::cleanUp()
{
mSolution.clear();
mWord.clear();
mWordList.clear();
mPrefix.clear();
}
void Solver::printSolution() const
{
for (const auto &s : mSolution) {
std::cout << s.getWord() << " ";
}
std::cout << '\n';
}