fucking-algorithm icon indicating copy to clipboard operation
fucking-algorithm copied to clipboard

回溯算法秒杀所有排列/组合/子集问题 :: labuladong的算法小抄

Open utterances-bot opened this issue 3 years ago • 42 comments

文章链接点这里:回溯算法秒杀所有排列/组合/子集问题

评论礼仪 见这里,违者直接拉黑。

utterances-bot avatar Mar 08 '22 02:03 utterances-bot

Days-Go-By avatar Mar 08 '22 02:03 Days-Go-By

顶礼膜拜,太牛逼了!

Wuui-97 avatar Mar 09 '22 06:03 Wuui-97

太绝了!

ShawnGeek avatar Mar 16 '22 06:03 ShawnGeek

感谢大佬的文章。大佬的文章最强的地方在于,通过归纳总结框架,高度抽象出来了一类问题。比如要不要重复选,其实就取决于下一次递归是i还是i+1。很好。

关于全排列去重,个人觉得有一个更好理解的方案。按照回溯框架的思想,每次递归的时候遍历可选列表。什么时候会造成答案重复呢?在同一个位置上,同样值的元素被选了多次。比如[2, 2],如果选了第一个2,下次还选2,就会造成重复。为了避免这个重复,可以在遍历的时候判断一下现在要选的元素是否和上一次选择过的相等。相等就跳过。伪代码如下:

for (int i = 0, last = -1; i < nums.length; ++i) {
    if (last > -1 && nums[i] == nums[last]) continue;
    if (visited[i])   continue;
    // make choice
    last = i;
    visited[i] = true;
    path.add(nums[i]);
    
    // recursive call
    recur(path, nums);
    
    // cancel choice
    visited[i] = false;
    path.removeLast();
}

BIT-zhaoyang avatar Mar 19 '22 07:03 BIT-zhaoyang

不错

wulangcode avatar Mar 19 '22 10:03 wulangcode

@BIT-zhaoyang 这个思路也不错!我的代码:

void backtrack(int[] nums, LinkedList<Integer> track) {
    if (track.size() == nums.length) {
        res.add(new LinkedList(track));
        return;
    }

    // 记录之前树枝上元素的值
    // 题目说 -10 <= nums[i] <= 10,所以初始化为特殊值
    int prevNum = -666;
    for (int i = 0; i < nums.length; i++) {
        // 排除不合法的选择
        if (used[i]) {
            continue;
        }
        if (nums[i] == prevNum) {
            continue;
        }

        track.add(nums[i]);
        used[i] = true;
        // 记录这条树枝上的值
        prevNum = nums[i];

        backtrack(nums, track);

        track.removeLast();
        used[i] = false;
    }
}

labuladong avatar Mar 20 '22 16:03 labuladong

第 40 题「 组合总和 II」的backtrack在 往track里添加元素的代码是不是写错了, 应该是 track.add(nums[i]); 而不是 track.add([i]); ?

TimurJiangShan avatar Mar 24 '22 13:03 TimurJiangShan

@TimurJiangShan 是写错了,已修复,感谢指正

labuladong avatar Mar 24 '22 13:03 labuladong

讲的很好,点个赞!另外有两个格式的小问题,

    1. 排队(元素可查不可复选)解法代码中,backtrack 函数调用参数数量不一致
List<List<Integer>> res = new LinkedList<>();
LinkedList<Integer> track = new LinkedList<>();
boolean[] used;

public List<List<Integer>> permuteUnique(int[] nums) {
    ...
    // 调用是传了两个参数
    backtrack(nums, track);
    ...
}
// 只有一个参数
void backtrack(int[] nums) {
      .....
}
    1. 读者解法中,new 前后多了加粗标记 *
void backtrack(int[] nums, LinkedList<Integer> track) {
    if (track.size() == nums.length) {
        // 这里多了*
        res.add(**new** LinkedList(track));
        return;
    }
    ....
}

zhyozhi avatar Mar 26 '22 13:03 zhyozhi

@zhyozhi 感谢指正,已修复~

labuladong avatar Mar 27 '22 03:03 labuladong

第39题的回溯部分应该是i而不是i +1吧?不然没法重复选择啊。一点拙见,如果不对的话希望能指正一下,谢谢大神!

class Solution:

    def __init__(self):
        self.res = []

    def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
        candidates.sort()
        track = []
        self.backtrack(candidates, target, 0, track, 0)
        return self.res

    def backtrack(self, candidates, target, start, track, trackSum):
        if trackSum == target:
            self.res.append(copy.deepcopy(track))
            return
        elif trackSum > target:
            return
        for i in range(start, len(candidates)):
            if i > start and candidates[i] == candidates[i - 1]:
                continue
            track.append(candidates[i])
            trackSum += candidates[i]
            self.backtrack(candidates, target, i, track, trackSum)
            track.pop()
            trackSum -= candidates[i]

yuhaoban avatar Mar 30 '22 23:03 yuhaoban

@yuhaoban 你看错了吧,我写的就是 i 不是 i + 1 啊?

labuladong avatar Mar 31 '22 09:03 labuladong

216题 组合总和3 有兴趣的朋友可以继续改进,此处实在想不到如何删除个数不符合 k要求的组合,所以直接在外部对result动手脚,最后虽然是 1ms 还是只能击败 14%的人 哈哈,

class Solution {
    List<List<Integer>> result = new LinkedList<>();
    LinkedList<Integer> track = new LinkedList<>();
    public List<List<Integer>> combinationSum3(int k, int n) {
        backTrack(n,k,0,1);
        Iterator<List<Integer>> iterator = result.iterator();
        while(iterator.hasNext()){
            if(iterator.next().size() != k){
                iterator.remove();
            }
        }
        return result;
    }
    private void backTrack(int target,int k,int sum,int start){
        if(sum == target){
            result.add(new LinkedList<>(track));
            return;
        }
        if(sum > target){
            return;
        }
        for(int i = start; i<= 9; i++){
            sum += i;
            k--;
            track.add(i);
            backTrack(target,k,sum,i+1);
            track.removeLast();
            k++;
            sum -= i;
        }
    }
}

zhongweiLeex avatar Apr 03 '22 00:04 zhongweiLeex

发现对于排列(元素可重不可复选)的剪枝部分,使用 if(i > 0 && nums[i] == nums[i - 1] && used[i-1])也可以通过测试,具体解释为 如果前面的相邻相等元素被用过,则跳过。 貌似更好理解些。:)

FZ20192019 avatar Apr 03 '22 09:04 FZ20192019

用过,则跳过。 貌似更好理解些

即使used[i -1] = true(添加了nums[i - 1]),也是有可能需要选择nums[i]的。

&& used[i-1])直接忽略了上述的可能性,结果可能出错

sxcnmslll avatar Apr 04 '22 09:04 sxcnmslll

@sxcnmslll @FZ20192019 其实你们的说法都不太准确,我把对 used[i-1] 的分析补充在文中了,可以看下

labuladong avatar Apr 04 '22 11:04 labuladong

2022.4.5 mark

billgoo avatar Apr 05 '22 10:04 billgoo

对于某些重复元素的题 我可不可以先去重再按无重复不可复选的思路来做😂

xiaoyu-x avatar Apr 06 '22 03:04 xiaoyu-x

讲的很细致啊,明天再复习一下

Xiaotong-Hu avatar Apr 08 '22 05:04 Xiaotong-Hu

好文赞一个。@zhongweiLeex,按照模板的写法这道216可以写成这样

class Solution {
vector<vector<int>> res;
vector<int> track;
public:
    vector<vector<int>> combinationSum3(int k, int n) {
        backtrack(n, k, 1, 0);
        return res;
    }
    void backtrack(int n, int k, int start, int curSum){
        if(track.size() == k && curSum == n){
            res.push_back(track);
            return;
        }
        if(curSum > n){
            return;
        }
        for(int i=start; i<=9; i++){
            track.push_back(i);
            curSum += i;
            backtrack(n, k, i+1, curSum);
            track.pop_back();
            curSum -= i;
        }
    }
};

Jing-lun avatar Apr 15 '22 01:04 Jing-lun

好文赞一个。@zhongweiLeex,按照模板的写法这道216可以写成这样

class Solution {
vector<vector<int>> res;
vector<int> track;
public:
    vector<vector<int>> combinationSum3(int k, int n) {
        backtrack(n, k, 1, 0);
        return res;
    }
    void backtrack(int n, int k, int start, int curSum){
        if(track.size() == k && curSum == n){
            res.push_back(track);
            return;
        }
        if(curSum > n){
            return;
        }
        for(int i=start; i<=9; i++){
            track.push_back(i);
            curSum += i;
            backtrack(n, k, i+1, curSum);
            track.pop_back();
            curSum -= i;
        }
    }
};

@Jing-lun 我忘了 可以直接添加一个 track.size()==k 的判断,就可以做到剪枝操作了。哈哈哈谢谢!🤞

zhongweiLeex avatar Apr 15 '22 05:04 zhongweiLeex

216:

class Solution {
     private List<List<Integer>> res = new LinkedList<>();
    private LinkedList<Integer> path = new LinkedList<>();
    private int trackSum = 0;

    public List<List<Integer>> combinationSum3(int k, int n) {
        backtrack(1, n, k);
        return res;
    }

    public void backtrack(int start, int n, int k) {
        if (k == path.size() && trackSum == n) {
            // 遍历到了第 k 层,trackSun == n,收集
            res.add(new LinkedList<>(path));
            return;
        }
        if (trackSum > n) {
            return;
        }
        for (int i = start ; i <= 9; i++) {
            trackSum += i;
            path.addLast(i);
            backtrack(i + 1, n, k);
            path.removeLast();
            trackSum -= i;
        }
    }
}

ZepPellN avatar Apr 21 '22 06:04 ZepPellN

对于 排列(元素可重不可复选)的剪枝理解:

if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) {
            continue;
        }
  1. 如果use[i-1] == true : 说明 track 里已经有和 nums[i] 相同的元素了,那么 nums[i] 这个元素是可以被选择放进 track 里的。 例如:track = [1, 2] , 可以再放进一个 1 变为 [1, 2, 1]。

  2. 如果 use[i-1] == false : 说明 track 里还没有和 nums[i] 相同的元素,如果有多个与 nums[i] 相同的元素,我们只需把第一个放进track 里,其他的不用管(剪枝)。 所以只有当 i == 0 ,的时候才需要做 add 操作,i > 0 的时候 continue 。(只选第一个) 例如:track = [2] , 外面还有 1 和 1' ,那么我们只需要放第一个 1 进 track = [2, 1] 。[2, 1'] 的情况是要被剪枝的。

davidditao avatar Apr 30 '22 04:04 davidditao

感谢!

Rookie-xixi avatar Apr 30 '22 16:04 Rookie-xixi

去重第一时间想到的就是set,哈哈,学到了

861130245 avatar May 02 '22 13:05 861130245

关于全排列去重,个人有一个不用排序的解决方案,这里直接使用力扣的签名。

力扣题目地址:https://leetcode.cn/problems/permutations-ii/

代码:

class Solution {
    List<List<Integer>> result=new LinkedList<>();
    public List<List<Integer>> permuteUnique(int[] nums) {
        process(nums,new LinkedList<>(),new boolean[nums.length]);
        return result;
    }

    public void process(int[] nums, LinkedList<Integer> track,boolean[] used){
        if(track.size()==nums.length){
            result.add(new LinkedList<>(track));
            return;
        }

        HashSet<Integer> set=new HashSet<>();

        for(int i=0;i<nums.length;i++){
            if(used[i]) continue;
            if(set.contains(nums[i])) continue;
            track.add(nums[i]);
            used[i]=true;
            set.add(nums[i]);
            process(nums,track,used);
            used[i]=false;
            track.removeLast();
        }
    }
}

wxler avatar May 24 '22 10:05 wxler

@wxler 嗯👍,这是一种很直接的去重方法,不过在递归函数中 new 集合会导致每一层递归中都消耗 O(N) 的空间复杂度,恐怕没有排序的方案效率高

labuladong avatar May 24 '22 18:05 labuladong

排列/组合/子集问题的三种形式,打卡,感谢楼主!

bert82503 avatar Jun 01 '22 14:06 bert82503

东哥真是我的老师,感谢

LebronHarden1 avatar Jun 05 '22 08:06 LebronHarden1

377题组合总和 IV 好像是无重可复选的排列问题

Ref-Rain07 avatar Jun 10 '22 13:06 Ref-Rain07