Boost logo

Boost :

From: Howard Hinnant (hinnant_at_[hidden])
Date: 2005-09-05 15:03:42


On and off for the past 7 months (well, mostly off), I've been working
on a few combination/permutation algorithms. I was inspired by the CUJ
article written by John Dibling in the Feb. 2005 issue titled
"Extending the STL".

The std::lib currently has next_permutation, perv_permutation. But
these functions are lacking in two respects:

1. They only allow the entire sequence to be permuted. They do not
allow (for example) the ability to find all permutations of 5 items
considered only 2 at a time.

Consider 5 items 2 at a time from this sequence
1 : a b c d e
Permutations
1 : a b
2 : a c
3 : a d
4 : a e
5 : b a
6 : b c
7 : b d
8 : b e
9 : c a
10 : c b
11 : c d
12 : c e
13 : d a
14 : d b
15 : d c
16 : d e
17 : e a
18 : e b
19 : e c
20 : e d

When you know that you need to consider only a few items at a time,
std::next_permutation is excessively expensive as it will loop through
many more iterations. (e.g. consider the permutations of 5 items 5 at
a time leads to a list of 120 - vs 20 sequences).

2. std::next/prev_permutation require a sorted sequence and an
ordering predicate. This is not always convenient.

In addition to permutations of N objects taken K at a time, other
useful algorithms would be:

* Circular permutations of N objects taken K at a time.
* Combinations of N objects taken K at a time.
* Reversible permutations of N objects taken K at a time.
* Reversible circular permutations of N objects taken K at a time.

Out of these 5 algorithms, today I can only present the first 3. The
reversible versions are meant to take a sequence over all permutations
where an order, and the reverse order are considered to be the same
sequence. E.g.

1 2 3 4 is the same permutation as 4 3 2 1 for a "reversible"
permutation.

This is handy in situations where you are scoring "distances" between
items in the list, and it doesn't matter which direction the list is
traveled in. Unfortunately I haven't yet found a way to efficiently
compute these "reversible" permutations.

---
Interface
---
I've followed the advice of John Dibling in the CUJ article except that 
the functor doesn't receive the entire range, but only the first K 
elements.  For example consider:
char array[] = {'a', 'b', 'c', 'd', 'e'};
unsigned size_array = sizeof(array)/sizeof(array[0]);
const int k = 3;
print p = std::for_each_combination(array, array + size_array, k, 
print());
This calls the print() functor (say p) like:
     p(array, array+k);
for array permuted such that [array, array+k) will hold each 
combination of 3 items taken from the total of 5 items.  And after the 
for_each_combination call, array will be in its original order.  While 
always permuting the elements into positions [0, k) may at first seem 
limiting in how the for_each_* algorithms can be implemented, it offers 
interesting possibilities for the functors.
If a functor knows the total length of the sequence (say N), and 
receives [array, array+k) as a subsequence representing the current 
combination/permutation, that functor can then call for_each_* on the 
remaining sequence [array+k, array+N).  This allows for a very complex 
analysis where one considers all combinations (or permutations) of the 
first k elements, and then simultaneously considers permutations of the 
remaining elements for each of the outer combinations.
One could use this capability to search for solutions to variations of 
the traveling salesman problem.  Instead of minimizing the route of 1 
salesman over N cities.  You might want to minimize two salesmen's 
routes over the same set of cities, each city being visited by one and 
only one salesman.  For example you might consider:
For each circular-reversible permutation of 10 cities considered 5 at a 
time:
     Compute distance around the 5 city route and add to that:
     For each circular-reversible permutation of the remaining 5 cities 
considered 5 at a time:
        Compute distance around the 5 city route.
        Record minimum combined distance and routes.
Naturally this technique can easily be generalized to M salesmen 
covering N cites, N/M cites covered by each salesman.  Unfortunately I 
currently lack the circular-reversible permutation algorithm and have 
to instead use the twice-as-expensive circular permutation algorithm.
The three algorithms I do have are here:
#include <iterator>
#include <algorithm>
namespace test
{
namespace detail
{
template<class BidirectionalIterator, class Function, class Size>
Function
permute(BidirectionalIterator first, BidirectionalIterator mid,
           BidirectionalIterator active, BidirectionalIterator last,
           Size k, Function f, Size n)
{
     if (k == 0)
         f(first, mid);
     else
     {
         using std::swap;
         BidirectionalIterator activep1 = active;
         ++activep1;
         BidirectionalIterator p = active;
         ++p;
         for (Size i = 0; i < n; ++i, ++p)
         {
             if (k == 1)
                 f(first, mid);
             else
                 f = permute<BidirectionalIterator,Function,Size>(first, 
mid, activep1, last, k-1, f , n-1);
             if (i != n-1)
                 swap(*active, *p);
         }
         std::rotate(active, activep1, last);
     }
     return f;
}
template<class BidirectionalIterator, class Function, class Size>
Function
combine(BidirectionalIterator first, BidirectionalIterator mid,
            BidirectionalIterator active, BidirectionalIterator last,
            Size k, Function f, Size n)
{
     if (k == 0)
         f(first, mid);
     else
     {
         Size e = n - k + 1;
         Size nn = n-1;
         BidirectionalIterator l2 = last;
         BidirectionalIterator ap1 = active;
         ++ap1;
         BidirectionalIterator apk = active;
         std::advance(apk, k);
         for (Size i = 0; i < e; ++i, --nn, --l2)
         {
             if (k == 1)
                 f(first, mid);
             else
                 f = combine<BidirectionalIterator,Function,Size>(first, 
mid, ap1, l2, k-1, f, nn);
             std::rotate(active, i < e-1 ? ap1 : apk, last);
         }
     }
     return f;
}
}  // detail
template<class BidirectionalIterator, class Function, class Size>
Function
for_each_permutation(BidirectionalIterator first, BidirectionalIterator 
last, Size k, Function f)
{
     typedef typename 
std::iterator_traits<BidirectionalIterator>::difference_type 
difference_type;
     Size n = static_cast<Size>(std::distance(first,last));
     if (k < 1 || n == 0)
         return f;
     if (k > n)
         k = n;
     BidirectionalIterator mid = first;
     std::advance(mid, k);
     return detail::permute<BidirectionalIterator,Function,Size>(first, 
mid, first, last, k, f, n);
}
template<class BidirectionalIterator, class Function, class Size>
Function
for_each_circular_permutation(BidirectionalIterator first, 
BidirectionalIterator last, Size k, Function f)
{
     typedef typename 
std::iterator_traits<BidirectionalIterator>::difference_type 
difference_type;
     Size n = static_cast<Size>(std::distance(first,last));
     if (k < 1 || n == 0)
         return f;
     if (k > n)
         k = n;
     BidirectionalIterator mid = first;
     std::advance(mid, k);
     BidirectionalIterator f2 = first;
     ++f2;
     for (Size i = 0, e = n - k + 1; i < e; ++i, --n, ++first, ++f2, 
++mid)
         f = detail::permute<BidirectionalIterator,Function,Size>(first, 
mid, f2, last, k-1, f, n-1);
     return f;
}
template<class BidirectionalIterator, class Function, class Size>
Function
for_each_combination(BidirectionalIterator first, BidirectionalIterator 
last, Size k, Function f)
{
     typedef typename 
std::iterator_traits<BidirectionalIterator>::difference_type 
difference_type;
     Size n = static_cast<Size>(std::distance(first,last));
     if (k < 1)
         return f;
     if (k > n)
         k = n;
     BidirectionalIterator mid = first;
     std::advance(mid, k);
     return detail::combine<BidirectionalIterator,Function,Size>(first, 
mid, first, last, k, f, n);
}
}  // test
And here's a demo that exercises all 3 algorithms:
// demo
#include <iostream>
#include <iterator>
#include <algorithm>
class print
{
public:
     print() : i_(0) {}
     unsigned get() const {return i_;}
     template <class It>
     void operator()(It first, It last)
     {
         typedef typename std::iterator_traits<It>::value_type 
value_type;
         std::ostream_iterator<value_type> out(std::cout, " ");
         std::cout << ++i_ << "\t: ";
         std::copy(first, last, out);
         std::cout << '\n';
     }
private:
     unsigned i_;
};
int main()
{
     char array[] = {'a', 'b', 'c', 'd', 'e'};
     unsigned size_array = sizeof(array)/sizeof(array[0]);
     const int k = 3;
     std::cout << "Consider " << size_array << " items " << k << " at a 
time from this sequence\n";
     print()(array, array + size_array);
     {
     std::cout << "Permutations\n";
     print p = test::for_each_permutation(array, array + size_array, k, 
print());
     std::cout << "--- Total of " << p.get() << " calls ---\n";
     std::cout << "Sequence left in orginal order\n";
     print()(array, array + size_array);
     std::cout << '\n';
     }
     {
     std::cout << "Circular Permutations\n";
     print p = test::for_each_circular_permutation(array, array + 
size_array, k, print());
     std::cout << "--- Total of " << p.get() << " calls ---\n";
     std::cout << "Sequence left in orginal order\n";
     print()(array, array + size_array);
     std::cout << '\n';
     }
     {
     std::cout << "Combinations\n";
     print p = test::for_each_combination(array, array + size_array, k, 
print());
     std::cout << "--- Total of " << p.get() << " calls ---\n";
     std::cout << "Sequence left in orginal order\n";
     print()(array, array + size_array);
     std::cout << '\n';
     }
}
The output should be:
Consider 5 items 3 at a time from this sequence
1   : a b c d e
Permutations
1   : a b c
2   : a b d
3   : a b e
4   : a c b
5   : a c d
6   : a c e
7   : a d b
8   : a d c
9   : a d e
10  : a e b
11  : a e c
12  : a e d
13  : b a c
14  : b a d
15  : b a e
16  : b c a
17  : b c d
18  : b c e
19  : b d a
20  : b d c
21  : b d e
22  : b e a
23  : b e c
24  : b e d
25  : c a b
26  : c a d
27  : c a e
28  : c b a
29  : c b d
30  : c b e
31  : c d a
32  : c d b
33  : c d e
34  : c e a
35  : c e b
36  : c e d
37  : d a b
38  : d a c
39  : d a e
40  : d b a
41  : d b c
42  : d b e
43  : d c a
44  : d c b
45  : d c e
46  : d e a
47  : d e b
48  : d e c
49  : e a b
50  : e a c
51  : e a d
52  : e b a
53  : e b c
54  : e b d
55  : e c a
56  : e c b
57  : e c d
58  : e d a
59  : e d b
60  : e d c
--- Total of 60 calls ---
Sequence left in orginal order
1   : a b c d e
Circular Permutations
1   : a b c
2   : a b d
3   : a b e
4   : a c b
5   : a c d
6   : a c e
7   : a d b
8   : a d c
9   : a d e
10  : a e b
11  : a e c
12  : a e d
13  : b c d
14  : b c e
15  : b d c
16  : b d e
17  : b e c
18  : b e d
19  : c d e
20  : c e d
--- Total of 20 calls ---
Sequence left in orginal order
1   : a b c d e
Combinations
1   : a b c
2   : a b d
3   : a b e
4   : a c d
5   : a c e
6   : a d e
7   : b c d
8   : b c e
9   : b d e
10  : c d e
--- Total of 10 calls ---
Sequence left in orginal order
1   : a b c d e
Note that the functor (print() in this case) can be stateful, and that 
state is accumulated from call to call, and returned back to the 
client.  In this case the state is a simple line number counter.
Anyway, I offer these up on the chance they might be useful, and on the 
chance that someone might help complete the library to compute 
"reversible" permutations, which may indeed be quite useful (as in the 
traveling salesman problem, and variations of it).
-Howard

Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk