【问题标题】:Get all possible combinations for a matrix in C++获取 C++ 中矩阵的所有可能组合
【发布时间】:2019-09-27 09:28:58
【问题描述】:

我们有一个教室。我们的主要目标是让成对的学生一起工作。我们要怎么做?通过一个矩阵。这个矩阵(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) - 等等。

因此,我们需要找到使用回溯算法的解决方案,以达到此目的。解决方案是一个向量,其中包含使该最大值的对。

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

我们尝试的最后一件事是通过一种方法计算程序需要多少次迭代才能生成所有对,并使用模块知道何时需要更改解向量中的顺序。我不是这种方法的忠实拥护者。无论如何,我们无法让它工作。

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

【问题讨论】:

  • 你的问题是什么?
  • 如何获取学生所有可能的组合 (0,1), (2,3), (4,5) ... // (0,2), (1, 3), (4,5) ... // (0,3), (1,2), (4,5) ... //
  • 您想尽量减少对他们配对的普遍不满吗?
  • 我们希望最大限度地提高他们对的总体满意度。话虽如此,请找到对这些学生进行分类的最佳方法,并确保我们获得最高的全球满意度。
  • 但是这两个嵌套循环如何?每次更改初始对时,都应该有一个包含所有可能性的新树...

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


【解决方案1】:

所以你有一个方阵,每行i-th 代表学生i 的所有可能对,每个学生只能有一对。

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

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

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

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

这里是一种算法的 java sn-p,它可以找到对的所有可能变化:

    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]

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

【讨论】:

  • 针对不同语言 (C++) 标记的问题的 Java sn-p?伪代码会更合适。
【解决方案2】:

你的问题让我想起了minimum transportation cost 问题。这是一种众所周知的线性规划问题,您的问题可能是它的一个特例。

以下是可能的成本表示例:

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

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

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

【讨论】:

    【解决方案3】:

    我认为这是一个有趣的问题,它需要一些巧妙的动态编程。但是,我会从一些简单的蛮力开始,然后尝试改进它。据我了解你的问题,你有点处于那个阶段,并试图找到一种方法来枚举所有可能的配对。

    可视化

    对于 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,我基本上是从根到它的一个叶子遍历树。在下一次迭代中,人们可以考虑从一些初始配置开始,根据需要向上和向下遍历树。

    【讨论】:

    • 您好!非常感谢您的回答,它确实有助于澄清某些方面。确实,我们必须使用某种蛮力,因为我们需要使用回溯算法来做到这一点。问题是,我们开始探索“树”并挑选学生 1。哪个学生将配对?假设2会。第一对是 (1,2)。完成所有操作后,我们应该返回(回溯)并尝试 (1,3) 作为第一对。这种情况一直持续下去,直到我们找到最佳解决方案。
    • @alexhzr 我不知何故被这个问题迷住了,忍不住清理一下我的代码以发布它;)
    猜你喜欢
    • 2015-11-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-01-23
    • 1970-01-01
    • 1970-01-01
    • 2014-10-27
    • 1970-01-01
    相关资源
    最近更新 更多