获取C ++中矩阵的所有可能组合

时间:2019-05-09 14:02:27

标签: c++ algorithm matrix backtracking recursive-backtracking

我们有一间教室。我们的主要目标是使成对的学生一起工作。我们该怎么做?通过矩阵。这个矩阵(n x n,n是对)存储每个学生对另一个学生的“偏好”级别。例如,i是一名学生,j是另一名学生:

matrix[i][j] = 40

matrix[j][i] = 20

因此,(i,j)的偏好级别可能与(j,i)对的偏好级别不同。

假设我们有10个学生。第一次迭代将使该对:(0,1),(2,3),(4,5)...(9,10)。 下一个是:(0,2),(1,3),(4,5)...(9,10)-等等。

因此,我们需要使用回溯算法找到一种解决方案,以为此目的获得最高价值。解决方案是一个向量,其中的对使这个最大值。

我们认为正确的方法是生成树,但是我们不知道如何生成树。

我们尝试的最后一件事是通过一种方法来计算程序对所有对进行迭代所需的迭代次数,并使用模块来了解何时需要更改解向量的顺序。我不是这种方法的忠实拥护者。无论如何,我们无法使其正常工作。

由于矩阵是随机生成的,因此我们没有实际的“预期结果”。我们需要的是一种确保每对学生和可能的学生组合都能完成的方法。

3 个答案:

答案 0 :(得分:1)

因此,您在i-th的每一行都有一个方矩阵,其中代表学生i的所有可能对,而每个学生只能有一对。

要获取所有可能的对组合,可以使用以下递归:

  1. 为第i个学生抛出所有可能的对:

    • 如果可以配对(不是他本人,并且不使用配对),则将第i个学生及其配对标记为“二手”并存储。
    • 如果(i + 1)小于学生数,则与第(i + 1)个学生一起进入步骤1,否则返回存储的对。

如果学生人数是奇数,您可能会遇到一些困难。在这种情况下,您可以向任何一对学生添加最大容忍度的“假”学生。因此,您始终能够配对并计算总体满意度。

以下是其中一种算法的Java代码段,该算法可找到所有可能的配对对变体:

    List<List<Integer[]>> allVariationsOfPairs = new ArrayList<>();

    void retrieveAllVariationsOfPairs(int[][] array, int studentIndex, List<Integer[]> pairs) {
        if (studentIndex == array.length) {
            allVariationsOfPairs.add(pairs);
            return;
        }
        boolean hasPair = false;
        for (int i = 0; i < array[studentIndex].length; ++i) {
            if (studentIndex == i || array[studentIndex][i] == 1 || array[studentIndex][studentIndex] == 1) {
                continue;
            }
            hasPair = true;
            List<Integer[]> copyPairs = new ArrayList<>(pairs);
            copyPairs.add(new Integer[]{studentIndex, i});
            int[][] copyArray = Arrays.stream(array).map(r -> r.clone()).toArray(int[][]::new);
            for (int[] row : copyArray) {
                row[studentIndex] = 1;
                row[i] = 1;
            }
            retrieveAllVariationsOfPairs(copyArray, studentIndex + 1, copyPairs);
        }
        if (!hasPair) {
            retrieveAllVariationsOfPairs(array, studentIndex + 1, pairs);
        }
    }

用法示例:

retrieveAllVariationsOfPairs(new int[6][6], 0, new ArrayList<>());

输出:

[0, 1]
[2, 3]
[4, 5]

[0, 1]
[2, 4]
[3, 5]

[0, 1]
[2, 5]
[3, 4]

[0, 2]
[1, 3]
[4, 5]

[0, 2]
[1, 4]
[3, 5]

[0, 2]
[1, 5]
[3, 4]

[0, 3]
[1, 2]
[4, 5]

[0, 3]
[1, 4]
[2, 5]

[0, 3]
[1, 5]
[2, 4]

[0, 4]
[1, 2]
[3, 5]

[0, 4]
[1, 3]
[2, 5]

[0, 4]
[1, 5]
[2, 3]

[0, 5]
[1, 2]
[3, 4]

[0, 5]
[1, 3]
[2, 4]

[0, 5]
[1, 4]
[2, 3]

您可以计算出所有对对的总体满意度并选择最合适的对。

答案 1 :(得分:1)

您的问题使我想起了minimum transportation cost问题。这是一种众所周知的线性编程问题,您的问题可能是其中的特例。

这是可能的费用表的示例:

╔═══════════╦════════════╦═════════════╦═════════════╦═════════════╗
║           ║ Student A  ║ Student B   ║ Student C   ║  Supply     ║
╠═══════════╬════════════╬═════════════╬═════════════╬═════════════╣
║           ║DissatisfAB ║DissatisfBA  ║DissatisfCA  ║     1       ║
║           ║DissatisfAC ║DissatisfBC  ║DissatisfCB  ║     1       ║
║ Demand    ║    1       ║      1      ║     1       ║             ║
╚═══════════╩════════════╩═════════════╩═════════════╩═════════════╝

每个学生都需要一个“对”,每个学生都可以向其他人供餐。作为运输成本,我们可以使用对他们的不满意程度。解决该问题将满足需求并最大程度地减少整体不满。

当然,您可以找到很多用c ++解决此问题的库。或者,您甚至可以尝试在线calculators

答案 2 :(得分:1)

我认为这是一个有趣的问题,它需要一些聪明的动态编程。但是,我将从一些简单的蛮力开始,然后尝试对其进行完善。据我了解您的问题,您目前处于这个阶段,并尝试找到一种方法来枚举所有可能的配对。

可视化

对于4个学生,您有3种可能的组合

(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]

现在,例如,如果您想知道第5个组合是什么,可以通过以下方式获取它...

一旦我们选择了第一对,第二个索引仍然有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指的是第n个尚未分配的学生。

将它们放在一起:

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,我基本上是从树的根部遍历到其叶子之一。在下一次迭代中,您可能会想到仅从某些初始配置开始视需要遍历整个树。