finitedomain
Version:
A fast feature rich finite domain solver
184 lines (165 loc) • 5.9 kB
JavaScript
import {
NO_SUCH_VALUE,
ASSERT,
} from './helpers';
import {
space_createClone,
space_updateUnsolvedVarList,
space_propagate,
} from './space';
import {
domain_isSolved,
} from './domain';
import distribution_getNextVarIndex from './distribution/var';
import distribute_getNextDomainForVar from './distribution/value';
// BODY_START
/**
* Depth first search.
*
* Traverse the search space in DFS order and return the first (next) solution
*
* state.space must be the starting space. The object is used to store and
* track continuation information from that point onwards.
*
* On return, state.status contains either 'solved' or 'end' to indicate
* the status of the returned solution. Also state.more will be true if
* the search can continue and there may be more solutions.
*
* @param {Object} state
* @property {$space} state.space Root space if this is the start of searching
* @property {boolean} [state.more] Are there spaces left to investigate after the last solve?
* @property {$space[]} [state.stack]=[state,space] The search stack as initialized by this class
* @property {string} [state.status] Set to 'solved' or 'end'
* @param {$config} config
* @param {Function} [dbgCallback] Call after each epoch until it returns false, then stop calling it.
*/
function search_depthFirst(state, config, dbgCallback) {
let stack = state.stack;
let epochs = 0;
// the stack only contains stable spaces. the first space is not
// stable so we propagate it first and before putting it on the stack.
let isStart = !stack || stack.length === 0;
if (isStart) {
if (!stack) stack = state.stack = [];
let solved = search_depthFirstLoop(state.space, config, stack, state);
if (dbgCallback && dbgCallback(++epochs)) dbgCallback = undefined;
if (solved) return;
}
while (stack.length > 0) {
// take the top space and generate the next offspring, if any
let childSpace = search_createNextSpace(stack[stack.length - 1], config);
if (childSpace) {
// stabilize the offspring and put it on the stack
let solved = search_depthFirstLoop(childSpace, config, stack, state);
if (dbgCallback && dbgCallback(++epochs)) dbgCallback = undefined;
if (solved) return;
} else {
// remove the space, it has no more children. this is a dead end.
stack.pop();
}
}
// there are no more spaces to explore and therefor no further solutions to be found.
state.status = 'end';
state.more = false;
}
/**
* One search step of the given space
*
* @param {$space} space
* @param {$config} config
* @param {$space[]} stack
* @param {Object} state See search_depthFirst
* @returns {boolean}
*/
function search_depthFirstLoop(space, config, stack, state) {
let rejected = space_propagate(space, config);
return search_afterPropagation(rejected, space, config, stack, state);
}
/**
* Process a propagated space and the result. If it rejects, discard the
* space. If it passed, act accordingly. Otherwise determine whether the
* space has children. If so queue them. Otherwise discard the space.
*
* @param {boolean} rejected Did the propagation end due to a rejection?
* @param {$space} space
* @param {$config} config
* @param {$space[]} stack
* @param {Object} state See search_depthFirst
* @returns {boolean|undefined}
*/
function search_afterPropagation(rejected, space, config, stack, state) {
if (rejected) {
_search_onReject(state, space, stack);
return false;
}
let solved = space_updateUnsolvedVarList(space, config);
if (solved) {
_search_onSolve(state, space, stack);
return true;
}
// put on the stack so the next loop can branch off it
stack.push(space);
return undefined; // neither solved nor rejected
}
/**
* Create a new Space based on given Space which basically
* serves as a child node in a search graph. The space is
* cloned and in the clone one variable is restricted
* slightly further. This clone is then returned.
* This takes various search and distribution strategies
* into account.
*
* @param {$space} space
* @param {$config} config
* @returns {$space|undefined} a clone with small modification or nothing if this is an unsolved leaf node
*/
function search_createNextSpace(space, config) {
let varIndex = distribution_getNextVarIndex(space, config);
ASSERT(typeof varIndex === 'number', 'VAR_INDEX_SHOULD_BE_NUMBER');
ASSERT(varIndex >= 0, 'VAR_INDEX_SHOULD_BE_POSITIVE');
if (varIndex !== NO_SUCH_VALUE) {
let domain = space.vardoms[varIndex];
if (!domain_isSolved(domain)) {
let choice = space.next_distribution_choice++;
let nextDomain = distribute_getNextDomainForVar(space, config, varIndex, choice);
if (nextDomain) {
let clone = space_createClone(space);
clone.updatedVarIndex = varIndex;
clone.vardoms[varIndex] = nextDomain;
return clone;
}
}
}
// space is an unsolved leaf node, return undefined
}
/**
* When search fails this function is called
*
*
* @param {Object} state The search state data
* @param {$space} space The search node to fail
* @param {$space[]} stack See state.stack
*/
function _search_onReject(state, space, stack) {
// Some propagators failed so this is now a failed space and we need
// to pop the stack and continue from above. This is a failed space.
space.failed = true;
}
/**
* When search finds a solution this function is called
*
* @param {Object} state The search state data
* @param {Space} space The search node to fail
* @param {Space[]} stack See state.stack
*/
function _search_onSolve(state, space, stack) {
state.status = 'solved';
state.space = space; // is this so the solution can be read from it?
state.more = stack.length > 0;
}
// BODY_STOP
export default search_depthFirst;
export {
search_afterPropagation,
search_createNextSpace,
};