简单的排列组合问题——算法系列教程(c++版)

梦想不会自己发光,真正闪耀的是那个为梦狂奔的你。献给知行的孩子们!(Eric.He著)


  枚举算法是解决简单组合、排列问题的首选方案(数据规模小),核心是通过有序遍历所有可能的组合 / 排列候选,再筛选符合条件的结果。

  1. 排列:从n个不同的元素中,取出k(1≤k≤n)个元素,并按照一定的顺序排成一列, 所得到的不同排列方法数称为排列。排列强调元素的顺序性,即元素的选择及其在序列中的位置都影响最终结果。因此,当“顺序重要”时,应使用排列来计数。
    • 所有这样的排列的总数,叫做排列数,记为A$\binom{k}{n}$​(n代表元素的总数,k代表每次取出的元素个数。)。
    • 公式:A$\binom{k}{n}$​=$\frac {n!} {(n-k)!}$
    • 示例1:从 5 个不同的球中选 3 个按顺序排成一列,排列数是多少?
      • 解:A$\binom{3}{5}$​=$\frac {5!} {(5-3)!}$=$\frac {5×4×3×2×1} {2×1}$=5×4×3​=60
    • 示例2:从 10 名同学中选 2 名分别当班长和副班长?→ 排列(班长和副班长有顺序)
      • 解:A$\binom{2}{10}$​=$\frac {10!} {(10-2)!}$=$\frac {10×9×8×7×6×5×4×3×2×1} {8×7×6×5×4×3×2×1}$=10×9​=90
  2. 组合:从n个不同的元素中,取出k(1≤k≤n)个元素,不考虑顺序地拼成一组,所得到的不同选取方法数称为组合。组合强调元素的集合性,即只关注选择了哪些元素而不关心它们的排列次序。因此,当“顺序不重要”时,应使用组合来计数。
    • 所有这样的组合的总数,叫做组合数,记为C$\binom{k}{n}$​(读作 “n 选 k”)。
    • 公式:C$\binom{k}{n}$​=$\frac {n!} {k!×(n-k)!}$
    • 示例1:从 5 个不同的球中选 3 个组成一组(不考虑顺序),组合数是多少?
      • 解:C$\binom{3}{5}$​=$\frac {5!} {3!×(5-3)!}$=$\frac {5×4×3×2×1} {(3×2×1)×(2×1)}$=$\frac {5×4×3} {3×2×1}$​=10
    • 示例2:从 10 名同学中选 2 名参加座谈会?→ 组合(参加座谈会无顺序)
      • 解:C$\binom{2}{10}$​=$\frac {10!} {2!×(10-2)!}$=$\frac {10×9×8×7×6×5×4×3×2×1} {(2×1)×(8×7×6×5×4×3×2×1)}$=$\frac {10×9} {2×1}$​=45
    • 示例3:从 6 名男生和 4 名女生中选 3 人参加合唱,要求男女都有,有多少种选法?
      • 解:总选法C$\binom{3}{10}$,全男C$\binom{3}{6}$,全女C$\binom{3}{4}$,C$\binom{3}{10}$-C$\binom{3}{6}$-C$\binom{3}{4}$=120−20−4=96

  以下是 6 个典型实例,覆盖无重复组合、有重复组合、全排列、部分排列等常见场景。

教程目录导航

一、全排列:n 个元素的所有排列

问题描述:

算法解析:

  1. 确定解空间:n 个元素的所有顺序组合;
  2. 确定枚举顺序:三重循环(对应 3 个元素),逐一枚举每个位置的元素,确保元素不重复;
  3. 预处理候选解:无
  4. 验证有效解:三个位置的元素互不相同;

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> nums = {1, 2, 3};
    cout << "[" << 1 << "," << 2 << "," << 3 << "]的所有全排列:" << endl;

    // 三重循环枚举所有排列,确保元素不重复
    int n = nums.size();
    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < n; ++j) {
            if (j == i) continue; // 避免重复选取同一个元素
            for (int k = 0; k < n; ++k) {
                if (k == i || k == j) continue; // 避免重复选取同一个元素
                cout << "{" << nums[i] << ", " << nums[j] << ", " << nums[k] << "}" << endl;
            }
        }
    }
    return 0;
}
        

输出结果


[1,2,3]的所有全排列:
{1, 2, 3}
{1, 3, 2}
{2, 1, 3}
{2, 3, 1}
{3, 1, 2}
{3, 2, 1}
            

二、部分排列:从 n 个元素中选取 k 个的排列

问题描述:

算法解析:

  1. 确定解空间:n 个元素中选取 k 个的所有顺序组合;
  2. 确定枚举顺序:双重循环,外层枚举第一个元素,内层枚举第二个元素(索引与外层不同);
  3. 预处理候选解:无
  4. 验证有效解:两个元素的索引不同(确保不重复选取);

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> nums = {1, 2, 3, 4};
    int k = 2; // 选取2个元素
    cout << "从[" << 1 << "," << 2 << "," << 3 << "," << 4 << "]中选取" << k << "个的所有排列:" << endl;

    // 枚举所有部分排列(i ≠ j 确保不重复且考虑顺序)
    int n = nums.size();
    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < n; ++j) {
            if (i == j) continue; // 避免重复选取同一个元素
            cout << "{" << nums[i] << ", " << nums[j] << "}" << endl;
        }
    }
    return 0;
}
        

输出结果


从[1,2,3,4]中选取2个的所有排列:
{1, 2}
{1, 3}
{1, 4}
{2, 1}
{2, 3}
{2, 4}
...(后续省略)
            

三、无重复组合:从 n 个数中选取 k 个的所有组合

问题描述:

算法解析:

  1. 确定解空间:数组中所有满足 i < j(避免重复组合)的元素对;
  2. 确定枚举顺序:双重循环,外层枚举第一个元素,内层枚举第二个元素(索引大于外层);
  3. 预处理候选解:无
  4. 验证有效解:仅保留索引递增的组合(确保无重复、无顺序差异);

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> nums = {1, 2, 3, 4};
    int k = 2; // 选取2个元素
    cout << "从[" << 1 << "," << 2 << "," << 3 << "," << 4 << "]中选取" << k << "个的所有组合:" << endl;

    // 枚举所有无重复组合(i < j 避免重复)
    int n = nums.size();
    for (int i = 0; i < n; ++i) {
        for (int j = i + 1; j < n; ++j) {
            cout << "{" << nums[i] << ", " << nums[j] << "}" << endl;
        }
    }
    return 0;
}  
        

输出结果


从[1,2,3,4]中选取2个的所有组合:
{1, 2}
{1, 3}
{1, 4}
{2, 3}
{2, 4}
{3, 4}
    

四、可重复组合:从 n 个数中选取 k 个的可重复组合

问题描述:

算法解析:

  1. 确定解空间:数组中所有满足 i ≤ j(允许重复、避免顺序差异)的元素对;
  2. 确定枚举顺序:双重循环,外层枚举第一个元素,内层枚举第二个元素(索引大于等于外层);
  3. 预处理候选解:无
  4. 验证有效解:保留索引非递减的组合(确保可重复、无顺序差异);

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> nums = {1, 2, 3};
    int k = 2; // 选取2个元素
    cout << "从[" << 1 << "," << 2 << "," << 3 << "]中选取" << k << "个的可重复组合:" << endl;

    // 枚举所有可重复组合(i ≤ j 允许重复且避免顺序差异)
    int n = nums.size();
    for (int i = 0; i < n; ++i) {
        for (int j = i; j < n; ++j) {
            cout << "{" << nums[i] << ", " << nums[j] << "}" << endl;
        }
    }
    return 0;
} 
        

输出结果


从[1,2,3]中选取2个的可重复组合:
{1, 1}
{1, 2}
{1, 3}
{2, 2}
{2, 3}
{3, 3}
            

五、 字符组合:生成指定长度的字符组合(可重复)

问题描述:

算法解析:

  1. 确定解空间:字符集的所有长度为 k 的拼接组合;
  2. 确定枚举顺序:双重循环,分别枚举第 1 位和第 2 位字符;
  3. 预处理候选解:无
  4. 验证有效解:无额外条件(允许重复字符);

#include <iostream>
#include <string>
#include <vector>

using namespace std;

int main() {
    vector<char> chars = {'a', 'b', 'c'};
    int len = 2; // 组合长度
    cout << "从{'a','b','c'}中生成长度为" << len << "的可重复字符组合:" << endl;

    // 枚举所有字符组合
    int n = chars.size();
    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < n; ++j) {
            string combo;
            combo += chars[i];
            combo += chars[j];
            cout << "\"" << combo << "\"" << endl;
        }
    }
    return 0;
}
        

输出结果


从{'a','b','c'}中生成长度为2的可重复字符组合:
"aa"
"ab"
"ac"
"ba"
"bb"
"bc"
"ca"
"cb"
"cc"
            

六、 组合求和:查找和为指定值的 k 元素组合(无重复)

问题描述:

算法解析:

  1. 确定解空间:所有 k 元素无重复组合(i < j);
  2. 确定枚举顺序:双重循环枚举 i < j 的元素对;
  3. 预处理候选解:无
  4. 验证有效解:元素和等于目标值;

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> nums = {1, 2, 3, 4, 5};
    int k = 2; // 选取2个元素
    int targetSum = 6; // 目标和
    cout << "从[" << 1 << "," << 2 << "," << 3 << "," << 4 << "," << 5 << "]中选取" << k << "个和为" << targetSum << "的组合:" << endl;

    // 枚举无重复组合,筛选和为目标值的结果
    int n = nums.size();
    for (int i = 0; i < n; ++i) {
        for (int j = i + 1; j < n; ++j) {
            if (nums[i] + nums[j] == targetSum) {
                cout << "{" << nums[i] << ", " << nums[j] << "}" << endl;
            }
        }
    }
    return 0;
}
        

输出结果


从[1,2,3,4,5]中选取2个和为6的组合:
{1, 5}
{2, 4}
            

七、总结

枚举算法解决简单组合 / 排列问题的共性特点:

  1. 数据规模小(元素个数通常≤5,组合 / 排列长度≤3),枚举效率可接受;
  2. 实现简单:通过多层循环直接遍历所有候选,无需递归或高级算法;
  3. 边界清晰:通过索引判断(i < j / i ≤ j / i ≠ j)区分 “无重复 / 可重复 / 考虑顺序”;
  4. 灵活扩展:可通过增加筛选条件(如和为指定值)适配带约束的组合 / 排列场景。

上述实例覆盖了数值、字符、带条件等常见场景,适合入门者理解组合 / 排列的本质及枚举算法的应用。


返回顶部