Through 4 classic applications, you will be familiar with the backtracking algorithm

Abstract: The idea of ​​backtracking is somewhat similar to enumeration search.

This article is shared from the HUAWEI CLOUD community " Backtracking Algorithm in Simple Ways ", author: Embedded Vision.

1. How to understand the backtracking algorithm

The depth-first search algorithm uses the backtracking algorithm idea, but in addition to being used to guide the classic algorithm design such as depth-first search, it can also be used in many actual software development scenarios, such as regular expression matching, compilation principles Syntax analysis in etc.

In addition, many classic mathematical problems can be solved by backtracking algorithms, such as Sudoku, eight queens, 0-1 knapsack, coloring of graphs, traveling salesman problem, full permutation, etc.

The idea of ​​backtracking is somewhat similar to enumeration search . Violently enumerate all solutions and find a solution that meets expectations. In order to enumerate all possible solutions regularly and avoid omission and repetition, we divide the problem solving process into multiple stages. At each stage, we will face a fork in the road. We first choose a road at random. When we find that this road does not work (the solution does not meet the expectations), we will go back to the previous fork in the road and choose another one. The way to go is to keep going.

The template code for the backtracking algorithm is summarized as follows:

void backtracking(参数) {
 if (终止条件) {
 存放结果;
 return;
 }
 for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
 处理节点;
 backtracking(路径,选择列表); // 递归
 回溯,撤销处理结果
 }
}

Second, the classic application of the backtracking algorithm

2.1, Eight queens problem

There is an 8x8 chessboard, and it is desired to put 8 chess pieces (queens) in it, and each chess piece cannot have another chess piece in its row, column, or diagonal. The "diagonal" here refers to all diagonals, not just the two diagonals that bisect the entire board.

Solution idea : This problem can be divided into 8 stages, and the 8 chess pieces are placed in the first row, the second row, the third row...the eighth row, and each row has 8 middle placement methods (8 columns). During the placement process, we keep checking whether the current placement method meets the requirements. If it is satisfied, then jump to the next row and continue to place the chess pieces; if not, then change another way of placement and continue to try. The idea of ​​backtracking is used here, and the backtracking algorithm is also very suitable for implementation with recursive code.

// N 皇后问题 leetcode 51 https://leetcode-cn.com/problems/n-queens/
class Solution {
private:
    vector<vector<string>> result;
 void backtracking(int n, int row, vector<string>& chessboard){
 if(row == n) {
 result.push_back(chessboard);
 return;
 }
 for(int column=0; column < n; column++){ // 每一行都有8中放法
 if (isOK(row, column, n, chessboard)){
                chessboard[row][column] = 'Q'; // 放置皇后
 backtracking(n, row+1, chessboard);
                chessboard[row][column] = '.'; // 回溯,撤销处理结果
 }
 }
 }
 // 判断 row 行 column 列放置皇后是否合适
    bool isOK(int row, int column, int n, vector<string>& chessboard){
        int leftup = column - 1; int rightup = column + 1; // 左上角和右上角
 for(int i = row-1; i>=0; i--){ // 逐行网上考察每一行
 // 判断第 i 行的 column 列是否有棋子
 if(chessboard[i][column] == 'Q') {
 return false;
 }
 // 考察左上对角线:判断第i行leftup列是否有棋子 
 if(leftup >=0 ){
 if(chessboard[i][leftup] == 'Q') return false;
 }
 // 考察左上对角线:判断第i行rightup列是否有棋子
 if(rightup < n){
 if(chessboard[i][rightup] == 'Q') return false;
 }
 --leftup;
 ++rightup;
 }
 return true;
 } 
public:
    vector<vector<string>> solveNQueens(int n) {
 result.clear();
 std::vector<std::string> chessboard(n, std::string(n, '.'));
 backtracking(n, 0, chessboard);
 return result;
 }
};

2.2, 0-1 knapsack problem

0-1 knapsack is a very classic algorithm problem. There are many variants of the 0-1 knapsack problem, here is a basic one. We have a knapsack, and the total carrying weight of the knapsack is W kg. Now we have n items, each of which has different weights and is indivisible, that is, for each item, there are two choices, whether to put it in a backpack or not to put it in a backpack, for n items, the total There are 2^n ways to install .

We now expect to select several items to load into the backpack. Under the premise of not exceeding the weight W that the backpack can carry, how to maximize the total weight of the items in the backpack?

Why can't the 0-1 knapsack problem be solved with a greedy algorithm?

Because it is indivisible, it is impossible to judge which item contributes more to the expected value in the current situation, that is, there is no current optimal choice, so the greedy algorithm cannot be used.

An efficient solution to the 0-1 knapsack problem is a dynamic programming algorithm, but it can also be solved by a less efficient backtracking method. We can arrange the items in order, and the whole problem is decomposed into n stages, and each stage corresponds to how to choose an item. Process the first item first, choose to put it in or not, and then process the remaining items recursively.

int maxW = 0;
// cw 表示当前装进背包的物品的重量和,w 表示背包承载的重量
// items  表示物体的重量数组,n 表示总的物品个数, i 表示考察到第 i 个物品
int f(int i, int cw, vector<int> items, int n, int w){
 // 递归结束条件:cw == w 表示背包已经装满,i==n 表示考察完所有物品
 if(cw == w || i == n){
 if(cw > maxW) maxW = cw;
 return;
 }
 f(i+1, cw, items, n, w); // 不装
 // 剪枝过程,当装入的物品重量大于背包的重量,就不继续执行
 if(cw+items[i] <= w){
 f(i+1, cw+items[i], items, n, w); // 装
 }
}

The key to understanding the backtracking solution to the 0-1 knapsack problem is: for an item, there are only two cases, two cases of not putting it in the backpack and putting it in the backpack . The corresponding functions are f(i+1, cw, items, n, w) and f(i+1, cw + items[i], items, n, w).

2.3, wildcard matching

Assuming that the regular expression only contains two wildcard characters "*" and "?" "?" matches zero or one of any character. Based on the above background assumptions, how to use the backtracking algorithm to judge whether a given text matches a given regular expression?

If we encounter special characters, we have a variety of ways to deal with them, which is the so-called fork in the road. For example, "*" has multiple matching schemes, which can match any character in a text string, and we will choose it at will first. A matching scheme, and then continue to examine the remaining characters. If we find that we cannot continue to match, we will return to this fork in the road, choose a matching scheme again, and then continue to match the remaining characters.

// 暴力递归 --> 记忆化 --> DP --> 状态压缩DP;
class Solution{
private:
    bool matched = false;
 void backtracking(int ti, int pj, string text, string pattern){
 if (matched) return;
 if(pj == pattern.size()){ // 正则表达式到末尾了
 if(ti == text.size())
                matched = true;
 return;
 }
 // *匹配任意个字符
 if(pattern[pj] == '*'){
 for(int k=0; k< text.size()-ti;k++)
 backtracking(ti+k, pj+1, text, pattern);
 }
 // ?匹配0个或者1个字符
 else if(pattern[pj] == '?'){
 backtracking(ti, pj+1, text, pattern);
 backtracking(ti+1, pj+1, text, pattern);
 }
 // 纯字符匹配才行 
 else if(ti < pattern.size() && pattern[pj] == text[ti]) { 
 backtracking(ti+1, pj+1, text, pattern);
 }
 }
public:
    bool isMatch(string text, string pattern){
        matched = false;
 backtracking(0, 0, text, pattern);
 return matched;
 }
};

2.4, leetcode regular expression matching

There are also variant questions in leetcode (leetcode10: regular expression matching) as follows:

Other variant questions: leetcode44-wildcard matching

Given a string s and a character pattern p, please implement a regular expression matching that supports '.' and '*'.

  • '.' matches any single character
  • '*' matches zero or more of the preceding element

The so-called match is to cover the entire string s, not part of the string.

Method 1: Backtracking (discussion by stage and situation, violent search and pruning)

First , consider the case where the only special character is '.'. This case will be very simple: we only need to check whether s[i] and p[i] match sequentially from left to right.

def isMatch(self,s:str, p:str) -> bool:
 """字符串 s 和字符规律 p"""
 if not p: return not s # 边界条件
 first_match = s and p[0] in {s[0],'.'} # 比较第一个字符是否匹配
 return first_match and self.isMatch(s[1:], p[1:])

Finally , consider the case of '*', it will appear in the position of p[1], there will be two situations in the matching process:

  • An asterisk means match 0 of the preceding elements. Such as '##' and a*##, at this time we directly ignore the a* of p, and compare ## and ##, that is, continue to recursively compare s and p[i + 2:] ;
  • An asterisk matches one or more of the preceding elements. Such as aaab and a*b, then we will ignore the first element of s, compare aab and a*b, that is, continue to recursively compare s[i + 1:] and p . (Here, by default, check whether s[0] and p[0] are equal).

The Python3 code is as follows:

class Solution:
 def isMatch(self, s: str, p: str) -> bool:
 if not p: return not s
 first_match = bool(s and p[0] in {s[0],'.'}) # 比较第一个字符是否匹配
 if len(p) >=2 and p[1] == '*':
 # * 匹配前面一个字符 0 次或者多次
 return self.isMatch(s, p[2:]) or first_match and self.isMatch(s[1:], p)
 else:
 return first_match and self.isMatch(s[1:], p[1:])

The C++ code is as follows:

// letcode10 正则表达式匹配
#include <vector>
#include <string>
using namespace std;
class Solution{
public:
    bool isMatch(string s, string p){
 // 如果正则串 p 为空字符串,s 也为空,则匹配成功
 if(p.empty()) return (s.empty());
 // 判断 s 和 p 的首字符是否匹配,注意要先判断 s 不为空
        bool match = (!s.empty()) && (s[0] == p[0] || p[0] == '.');
 // 如果p的第一个元素的下一个元素是 *,则分别对两种情况进行判断
 if(p.size() >= 2 && p[1] == '*'){
 // * 匹配前面一个字符 0 次或者多次
 return isMatch(s, p.substr(2)) || (match && isMatch(s.substr(1), p));
 }
 else{ // 单个匹配
 return match && isMatch(s.substr(1), p.substr(1));
 }
 }
};

The time complexity of direct recursion is too large (exponential), you can record the previous recursive process, and exchange space for time. The C++ code for memoized recursion is as follows:

class Solution{
public:
    bool isMatch(string s, string p){
 unordered_map<int, bool> memo;
 return backtracking(s, 0, p, 0, memo);
 }
    bool backtracking(string s, int i, string p, int j, unordered_map<int, bool> & memo){
 // # 检查 s[i] 是否能被匹配,注意要先判断 s 不为空
        bool match = (i < s.size()) && (s[i] == p[j] || p[j] == '.');
 if(j >= p.size()) return i >= s.size(); // p 和 s 同时遍历完
        int key = i * (p.size() + 1) + j; // 哈希键
 if (memo.find(key) != memo.end()) // 这个状态之前经历过,可以返回结果
 return memo[key];
 else if (i == s.size() && j == p.size()) // 如果s和p同时用完,匹配成功
 return memo[key] = true;
 else if((p.size()-j) >= 2 && p[j+1] == '*'){
 // * 匹配前面一个字符 0 次或者多次
 if(backtracking(s, i, p, j+2, memo) || match && backtracking(s, i+1, p, j, memo))
 return memo[key] = true;
 }
 else { // 单个匹配
 if(match && backtracking(s, i+1, p, j+1, memo))
 return memo[key] = true;
 }
 return memo[key] = false; // 没辙了,匹配失败
 }
};

Method 2: Dynamic programming method

  • [ ] Algorithm idea
  • [ ] Code

Three, summary

The idea of ​​the backtracking algorithm is very simple. In most cases, it is used to solve generalized search problems, that is, to select a solution that meets the requirements from a set of possible solutions. The backtracking algorithm is very suitable to be implemented by recursion. In the process of implementation, the pruning operation is a technique to improve the backtracking efficiency. With pruning, we don't need to exhaustively search all cases, thus improving the search efficiency.

Although the principle of the backtracking algorithm is very simple, it can solve many problems, such as the depth-first search we mentioned at the beginning, the eight queens, the 0-1 knapsack problem, the coloring of the graph, the traveling salesman problem, Sudoku, full permutation, and regular expressions pattern matching and so on.

The problems that can be solved by the backtracking algorithm can basically be solved by dynamic programming, which has lower time complexity and higher space complexity, and uses space for time.

References

  • Leetcode 8 queen problem solution
  • Backtracking algorithm: Learn the core idea of ​​backtracking algorithm from the movie "Butterfly Effect"
  • Rotten Oranges Solution - Backtracking and Dynamic Programming

 

Click to follow and learn about Huawei Cloud's fresh technologies for the first time~

{{o.name}}
{{m.name}}

Guess you like

Origin my.oschina.net/u/4526289/blog/8652612