I think this is an interesting problem and it requires some clever dynamic programming. However, I would start with some simple brute force and then try to refine it. As I understand your question, you are somewhat in that stage and try to find a way to enumerate all possible pairings.
Visualize it
For 4 students you have three possible combinations
(0 1) (2 3) (0 2) (1 3) (0 3) (1 2)
1 2 3
0 x o o o x o o o x
1 o o o x x o
2 x o o
Note that we only need to draw half of the matrix, because it is symmetric (if 1 is paired with 2 then also 2 is paired with 1). We also can ignore the diagonal. Already with 4 students it looks somewhat complicated. So let's get this straight.
Counting
Lets say you have N
students not yet assigned to a pair. How many combinations are there? Lets call it C(N)
...
For 2 students there is only one combination, hence C(2)= 1
.
For more than two unassigned students we can pick the first student without loss of generality. There are N-1
other student to whom we could pair him, hence in total C(N) = N-1 * C(N-2)
.
Lets make it a bit more concrete by listing the numbers:
N N-1 C(N)
2 1 1
4 3 3
6 5 15
8 7 105
...
n n-1 (n-1)!!
Now we already know how to count them. There are 105 possibilities for 8 students. And in general for n
students there are (n-1)!!
possibilities ( x!! == x*(x-2)*(x-4)*...
).
Constructing
Already while counting we used the following strategy to construct the solution:
- Pick the first free student
- Pick one of the other free students
- Repeat with the rest
Obviously we need n/2
steps to have all students assigned to a pair. Lets consider an example. With 6 students, we have
( 5 ) * ( 3 ) * ( 1 )
possible combinations. Next we realize that we can always use an index to enumare only the students that are still available. Hence the indices we have to pick are
[0...4] x [0...2] x [0]
Now if for example you want to know what is the 5
th combination you can get it in the following way...
Once we pick the first pair, there are still 3
possible choices for the second index (one only one to make the last pair from the only two available students). Hence you get the indices as
x0 = 5/3; // -> 1
x1 = (5-x0)/1; // -> 2
Ie that would be
- We pick the first student for the first pair:
0
- Available studnets at this point:
available = {1,2,3,4,5}
- We pick
available[x0]
to pair him with 0
: (0 2)
- Available students at this point:
available = {1,3,4,5}
- We pick the first available for the next pair:
1
- Available students at this point:
available = {3,4,5}
- We pick
available[x1]
to pair him with 1
: (1 5)
- Only two left for the last pair
(3 4)
-> The pairing with index 5
is (0 2)(1 5)(3 4)
.
Note that is might not be the most efficient way when implemented too literally, though it can be a starting point.
The code
For counting the combinations we need the x!!
function (!!
in the sense as explained above):
size_t double_fac(int n){
size_t result = 1;
while(n > 0) {
result*=n;
n-=2;
}
return result;
}
Using this I can compute the total number of combinations
size_t total_number_of_combinations(size_t n_students){
return double_fac(n_students-1);
}
I will need a function to find the index of the nth not yet assigned student, for this I will use some helper functions:
template <typename IT>
IT find_skip(IT begin,IT end,size_t skip,typename IT::value_type x){
if (skip){
return find_skip( ++ std::find(begin,end,x), end, skip-1,x);
} else {
return std::find(begin,end,x);
}
}
template <typename IT>
size_t find_skip_index(IT begin,IT end,size_t skip,typename IT::value_type x){
return std::distance(begin,find_skip(begin,end,skip,x));
}
Also I will use a flat index and then expand it as outline above (actually I dont like the above explanation too much, but I hope it is convincing enough...):
std::vector<size_t> expand_index(size_t n_students, size_t flat_index){
std::vector<size_t> expanded_index;
auto students_to_be_assigned = n_students;
for (unsigned step=0;step<n_students/2;++step){
int size_of_subspace = total_number_of_combinations(students_to_be_assigned-2);
auto x = flat_index / size_of_subspace;
expanded_index.push_back(x);
flat_index -= x*size_of_subspace;
students_to_be_assigned-=2;
}
return expanded_index;
}
In a nutshell: In each step I will choose a partner for the first free student. For flat_index == 0
the first pair is (0 1)
. Because there are size_of_subspace == total_number_of_combinations(n_students-2)
combinations after picking that pair, the index of picking (0 2)
as first pair is flat_index==size_of_subspace
. However, please dont get confused, I do not transform the flat_index
directly into the students index, but rather expandend_index == n
refers to the n
th not yet assigned student.
Putting it together:
using combination = std::vector<std::pair<size_t,size_t>>;
combination nth_combination(size_t n_students,size_t flat_index){
combination result;
auto expanded_index = expand_index(n_students,flat_index);
std::vector<bool> available(n_students,true);
for (const auto& index : expanded_index) {
std::pair<size_t,size_t> next_pair;
next_pair.first = find_skip_index(available.begin(),available.end(),0,true);
available[next_pair.first] = false;
next_pair.second = find_skip_index(available.begin(),available.end(),index,true);
available[next_pair.second] = false;
result.push_back(next_pair);
}
return result;
}
Now for taking again n_students == 6
as example, this:
template <typename T>
void print_pairs(const T& t){
for (auto e: t) std::cout << "(" << e.first << "," << e.second << ") ";
std::cout << "\n";
}
int main(){
size_t n_students = 6;
for (size_t i=0;i<total_number_of_combinations(n_students);++i){
std::cout << i << "\t";
print_pairs(nth_combination(n_students,i));
}
}
prints:
0 (0,1) (2,3) (4,5)
1 (0,1) (2,4) (3,5)
2 (0,1) (2,5) (3,4)
3 (0,2) (1,3) (4,5)
4 (0,2) (1,4) (3,5)
5 (0,2) (1,5) (3,4)
6 (0,3) (1,2) (4,5)
7 (0,3) (1,4) (2,5)
8 (0,3) (1,5) (2,4)
9 (0,4) (1,2) (3,5)
10 (0,4) (1,3) (2,5)
11 (0,4) (1,5) (2,3)
12 (0,5) (1,2) (3,4)
13 (0,5) (1,3) (2,4)
14 (0,5) (1,4) (2,3)
I hope with this output also the algorithm gets more clear. After picking the first pair, there are 3
possibilities for the second pair and only one combination for the last.
Live Demo
Disclaimer: As mentioned above, I am not claiming that this is an efficient implementation. The code is rather meant to be a verbose reference implementation. I am basically traversing the tree from the root till one of its leaves for each flat_index
. In the next iteration one could think of traversing the tree only as much up and down as necessary staring from some intial configuration.