Backtracking (depth first reset state + + prune)

1. What is backtracking algorithm

"Backtracking" algorithm, also known as "backtracking search" algorithm, mainly used in a huge space search problems we need a solution. "Backtracking" refers to the "reset state" can be understood as "Back to the past", "recovery site," is in the process of coding, a technique used in order to save space. The back is actually a "depth-first traversal" peculiar a phenomenon. The reason is the "depth-first traversal" because the problem we have to solve is usually done in a tree, in answer to this tree search needs, generally using a depth-first traversal.

2. Example 1 (back + reset state)

"Full array" is a very classic "retrospective" application of the algorithm. We know full array of N numbers a total of N! So many.

An array [1, 2, 3] of the whole arrangement of an example.

  • We first wrote to the beginning of the full array 1, which are: [1, 2, 3], [1, 3, 2];
  • In the beginning of the full array 2, which is: [2, 1, 3], [2, 3, 1];
  • Finally, in order to write the beginning of the full array 3, which is: [3, 1, 2], [3, 2, 1]

We just need to enumerate according to the order of every situation that may arise, we have selected the numbers do not appear in the figures to be determined in the next. In this strategy selected will be able to do do not leak, the whole arrangement might have enumerated.

Obtained using the method of programming full array is programmed in such a tree structure as shown below, in particular, is to perform a depth-first traversal, formed from the root of the tree to the leaf node of a path is the full array .

Below we explain how to encode:
(1) In addition to the first tree root and leaf nodes, each node is actually doing the same, that has been elected under the premise of some of the numbers, we need to left the number of not yet selected in the order of sequentially selecting a number, which is obviously a recursive structure
(2) recursive termination condition that the number has been selected from enough, and therefore we need a recursive variable to indicate the current to the first layers, we this variable is called depth
(3) of these nodes actually represent a search (to find) the different stages full array of issues, in order to distinguish between these different stages, we need some variables to record the program where to step, and here we need two variables:
[a] which has been selected from the number of the leaf nodes when these numbers have been selected to form a full array (path)
[b] a Boolean array isused, when they are initialized to false indicates that these numbers has not been selected, when we selected a number of times, this will be the corresponding position in the array is set to true, so the next consideration When the position, it is possible to O (1) time complexity of judging whether the number been selected, it is a "space for time" thinking

These two variables called " state variables ", they represent a stage where we are solving a problem of time.

(4) at a non-leaf node, resulting in different branches, the semantics of this operation is: select an element number has not been selected as the location of the next element, which obviously have to be achieved by one cycle.
(5) Since the implementation of depth-first traversal, returns from deeper to shallower junction node when the need to do ** "reset state", or "return to the past", "restore the site," the recently was added to eject path variable number, and the position is set to False ** isused array corresponding to
a program implemented

def permute(self, nums: List[int]) -> List[List[int]]:
        pre = []     # pre相当于上面说的path
        res = []
        isused = [False for _ in nums]
        self.digui(nums, isused, pre, res)
        return res
        
    def digui(self, nums, isused, pre, res):
        
        #判断递归结束
        if len(pre) == len(nums):
            res.append(pre.copy())   # 此处需要注意,不可使用res.append(pre), 因为后面对pre的改变此处也会随之改变,因此需要深度复制
            return
        
        for i in range(len(nums)):
            if isused[i]:
                continue
            else:
                pre.append(nums[i])
                isused[i] = True
            
            self.digui(nums, isused, pre, res)
            isused[i] = False
            pre.pop()

2.2 Complexity Analysis

(Back in time complexity of the algorithm is generally higher, some issues are complex analysis, I personally feel no need to master, and pruning shears well, then, the complexity will drop very low, and therefore the worst time complexity analysis the significance is not great, as the case may be).
the time complexity: O (NxN)!

3. Example 2 (without backtracking reset state)

Given two integers n and k, 1 ... Returns the number of all possible combinations of n, k

输入: n = 4, k = 2
输出:
[
  [2,4],
  [3,4],
  [2,3],
  [1,2],
  [1,3],
  [1,4],
]

In this example, no need to provide isusedan array to which the digital recording is used, because different combinations and permutations, for the combination, the [1, 2] and [2, 1] is the same combination, but for the purposes of alignment, it is different arrangements

Complete program

class Solution:
    def combine(self, n: int, k: int) -> List[List[int]]:

        if n == [] or k>n:
            return []
        path = []
        result = []
        start = 1
        self.digui(path, result, start, k, n)
        return result

    def digui(self, path, result, start, k, n):
        if len(path) == k:
            result.append(path[:])
            return

        for i in range(start, n+1):
            path.append(i)
            start += 1
            self.digui(path, result, start, k, n)
            path.pop()

4. Example 3 (back + + pruning reset state)

Given a sequence of numbers may include repeat, return all distinct full array.

输入: [1,1,2]
输出:
[
  [1,1,2],
  [1,2,1],
  [2,1,1]
]

Here the subject asked to find all distinct full array, i.e., required to weight, so backtracking must be pruned to achieve a de-emphasis effect, the following procedures:

class Solution:
    def permuteUnique(self, nums: List[int]) -> List[List[int]]:
        res = []
        pre = []
        isused = [False]*len(nums)
        # 先进行排序
        nums.sort()
        self.digui(nums, isused, pre, res)
        return res
        
    def digui(self, nums, isused, pre, res):
        
        if len(pre) == len(nums):
            res.append(pre.copy())
            return
        
        for i in range(len(nums)):
            # 增加条件剪枝去重
            if i>0 and nums[i]==nums[i-1] and isused[i-1]==False or isused[i]:
                continue
            else:
                isused[i]=True
                pre.append(nums[i])
            
            self.digui(nums, isused, pre, res)
            isused[i] = False
            pre.pop()

The general idea is the first element of the list is sorted, then the method according to Example 1 backtracking to find all permutations, add conditions to nums[i]==nums[i-1] and isused[i-1]==Falsecarry out pruning, and why this condition can pruning, tree painted himself lookup process simulation can be found

5. Summary

First drawing, drawing is very important, drawing can only help us think clearly recursive structure, to know how to prune. In the process of drawing thinking clearly:
(1) How to generate branch;
solution (2) Where the subject in need? In the leaf nodes, or in the non-leaf nodes, or from the node with the leaf nodes of the path?
(3) do not need to search for solutions which will produce? For example: generate repeat whatever reason, if shallow knew this branch can not produce the desired results, it should prune in advance, what the conditions are pruning, how to write code?

6. References

leetcode full array solution to a problem

Published 33 original articles · won praise 1 · views 2605

Guess you like

Origin blog.csdn.net/orangerfun/article/details/104281867