Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

C/C++

Solving Combinatorial Problems with STL and Backtracking


>Listing One provides the code for an STL-style backtracking algorithm. The algorithm is implemented as a functor, which is a class with operator() defined so the class can be used like a function [1]. Listing Two contains a program that performs map coloring on the U.S.

Listing One: An STL-style backtracking functor
#ifndef BackTrack_h
#define BackTrack_h

template <class T, class I, class V>
class BackTrack {
public:

   // precondition: first <= last
   BackTrack(const T&  first, const T&  last);


   // Finds the next solution to the problem. Repeated calls 
   // will find all solutions to a problem if multiple solutions 
   // exist.
   // Returns true if a solution was found.
   //
   // Set first to true to get the first solution.
   //
   bool operator() (const I& begin, I end, bool& first);


private:
   // Finds the next valid sibling of the leaf (end-1). 
   // Returns true is if a valid sibling was found.
   bool FindValidSibling (const I& begin, const I& end);


   // Backtracks through the decision tree until it finds a node
   // that hasn't been visited. Returns true if an unvisited 
   // node was found.
   bool VisitNewNode (const I& begin, I& end);

   void CreateLeftLeaf (I& end);

   T left_child;
   T right_child;

   V IsValid;
};

template <class T, class I, class V>
BackTrack<T,I,V>::BackTrack(const T& first, const T& last) 
   : left_child (first), right_child (last)
{
}

template <class T, class I, class V>
bool BackTrack<T,I,V>::VisitNewNode(const I& begin, I& end)
{
   // ALGORITHM:
   //
   // If the current node is the rightmost child we must 
   // backtrack one level because there are no more children at 
   // this level. So we back up until we find a non-rightmost 
   // child, then generate the child to the right. If we back 
   // up to the top without finding an unvisted child, then all 
   // nodes have been generated.

   // Back up as long as the node is the rightmost child of 
   // its parent.
   while (end-begin > 0 && *(end-1) == right_child)
      --end;

   I back = end-1;
   if (end-begin > 0 && *back != right_child) {
      ++(*back);
      return true;
   } else
      return false;
}

template <class T, class I, class V> 
bool BackTrack<T,I,V>::FindValidSibling
(const I& begin, const I& end)
{
   // Note that on entry to this routine the leaf has not yet 
   // been used or tested for validity, so the leaf is 
   // considered a "sibling" of itself.

   I back = end-1;
   while (true) {
      if (IsValid (begin, end))
         return true;
      else if (*back != right_child)
         ++(*back);
      else
         return false;
   }
}

template <class T, class I, class V>
inline void BackTrack<T,I,V>::CreateLeftLeaf (I& end)
{
   *(end++) = left_child;
}

template <class T, class I, class V> 
bool BackTrack<T,I,V>::operator () 
(const I& begin, I end, bool& first)
{
   const int size = end - begin;

   // If first time, need to create a root.
   // Otherwise need to visit new node since vector
   // contains the last solution.
   if (first) {
      first = false;
      end = begin;
      CreateLeftLeaf (end);
   } else if (!VisitNewNode (begin, end))
      return false;

   while (true) {
      if (FindValidSibling (begin, end)) {
         if (end - begin < size)
            CreateLeftLeaf (end);
         else
            return true;

      } else if (!VisitNewNode (begin, end))
         return false; // the tree has been exhausted, 
                       // so no solution exists.
   }
}
Listing 2: A program that colors a U.S. map with four colors, such that no adjacent states are the same color
#include <assert.h>

#include <functional>
#include <vector>

#include "BackTrack.h"


enum State
   {ME, NH, VT, MA, CT, RI, NY, PA, NJ, DE, MD, DC, 
    VA, NC, WV, SC, GA, FL, AL, TN, KY, OH, IN, MI, 
    MS, LA, AR, MO, IL, WI, IA, MN, ND, SD, NE, KS, 
    OK, TX, NM, CO, WY, MT, ID, UT, AZ, NV, CA, OR, WA};

const int NumberStates = 49;
const int MaxNeighbors = 8;

enum Color {Blue, Yellow, Green, Red};

inline Color& operator++ (Color& c)
{
   c = Color (c + 1);
   return c;
}

inline State& operator++ (State& c)
{
   c = State (c + 1);
   return c;
}

typedef std::vector<Color> Map;
typedef Map::iterator MapIter;


// store neighbor's of each state.
// Neighbor [i][0] == # of neighbors  of state i
// Neighbor [i][j] == jth neighbor of state i
State Neighbor [NumberStates][MaxNeighbors+1]; 

inline Connect (State s1, State s2)
{
   int count = ++Neighbor [s1][0];
   Neighbor [s1][count] = s2;

   count = ++Neighbor [s2][0];
   Neighbor [s2][count] = s1;

   assert (Neighbor [s1][0] <= MaxNeighbors);
   assert (Neighbor [s2][0] <= MaxNeighbors);
}


void BuildMap ()
{
   for (int i = 0; i < NumberStates; i++)
         Neighbor [i][0] = State(0);


   Connect (ME,NH);
   Connect (NH,VT); Connect (NH,MA);
   Connect (VT,MA); Connect (VT,NY);
   Connect (MA,NY); Connect (MA,CT); Connect (MA,RI);
   Connect (CT,RI); Connect (CT,NY);
   Connect (NY,NJ); Connect (NY,PA); Connect (NY,OH);
   Connect (PA,NJ); Connect (PA,DE); Connect (PA,MD); 
   Connect (PA,WV); Connect (PA,OH);

   // ... omitted to save space -- full source code available
   // on CUJ ftp site (see p. 3 for downloading instructions)

   Connect (UT,NV); Connect (UT,AZ);
   Connect (AZ,NV); Connect (AZ,CA);
   Connect (NV,OR); Connect (NV,CA);
   Connect (CA,OR);
   Connect (OR,WA);
}


struct ColorIsValid : 
   public std::binary_function<MapIter, MapIter, bool> 
{
   bool operator() (const MapIter& begin, const MapIter& end) 
   {
      State LastState = State (end-begin-1);
      Color LastColor = *(end-1);
      bool Valid = true;
      for (int i = 1; i <= Neighbor [LastState][0]; i++) {
         State NeighborState = Neighbor [LastState][i];
         if (NeighborState < LastState &&
             *(begin+NeighborState) == LastColor)
             return false;
      }
      return true;
   }
};



int main (int argc, char* argv [])
{
   Map tree (NumberStates);

   BackTrack <Color, MapIter, ColorIsValid> ColorMap (Blue, Red);
   BuildMap ();

   bool FirstTime = true;
   // find first 100 valid colorings of the U.S.
   for (int i = 0; i < 100; i++)
      bool Valid = 
         ColorMap (tree.begin(), tree.end(), FirstTime);

   return 0;
}

There are five steps in using the backtracking algorithm:

1. Define the types.

2. Define the operators.

3. Define a validator function.

4. Construct a BackTrack object.

5. Call it.

1. Define the Types

The BackTrack class has three template arguments:

template
<class I, class T, class V>
class BackTrack {...};

I is the iterator type for the container. T is the type of data being stored in the container, and V is a user-defined boolean function that will return true if the current decision tree is valid.

You will typically use a discrete type for T, such as int or enum, and an STL container iterator for I. For the map coloring problem you may choose to use an enum for the colors, and to store them in a vector. Thus:

enum Color {
   Blue,
   Yellow,
   Green,
   Red
};

Add some typdefs to make simpler type names, and declare the container:

typedef vector<Color> Map;
typedef Map::iterator MapIter;
Map tree (49);

2. Define the Operators

BackTrack uses the following operators on T: &, ==, !=, =, and ++ (prefix). The compiler predefines these operators for int. If you are using int for T, your work is done.

However, ++ is not defined for enum. So define your own:

Color& operator++ (Color& c){...}

3. Define a Validator Function

Derive your validator function from std::binary_function:

template<class T>
struct ColorIsValid:
   public std::binary_function<MapIter,
             MapIter, bool> {
   bool operator()
      (const MapIter& begin,
       const MapIter& end) const
   {
      // .. put validation code here
   }
};

The validator function takes iterators pointing to the beginning and end of the container holding the tree. This function should return true if the coloring is valid. Remember that at the time of the call every element except the last has already been checked for validity, so you only need to check that the back element, *(end-1), is valid.

The problem statement almost always exactly defines what the validator needs to test. For example, the n-color problem requires that no adjacent area have the same color. In pseudocode the test would look like this:

for (each neighbor of  *(end-1))
   if (neighbor's color ==
       *(end-1)'s color)
      return false;
return true;

4. Construct a BackTrack Object

To generate the decision tree the algorithm needs to know the valid range for T, the type. The BackTrack constructor accepts these values as parameters:

BackTrack(const T&  first,
          const T&  last);

Now you can construct a BackTrack object:

BackTrack<Color, MapIter, ColorIsValid>
ColorMap (Blue, Red);

5. Call the BackTrack Object

BackTrack's operator() takes the begin and end iterators to your container, along with a bool parameter that specifies whether this is the first call. (I discuss that parameter shortly.) Finding a valid coloring now requires one line of code:

bool FirstTime = true;
bool IsValid =
   ColorMap(tree.begin(), tree.end(),
      FirstTime);

This function returns true if a valid solution was found. If one was found, the tree now contains the coloring for each state.

Many problems, including map coloring, have multiple solutions. Once you have found the first solution you can call operator() again to get the next solution. This works because the current position in the tree and the solution have the same representation. Backtracking from the current solution will result in the next valid solution to be found. The variable FirstTime is set to false when operator() is called, guaranteeing that subsequent calls find the next solution.

Implementation Details

operator() starts by creating an empty tree. It generates the first node by calling CreateLeftLeaf. This private function takes an iterator pointing to the end of the container, and appends the first valid value of T, which is stored in the private variable left_child.

operator() now enters a loop that will generate the lower levels of the tree. The loop first passes the decision tree to the function bool FindValidSibling(). This function finds the first valid sibling of the current leaf. At the time of call the current leaf has not been checked for validity, so the function first checks if this leaf is valid. It does this by calling the user-defined validator function stored in the private variable IsValid. If the leaf is valid, FindValidSibling returns true without changing the decision tree. However, if the leaf is invalid, FindValidSibling will generate the next sibling to the leaf by using operator++ to increment the leaf's value. FindValidSibling successively calls IsValid and then increments the leaf until IsValid returns true or all siblings have been generated without success. If a valid sibling is found, true is returned.

operator() checks the result of FindValidSibling. If it returned true, the node is valid. If the tree has been completely generated, the solution is valid and operator() returns true. Otherwise operator() must generate the next level in the tree and test it. So it calls CreateLeftLeaf and returns to the top of the loop, causing FindValidSibling to be called on the new tree.

However, if the node was invalid, operator() must now backtrack to a valid node. Function VisitNewNode accomplishes this. It backs up one level as long as the current leaf is the last valid value of T, which is stored in private variable right_child. Once VisitNewNode finds a leaf unequal to right_child, it creates a new leaf by incrementing the current leaf's value and returns true. VisitNewNode returns false if it returns all the way to the root, having searched the entire tree.

Efficiency

How a problem is represented greatly influences how long it takes to find a solution. You must consider two things. First, the nearer to the root you can prune the tree the greater the reduction in run time. Second, you should remember that whenever the validator function is called, all but the last element have already been validated.

Consider map coloring. You can color the states in any order you want. Suppose you order the states alphabetically. The algorithm would first color Alabama, then Arizona. Because these states are not adjacent, you would never be able to prune the tree at this level, since all possible colorings of these two states can lead to valid solutions. However, if you order the states by adjacency, so that Maine is colored first, followed by New Hampshire, you can prune at this level. Of the 16 nodes at level 2, four will be pruned, which removes over 1029 nodes from consideration.

Conclusion

Using my BackTrack class is straightforward. The hardest task is writing the validator function, but even this is easy, as the problem statement completely defines its functionality. I can typically whip up a solution in half an hour or less. After a few attempts I expect other users will be able to do the same.

Choosing the best algorithm for a combinatorial problem is beyond the scope of this paper. There are many combinatorial problems for which backtracking does not work, or does not work quickly enough. The best survey of these algorithms that I have found is the book Heuristics: Intelligent Search Strategies for Computer Problem Solving, by Judea Pearl [2]. While out of print, the book's clear presentation of the topic will more than repay the effort it may take to find it.

Even when backtracking is not the best choice, my class can still be useful. You can use it to unit test your specialized algorithm. Better yet, use BackTrack as a backup debug-only algorithm to double check the results of the specialized algorithm, using #ifdef #endif blocks to remove the code from your production builds. (This technique is discussed in Steve Maguire's Writing Solid Code [3].)

References

[1] Bjarne Stroustrup. The C++ Programming Language, 3rd Edition (Addison-Wesley, 1997), p. 514.

[2] Judea Pearl. Heuristics: Intelligent Search Strategies for Computer Problem Solving (Addison-Wesley, 1984).

[3] Steve Maguire. Writing Solid Code (Microsoft Press, 1993), pp. 33-38.


Roger Labbe is the C++ Simulation Manager for DCS Corporation in Virginia, where he develops and manages avionics simulations as well as flight planning software. He has previously developed embedded software for flight management computers..


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.