Basic framework for active backtrack solver

This module provides a simple abstract class BacktrackProblem[S,A] to be specialized for a specific problem.

The concrete class BacktrackSolver is used to configure, query, and run a solver for a given problem.

For an example, see the queens.nit program in the examples subdirectory.

Introduced classes

class BacktrackNode[A: nullable Object]

ai :: BacktrackNode

A node in the backtrack-zipper visited by a BacktrackSolver.
abstract class BacktrackProblem[S: Object, A: nullable Object]

ai :: BacktrackProblem

Abstract backtrack problem of states (S) and actions (A).
class BacktrackSolver[S: Object, A: nullable Object]

ai :: BacktrackSolver

A running solver for a given problem, that can be configured and controlled.

All class definitions

class BacktrackNode[A: nullable Object]

ai $ BacktrackNode

A node in the backtrack-zipper visited by a BacktrackSolver.
abstract class BacktrackProblem[S: Object, A: nullable Object]

ai $ BacktrackProblem

Abstract backtrack problem of states (S) and actions (A).
class BacktrackSolver[S: Object, A: nullable Object]

ai $ BacktrackSolver

A running solver for a given problem, that can be configured and controlled.
package_diagram ai::backtrack backtrack core core ai::backtrack->core ai::ai ai ai::ai->ai::backtrack ai::queens queens ai::queens->ai::backtrack a_star-m a_star-m a_star-m->ai::ai a_star-m->ai::queens a_star-m... ... a_star-m...->a_star-m

Ancestors

module abstract_collection

core :: abstract_collection

Abstract collection classes and services.
module abstract_text

core :: abstract_text

Abstract class for manipulation of sequences of characters
module array

core :: array

This module introduces the standard array structure.
module bitset

core :: bitset

Services to handle BitSet
module bytes

core :: bytes

Services for byte streams and arrays
module circular_array

core :: circular_array

Efficient data structure to access both end of the sequence.
module codec_base

core :: codec_base

Base for codecs to use with streams
module codecs

core :: codecs

Group module for all codec-related manipulations
module collection

core :: collection

This module define several collection classes.
module environ

core :: environ

Access to the environment variables of the process
module error

core :: error

Standard error-management infrastructure.
module exec

core :: exec

Invocation and management of operating system sub-processes.
module file

core :: file

File manipulations (create, read, write, etc.)
module fixed_ints

core :: fixed_ints

Basic integers of fixed-precision
module fixed_ints_text

core :: fixed_ints_text

Text services to complement fixed_ints
module flat

core :: flat

All the array-based text representations
module gc

core :: gc

Access to the Nit internal garbage collection mechanism
module hash_collection

core :: hash_collection

Introduce HashMap and HashSet.
module iso8859_1

core :: iso8859_1

Codec for ISO8859-1 I/O
module kernel

core :: kernel

Most basic classes and methods.
module list

core :: list

This module handle double linked lists
module math

core :: math

Mathematical operations
module native

core :: native

Native structures for text and bytes
module numeric

core :: numeric

Advanced services for Numeric types
module protocol

core :: protocol

module queue

core :: queue

Queuing data structures and wrappers
module range

core :: range

Module for range of discrete objects.
module re

core :: re

Regular expression support for all services based on Pattern
module ropes

core :: ropes

Tree-based representation of a String.
module sorter

core :: sorter

This module contains classes used to compare things and sorts arrays.
module stream

core :: stream

Input and output streams of characters
module text

core :: text

All the classes and methods related to the manipulation of text entities
module time

core :: time

Management of time and dates
module union_find

core :: union_find

union–find algorithm using an efficient disjoint-set data structure
module utf8

core :: utf8

Codec for UTF-8 I/O

Parents

module core

core :: core

Standard classes and methods used by default by Nit programs and libraries.

Children

module ai

ai :: ai

Simple toolkit for artificial intelligence.
module queens

ai :: queens

Example of the famous eight-queens problem solved with the ai::backtrack module.

Descendants

module a_star-m

a_star-m

# Basic framework for active backtrack solver
#
# This module provides a simple abstract class `BacktrackProblem[S,A]` to be specialized for a specific problem.
#
# The concrete class `BacktrackSolver` is used to configure, query, and run a solver for a given problem.
#
# For an example, see the `queens.nit` program in the `examples` subdirectory.
module backtrack

# Abstract backtrack problem of states (`S`) and actions (`A`).
#
# This class serves to model search problems using a backtracking approach.
# A state, `S`, is a point in the search problem and fully model a given state of the world.
# An action, `A`, is an available mean of transition between two states.
# While there is a potential large number of distinct states and actions, there should be only
# a small number of possible actions from a specific state (thus, a small, or at least finite, branching factor).
#
# The point this class is that the state is a mutable object, the roles of the actions is to modify
# the state.
#
# This abstract class is generic and made to work with any kind of states and actions.
# Therefore, specific subclasses must be developed to implements the required services:
#
# * `initial_state`
# * `actions`
# * `apply_action`
# * `backtrack`
# * `is_goal`
#
# # Basic search
#
# The method `solve` returns a new solver for a backtrack search.
abstract class BacktrackProblem[S: Object,A]
	# The starting state of the problem.
	# It is this object that will be modified by `apply_action` and `backtrack`.
	fun initial_state: S is abstract

	# The available and applicable actions for a given state
	# Because of `backtracking`, actions must also be reversible (see `backtrack`).
	#
	# If there is no available actions, null (or an empty collection) must be returned.
	#
	# In order to optimise the search time, it is sensible to return `null`
	# (or an empty collection) as early as possible.
	#
	# Node: to help some specific implementations, the current node is also available.
	# See `BacktrackNode` for details.
	fun actions(state: S, node: BacktrackNode[A]): nullable Collection[A] is abstract

	# Modify `state` by applying `action`
	# The `action` comes from an earlier invocation of `actions`.
	fun apply_action(state: S, action: A) is abstract

	# Modify `state` by undoing `action`
	# Because of this method, it is important that any action can be undone
	# knowing only the post-state and the action.
	fun backtrack(state: S, action: A) is abstract

	# Is the state a goal state?
	# Once a goal state is found, the solver is automatically stopped.
	# See `BacktrackSolver.run`.
	fun is_goal(state: S): Bool is abstract

	# Return a new solver
	fun solve: BacktrackSolver[S,A] do
		return new BacktrackSolver[S,A](self, initial_state)
	end
end

# A running solver for a given problem, that can be configured and controlled.
#
#
# # Basic run and results.
#
# 1. Instantiate it with the method `solve` from `BacktrackProblem`.
# 2. Apply the method `run`, that will search and return a solution.
# 3. Retrieve information from the solution.
#
# ~~~~nitish
# var p: BacktrackProblem = new MyProblem
# var solver = p.solve
# var res = solver.run
# if res != null then
#   print "Found solution in {res.depth} actions: {res.plan.join(", ")}"
#   print "The state of the solution is: {solver.state}"
# end
# ~~~~
#
#
# # Step-by-step runs and multiple runs
#
# The `run_steps` method (see also `steps`, and `steps_limit`) can be used to run only a maximum number of steps.
# Thus, this method can be used as a *co-routine* and be run periodically for a small amount of time.
#
# `run` and `run_steps` return the next solution.
# A subsequent call to `run` returns the following solution and so on.
#
# When there is no more solutions available, `null` is returned and `is_running` become false.
#
# Between run, the state of the current search can be read.
#
#
# # Search-trees
#
# Internally, solvers use a zipper on the virtual search-tree where nodes are elements in the apply/backtrack graph.
# See the class `BacktrackNode` for details
#
# The `run` and `node` methods return a `BacktrackNode` that can be used to retrieve a lot of useful information,
# like the full `path` or the `plan`.
# If only the solved state is required, the `state` method from the solver gives it.
class BacktrackSolver[S: Object, A]
	# The problem currently solved
	var problem: BacktrackProblem[S,A]

	# The current state.
	# Do not modify it directly: the solver will do that by its own use of
	# `problem.apply_action` and `problem.backtrack`.
	var state: S

	# The current `node` in the backtrack-zipper.
	var node: nullable BacktrackNode[A] = null

	# Is the solver still running?
	# A running solver has not yet exhausted all the possible solutions.
	var is_running = true

	# Initialize `node`
	private fun start: BacktrackNode[A]
	do
		assert node == null
		var node = new BacktrackNode[A](null, null, 0, 0)
		self.node = node
		return node
	end


	# The total steps executed since the beginning.
	var steps = 0

	# Limit in the number of steps for a `run`.
	#
	# One can modify this value then `run` or just call `run_steps`.
	#
	# Use 0 for no limit.
	# Default: 0
	var steps_limit = 0 is writable

	# Update `steps_limit` then just run some additional steps.
	# Return the `node` corresponding to the found solution, or `null` if no solution is found.
	fun run_steps(steps: Int): nullable BacktrackNode[A]
	do
		steps_limit = self.steps + steps
		return run
	end

	# Run the solver and return the next solution found (if any).
	# Return null is one of these is true:
	# * `steps_limit` is reached
	# * no more reachable solution, in this case `is_running` become false.
	fun run: nullable BacktrackNode[A]
	do
		var node = self.node
		# Not yet started, of finished?
		if node == null then
			if steps > 0 then return null
			node = start
			var res = problem.is_goal(state)
			if res then return node
		end

		loop
			if steps_limit > 0 and steps > steps_limit then break
			steps += 1

			var totry = node.totry

			# It is the first visit in this state?
			if totry == null then
				var actions = problem.actions(state, node)
				if actions != null and not actions.is_empty then
					totry = actions.to_a
					node.totry = totry
				end
			end

			#print state
			#print node

			# No remaining actions?
			if totry == null or totry.is_empty then
				#print "Backtrack"
				var a = node.action
				if a == null then
					#print "no more action"
					is_running = false
					self.node = null
					return null
				end

				problem.backtrack(state, a)
				node = node.parent
				assert node != null
				continue
			end

			var a = totry.pop
			problem.apply_action(state, a)
			#print "Play {a or else ""}"
			node = new BacktrackNode[A](node, a, node.depth+1, steps)

			var res = problem.is_goal(state)
			if res then
				self.node = node
				return node
			end
		end
		self.node = node
		return null
	end

	redef fun to_s do return "{node or else "#0"}"
end

# A node in the backtrack-zipper visited by a `BacktrackSolver`.
#
# The solver visits the virtual search tree with a zipper.
#
# A node is the zipper (this class) is associated to:
# * a state of the problem (indirectly),
# * the actions not yet explored from the state (see `totry`)
# * the action that yields to the state (see `action`), used to backtrack.
# * and the parent node in the zipper (see `parent`).
#
# There is no direct link between a node and a state; it is unneeded
# since the same state is used, and mutated, during the whole execution of the solver.
#
# This class is exposed to allow queries on the solution provided by the solver.
class BacktrackNode[A]
	# The previous node in the backtrack-zipper
	var parent: nullable BacktrackNode[A]

	# The last action executed
	var action: nullable A

	# The remaining actions to try from this node
	var totry: nullable Array[A] = null

	# The depth of `self` in the search-tree.
	var depth: Int

	# The number of steps needed by the solver to process `self`.
	# This is just a useless generation number, but could be used to evaluate
	# the behavior of search algorithms.
	var steps: Int

	# Build a sequence of nodes from the initial node to `self`
	# ensure `result.first.parent == null and result.last == self`
	fun path: Sequence[BacktrackNode[A]]
	do
		var res = new List[BacktrackNode[A]]
		res.add(self)
		var node = parent
		while node != null do
			res.unshift(node)
			node = node.parent
		end
		return res
	end

	# Build a sequence of actions from the initial state to `self`
	# See `path` for a more detailed plan.
	fun plan: Sequence[A]
	do
		var res = new List[A]
		var node: nullable BacktrackNode[A] = self
		while node != null do
			var a = node.action
			if a != null then res.unshift(a)
			node = node.parent
		end
		return res
	end

	redef fun to_s do
		var res = "#{steps} d={depth}"
		var tt = totry
		if tt != null then res += " tt={tt.join(" ")}"
		return res
	end
end
lib/ai/backtrack.nit:11,1--300,3