当前位置: 首页 > news >正文

深度优先搜索(DFS)在排列组合问题中的应用详解:C++实现与优化

一、排列问题(Permutations)

目标:生成所有可能的排列(元素顺序不同视为不同结果)。
示例:输入 [1,2,3],输出所有长度为3的排列,共6种。

C++实现代码

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

void backtrack(vector<int>& nums, vector<bool>& used, vector<int>& path, vector<vector<int>>& res) {
    if (path.size() == nums.size()) {
        res.push_back(path);  // 保存当前排列
        return;
    }
    for (int i = 0; i < nums.size(); ++i) {
        if (!used[i]) {  // 选择未使用的元素
            used[i] = true;
            path.push_back(nums[i]);
            backtrack(nums, used, path, res);  // 递归
            path.pop_back();  // 回溯
            used[i] = false;
        }
    }
}

vector<vector<int>> permute(vector<int>& nums) {
    vector<vector<int>> res;
    vector<bool> used(nums.size(), false);
    vector<int> path;
    backtrack(nums, used, path, res);
    return res;
}

int main() {
    vector<int> nums = {1, 2, 3};
    auto permutations = permute(nums);
    for (auto& p : permutations) {
        for (int num : p) {
            cout << num << " ";
        }
        cout << endl;
    }
    return 0;
}

输出

1 2 3 
1 3 2 
2 1 3 
2 3 1 
3 1 2 
3 2 1 

关键点

  • 去重:通过used数组标记已使用的元素,避免重复选择。

  • 回溯:递归后需撤销当前选择(path.pop_back()used[i] = false),以尝试其他分支。


二、组合问题(Combinations)

目标:生成所有可能的组合(元素顺序不同视为相同结果)。
示例:从 [1,2,3] 中选2个元素,输出 {1,2}{1,3}{2,3}

C++实现代码

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

void backtrack(int start, int k, vector<int>& nums, vector<int>& path, vector<vector<int>>& res) {
    if (path.size() == k) {
        res.push_back(path);  // 保存当前组合
        return;
    }
    for (int i = start; i < nums.size(); ++i) {
        path.push_back(nums[i]);
        backtrack(i + 1, k, nums, path, res);  // 下一层从i+1开始,避免重复
        path.pop_back();  // 回溯
    }
}

vector<vector<int>> combine(vector<int>& nums, int k) {
    vector<vector<int>> res;
    vector<int> path;
    backtrack(0, k, nums, path, res);
    return res;
}

int main() {
    vector<int> nums = {1, 2, 3};
    int k = 2;
    auto combinations = combine(nums, k);
    for (auto& c : combinations) {
        for (int num : c) {
            cout << num << " ";
        }
        cout << endl;
    }
    return 0;
}

输出

1 2 
1 3 
2 3 

关键点

  • 顺序控制:每次递归从i+1开始,避免选择之前的元素,保证组合无序性。

  • 剪枝优化:若剩余元素不足以填满组合,可提前终止循环。


三、处理重复元素

1. 含重复元素的排列去重

示例:输入 [1,1,2],输出不重复的排列 [1,1,2][1,2,1][2,1,1]

优化代码

void backtrack(vector<int>& nums, vector<bool>& used, vector<int>& path, vector<vector<int>>& res) {
    if (path.size() == nums.size()) {
        res.push_back(path);
        return;
    }
    for (int i = 0; i < nums.size(); ++i) {
        if (used[i] || (i > 0 && nums[i] == nums[i-1] && !used[i-1])) {
            continue;  // 跳过重复元素
        }
        used[i] = true;
        path.push_back(nums[i]);
        backtrack(nums, used, path, res);
        path.pop_back();
        used[i] = false;
    }
}

vector<vector<int>> permuteUnique(vector<int>& nums) {
    sort(nums.begin(), nums.end());  // 必须排序
    vector<vector<int>> res;
    vector<bool> used(nums.size(), false);
    vector<int> path;
    backtrack(nums, used, path, res);
    return res;
}

关键点

  • 排序:预处理数组,使相同元素相邻。

  • 剪枝条件nums[i] == nums[i-1] && !used[i-1] 表示跳过重复且未被使用的元素。

2. 含重复元素的组合去重

示例:输入 [1,2,2],输出不重复的组合 [1,2][2,2]

优化代码

void backtrack(int start, int k, vector<int>& nums, vector<int>& path, vector<vector<int>>& res) {
    if (path.size() == k) {
        res.push_back(path);
        return;
    }
    for (int i = start; i < nums.size(); ++i) {
        if (i > start && nums[i] == nums[i-1]) {
            continue;  // 跳过重复元素
        }
        path.push_back(nums[i]);
        backtrack(i + 1, k, nums, path, res);
        path.pop_back();
    }
}

vector<vector<int>> combineUnique(vector<int>& nums, int k) {
    sort(nums.begin(), nums.end());  // 必须排序
    vector<vector<int>> res;
    vector<int> path;
    backtrack(0, k, nums, path, res);
    return res;
}

关键点

  • 排序:预处理数组,使相同元素相邻。

  • 剪枝条件i > start && nums[i] == nums[i-1] 跳过同一层中的重复元素。


四、时间复杂度分析

问题类型时间复杂度空间复杂度
排列(无重复)O(n×n!)O(n)
组合(无重复)O(C(n,k) ×k)O(k)
排列(有重复)O(n×n!/m!)(m为重复数)O(n)
组合(有重复)O(C(n,k) ×k)O(k)

五、应用场景

  • 排列:密码破解、任务调度、路径规划。

  • 组合:子集生成、商品搭配、团队选拔。


六、总结

  • DFS核心:通过递归和回溯遍历所有可能的分支。

  • 排列与组合区别:排列关注顺序,组合不关注顺序。

  • 去重关键:排序 + 剪枝条件(跳过重复元素)。

  • 优化技巧:剪枝提前终止无效分支,减少递归深度。


七、排列问题例题

1、输出全排列

P1706 全排列问题 - 洛谷

算法代码: 

#include<bits/stdc++.h>  // 包含常用的C++标准库头文件
using namespace std;    // 使用标准命名空间

int n;                  // 定义一个全局变量n,表示排列的长度
int vis[10];            // 定义一个大小为10的数组vis,用于标记某个数字是否已经被使用过
int a[10];              // 定义一个大小为10的数组a,用于存储1到n的数字
int b[10];              // 定义一个大小为10的数组b,用于存储当前生成的排列

// 定义一个递归函数dfs,用于生成所有可能的排列
void dfs(int step)
{
    // 如果step等于n+1,说明已经生成了一个完整的排列
    if(step==n+1)
    {
        // 输出当前排列
        for(int i=1;i<=n;i++)
        {
            printf("%5d",b[i]);  // 每个数字占5个字符宽度
        }
        printf("\n");  // 换行
        return;  // 返回上一层递归
    }

    // 遍历1到n的所有数字
    for(int i=1;i<=n;i++)
    {
        // 如果数字i没有被使用过
        if(vis[i]==0)
        {
            b[step]=a[i];  // 将数字i放入当前排列的第step个位置
            vis[i]=1;      // 标记数字i为已使用
            dfs(step+1);   // 递归调用dfs,生成下一个位置的数字
            vis[i]=0;      // 回溯,取消数字i的标记
        }
    }
    return;  // 返回上一层递归
}

int main()
{
    cin>>n;  // 输入排列的长度n

    // 初始化数组a,a[i] = i
    for(int i=1;i<=n;i++)
    {
        a[i]=i;          
    }

    dfs(1);  // 从第1个位置开始生成排列

    return 0;  // 程序正常结束
}

图解流程: 


2、输出部分排列(需注意)

        上述的代码是实现打印n个数的全排列,若我需要打印从 n 个数中任意选取 m 个数的排列,需要做以下两处修改:

  1. 修改 dfs 函数的终止条件

    • 原代码的终止条件是 step == n + 1,表示生成了完整的 n 个数的排列。

    • 现在需要改为 step == m + 1,表示生成了 m 个数的排列。

  2. 修改主函数中的输入和初始化

    • 需要额外输入一个变量 m,表示从 n 个数中选取 m 个数进行排列。

#include<bits/stdc++.h>  // 包含常用的C++标准库头文件
using namespace std;    // 使用标准命名空间

int n, m;              // 定义全局变量n和m,n表示总数,m表示选取的数的个数
int vis[10];            // 定义一个大小为10的数组vis,用于标记某个数字是否已经被使用过
int a[10];              // 定义一个大小为10的数组a,用于存储1到n的数字
int b[10];              // 定义一个大小为10的数组b,用于存储当前生成的排列

// 定义一个递归函数dfs,用于生成从n个数中选取m个数的排列
void dfs(int step)
{
    // 如果step等于m+1,说明已经生成了一个完整的m个数的排列
    if(step == m + 1)
    {
        // 输出当前排列
        for(int i = 1; i <= m; i++)
        {
            printf("%5d", b[i]);  // 每个数字占5个字符宽度
        }
        printf("\n");  // 换行
        return;  // 返回上一层递归
    }

    // 遍历1到n的所有数字
    for(int i = 1; i <= n; i++)
    {
        // 如果数字i没有被使用过
        if(vis[i] == 0)
        {
            b[step] = a[i];  // 将数字i放入当前排列的第step个位置
            vis[i] = 1;      // 标记数字i为已使用
            dfs(step + 1);  // 递归调用dfs,生成下一个位置的数字
            vis[i] = 0;     // 回溯,取消数字i的标记
        }
    }
    return;  // 返回上一层递归
}

int main()
{
    cin >> n >> m;  // 输入总数n和选取的数的个数m

    // 初始化数组a,a[i] = i
    for(int i = 1; i <= n; i++)
    {
        a[i] = i;          
    }

    dfs(1);  // 从第1个位置开始生成排列

    return 0;  // 程序正常结束
}

示例运行:

假设输入:

n = 4
m = 2

输出将是:

    1    2
    1    3
    1    4
    2    1
    2    3
    2    4
    3    1
    3    2
    3    4
    4    1
    4    2
    4    3 

3、输出组合(打印n个数中m个数的组合)(重要)

在DFS中,选择或者不选择第k个数就实现了各种组合。以下举两个例子:

(1)输出二进制

以打印000~111为例:

#include<bits/stdc++.h>  // 包含常用的C++标准库头文件
using namespace std;    // 使用标准命名空间

int vis[10];            // 定义一个大小为10的数组vis,用于存储当前的状态(0或1)

// 定义一个递归函数dfs,用于生成所有可能的二进制组合
void dfs(int k)
{
    // 如果k等于4,说明已经生成了一个完整的3位二进制组合
    if(k == 4)
    {
        // 输出当前生成的3位二进制组合
        for(int i = 1; i < 4; i++)
        {
            cout << vis[i];  // 输出vis数组的第1到第3个元素
        }
        cout << " ";  // 换行
    }
    else
    {
        vis[k] = 0;    // 将当前位设置为0
        dfs(k + 1);    // 递归调用dfs,生成下一位
        vis[k] = 1;    // 将当前位设置为1
        dfs(k + 1);    // 递归调用dfs,生成下一位
    }
}

int main()
{
    dfs(1);  // 从第1位开始生成二进制组合
    return 0;  // 程序正常结束
}

代码功能:

这段代码的功能是生成所有可能的3位二进制组合(从 000 到 111),并输出这些组合。

代码流程:

  1. 初始化

    • 定义了一个全局数组 vis,用于存储每一位的状态(0或1)。

    • 数组大小为10,但实际只用到前3位(索引1到3)。

  2. 递归函数 dfs

    • 参数 k 表示当前正在处理的位数。

    • 如果 k == 4,说明已经生成了一个完整的3位二进制组合,输出 vis[1] 到 vis[3] 的值。

    • 否则:

      • 将当前位 vis[k] 设置为0,然后递归调用 dfs(k + 1),生成下一位。

      • 将当前位 vis[k] 设置为1,然后递归调用 dfs(k + 1),生成下一位。

  3. 主函数 main

    • 调用 dfs(1),从第1位开始生成二进制组合。

输出结果:

运行代码后,输出将是所有3位二进制组合:

000 001 010 011 100 101 110 111

如果要反过来,只需交换选择数字0或1的逻辑选择代码行就可以了,输出:111 110 101 100 011 010 001 000,即:

#include<bits/stdc++.h>  // 包含常用的C++标准库头文件
using namespace std;    // 使用标准命名空间

int vis[10];            // 定义一个大小为10的数组vis,用于存储当前的状态(0或1)

// 定义一个递归函数dfs,用于生成所有可能的二进制组合
void dfs(int k)
{
    // 如果k等于4,说明已经生成了一个完整的3位二进制组合
    if(k == 4)
    {
        // 输出当前生成的3位二进制组合
        for(int i = 1; i < 4; i++)
        {
            cout << vis[i];  // 输出vis数组的第1到第3个元素
        }
        cout << " ";  // 换行
    }
    else
    {
        vis[k] = 1;    // 将当前位设置为0
        dfs(k + 1);    // 递归调用dfs,生成下一位
        vis[k] = 0;    // 将当前位设置为1
        dfs(k + 1);    // 递归调用dfs,生成下一位
    }
}

int main()
{
    dfs(1);  // 从第1位开始生成二进制组合
    return 0;  // 程序正常结束
}

(2)输出组合

以包含3个元素的集合{a,b,c}为例子,输出它的所有的子集。

#include<bits/stdc++.h>  // 包含常用的C++标准库头文件,如iostream、vector、algorithm等
using namespace std;    // 使用标准命名空间,避免每次调用标准库函数时都要加std::

char a[] = {' ', 'a', 'b', 'c'};  // 定义字符数组a,包含空格和字母a、b、c,用于生成组合时选择字符
int vis[10];  // 定义状态数组vis,用于存储当前的状态(0或1),表示是否选择对应位置的字符

// 定义一个递归函数dfs,用于生成所有可能的组合
void dfs(int k)
{
    // 如果k等于4,说明已经生成了一个完整的3位组合
    if(k == 4)
    {
        // 输出当前生成的3位组合
        for(int i = 1; i < 4; i++)
        {
            if(vis[i])
                cout << a[i];  // 如果vis[i]为1,输出字符数组a中对应的字符
        }
        cout << " ";  // 输出空格分隔不同的组合
    }
    else
    {
        vis[k] = 0;  // 将当前位设置为0,表示不选择字符数组a中的对应字符(即选择空格)
        dfs(k + 1);  // 递归调用dfs,生成下一位的组合
        vis[k] = 1;  // 将当前位设置为1,表示选择字符数组a中的对应字符
        dfs(k + 1);  // 递归调用dfs,生成下一位的组合
    }
}

int main()
{
    dfs(1);  // 从第1位开始生成组合
    return 0;  // 程序正常结束
}

代码功能解释:

  • 该代码通过递归函数 dfs 生成所有可能的3位组合,组合中的每一位可以是空格或字符 abc

  • vis 数组用于记录每一位是否选择字符(1表示选择,0表示不选择)。

  • 当 k == 4 时,表示已经生成了一个完整的3位组合,此时遍历 vis 数组,输出对应的字符。

  • 递归过程中,每次调用 dfs 时,都会尝试将当前位设置为0或1,然后继续递归生成下一位的组合。

输出示例:

运行该代码会输出所有可能的3位组合,例如:

   a b c ab ac bc abc

(其中空格表示未选择字符)

相关文章:

  • 第一章,网络发展史///OSI七层模型
  • 【开题报告+论文+源码】基于SpringBoot+Vue的酒店餐饮管理系统设计与实现
  • CTF类题目复现总结-[羊城杯 2020]TCP_IP 1
  • 25. 策略模式
  • Java 编译 API(javax.tools 包)的使用方法及关键点总结,适用于在运行时动态编译 Java 代码
  • 【MySQL篇】DEPENDENT SUBQUERY(依赖性子查询)优化:从百秒到秒级响应的四种优化办法
  • 芋道 Spring Cloud Alibaba 消息队列 RocketMQ 入门
  • LeetCode 2255.统计是给定字符串前缀的字符串数目:使用库函数+计数
  • wordpress-网站百宝箱插件
  • Spring Boot - 动态编译 Java 类并实现热加载
  • 第二天 流程控制(if/for/while) - 列表/元组/字典操作
  • [笔记] SpringBoot3 使用 EasyExcel 封装工具类实现复杂 Excel 数据处理:使用Java构建高效的数据导入解决方案
  • (UI自动化测试web端)第二篇:元素定位的方法_xpath属性定位
  • [网鼎杯 2020 白虎组]PicDown1 [反弹shell] [敏感文件路径] [文件描述符]
  • Unity 使用 Protobuf(Pb2)二进制数据全流程工具详解
  • Leetcode--151. 反转字符串中的单词(字符串+双指针---基础算法)
  • Android Compose 层叠布局(ZStack、Surface)源码深度剖析(十三)
  • Delphi语言的算法
  • 新版 React19使用 react-quill
  • 基于SpringBoot的图书借阅小程序+LW参考示例
  • 习近平在河南洛阳市考察调研
  • 搜狐一季度营收1.36亿美元,净亏损同比收窄超两成
  • 无人机企业从科技园区搬到乡村后,村子里变得不一样了
  • 江苏疾控:下设部门无“病毒研究所”,常荣山非本单位工作人员
  • 体育文化赋能国际交流,上海黄浦举办国际友人城市定向赛
  • 上海将建设万兆小区、园区及工厂,为模型训练数据的传输提供硬件支持