搜索中的排列组合问题(C+++)

本文探讨了如何解决排列、组合问题,包括无重复元素和有重复元素的情况,提供了递归算法的思路和代码示例。涉及全排列的递归遍历、重复元素的去重策略,以及组合问题的二进制枚举方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


以下内容整理自leetcode题解

排列

无重复元素

问题描述

给定一个没有重复数字的序列,返回其所有可能的全排列。
比如:
对于序列[2,3,1]。
需要返回[ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] ]。
P 3 3 P_{3}^{3} P33的所有可能情况。

思路

对于一个没有重复数字序列的全排列,我们应该是首先选择第一个数字,然后从剩余的数字中选择出第二个数字,直到所有的数字都被选择出来。这样我们就选择除了一个可行的排列,遍历每种选择可能即可得到所有的排列。
那么转化为递归的思路就是:
我们先选择出第一个数字,然后找到将剩余数字序列的全排列找出。即遍历第一个数字的所有可能,分别找到对应剩余数字序列的全排列,然后进行拼接。

代码

res是返回的所有的全排列,path是目前的排列,vis是存储对应数字是否已使用,nums是数字序列。

    void backtrack(vector< vector<int> >& res,vector<int>& path,vector<bool>& vis,vector<int> &nums,int start) {
        if (start == vis.size()) {
            res.push_back(path);
            return;
        }
        for (int i = 0; i < (int)nums.size(); ++i) {
            if (vis[i]) {
                continue;
            }
            path.emplace_back(nums[i]);
            vis[i] = true;
            backtrack(res, path, vis, nums,start+1);
            vis[i] = false;
            path.pop_back();
        }
    }

有重复元素

问题描述

给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
比如:
对于序列[2,2,1]。
需要返回[ [1,2,2], [2,1,2], [2,2,1] ]

思路

这里对于重复数字序列的全排列应该是高中的知识了。如果要计算排列的数量应该是 P 3 3 / P 2 2 P_{3}^{3}/P_{2}^{2} P33/P22。很明显 P 3 3 P_{3}^{3} P33是不包含重复数字的全排列。
这里我们假设2分为2a和2b,那么所有的全排列就是 P 3 3 P_{3}^{3} P33。但是[1,2a,2b]和[1,2b,2a]应该被认为是重复的,只能有一个,也就是和’2’的种类没有关系。而对于2个’2’,其分配的种类可能其实也是一个全排列 P 2 2 P_{2}^{2} P22
这样我们可以要求在全排列中2a一定在2b前面,从而形成一个全排列去重的方式,来从中剔除掉重复的全排列。
所以如果我们在选择的时候对2进行标号,只有当前面的2都已经被使用过,我们才可以使用当前2,否则不使用。这样我们就可是通过简单的判断进行去重了。

代码

这里改动的地方就是在If语句添加了一个新的判断条件。
i>0是为了防止后面的数组越界,因为重复数字数量一定大于2.
nums[i]==nums[i-1]表示这个数字与前一个相同,即不是第一次出现。
!vis[i-1]表示如果前一个重复数字没有使用,那么这个数字也不能使用,直接跳过。

    void backtrack(vector< vector<int> >& res,vector<int>& path,vector<bool>& vis,vector<int> &nums,int start) {
        if (start == vis.size()) {
            res.push_back(path);
            return;
        }
        for (int i = 0; i < (int)nums.size(); ++i) {
            if (vis[i]||(i>0&&nums[i]==nums[i-1]&&!vis[i-1])) {
                continue;
            }
            path.emplace_back(nums[i]);
            vis[i] = true;
            backtrack(res, path, vis, nums,start+1);
            vis[i] = false;
            path.pop_back();
        }
    }

组合

无重复元素

问题描述

给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
输入:
n = 4, k = 2
输出:
[ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4] ]

思路

组合问题可以看成二进制枚举的过程,假定我们用二进制序列来表示在Nums上的组合情况,比如对于n=4,k=2,二进制序列1001表示[1,4],即1表示在组合中,0表示不在组合中。那么我们通过如下的两个规则可以遍历所有的组合情况。初始化二进制序列为0011(4321)。即前k个数字为1,其余为0。

  1. 如果二进制序列以0结束,那么我们可推出从尾部开始一定有t个连续的0,然后是m个连续的1。那么我们只需将倒数第t+m+1的0和t+m的1进行调换,然后将剩余的1移到结尾处。例如011 0011 1000->011 0100 0011。
  2. 如果二进制序列以1结束,那么我们尾部一定有t个连续的1,那我们只需将t和t+1的0进行调换即可。
    按照这种方式,我们就可以遍历所有的组合可能。

当然,这种不使用递归的方式可能编码会复杂一点。在看一下递归的实现。
这个问题假设我们将组合从小到小进行排列,那么假设第一个数字为m。则这个问题就被分成了m+在[m+1,n]中选择k-1个数字的组合情况,问题的规模就被降低了。
因此我们可以在每个递归步骤内遍历所有的可选的’m’,并递归找到[m+1,n]中选择k-1个数字的组合。

代码

其中res是返回的所有组合情况,path是目前搜索到的位置,也是组合内的元素,start,n,k表示从[start,n]中选择出k个数字。

    void comb(vector<vector<int>>& res,vector<int>& path,int start,int n,int k){
        if(path.size()==k){
            res.emplace_back(path);
            return;
        }
        for(int i=start;i<=n;i++){
            path.emplace_back(i);
            comb(res,path,i+1,n,k);
            path.pop_back();
        }
    }

有重复元素

问题描述

给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
输入:
candidates = [10,1,2,7,6,1,5], target = 8
输出:
[ [1, 7], [1, 2, 5], [2, 6], [1, 1, 6] ]

思路

这里就只介绍递归的实现了,因为编码比较简单。
和全排列的去重方式类似,我们也是对数组进行排序,然后对于重复数字,如果前面有没有使用的重复数字,那我们就不会选择它。比如说对于[1,2a,2b,2c],target=5。我们第一次会找到[1,2a,2b],但是我们不会返回[1,2b,2c],因为前面的2a没有被选择,所以后面的也不会在组合内。即重复数字按顺序优先选择。按照这种遍历方式选择和为target的组合即可。

tip:当组合和大于target时则需要剪枝,因为所有数字都是正数,此时已不可能通过添加数字使得结果等于target。

代码

start,target表示在[start,~]中寻找组合中数字和为target的所有组合。

    void comb(vector<vector<int>>& res,vector<int>& path,vector<bool>& vis,vector<int>& nums,int start,int target){
        if(!target){
            res.emplace_back(path);
            return;
        }
        for(int i=start;i<(int)nums.size();i++){
            if(i>0&&nums[i-1]==nums[i]&&!vis[i-1]){
                continue;
            }
            if(target>=nums[i]){
                path.emplace_back(nums[i]);
                vis[i]=true;
                comb(res,path,vis,nums,i+1,target-nums[i]);
                path.pop_back();
                vis[i]=false;
            }
        }
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值