Recursive Backtracking

Avoid
Looping:
 
 | 0 1 2 ---------0| T T 1| T T 2| T T T 
 Recursive
Backtracking
 
 T = tree 
 In
the
ForestFire
example,
when
we
found
a
tree
we...
9 downloads 3 Views 49KB Size
Avoid
Looping:
 
 | 0 1 2 ---------0| T T 1| T T 2| T T T


 Recursive
Backtracking
 


T = tree


 In
the
ForestFire
example,
when
we
found
a
tree
we
would
then
try
to
burn
the
 adjacent
trees.


In
the
example
above,
if
we
start
at
index
(1,0)
we
would
go
to
the
 square
Above
at
(0,0)
and
since
it
has
a
tree,
we
would
look
at
the
square
Above
 that,
the
square
to
the
Right,
and
then
the
square
Below.

The
square
below
is
the
 original
tree
at
(1,0).

What
stopped
us
from
trying
to
look
at
the
tree
Above
again,
 which
would
take
us
back
to
the
tree
Below,
and
then
Above,
and
so
on?


 
 When
we
find
a
tree
we,
change
the
square
to
Burning
so
that
we
know
we
have
 visited
that
square
earlier
should
we
visit
it
again.

So
instead
of
expanding
a
tree
 already
visited,
we
end
that
exploration
and
finish
looking
at
the
adjacent
squares.
 
 
 One
student
asked
whether
you
could
write
burn
method
by
looping
over
the
2D
 array
changing
a
Tree
to
Burning
if
it
is
adjacent
to
a
Burning
tree.

Of
course,
you
 would
have
to
loop
over
the
2D
array
many
times,
because
Trees
that
had
 previously
not
been
next
to
a
Burning
tree,
could
now
be
next
to
a
newly
Burning
 tree.


 
 How
would
that
change
the
look
of
the
algorithm?
It
would
be
more
realistic
 because
the
burning
trees
would
appear
to
be
growing
outward
from
other
burning
 trees.
(If
you
were
looping
over
the
rows
of
the
2D
array,
the
fire
would
a
appear
to
 be
spreading
to
the
right
and
down.)

The
recursive
version
is
less
realistic
because
 you
get
a
burning
that
looks
like
it
is
threading
through
the
trees.
 
 How
would
it
change
the
efficiency
of
the
algorithm?

It
would
be
much
worse,
 because
you
would
have
to
traverse
the
2D
array
multiple
times.

How
many
times
 depends
on
the
size
of
the
array.

It
turns
out
that
the
recursive
algorithm
time
is
 proportional
to
the
size
of
the
array.

Once
you
have
changed
a
Tree
to
burning,
it
 can
be
looked
at
again
only
four
times
at
most,
from
its
four
adjacent
trees.
Each
 time,
the
method
quickly
returns
(in
constant
time).

That
is,
the
recursive
algorithm
 does
constant
work
on
each
element
of
the
array.
 



 Recursive
Backtracking
 
 Used
to
solve
puzzles:
 ‐ finding
a
path
through
a
maze
 ‐ placing
8
queens
on
a
chess
board
so
that
none
can
attack
 ‐ solving
Soduko
puzzles
 
 The
idea
is
explore
all
possible
legal
"moves"
systematically
to
find
a
solution.

How
 do
we
do
this
exploration?

 
 From
any
position
we
might
 ‐ Find
all
possible
moves
from
that
position
 ‐ Make
a
move
 ‐ Recursively
try
to
solve
the
problem
 ‐ If
that
move
leads
to
a
successful
solution,
return
the
success
 ‐ If
that
move
is
unsuccessful,
"undo"
the
move
(backtrack)
and
make
 another
move.
 ‐ If
no
moves
succeeds,
return
failure
 
 What
makes
recursive
backtracking
different
from
the
recursion
we've
seen
before
 is
that
typically
 ‐ you
loop
over
a
set
of
choices
(moves)
and
 ‐ you
try
a
choice,
and
then
undo
the
choice.
 ‐ 
 As
there
may
be
huge
number
of
possible
sequence
of
moves,
we
want
to
be
smart
 so
that
we
only
explore
choices
that
might
lead
to
the
solution.
 
 


Example:
In
Shut
the
Box
game,
determine
if
there
exists
a
set
of
boxes
(not
already
shut)
 that
sum
to
the
sum
of
the
dice.

 
 Key
Idea:

For
each
box,
either
the
box
is
included
in
the
sum
or
it
is
not.


 
 The
public
wrapper
method
simply
computes
the
sum
of
the
dice,
as
that
can
be
done
once,
 and
then
calls
the
recursive
method
starting
with
the
first
box.
 public static boolean canWin(int[] boxValues, int[] boxStates, int[] dice) { int diceSum = 0; for (int i = 0; i < dice.length; i++) { diceSum += dice[i]; } return canWin(boxValues, boxStates, diceSum, 0); }

The
recursive
method
(ignoring
the
base
case)
first
tries
to
exclude
the
current
box
from
 being
in
the
sum
(recursive
call
on
the
next
box).

If
that
fails
then
it
tries
to
include
the
box
 in
the
sum.

The
second
recursive
call
subtracts
that
box's
value
from
the
sum
to
solve
the
 problem
using
the
remaining
boxes
on
smaller
sum.


 
 As
with
all
recursive
methods,
it
starts
with
the
simplest
case:
There
are
no
more
boxes
left
 to
try.

If
the
sum
is
zero,
then
we
have
found
a
set
of
boxes
that
have
the
correct
sum
and
 the
method
returns
true.
If
the
sum
is
not
zero,
this
sequence
of
including
and
excluding
 boxes
failed
and
we
return
false,
and
we
try
another
set
of
boxes.
 
 private static boolean canWin(int[] boxValues, int[] boxStates, int sum, int index) { if (index >= boxValues.length) { return sum == 0; } if (boxStates[index] == ALREADY_SHUT) { // Skip this box return canWin(boxValues, boxStates, sum, index+1); } // Exclude this box and see if remaining boxes can make // the sum if canWin(boxValues, boxStates, sum, index+1) return true; // Include this box and see if remaining boxes can make // the remaining part of the sum return canWin(boxValues, boxStates, sum - boxValues[index], index+1); }

Below
is
alternate
version
that
"prunes"
the
search
by
returning
as
soon
as
it
finds
that
it
 has
found
a
solution
or
that
it
could
not
find
a
solution
with
the
boxes
used
so
far
because
 the
sum
has
gone
negative.

 private static boolean canWin(int[] boxValues, int[] boxStates, int sum, int index) { if (sum == 0) return true; if (sum < 0) return false; if (index >= boxValues.length) return false; if (boxStates[index] == ALREADY_SHUT) // Skip this box return canWin(boxValues, boxStates, sum, index+1); return canWin(boxValues, boxStates, sum, index+1) || canWin(boxValues, boxStates, sum - boxValues[index], index+1); }

Solve
the
Mountain
goats
puzzle
 
 public static void undoMove(int[]board, int index, int dist) { if (dist != 0){ board[index] = board[index + dist]; board[index + dist] = EMPTY; } } // Usually want to return a boolean so that you know whether the // attempted move leads to a solution or not. public static boolean solve1(int[]board) { int[] moves = allPossibleMoves(board); if (moves.length == 0) { return hasWon(board); } // // // // //

Try all possible moves and if one succeeds return true For each move, make the move and try to solve after the move If move succeeds return true, otherwise undo the move and try the next move

for (int i = 0; i < moves.length; i++) { int dist = distanceToMove(board, moves[i]); moveGoat(board, moves[i]); if (solve1(board)) return true; undoMove(board, moves[i], dist); } return false; }

// Returns a list of moves that solves the puzzle public static ArrayList solve2(int[]board) { int[] moves = allPossibleMoves(board); if (moves.length == 0) { if (hasWon(board)) return new ArrayList(); else return null; // indicates failure } for (int i = 0; i < moves.length; i++) { int dist = distanceToMove(board, moves[i]); moveGoat(board, moves[i]); ArrayList solution = solve2(board); if (solution != null) { solution.add(0, moves[i]); return solution; } undoMove(board, moves[i], dist); } return null; }