How to try to walk the maze? When encountering obstacles, continue to explore "backtracking" from the beginning. This is the visual interpretation of the backtracking algorithm.
More abstractly, the backtracking algorithm can be understood as a deep traversal of a tree, each leaf node is the final state of a scheme, and the judgment of a certain route may end before the leaf node is visited.
Compared with dynamic programming, backtracking can solve more complicated problems, especially for problems with aftereffects.
The reason why there can not handle dynamic programming aftereffect issues, because of its dp(i)=F(dp(j))
which 0<=j<i
caused because i
by i-1
derivation, if i-1
of a choice will i
select an impact, then the derivation is invalid.
In retrospect, since each branch judgment is independent of each other and does not affect each other, even if the previous choice has aftereffect, this aftereffect can continue to be affected in this selection route without affecting other branches.
Therefore, backtracking is a more widely applicable algorithm, but its cost (time complexity) is relatively higher, so only when there is no better algorithm, the backtracking algorithm should be considered.
intensive reading
After the above thinking, the realization of the backtracking algorithm is clear: recursion or iteration. Since the two can be converted to each other, and the cost of recursive understanding is low, I prefer to solve the problem recursively.
It must be mentioned here that the difference between work and algorithm competition thinking: due to the large depth of the recursive call stack, the overall performance is not as good as iteration, and the iterative writing method is not as natural as recursion, so when doing algorithm problems, in order to improve the performance a little, and If you inadvertently reveal your strength, you may be more inclined to solve problems in an iterative way.
However, in work, most of the scenarios are performance-insensitive. On the contrary, maintainability is more important. Therefore, the engineering code recommends using a more understandable recursive method to solve the problem, and handing the stack call to the computer to do it.
In fact, the pursuit of algorithm code is shorter, and it is the same thing that can be written in a line without changing lines. I hope that everyone can freely switch habits in different environments, instead of sticking to one style.
There is more than one way to use recursion to solve backtracking. Let me introduce my commonly used TS language methods:
function func(params: any[], results: any[] = []) {
// 消耗 params 生成 currentResult
const { currentResult, restParams } = doSomething(params);
// 如果 params 还有剩余,则递归消耗,直到 params 耗尽为止
if (restParams.length > 0) func(restParams, results.concat(currentResult));
}
Here params
is similar to the route behind the maze, and results
records the best route that has been taken. When the params
is exhausted, you will exit the maze, otherwise it will terminate and let other recursions continue.
So backtracking logic is actually quite easy to write. The difficulty is how to judge this question should be done with backtracking, and how to optimize the algorithm complexity.
Let's start with two introductory questions, namely the letter combination of the phone number and the recovery IP address.
Letter combination of phone number
The letter combination of a telephone number is a middle-level question, the topic is as follows:
Given a string containing only the number
2-9
, return all letter combinations it can represent. The answer can be returned any orderThe mapping of numbers to letters is given as follows (same as phone buttons). Note that 1 does not correspond to any letters.
The telephone number corresponding letter is actually a map, such as 2
map a,b,c
, 3
map d,e,f
, then 2,3
letter combinations can be represented there 3x3=9
species, and to print out such ad
, ae
this combination certainly use brute-force method , Exhaustive method is also a kind of backtracking, but only for every possibility, and the more complicated backtracking may not meet the requirements for every path.
So this problem is easy to do, as long as all possible combinations are constructed.
Next, let's look at a similar problem, but with a certain branch legal judgment, to recover the IP address.
Recovery IP address
Recovering an IP address is a middle-level question, the topic is as follows:
Given a numeric string containing only to indicate an IP address obtained from the return of all possible S valid IP address . You can return the answers in any order.
valid IP address exactly four integers (each integer is between 0 and 255, and cannot contain leading 0), and the integers are separated by'.'.
For example: "0.1.2.201" and "192.168.1.1" are valid IP addresses, but "0.011.255.245", "192.168.1.312" and "192.168@1.1" are invalid IP addresses .
First of all, be sure to read one character by one. The problem is that a string may represent multiple possible IPs. For example, 25525511135
can be represented as 255.255.11.135
or 255.255.111.35
. The reason is that 11.135
and 111.35
are both legal representations, so we must use the backtracking method. To solve the problem, it is just that during the backtracking process, it will dynamically determine which new branches are added based on the read data, and which branches are illegal.
For example, when [1,1,1,3,5]
11
and 111
are both legal, because the number at this position only 0~255
to be between 1113
exceeds this range, so it is ignored, so two ways are diverged from this scene :
- Current item:
11
, the remaining items135
. - Current item:
111
, the remaining items35
.
Then recurse until the illegal situation ends, such as 4 items are full but there are remaining numbers, or the IP range is not satisfied, etc.
It can be seen that as long as we sort out the legal and illegal situations, until how to dynamically generate new recursive judgments, this problem is not difficult.
The input of this question is very straightforward, and it is given directly. In fact, not every question is so easy to think about. Let's look at the next one.
Full array
The whole arrangement is a middle-level question, the topic is as follows:
Given an array of numbers no repeating nums
, which returns all possible full array . You can return answers in any order.
Similar to restoring the IP address, we also consume input, such as 123
, we can consume 1
first, and continue to combine with the remaining 23
But unlike IP recovery, the first number can be any of 1
2
3
, so it is different when generating the current item: the current item can be selected from all remaining items, and then recursively.
For example, 123
1
or 2
or 3
for the first time. For 1
, there is still 23
, so next time you can choose 2
or 3
. When there is only one item left, there is no need to choose.
Although the input of the full array is not as straightforward as the input of the restoration of the IP address, it is derived based on the given string at all. Then the input may be split into multiple for more complicated topics, which requires you to think flexibly. For example, parentheses generate questions.
Bracket generation
The generation of parentheses is a middle-level question with the following topics:
N represents the number of figures generated in parentheses, you design a function that can be used to generate all possible and effective bracket combination.
Example:
Input:n = 3
Output: ["((()))","(()())","(())()","()(())","()()()"]
The basic idea of this question is very similar to the previous question, and because the question asks all possibilities, not the optimal solution, it is impossible to use the dynamic rules, so we consider the backtracking algorithm.
The input of the previous IP question is a known string, and the input of this question requires you to use your brain. Is the input for this question a string? Obviously not, because the input is the number of parentheses, is only one number of parentheses enough? Not enough, because the title requires valid parentheses, what are valid parentheses? It is the closed one, so we thought of using the left and right parentheses to represent this number, that is, the input is n
, then it is converted to open=n, close=n
.
With input, how to consume input? We can use a left parenthesis open
or a right parenthesis close
each step, but the first one must be open
, and the current consumed close
must be less than the consumed open
before it can be added close
, because a close
must have the left side A open
forms a legal closure.
So this problem is easily solved. Looking in retrospect, you need to be able to think flexibly about the input parameters of backtracking, and this thinking depends on your experience. Therefore, the algorithms are interlinked, and proper knowledge transfer can get twice the result with half the effort.
Well, let's stop here. In fact, not all problems can be solved with retrospect, but some problems seem to be variants of retrospective problems, but they are not. Let’s go back to the previous full permutation question, which is more like next permutation . This question seems to be derived from the full permutation, but it cannot be solved by the backtracking algorithm. Let’s take a look at this question.
Next permutation
The next ranking is a middle-level question, the topic is as follows:
Achieve access next permutation function, the algorithm requires a given numeric sequence rearranged arranged next larger in lexicographic order.
If there is no next larger arrangement, the numbers are rearranged to the smallest arrangement (that is, ascending order).
160dbd7ce41556 must be place and must be modified. Only extra constant space is allowed.
such as:
Input: nums = [1,2,3]
Output: [1,3,2]
Input: nums = [3,2,1]
Output: [1,2,3]
If you are wondering whether you can learn from the idea of full permutation and naturally derive the next permutation in the process of full permutation, then there is a high probability that it is impossible to figure out, because the efficiency of deriving from the whole to the part is too low, this question is directly given For a local value, we must use a relatively "local method" to quickly derive the next value, so this problem cannot be solved with a backtracking algorithm.
For 3,2,1
, since it is already the largest permutation, the next permutation can only be the initialized 1,2,3
ascending order. This is a special case. In addition, there is the next larger arrangement, taking 1,2,3
as an example, the bigger one is 1,3,2
instead of 2,1,3
.
If we look at a longer example, such as 3,2,1,4,5,6
, we can find that no matter how the previous descending order is, as long as the last few are in ascending order, just reverse the last two: 3,2,1,4,6,5
.
What if it is 3,2,1,4,5,6,9,8,7
? Obviously, 9,8,7
will make the number smaller, which does not meet the requirements. We still have to exchange 5,6
.. not 6,9
, because 65x
much larger than 596
. Here we get a few rules:
- Exchange the following numbers as much as possible. The exchange of
5,6
will be larger than the exchange of6,9
, because6,9
is later and has a smaller number of bits. - We divide
3,2,1,4,5,6,9,8,7
into two sections, the front section3,2,1,4,5,6
and the rear section9,8,7
. We have to exchange the largest possible number in the front section and the smallest possible number in the rear section. At the same time, we must ensure that the smallest possible number in the rear section is smaller than the previous section. The largest possible number is .
In order to meet the second point, we must search from back to front. If it is in ascending order, skip until we find a number j
smaller than j-1
, then the first paragraph is exchanged for the j
item, and the latter paragraph is to find the smallest number with it. Exchange, due to the search algorithm, the back paragraph must be in descending order, so you can j
Finally, we found that after the exchange, the next item is not necessarily the perfect next item, because the latter paragraph is in descending order, and we have changed the previous smallest possible "large" bit to a larger one, and the latter must be in ascending order to satisfy the next arrangement. Therefore, arrange the latter paragraphs in ascending order.
Because the latter part already satisfies the descending order, the double-pointer exchange method can be used to exchange each other to become the ascending order. Do not use fast sorting in this step, which will increase the overall time complexity by O(nlogn).
In the end, the algorithm complexity is O(n) because it only scans once + reverses the back segment once.
From this question, you can find that you should not underestimate the seemingly variant questions. From the full arrangement to the next arrangement, you may have to completely change your thinking instead of optimizing the backtracking.
We continue to return to the problem of backtracking. The most classic problem of backtracking is Queen N, which is also the most difficult problem. It is similar to the problem of solving Sudoku, but they are all similar. We will still use Queen N as a representative to understand this time.
N queen problem
The N queen question is a difficult question, the topic is as follows:
n The queen problem studies how to place
n
queens onn×n
the queens from attacking each other.Give you an integer
n
and return all the differentn
Queen’s solutions.Each solution contains a different chess placement plan for the queen problem
n
'Q'
and'.'
represent the queen and the empty position respectively.
The Queen’s attack range is very wide, including horizontal, vertical, and oblique, so when n<4
is unsolvable, when it is magical, n>=4
has a solution, such as the following two pictures:
This question obviously has "strong" aftereffect, because the queen's attack range is determined by its position. In other words, after the position of one queen is determined, the possible placement of other queens will change, so it can only be used Backtracking algorithm.
So how to identify legal and illegal locations? The core is to create four arrays based on the horizontal, vertical, and oblique attack methods, respectively store which rows, columns, priming, and nap positions cannot be placed, and then use all legal positions as possible positions for the next recursion until the queen When it is finished, or there is no place to put it.
It is easy to think of four arrays, which store the occupied subscripts respectively. In this case, the conditional judgment branch in the recursion is more complicated, and the others are actually not difficult.
The advanced algorithm of the space complexity of this question is to use the binary method and use 4 numbers instead of four subscript arrays. When each array is converted to binary, the position of 1 represents occupied, and the position of 0 represents unoccupied. , Through the bit operation, the position can be occupied more quickly and at low cost, and it can be judged whether the current position is occupied.
Here is just one example, you can feel the charm of binary:
According to the line, only one queen can be placed in a line, so every time you look at the next line, there is no need to read the line limit (at least the next line cannot conflict with the previous line), so we only need to record the column, apostrophe, Navigate to three positions.
The difference is that we use binary numbers, as long as three numbers can represent column, skimming, and nap. The 1 in the binary bit means occupied, and 0 means not occupied.
For example, column, apostrophe, and na are variables x,y,z
respectively, and the corresponding binary may be:
0000001
0010000
0001100
The "not" logic is any 1 or 1, so the "not" logic can combine all 1s, that is, x | y | z
or 0011101
.
Then invert this result and use non-logic, that is, ~(x | y | z)
, the result is 1100010
, then all 1
here represent the position that can be placed, we remember this variable as p
, through p & -p
continue to take the last bit 1
get the placement position, that is Callable recursively.
From this question, it can be found that the difficulty of Queen N lies not in the backtracking algorithm, but in how to use binary to write an efficient backtracking algorithm. Therefore, the review of the backtracking algorithm is more comprehensive, because the algorithm itself is very modular and relatively "awkward", so more emphasis needs to be placed on optimization efficiency.
to sum up
The backtracking algorithm essentially uses the high-speed computing power of the computer to try all the possibilities. The only difference is the relatively violent solution, which may be terminated early in a branch (branch pruning), so it is actually a relatively cumbersome algorithm. It should be used only when it has aftereffects and cannot use greedy or clever solutions like the next permutation.
Finally, we have to summarize and compare backtracking and dynamic programming algorithms. In fact, the violent recursive process of dynamic programming algorithms is equivalent to backtracking, but dynamic programming can use the cache to store the previous results to avoid repeated calculations of repeated sub-problems. The problem has after-effects, there is no repeated sub-problem, so cache acceleration cannot be used, so the high complexity of the backtracking algorithm cannot be avoided.
The backtracking algorithm is called the "general problem-solving method" because it can solve many large-scale computing problems and is a good practice for using computer computing power.
The discussion address is: Intensive Reading "Algorithm-Backtracking" · Issue #331 · dt-fe/weekly
If you want to participate in the discussion, please click here , there are new topics every week, weekends or Mondays. Front-end intensive reading-to help you filter reliable content.
Follow front-end intensive reading WeChat public
Copyright notice: Freely reprinted-non-commercial-non-derivative-keep the signature ( Creative Commons 3.0 License )
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。