Backtracking algorithm title, do this, seckill!!

Posted by johnh2009 on Sun, 28 Jun 2020 07:42:12 +0200

This article will explain how to do the problem of leetcode backtracking algorithm. In this period of time, I have brushed the problem of leetcode backtracking algorithm all the time, and found some of the rules. So I want to write an article to summarize it for fear of forgetting later.

After finishing the topic of backtracking algorithm, I found that it can be summarized into three categories: subset problem, combination problem and arrangement problem. What do these three categories mean? Let me give an example to illustrate them.

Subset problem, for example, array [1,2,3], then the corresponding subset problem is that the subset of this array includes [], [1], [2], [3], [1,3], [2,3], [1,2], [1,2,3], which is the subset of this array. There are many problems in leetcode, and some elements in the list group can be repeated, and then the subset problem is solved.

The combination problem, for example, array [1,2,3], combines the possible choice with target 3, then there are: [1,2], [3], which is the combination problem in leetcode.

Permutation problem, permutation problem is relatively simple, for example, our common full permutation problem, leetcode also has this kind of problem.

In this article, we will talk about how to use backtracking algorithm to solve these problems.

1 step by step to explain the backtracking algorithm framework

At the beginning, I still want to show you how to solve the problem of backtracking algorithm step by step through a simple example. Finally, through this problem, we can roughly sort out a solution framework of backtracking algorithm. First, let's look at the following problem, which is a sub set problem with medium difficulty.

This topic, the framework given by the topic is like this.

    public List<List<Integer>> subsets(int[] nums) {
            
    }

So, as we know, let's build a return value of type list < list < integer > > first.

    List<List<Integer>> list = new ArrayList<>();

Next, we will start to write the backtracking method.

    public void backTrace(int start, int[] nums, List<Integer> temp){
        for(int j = 0; j < nums.length; j++){
            temp.add(nums[j]);
            backTrace(j+1,nums,temp);
            temp.remove(temp.size()-1);
        }
    }

At the beginning, it may be written as the above. The array nums, start and temp sets are passed in to save the results. Then, each time you traverse the array nums, you add the current element, and when you recursively return, you can backtrack and delete the element you just added. That's the idea of backtracking.

In this way, the basic framework is finished, and there is another question to be considered is base case. What is the base case of this topic? In fact, because it is a subset, each step needs to be added to the result set temp, so there are no restrictions.

    public void backTrace(int start, int[] nums, List<Integer> temp){
        //Save results every time
        list.add(new ArrayList<>(temp));
        for(int j = 0; j < nums.length; j++){
            temp.add(nums[j]);
            backTrace(j+1,nums,temp);
            temp.remove(temp.size()-1);
        }
    }

Finally, let's add the whole code and it will come out.

    List<List<Integer>> list = new ArrayList<>();
    public List<List<Integer>> subsets(int[] nums) {
        if(nums.length == 0){
            return null;
        }
        List<Integer> temp = new ArrayList<>();
        backTrace(0, nums, temp);
        return list;
    }

    public void backTrace(int start, int[] nums, List<Integer> temp){
        list.add(new ArrayList<>(temp));
        for(int j = 0; j < nums.length; j++){
            temp.add(nums[j]);
            backTrace(j+1,nums,temp);
            temp.remove(temp.size()-1);
        }
    }

ok, let's run it and see how it works.

He said that I exceeded the time limit, indicating that there is a problem with the algorithm. Let's take a look at the code we wrote above. We found that in fact, every time we traverse an array, we start from 0, resulting in a lot of repeated element traversal, that is, we don't use the start variable. Finally, we start the traversal not from 0, but from the current sta RT starts to traverse. Let's exclude the selected elements and see the results.

    List<List<Integer>> list = new ArrayList<>();
    public List<List<Integer>> subsets(int[] nums) {
        if(nums.length == 0){
            return null;
        }
        List<Integer> temp = new ArrayList<>();
        backTrace(0, nums, temp);
        return list;
    }

    public void backTrace(int start, int[] nums, List<Integer> temp){
        list.add(new ArrayList<>(temp));
        //Traverse from start to avoid repetition
        for(int j = start; j < nums.length; j++){
            temp.add(nums[j]);
            backTrace(j+1,nums,temp);
            temp.remove(temp.size()-1);
        }
    }

Find the perfect pass, good job!!

In addition, we should pay attention to the following points: list.add (New ArrayList < > (Temp)); do not write as list.add(temp);, otherwise, the output is an empty set. You should know why by thinking about it.

In fact, we can sort out a general framework of the backtracking algorithm through this topic, and then we can do other topics in the future, just follow the cat and draw the tiger, and operate it at one time.

Back to the backTrace function, it is actually a process of selection / deselection. The for loop is also a process of selection. Another point is that base case needs to be processed in this function. Then we can sort out the framework.

    public void backTrace(int start, int[] nums, List<Integer> temp){
        base case processing
        //Selection process
        For (circular selection){
            choice
            Backtrace (recursion);
            Unselect
        }
    }

ok, I've already talked about the problem of a subset. Next, I'd like to talk about a more interesting subset.

2 subset problem

In fact, the topic used to introduce the backtracking algorithm framework is relatively simple, but the idea is unchanged. This framework is very important. Other topics are basically modified in the above framework, such as pruning operations.

90. Subset II medium difficulty

The difference between this topic and the previous subset topic is that the supplement contains repeated subsets, that is, it cannot be changed in order, and the same subset of elements appears.

This topic framework is still the same, but we need to do a simple pruning operation: how to eliminate the repeated subsets.

There are two ways to solve this problem. In addition, these two ways can be used to solve other problems when there are restrictions such as duplicate subsets.

  • Method 1: using Set de duplication feature to solve problems

Let's move the above frame down first and then modify it.

    List<List<Integer>> list = new ArrayList<>();
    public List<List<Integer>> subsets(int[] nums) {
        if(nums.length == 0){
            return null;
        }
        List<Integer> temp = new ArrayList<>();
        backTrace(0, nums, temp);
        return list;
    }

    public void backTrace(int start, int[] nums, List<Integer> temp){
        list.add(new ArrayList<>(temp));
        //Traverse from start to avoid repetition
        for(int j = start; j < nums.length; j++){
            temp.add(nums[j]);
            backTrace(j+1,nums,temp);
            temp.remove(temp.size()-1);
        }
    }

Because we need to use the Set feature to de duplicate, we need to add this variable Set < list < integer > > Set = new HashSet < > (); in addition, in order to ensure the order, we need to sort again Arrays.sort(nums), which can avoid repeated subsets with the same elements but different order.

So, the result came out.

    List<List<Integer>> list = new ArrayList<>();
    Set<List<Integer>> set = new HashSet<>();
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        if(nums.length == 0){
            return null;
        }
        //sort
        Arrays.sort(nums);
        List<Integer> temp = new ArrayList<>();
        backTrace(0, nums, temp);
        return list;
    }

    public void backTrace(int start, int[] nums, List<Integer> temp){
        //set de duplication
        if(!set.contains(temp)){
            set.add(new ArrayList<>(temp));
            list.add(new ArrayList<>(temp));
        }
        
        for(int j = start; j < nums.length; j++){
            temp.add(nums[j]);
            backTrace(j+1,nums,temp);
            temp.remove(temp.size()-1);
        }
    }

It turns out that the efficiency is not very good.

Let's take a look at another pruning strategy to remove duplication.

  • Method 2: I > Start & & nums [I-1] = = nums [i]

Why is this pruning strategy possible? Don't worry, I'll draw a picture to explain it.

So we can do it this way.

    List<List<Integer>> list = new ArrayList<>();
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        if(nums.length == 0){
            return null;
        }
        Arrays.sort(nums);
        List<Integer> temp = new ArrayList<>();
        backTrace(0, nums, temp);
        return list;
    }

    public void backTrace(int start, int[] nums, List<Integer> temp){
        list.add(new ArrayList<>(temp));
        
        for(int i = start; i < nums.length; i++){
            //pruning strategy 
            if(i > start && nums[i] == nums[i-1]){
                continue;
            }
            temp.add(nums[i]);
            backTrace(i+1,nums,temp);
            temp.remove(temp.size()-1);
        }
    }

Ouch, it seems to be OK.

3 combination problem

After the previous subset problems are solved, you will find that the later combination problems and arrangement problems are not big problems, but basically routine problems.

39. Medium difficulty in combination

There is no big difference between this topic and the previous one, but we need to pay attention to one point: each number can be selected by unlimited repetition. What we need to do is to start from i instead of i+1.

    backTrace(i,candidates,target-candidates[i], temp);

Let's take a look at the full code.

    List<List<Integer>> list = new ArrayList<>();
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        if(candidates.length == 0 || target < 0){
            return list;
        }
        List<Integer> temp = new ArrayList<>();
        backTrace(0,candidates,target,temp);
        return list;
    }

    public void backTrace(int start, int[] candidates, int target, List<Integer> temp){
        //Termination condition of recursion
        if (target < 0) {
            return;
        }

        if(target == 0){
            list.add(new ArrayList<>(temp));
        } 

        for(int i = start; i < candidates.length; i++){
            temp.add(candidates[i]);
            backTrace(i,candidates,target-candidates[i], temp);
            temp.remove(temp.size()-1);
        }
    }

It's so simple!!!

So, another combination problem.

40. Combination II medium difficulty

As soon as you look at the problem, you can see that it's almost the same. It's true that each number here can only be used once, but also can't contain repeated combinations. So, use the above de duplication method to solve it. Don't say much, go to the code.

    List<List<Integer>> lists = new LinkedList<>();
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        if(candidates.length == 0 || target < 0){
            return lists;
        }
        Arrays.sort(candidates);
        List<Integer> list = new LinkedList<>();
        backTrace(candidates,target,list, 0);

        return lists;
    }

    public void backTrace(int[] candidates, int target, List<Integer> list, int start){
        if(target == 0){
            lists.add(new ArrayList(list));
        }
        
        for(int i = start; i < candidates.length; i++){
            if(target < 0){
                break;
            }
            //Pruning: ensure that there is only one same element in the same layer, and there can be duplicate elements in different layers
            if(i > start && candidates[i] == candidates[i-1]){
                continue;
            }
            list.add(candidates[i]);
            backTrace(candidates,target-candidates[i],list,i+1);
            list.remove(list.size()-1);
        }
    }

Perfect solution!!

4. Full Permutation

Let's start with the most basic problem of Full Permutation and solve it quickly.

46. Medium difficulty in full arrangement

This is a complete arrangement, but the order of elements is different, so the pruning we need to do is: exclude some in the temp set.

Upper code.

    List<List<Integer>> lists = new ArrayList<>();
    public List<List<Integer>> permute(int[] nums) {
        if(nums.length == 0){
            return lists;
        }
        List<Integer> list = new ArrayList<>();

        backTrace(nums,list,0);

        return lists;
    }

    public void backTrace(int[] nums, List<Integer> temp, int start){
        if(temp.size() == nums.length){
            lists.add(new ArrayList(temp));
            return;
        }

        for(int i = 0; i < nums.length; i++){
            //Exclude existing elements
            if(temp.contains(nums[i])){
                continue;
            }
            temp.add(nums[i]);
            backTrace(nums,temp,i+1);
            temp.remove(temp.size() - 1);
        }
    }

If it's not exciting, arrange it!!

47. The difficulty of full arrangement II is medium

Although this topic is also a full permutation, it is more difficult than the previous one. There are two restrictions: there are repeating elements, but they cannot contain repeating permutations.

We know how to solve the problem of non repeated permutation. We can use the previous de duplication method. However, how can we ensure that the same set of elements does not have repeated permutation?

Here we need to add a visited array to record whether the current element has been accessed, so that we can solve the problem.

  public List<List<Integer>> result = new ArrayList<>();
    public List<List<Integer>> permuteUnique(int[] nums) {
        if(nums.length == 0){
            return result;
        }
        Arrays.sort(nums);
        findUnique(nums,new boolean[nums.length],new LinkedList<Integer>());
        return result;
    }
    public void findUnique(int[] nums, boolean[] visited,List<Integer> temp){
        //End condition
        if(temp.size() == nums.length){
            result.add(new ArrayList<>(temp));
            return ;
        }
        //Selection list
        for(int i = 0; i<nums.length; i++){
            //You don't need to put in what you've chosen
            if(visited[i]) continue;
            //duplicate removal
            if(i>0 && nums[i] == nums[i-1] && visited[i-1]) break;
            
            temp.add(nums[i]);
            visited[i] = true;

            findUnique(nums,visited,temp);

            temp.remove(temp.size()-1);
            visited[i] = false;
        }
    }

This will solve the problem.

5 is not a summary

So far, we have solved the problems of subset, combination and full arrangement. From the step-by-step explanation of the framework to the analysis of specific problems, everything is covered. Ha ha, of course, there are still some places that haven't been considered. I hope you can give me some advice.

This article has been written for two days. It's almost here. It's not easy to be original. Please like it!

Topics: Java