我认为这是一个有趣的问题,它需要一些巧妙的动态编程。但是,我会从一些简单的蛮力开始,然后尝试改进它。据我了解你的问题,你有点处于那个阶段,并试图找到一种方法来枚举所有可能的配对。
可视化
对于 4 名学生,您有三种可能的组合
(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
请注意,我们只需要绘制一半的矩阵,因为它是对称的(如果 1 与 2 配对,那么 2 也与 1 配对)。我们也可以忽略对角线。已经有 4 个学生,看起来有些复杂。所以让我们直截了当。
计数
假设您有 N 学生尚未分配到一对。有多少种组合?让我们称之为C(N)...
对于 2 名学生,只有一种组合,因此是 C(2)= 1。
对于两个以上未分配的学生,我们可以在不失一般性的情况下选择第一个学生。还有N-1其他学生我们可以配对他,因此总共C(N) = N-1 * C(N-2)。
让我们通过列出数字使其更加具体:
N N-1 C(N)
2 1 1
4 3 3
6 5 15
8 7 105
...
n n-1 (n-1)!!
现在我们已经知道如何计算它们了。 8 名学生有 105 种可能性。一般来说n 的学生有(n-1)!! 的可能性(x!! == x*(x-2)*(x-4)*...)。
构造
在计算时,我们已经使用以下策略来构建解决方案:
- 选择第一个免费学生
- 选择其他免费学生之一
- 与其余部分重复
显然,我们需要n/2 步骤将所有学生分配到一对。让我们考虑一个例子。有 6 名学生,我们有
( 5 ) * ( 3 ) * ( 1 )
可能的组合。接下来我们意识到我们总是可以使用索引来枚举仍然可用的学生。因此,我们必须选择的指数是
[0...4] x [0...2] x [0]
现在,如果您想知道5th 组合是什么,您可以通过以下方式获得它...
一旦我们选择了第一对,第二个索引仍然有3 可能的选择(只有一个可以从仅有的两个可用学生中选出最后一对)。因此你得到的索引为
x0 = 5/3; // -> 1
x1 = (5-x0)/1; // -> 2
也就是说
- 我们为第一对选择第一个学生:
0
- 此时可用的学生网:
available = {1,2,3,4,5}
- 我们选择
available[x0] 将他与0 配对:(0 2)
- 此时可用的学生:
available = {1,3,4,5}
- 我们为下一对选择第一个可用的:
1
- 此时可用的学生:
available = {3,4,5}
- 我们选择
available[x1] 将他与1 配对:(1 5)
- 最后一对只剩下两个
(3 4)
-> 与索引5 的配对是(0 2)(1 5)(3 4)。
请注意,如果按字面意思实现,这可能不是最有效的方法,尽管它可以作为一个起点。
代码
为了计算组合,我们需要x!! 函数(!!,如上文所述):
size_t double_fac(int n){
size_t result = 1;
while(n > 0) {
result*=n;
n-=2;
}
return result;
}
使用这个我可以计算组合的总数
size_t total_number_of_combinations(size_t n_students){
return double_fac(n_students-1);
}
我需要一个函数来查找第 n 个尚未分配的学生的索引,为此我将使用一些辅助函数:
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));
}
另外我会使用一个平面索引,然后将其展开为上面的大纲(其实我不太喜欢上面的解释,但我希望它足够有说服力……):
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;
}
简而言之:在每个步骤中,我都会为第一个免费学生选择一个合作伙伴。对于flat_index == 0,第一对是(0 1)。因为在挑选那对之后有size_of_subspace == total_number_of_combinations(n_students-2) 组合,所以挑选(0 2) 作为第一对的索引是flat_index==size_of_subspace。但是,请不要混淆,我没有将flat_index 直接转换为学生索引,而是expandend_index == n 指的是nth 尚未分配的学生。
把它放在一起:
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;
}
现在再次以n_students == 6 为例,这个:
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));
}
}
打印:
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)
我希望通过这个输出,算法也变得更加清晰。选择第一对后,第二对有3 的可能性,最后一个只有一个组合。
Live Demo
免责声明:如上所述,我并不是说这是一种有效的实现方式。该代码是一个详细的参考实现。对于每个flat_index,我基本上是从根到它的一个叶子遍历树。在下一次迭代中,人们可以考虑从一些初始配置开始,根据需要向上和向下遍历树。