递归回溯(DFS)三剑客:子集、排列、组合 复习笔记

一、核心概念总览

1. 递归(DFS)本质

函数自身调用自身,“一路向前” 处理下一个元素 / 位置,直到触发终止条件后 “回溯返回”,核心作用是枚举所有可能情况(无需手动写多层循环)。

2. 回溯核心操作

“修改状态 → 递归推进 → 恢复状态”,避免前一种选择影响后一种(类似 “试衣服→脱下来→试下一件”)。

3. 三大问题核心区别(基础认知)

问题类型顺序要求选择规则核心特征(n=3 为例)
子集(选与不选)无顺序每个元素可选 / 可不选输出所有子集(含空集),共 2ⁿ 种
排列有顺序选 k 个元素,每个元素仅用一次输出所有顺序组合,共 n!/(n-k)! 种
组合(固定个数)无顺序选 k 个元素,每个元素仅用一次输出无顺序组合,共 C (n,k) 种(C 为组合数)

二、三大 DFS 详细拆解(以 n=3 为例)

(一)DFS1:子集问题(选与不选)

1. 功能定位

从 1~n 中选择任意个数元素(0~n 个),不考虑顺序,输出所有子集(可优化去空集)。

2. 核心代码
#include <iostream>
using namespace std;
const int N = 20;
int n;
int st[N]; // 0=未考虑,1=选中,2=不选

// u:当前处理第u个元素(按元素顺序推进)
void dfs(int u) {
    // 终止条件:所有元素处理完毕(u > n)
    if (u > n) {
        bool flag = false;
        for (int i = 1; i <= n; i++) {
            if (st[i] == 1) {
                cout << i << " ";
                flag = true;
            }
        }
        if (flag) cout << endl; 
        return;
    }

    // 选择1:不选第u个元素
    st[u] = 2;
    dfs(u + 1);
    st[u] = 0; // 回溯:恢复状态

    // 选择2:选中第u个元素
    st[u] = 1;
    dfs(u + 1);
    st[u] = 0; // 回溯:恢复状态
}

int main() {
    cin >> n;
    dfs(1); // 从第1个元素开始处理
    return 0;
}
3. 关键细节
组件作用说明
变量 st[N]标记单个元素的状态(1 = 选,2 = 不选,0 = 未考虑),直接对应 “元素是否入选子集”。
递归参数 u按 “元素顺序” 推进,每个元素仅处理一次(处理完 u,直接推进到 u+1)。
选择逻辑每个元素仅有 2 种固定选择(选 / 不选),无额外分支。
回溯操作试完一种选择后,将 st[u] 恢复为 0,避免影响后续元素的选择判断。
终止条件所有元素处理完毕(u > n),遍历 st 数组,输出选中的元素。
4. 运行结果(n=3)

plaintext

  //还有个空
1 
2 
3 
1 2 
1 3 
2 3 
1 2 3 

(二)DFS2:排列问题(选 k 个,有顺序)

1. 功能定位

从 1~n 中选择固定 k 个元素(k=1~n),考虑顺序,输出所有不重复排列(每个元素仅用一次)。

2. 核心代码
#include <iostream>
using namespace std;
const int N = 20;
int n;
int a[N];      
bool vis[N];   

void dfs(int v, int tar) {
    // 终止条件:排列的tar个位置全部填充完毕(v = tar+1)
    if (v == tar + 1) {
        for (int i = 1; i <= tar; i++) {
            if (i > 1) cout << " "; // 去掉末尾空格
            cout << a[i];
        }
        cout << endl;
        return;
    }

    // 选择逻辑:遍历所有未被选的元素,填入当前位置v
    for (int i = 1; i <= n; i++) {
        if (!vis[i]) {
            vis[i] = true;  // 标记为已选
            a[v] = i;       // 填入第v个位置
            dfs(v + 1, tar); // 推进到下一个位置
            vis[i] = false; // 回溯:取消标记
            a[v] = 0;       // 回溯:清空位置(可选,更严谨)
        }
    }
}

int main() {
    cin >> n;
    // 依次输出选1个、2个、...、n个的所有排列
    for (int k = 1; k <= n; k++) {
        dfs(1, k);
    }
    return 0;
}
3. 关键细节
组件作用说明
变量 a[N]按 “位置” 存储排列结果(如 a [1]=1、a [2]=2 表示排列 “1 2”)。
变量 vis[N]标记元素是否被选,避免同一个元素重复填入多个位置(如 1 不能同时在两个位置)。
递归参数v:按 “位置顺序” 推进(填完 v,推进到 v+1);tar:控制排列长度(选 k 个)。
选择逻辑每个位置遍历所有未被选的元素(多个选择),如第 1 个位置可选 1/2/3。
回溯操作试完一个元素后,vis[i] 恢复为 false,a[v] 清空,方便下一个元素填入。
终止条件排列的 tar 个位置填完(v=tar+1),直接输出 a 数组中的排列。
4. 运行结果(n=3)

plaintext

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

三)DFS3:组合问题(选 k 个,无顺序)

1. 功能定位

从 1~n 中选择固定 k 个元素(k=1~n),不考虑顺序,输出所有不重复组合(每个元素仅用一次)。

2. 核心代码

#include <iostream>
using namespace std;
const int N = 20;
int n;
int a[N]; 


void dfs(int v, int start, int tar) {
    // 终止条件:组合的tar个位置全部填充完毕(v = tar+1)
    if (v == tar + 1) {
        for (int i = 1; i <= tar; i++) {
            if (i > 1) cout << " ";
            cout << a[i];
        }
        cout << endl;
        return;
    }

    // 选择逻辑:从start开始选数,不回头(避免重复组合)
    for (int i = start; i <= n; i++) {
        a[v] = i; // 填入当前位置v
        dfs(v + 1, i + 1, tar); // 下一个位置从i+1开始(不回头)
        a[v] = 0; // 回溯:清空位置(可选,更严谨)
    }
}

int main() {
    cin >> n;
    for (int k = 1; k <= n; k++) {
        dfs(1, 1, k);
    }
    return 0;
}
3. 关键细节
组件作用说明
变量 a[N]按 “位置” 存储组合结果(如 a [1]=1、a [2]=3 表示组合 “1 3”)。
递归参数v:按 “位置顺序” 推进;start:选数起始索引(核心去重手段);tar:组合长度。
选择逻辑每个位置从start开始选数(不回头),如第 1 个位置选 1 后,第 2 个位置只能从 2/3 选。
回溯操作仅需清空a[v](可选),无需额外标记(start已保证不重复选同一元素)。
终止条件组合的 tar 个位置填完(v=tar+1),输出 a 数组中的组合。
核心去重start参数:下一个位置的选数起始索引 = 当前选数索引 + 1,避免 “1 2” 和 “2 1” 重复。
4. 运行结果(n=3)

plaintext

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

三、三大 DFS 核心差异总表(重点对比)

对比维度DFS1(子集:选与不选)DFS2(排列:选 k 个)DFS3(组合:选 k 个)
核心目标枚举所有无顺序子集(选任意个)枚举所有有顺序排列(选 k 个)枚举所有无顺序组合(选 k 个)
推进方式按 “元素” 推进(u:第 u 个元素)按 “位置” 推进(v:第 v 个位置)按 “位置” 推进(v:第 v 个位置)
选择逻辑每个元素 2 种选择(选 / 不选)每个位置遍历所有未被选元素(多选择)每个位置从start开始选数(不回头,多选择)
关键变量 / 参数st[N](标记元素状态)a[N](存排列)+ vis[N](标记已选)a[N](存组合)+ start(起始索引)
去重 / 防重复手段元素仅处理一次,天然无重复子集vis[N]标记元素,避免重复使用start参数限制选数范围,避免重复组合
终止条件判断所有元素处理完(u > n)所有位置填完(v == tar+1)所有位置填完(v == tar+1)
输出结果特征无顺序、元素不重复、可含空集(已优化去空)有顺序、元素不重复、长度固定(tar 个)无顺序、元素不重复、长度固定(tar 个)
时间复杂度O (2ⁿ)(每个元素 2 种选择)O (n! / (n-k)!)(排列数)O (C (n,k))(组合数)
核心关键词子集、选或不选、任意个数排列、有顺序、固定个数、vis 标记组合、无顺序、固定个数、start 参数

四、适用场景速查(举一反三关键)

1. 选 DFS1(子集)的场景

  • 需求是 “枚举所有可能的选择”(选多选少都行);
  • 不考虑顺序,只关心 “元素是否被选中”。
    • 举例:1~n 的所有子集、数组中所有和为 target 的子集、子集去重(含重复元素的数组)。

2. 选 DFS2(排列)的场景

  • 需求是 “选 k 个元素,考虑顺序”;
  • 同一元素组合,不同顺序算不同结果。
    • 举例:1~n 的所有排列、字符串的所有字符排列(无重复字符)、从数组中选 k 个元素的所有排列。

3. 选 DFS3(组合)的场景

  • 需求是 “选 k 个元素,不考虑顺序”;
  • 同一元素组合,不同顺序算同一种结果。
    • 举例:从 n 个元素中选 k 个的所有组合、组合总和(选 k 个元素和为 target)、组合去重(含重复元素的数组)。

五、常见修改技巧(小白必会)

(一)DFS1(子集)修改

  1. 保留空集:删除flag判断,直接输出(哪怕无选中元素);
  2. 只保留选 k 个元素的子集:加cnt参数记录选中个数,终止时判断cnt==k
    void dfs(int u, int cnt) {
        if (u > n) {
            if (cnt == 2) { // 只输出选2个的子集
                for (int i=1; i<=n; i++) if (st[i]==1) cout << i << " ";
                cout << endl;
            }
            return;
        }
        st[u]=2; dfs(u+1, cnt); st[u]=0; // 不选,cnt不变
        st[u]=1; dfs(u+1, cnt+1); st[u]=0; // 选,cnt+1
    }
    // 调用:dfs(1, 0);
    

(二)DFS2(排列)修改

  1. 只输出选 k 个的排列:主函数去掉循环,直接输入 k 并调用dfs(1, k)
  2. 含重复元素的排列去重:先排序数组,再在循环中跳过重复元素:
    // 假设数组为nums[],已排序
    for (int i=1; i<=n; i++) {
        if (i>1 && nums[i]==nums[i-1] && !vis[i-1]) continue; // 去重
        if (!vis[i]) { ... }
    }
    

(三)DFS3(组合)修改

  1. 只输出选 k 个的组合:主函数去掉循环,直接输入 k 并调用dfs(1, 1, k)
  2. 组合元素需满足特定条件(如和为 target):加sum参数记录当前和,终止时判断:
    void dfs(int v, int start, int tar, int sum) {
        if (v == tar+1) {
            if (sum == 5) { // 只输出和为5的组合
                for (int i=1; i<=tar; i++) cout << a[i] << " ";
                cout << endl;
            }
            return;
        }
        for (int i=start; i<=n; i++) {
            a[v] = i;
            dfs(v+1, i+1, tar, sum+i); // 累加当前元素值
        }
    }
    // 调用:dfs(1, 1, 2, 0); // 选2个,和为5
    

六、易错点提醒

  1. 回溯操作不能漏:如 DFS1 的st[u]=0、DFS2 的vis[i]=false,漏了会导致状态混乱;
  2. 循环边界要精准:如子集 / 组合遍历从i=1开始(避免a[0]无用数据);
  3. 参数传递要正确:DFS3 的start需传递i+1(而非start+1),否则无法去重;
  4. 全局变量初始化:st[N]vis[N]作为全局变量默认值为 0/false,局部变量需用memset初始化;
  5. 输出格式优化:避免末尾空格,用 “第一个元素不加空格,后续元素前加空格” 的逻辑。

七.给小白看的!

如果是第一次看dfs,这三种类型可能会把你搞晕,所以,加了点干的。

DFS 类型对应生活任务(一句话记住)核心动作
DFS1(子集)收拾衣柜,每件衣服 “要么放进箱子,要么不放”(选任意件)逐个问 “要 / 不要”,不排序
DFS2(排列)选 3 个同学排值日表(1st、2nd、3rd,顺序不同算不同)先选谁站 1st,再选谁站 2nd,要排序
DFS3(组合)选 3 个同学组队参加比赛(没顺序,123 和 321 是同一队)从剩下的人里挑,不回头、不排序

举个 n=3(A、B、C)的例子,任务结果一眼分清:

1. DFS1(子集:要 / 不要)

  • DFS1(子集):空、A、B、C、AB、AC、BC、ABC(8 种,选多选少都行);

2. DFS2(排列:选 k 个 + 要顺序)

  • DFS2(排列,选 2 个):AB、BA、AC、CA、BC、CB(6 种,顺序算钱);

3. DFS3(组合:选 k 个 + 不要顺序)

  • DFS3(组合,选 2 个):AB、AC、BC(3 种,顺序不算数)。

最后用 “最小代码片段” 对比,只看差异点

不用看全代码,只对比核心部分,差异一眼看穿:

部分DFS1(子集)DFS2(排列)DFS3(组合)
函数参数dfs (u)(u = 当前元素)dfs (v, tar)(v = 当前位置)dfs (v, start, tar)(start = 选数起点)
核心循环无循环(只 2 个选择)for (i=1;i<=n;i++)(遍历所有未选)for (i=start;i<=n;i++)(从 start 开始)
去重 / 防重复天然无重复(元素只问一次)vis [i] 标记(避免重复选同一人)start 递增(避免回头选,去重组合)
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值