算法练习-组合【回溯算法】(思路+流程图+代码)

难度参考

        难度:困难

        分类:回溯算法

        难度与分类由我所参与的培训课程提供,但需 要注意的是,难度与分类仅供参考。且所在课程未提供测试平台,故实现代码主要为自行测试的那种,以下内容均为个人笔记,旨在督促自己认真学习。

题目

        给定两个整数n 和k,返回1..n中所有可能的k个数的组合。

        示例1:输入:n=4,k= 2

        输出:[[2,4],[3,4],[2,3], [1,2],[1,3],[1,4], ]

        示例2:输入:n=1,k= 1        

        输出:[[1]]

        提示:<=n<=201<=k<=n

思路

        1. 初始化:首先,我们需要一个结果集合来存储所有符合条件的组合,以及一个临时数组(或者称之为路径)来存储当前探索的组合。

        2. 递归与回溯:我们从1开始遍历到n,尝试将每个数加入到当前的组合中,并递归地继续这个过程,直到当前组合的长度等于k,表示我们找到了一个有效的组合,将其添加到结果集中。每次递归调用后,我们需要回溯,即移除当前组合的最后一个元素,以尝试下一个可能的数字。

        3. 终止条件:当当前组合的长度等于k时,将其加入到结果集中,并返回上一层递归。

        具体来说就是,

        1. 定义辅助函数:定义一个辅助函数`combineHelper`,它接受当前数字`start`、当前路径`path`和结果集`result`作为参数。这个函数将负责递归地构建组合。

        2. 递归构建组合:从`start`开始,遍历到n,对于每个数`i`,将其加入到`path`中,然后递归地调用`combineHelper`,将`i+1`作为新的起始点,因为组合是不考虑顺序的,所以我们不需要重复包含之前的数字。

        3. 回溯:在每次递归调用返回后,我们需要从`path`中移除最后加入的数字,这样我们就可以尝试下一个数字。

        4. 收集结果:每当`path`的长度等于k时,就将其复制到结果集中,因为我们找到了一个有效的组合。

示例

        当然,让我们通过一个具体的例子来详细解释这个过程。假设我们有n = 4和k = 2,我们的目标是找到从1到4的所有可能的2个数的组合。

        初始状态

        - 结果集(用于存储所有组合):[]
        - 当前路径(当前探索的组合):[]
        - 开始数字:1

        第一层递归

        我们从数字1开始探索。

        - 当前路径:[1]

        接下来,我们进入下一层递归,尝试添加2、3、4到当前路径。

        第二层递归(以1开始)

        - 尝试添加2,当前路径变为[1, 2]。因为当前路径的长度等于k(2),我们找到了一个有效组合,将其添加到结果集中,然后回溯,移除2,当前路径回到[1]。
        - 接着尝试添加3,当前路径变为[1, 3]。同样,这是一个有效组合,添加到结果集,然后回溯,移除3,当前路径再次回到[1]。
        - 最后尝试添加4,当前路径变为[1, 4]。这也是一个有效组合,添加到结果集,然后回溯,移除4,当前路径回到[1]。

        此时,以1开头的所有可能组合都已探索完毕,我们移除1,回到初始状态,当前路径[],并尝试下一个数字。

        第一层递归(以2开始)

        - 当前路径:[2]

        重复之前的过程:

        - 尝试添加3,当前路径变为[2, 3]。这是一个有效组合,添加到结果集,然后回溯。
        - 尝试添加4,当前路径变为[2, 4]。这也是一个有效组合,添加到结果集,然后回溯。

        此时,以2开头的所有可能组合都已探索完毕,我们继续尝试以3和4开头的组合。

        结果

        按照这个过程,我们最终得到的结果集是:

        - [1, 2]
        - [1, 3]
        - [1, 4]
        - [2, 3]
        - [2, 4]
        - [3, 4]

        这就是所有从1到4中选2个数的组合。

        这个例子展示了回溯算法的工作原理:通过递归探索所有可能的路径,并在满足条件时收集结果,同时使用回溯来撤销最后的选择,从而尝试其他可能的选项。

梳理

        这种方法能够实现的原理基于回溯算法,这是一种通过递归来探索所有可能选择的算法框架,用于解决一类需要遍历所有可能情况来寻找所有解的问题。其核心思想和工作机制可以概括为以下几点:

        1. 选择与探索:从一系列的选项中逐一选择,然后向下探索,直到达到问题的底部(基本情况),这个过程类似于深度优先搜索(DFS)。

        2. 约束条件:在探索过程中,通过约束条件来剪枝,即提前排除那些明显不会得到解的路径。在组合问题中,约束条件可能是组合的长度达到了指定的值。

        3. 回溯:当达到基本情况或者当前路径不满足约束条件时,算法将回溯到上一个决策点,撤销最后的选择,并尝试其他可能的选项。这个过程保证了所有的可能性都被探索过。

        4. 记录解:每当找到一个满足条件的解时,就将其记录下来。在组合问题中,每当构建的组合长度达到指定的k时,就将其添加到结果集中。

        具体到组合问题(如从1到n的所有可能的k个数的组合),回溯算法的工作过程如下:

        - 从1开始,逐一尝试每个数作为组合的第一个数,然后递归地尝试所有可能的下一个数。
        - 在递归过程中,如果当前组合的长度达到了k,就将其记录为一个解。
        - 如果当前组合还没达到k,就继续添加下一个可能的数,直到无法再添加(因为已经达到n或者组合已满)。
        - 每当回溯到某个决策点时,尝试下一个数作为当前位置的数,直到所有数都尝试过。

        通过这种方式,回溯算法能够有效地遍历所有可能的组合,确保找到所有满足条件的解。这种方法之所以有效,是因为它通过递归和回溯机制,实现了对解空间的系统性搜索,同时通过约束条件和撤销选择(回溯)来减少不必要的搜索,从而提高效率。

代码


#include <iostream>  // 包含输入输出流库
#include <vector>     // 包含向量库
using namespace std;  // 使用标准命名空间

void combineHelper(int n, int k, int start, vector<int>& path, vector<vector<int>>& result) {
    // 当路径长度等于k时,将其加入结果集
    if (path.size() == k) {
        result.push_back(path);  // 将路径加入结果
        return;
    }
    
    // 从start开始遍历到n
    for (int i = start; i <= n; ++i) {
        // 将当前数字加入路径
        path.push_back(i);
        // 递归调用,注意下一次的起始数字是i+1,因为组合不考虑顺序
        combineHelper(n, k, i + 1, path, result);
        // 回溯,移除路径上的最后一个数字,尝试下一个可能的数字
        path.pop_back();
    }
}

vector<vector<int>> combine(int n, int k) {
    vector<vector<int>> result;  // 存储结果的二维向量
    vector<int> path;  // 存储当前路径的向量
    combineHelper(n, k, 1, path, result);  // 调用辅助函数组合路径
    return result;  // 返回全部结果
}

int main() {
    int n = 4, k = 2;  // 设定初始n和k
    vector<vector<int>> result = combine(n, k);  // 执行组合函数

    cout << "[";  // 输出左括号

    for (int i = 0; i < result.size(); ++i) {  // 遍历结果集
        cout << "[";  // 输出左括号
        for (int j = 0; j < result[i].size(); ++j) {  // 遍历每个组合
            cout << result[i][j];  // 输出当前数字
            if (j < result[i].size() - 1) cout << ",";  // 如果不是该组合的最后一个数字,输出逗号
        }
        cout << "]";  // 输出右括号
        if (i < result.size() - 1) cout << ",";  // 如果不是最后一个组合,输出逗号
    }

    cout << "]" << endl;  // 输出右括号并换行

    // 示例2
    n = 1; k = 1;  // 设定新的n和k
    result = combine(n, k);  // 执行组合函数

    cout << "[";  // 输出左括号

    for (int i = 0; i < result.size(); ++i) {  // 遍历结果集
        cout << "[";  // 输出左括号
        for (int j = 0; j < result[i].size(); ++j) {  // 遍历每个组合
            cout << result[i][j];  // 输出当前数字
            if (j < result[i].size() - 1) cout << ",";  // 如果不是该组合的最后一个数字,输出逗号
        }
        cout << "]";  // 输出右括号
        if (i < result.size() - 1) cout << ",";  // 如果不是最后一个组合,输出逗号
    }

    cout << "]" << endl;  // 输出右括号并换行

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

        时间复杂度:0(n*2^n)

        空间复杂度:0(n)

打卡

  • 17
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Yamai Yuzuru

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值