回溯算法中去重——used数组的用法

回溯算法used数组

在回溯算法中,有些时候不能选取重复的元素值,如组合问题、子序列问题等。为了标记元素值是否出现过,需要对元素值进行标记,即需要一个used数组(此处所指的used数组包括数组、哈希set、哈希map等)。

  1. 按照used数组变量的位置可以分为全局变量used数组局部变量used数组
  2. 按照处理输入数组中重复元素的方式可以分为可重复used数组不重复used数组
    例如同样的输入数组nums[1,1,2]:
    可重复used数组[0,1,1]表示nums中每一个元素(nums中共有3个元素1,1,2)目前是否被使用过;
    不重复used数组[0,1]则表示nums中每一个值(nums中共有两个值1,2)是否被使用过。
    另外,问题中的输入数组有的可排序(如组合问题,子集问题),有的不可排序(如子序列问题)。可排序问题在回溯前将输入数组进行排序可以简化回溯过程,而不可排序问题只能耗费更多空间使用局部used数组

那么何时使用何种used数组呢?先给出我的结论:

可排序问题可重复used不重复used
全局used
局部used×
不可排序问题可重复used不重复used
全局used××
局部used×

1.可排序问题

1.1使用全局used数组

以LeetCode 40.组合总和II为例(子集问题和组合问题几乎一样在此不做举例)
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的每个数字在每个组合中只能使用一次。
说明: 所有数字(包括目标数)都是正整数。 解集不能包含重复的组合。
示例 1: 输入: candidates = [10,1,2,7,6,1,5], target = 8, 所求解集为: [ [1, 7], [1, 2, 5], [2, 6], [1, 1, 6] ]
示例 2: 输入: candidates = [2,5,2,1,2], target = 5, 所求解集为: [ [1,2,2], [5] ]

全局used数组用来标记此元素的值是否使用过,在此题中,位于同一树枝上的两个“等值”的元素可以重复选取,但位于同一树层的两个“等值”的元素不可以重复使用(关于树层和树枝的介绍可以参考代代码随想录的文章40.组合总和II

组合问题的输入数组candidates可以被排序,在回溯中可以保证输入数组的有序性,这时使用全局可重复used数组。因此代码如下:

    vector<vector<int>> result;
    vector<int> path;
    vector<bool> used;//used[i]直接表示candidates[i]是否使用过
    void backtracking(vector<int>& candidates, int target, int sum, int startIndex) {
        if (sum == target) {
            result.push_back(path);
            return;
        }
        for (int i = startIndex; i < candidates.size()
         && sum + candidates[i] <= target; i++) {
            // used[i - 1] == true,说明同一树支candidates[i - 1]使用过
            // used[i - 1] == false,说明同一树层candidates[i - 1]使用过
            // 要对同一树层使用过的元素进行跳过
            if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false) {
                continue;
            }
            sum += candidates[i];
            path.push_back(candidates[i]);
            used[i] = true;
            backtracking(candidates, target, sum, i + 1); 
            used[i] = false;//全局used要进行回溯
            sum -= candidates[i];
            path.pop_back();
        }

    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        used =vector<bool> (candidates.size(), false);//这一步是创建可重复used
        path.clear();
        result.clear();
        // 首先把给candidates排序,让其相同的元素都挨在一起。
        //但子序列问题允许排列,因此不能使用全局used
        sort(candidates.begin(), candidates.end());
        backtracking(candidates, target, 0, 0);
        return result;
    }

同样,可以使用不重复全局used,代码如下:

    vector<vector<int>> result;
    vector<int> path;
    //由于题目中1 <= candidates[i] <= 50,所以用used[1]表示1是否使用过,
    //used[2]表示2是否使用过,依次类推,used[0]舍弃不用
    int used[51]={0};
    void backtracking(vector<int>& candidates, int target, int sum, int startIndex) {
        if (sum == target) {
            result.push_back(path);
            return;
        }
        for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) { 
        //这里的i>startIndex也可以写成i>0,实际上用了i>index的判断就不需要used数组了
        //i>startIndex表示的就是i不是本树层第一个元素
            if (i > startIndex && candidates[i] == candidates[i - 1] && used[candidates[i ]] == 0) {
                continue;
            }
            sum += candidates[i];
            path.push_back(candidates[i]);
            used[candidates[i ]] = 1;
            backtracking(candidates, target, sum, i + 1); 
            used[candidates[i ]] = 0;
            sum -= candidates[i];
            path.pop_back();
        }
    }

注意到,在可排序的问题中,用全局used确定元素是否重复使用过时,都使用了类似如下语句:

 //由于已经排序了,所以只需要看前一个元素是否和本元素相同,
 //如果相同并且前一个元素已经使用过 ,则本元素只能下一层的树枝下使用,而不能在本树层再次使用
 //(此时的全局used数组used[i-1]是false(或用0表示),因为回到本层时回溯了used,used变回false)
 if (i > startIndex && candidates[i] == candidates[i - 1] && used[candidates[i ]] == 0) {
 	continue;
 }

所以实际上在全局used的情况下,重复used和不重复used区别并不大。全局的used数组都需要对used数组进行回溯。

1.2 使用局部used数组

局部used不需要回溯,每次进入新的递归层会创建新的used,used只需要记录本层情况即可,但会占用更多内存。
使用局部不重复used的代码如下:

    vector<vector<int>> res;
    vector<int> path;
    int sum;
    void backtracking(const vector<int>& candidates, int target,int startIndex){
        if(sum==target){
            res.push_back(path);
            return;
        }
        //由于1 <= candidates[i] <= 50 ,所以设置51各变量(第0个索引不用)
        int used[51]={0};
        for(int i=startIndex;i<candidates.size()&&sum+candidates[i]<=target;++i){
            if(used[candidates[i]]==1) continue;
            path.push_back(candidates[i]);
            sum+=candidates[i];
            used[candidates[i]]=1;
            backtracking(candidates,target,i+1);
            path.pop_back();
            sum-=candidates[i];
        }
    }

局部used基本只能使用不重复used,没有必要使用重复used。

2.不可排序问题

子序列问题不可以对输入数组进行排列,因此不能保证回溯时数组的有序性,在全局used使用的判断语句就不能使用了。

//不可排序问题不能使用下面的语句了
 if (i > startIndex && candidates[i] == candidates[i - 1] && used[candidates[i ]] == 0) {
 	continue;
 }

不可排序问题使用可重复used时会很麻烦,因为寻找和此元素相同值的元素时每次都要从头搜索(因为输入数组无序)。推荐使用局部不重复used。
如491. 递增子序列:

给定一个整型数组, 你的任务是找到所有该数组的递增子序列,递增子序列的长度至少是2。
示例:
输入: [4, 6, 7, 7]
输出: [[4, 6], [4, 7], [4, 6, 7], [4, 6, 7, 7], [6, 7], [6, 7, 7], [7,7], [4,7,7]]
说明:
给定数组的长度不会超过15。
数组中的整数范围是 [-100,100]。
给定数组中可能包含重复数字,相等的数字应该被视为递增的一种情况。

局部不重复used。代码如下:

    vector<vector<int>> result;
    vector<int> path;
    void backtracking(const vector<int>& nums,int startIndex){
        if(path.size()>=2){
            result.push_back(path);
        }
        int used[201]={0};//题目给出的数字范围[-100,100],记录某一数字是否在同一树层使用过
        for(int i=startIndex;i<nums.size();++i){
            if((!path.empty()&&nums[i]<path.back())||used[nums[i]+100]==1) 
                continue;
            path.push_back(nums[i]);
            used[nums[i]+100]=1;
            backtracking(nums,i+1);
            path.pop_back();  
            //used[nums[i]+100]=0;局部used不需要回溯
         }
     }
            
    vector<vector<int>> findSubsequences(vector<int>& nums) {
        backtracking(nums,0);
        return result;
    }

3.总结

一般来说,局部不重复used可以稳定的去重,对可排序问题和不可排序问题都适用,只是需要更多内存。而可排序问题可以用全局used节省一些内存。可排序问题在采用全局uesd的情况下更推荐可重复used数组(一般来说输入数组的size小于输入数组数值范围)。事实上,只有在排序的时候才可以用可重复used,因为这样才能比较前一个元素是否和本元素等。

在去重逻辑上,局部used由于只记录本层元素并且不回溯,因此判断used[i]为1(true)时表示nums[i]使用过,要continue;而全局used(只用于可排序问题)记录所有树枝元素,所以需要回溯,因此当i > startIndex && candidates[i] == candidates[i - 1]时,used[i-1]=0表示nums[i-1]已经使用过并且回溯时从1变为0(全局可重复used和全局不重复used类似);

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值