After brushing these 15 backtracks, you can have an interview without brain violence

Posted by dilum on Sat, 11 Dec 2021 14:29:24 +0100

Foreword

Backtracking is a mindless rush. After hitting a wall, you retreat and continue to do it. It belongs to a kind of violent problem-solving idea;

In fact, the same is true. When we encounter some classified problems and can't think of a more sophisticated solution, we first consider violence, enumerate all situations, and then deal with them. Backtracking is such a violent method

The next tab is to learn about the conventional sorting algorithm

Drain it

After brushing these double pointer questions, you can tear the front end of the interview by hand

After brushing the 12 sliding windows, you can tear the front end by hand

After brushing these 20 dichotomous questions, you may still be unable to tear your hands off the big factory interview

After brushing these questions, you may still be unable to tear your hands off the big factory interview

After brushing these 30 tree questions, you may still be unable to tear your hands off the big factory interview

After brushing these 20 linked list questions, you may still be unable to tear your hands off the big factory interview

After brushing these 15 dp questions, I can only take a chance to enter the big factory

After brushing these bit operations, we'll consider the interview with a large factory -- we didn't pass the audit, and we'll do it after we change it later

text

In the process of doing backtracking questions, I will find that I am very confused, because many questions do not seem to need to return. In the process of performing the next step, I will make a good judgment, and then curb the possible failure. At this time, generally, what can continue to go down belongs to the OK operation. In fact, we can call this method "pruning"

I once fell into a deep thought. Is backtracking useless? As long as the brain is OK, in fact, it's good to prune and go back. Until I think of the core idea of backtracking, it's actually a violent solution. That is, if you can use other methods, you don't have to go back. It's a better idea. Generally, backtracking will be more complex

So when do you use backtracking? You can't preset the ending, or your choice is not only related to the choices of adjacent layers, but will have an impact on deeper layers, for example 51. Queen n

What we need is a complete chessboard. The choice of each layer will affect the layout of the whole chessboard. At this time, it is too difficult to think out all the possible situations at the moment of playing chess. At this time, backtracking is a good choice

For some that only have an impact on the upper level, pruning} is a good choice at this time;

In fact, when making a series summary, we will use a series of methods to solve as much as possible, but we also pursue multiple solutions to one problem, and what we want to achieve in the end is certainly not limited to a certain writing method, but can a come out as long as we see it;

So try to review most of the conventional tab s, and then fill them slowly, and summarize your own problem-solving scheme, which is the purpose of summarizing;

Work hard with everyone

Topic summary

array

combination

subset

cutting

route

Queen N

Problem solution

46. Full arrangement

analysis

  1. It does not contain duplicate numbers and requires full arrangement, so the arrangement in different orders must be counted. In this way, you should know what values you have obtained in the enumeration process
  2. During enumeration, cache two arrays arr, getIndex. Arr is the array during enumeration, and getIndex is the passed value state. If the value of the corresponding subscript of the current arr is 1, it is 0 if it does not pass
  3. When adding a value to the temporary array arr at each layer, you need to ensure that it will not be added repeatedly. You can traverse the ARR every time you encounter it. Because the value is unique, it is also possible;
  4. Here, space is used for time, and getIndex array is used to cache the corresponding state. The complexity of each search is {O(1)}O(1)
  5. The complete array needs to be enumerated n times each time, so the time complexity is {O(n^2)}O(n2) and the space complexity is {O(n)}O(n)
var permute = function (nums) {
  let ret = [];

  const dfs = (arr, getIndex) => {
    if (arr.length === nums.length) {
      ret.push(arr);
      return;
    }
    for (let i = 0; i < nums.length; i++) {
      const num = nums[i];
      if (!!getIndex[i]) continue; // If it exists, it means that it already has this value
      getIndex[i] = 1;
      dfs([...arr, num], getIndex);
      getIndex[i] = 0;
    }
  };
  const getIndexArr = new Array(nums.length)
  dfs([], getIndexArr);
  return ret;
};

Copy code

47. Full arrangement II

analysis

  1. Since duplicate numbers are included at this time, and there can be no duplicate values, you can consider sorting first
  2. Sorting ideas and question 1 have always been to cache two arrays, and because the values are repeated, we can't judge whether the values are the same, but only by subscript
  3. The difference is that for each backtracking, you need to judge whether the next value is the same as the current backtracking value. If it is the same, you need to skip to prevent repeated arrangement
  4. Time complexity {O(n^2)}O(n2), space complexity {O(n)}O(n)
var permuteUnique = function(nums) {
    const ret = []
    const len = nums.length
    nums.sort((a,b)=>a-b) // sort
    const dfs = (arr,indexArr) => {
        if(arr.length === len ){
            ret.push(arr)
            return 
        }
        for(let i = 0;i<len;i++){
            if(!!indexArr[i]) continue
            const num = nums[i]
            indexArr[i] = 1
            dfs([...arr,num],indexArr)
            indexArr[i] = 0
            // If the next value is the same, then you have to repeat the old way, so you'd better skip it directly
            while(nums[i+1]=== nums[i]) {
                i++
            }
        }
    }
    dfs([],[])
    return ret
}

console.log(permuteUnique([1,1,2]))
Copy code

39. Combination sum

analysis

  1. candidates is a non repeating, positive integer array
  2. Values can be repeated, but they cannot be retrieved backwards because they are independent of the arrangement, so an initial subscript value needs to be maintained; In contrast to [combined sum IV]
 var combinationSum = function(candidates, target) {
    const ret = []

    const dfs = (start,arr,sum) => {
        if(sum === target){
            ret.push(arr)
            return 
        }
        if(sum>target) return 

        for(let i = start;i<candidates.length;i++){
            // Because repeated fetching is allowed, it starts from the start node every time
            dfs(i,[...arr,candidates[i]],sum+candidates[i])
        }
    }

    dfs(0,[],0)
    return ret
}
Copy code

40. Combined sum II

analysis

  1. candidates is an array of positive integers with or without duplicates
  2. Each value in the array can only be taken once; Duplicate values cannot be taken, but duplicate values can be taken, that is, [1,1,2,3] - > can take [1,1,2], [1,3] - > 4
  3. In order not to get duplicate values, you have to skip the same values. At this time, you need to sort the array
  4. When enumerating at each layer, when duplicate values appear in the loop, cut off this part of the enumeration, because there must be the same part
  5. The subscript of the first input parameter of dfs should be + 1 because it cannot be retrieved repeatedly, which indicates that the last value cannot be retrieved repeatedly
 var combinationSum2 = function (candidates, target) {
    candidates.sort((a,b)=>a-b)
    const ret= []

    const dfs = (start,arr,sum) => {
        if(sum === target) {
            ret.push(arr)
            return 
        }
        if(sum>target || start>= candidates.length) return 
        for(let i = start;i<candidates.length;i++){
            // Cut off the duplicate
            if(i > start && candidates[i] === candidates[i-1]) continue
            // Here, start is the index of the start enumeration, but the value inserted into the temporary array is the value of the current index
            dfs(i+1,[...arr,candidates[i]],sum+candidates[i])
        }
    }
    dfs(0,[],0)
    return ret
}
Copy code

216. Combined sum III

analysis

  1. Given is not a specific array, but the length limit k, and the target value target -- equivalent to candidates is a non repeating, 1-9 positive integer array
  2. So it can be seen as 39. Combination sum In special circumstances, only the judgment conditions are different
var combinationSum3 = function (k, n) {
  const ret = [];

  const dfs = (start, arr, sum) => {
    if (arr.length === k && sum === n) {
      ret.push(arr);
      return;
    }
    if (arr.length > k || sum > n) {
      return;
    }

    for (let i = start + 1; i < 10; i++) {
      dfs(i, [...arr, i], sum + i);
    }
  };
  dfs(0, [], 0);
  return ret
};
Copy code

377. Combined total IV

Analysis - backtracking

  1. candidates is a non repeating, positive integer array, which can take repeated values and take different combinations
  2. This question is similar to Combined sum Very similar. The difference is that this question asks for the number of permutations, while question 1 asks for non repeated combinations
  3. Therefore, there is no need to limit the subscript of the combination start enumeration. Start from 0 every time
  4. Then it timed out

*/

var combinationSum4 = function (nums, target) {
  let ret = 0;
  const dfs = (sum) => {
    if (sum === target) {
      ret++;
      return;
    }
    if (sum > target) return;
    for (let i = 0; i < nums.length; i++) {
      dfs(sum + nums[i]);
    }
  };
  dfs(0);
  return ret;
};
Copy code

Analysis - dp

  1. dp[i] indicates the number of combinations that exist when the value is I
  2. State transition equation dp[i] = sum(dp[i-nums[k]])
  3. base case dp[0] = 1
var combinationSum4 = function (nums, target) {
    const dp = new Array(target+1)
    dp[0]= 1  // If the value you just get is 0, there will be 1, because not taking is also a way to take it
    for(let i = 1;i<target+1;i++){
        dp[i] = 0
        for(let j =0;j<nums.length;j++){
            if(i>=nums[j]){
                dp[i]+=dp[i-nums[j]]
            }
        }
    }
    return dp[target]
}
Copy code

78. Subset

Analysis -- find the law

  1. The array elements are different, and the return value does not contain duplicate subsets, that is, the position arrangement is not considered
  2. Since it has nothing to do with permutation, you only need to traverse num once. The values obtained without traversing once can be combined with the existing ret to form a new batch of arrays, and then combined with the old item to form a new enumeration array
  3. Time complexity {O(n^2)}O(n2)
 var subsets = function (nums) {
    let ret = [[]]
    for(let num of nums ){
        ret = [...ret,...ret.map(item => item.concat(num))]
    }
    return ret
}
Copy code

Analysis -- iterative backtracking

  1. Use the iterative method to enumerate all cases, which is no different from the traversal of multi fork tree
  2. Time complexity {O(N^2)}O(N2)
var subsets = function (nums) {
    const ret = []
    const dfs = (start,arr) => {
        ret.push(arr)
        if(arr.length === nums.length || start=== arr.length) return 
        for(let i = start;i<nums.length;i++){
            dfs(i+1,[...arr,nums[i]])
        }
    }
    dfs(0,[])
    return ret
}
Copy code

90. Subset II

Analysis - duplicate values

  1. and 78. Subset In contrast, there are many duplicate values, and duplicate values are not allowed to appear in the return array, so it is obvious to sort first
  2. Then, in the backtracking process, if the value of the next iteration is the same as the current value, it will be skipped to achieve the effect of de duplication
var subsetsWithDup = function (nums) {
    nums.sort((a,b)=> a-b)
    const ret = []
    const dfs = (start,arr) => {
        ret.push(arr)
        if(start === nums.length ) return // If start exceeds the subscript, it is time to get the maximum subscript value
        for(let i = start;i<nums.length;i++){
            dfs(i+1,[...arr,nums[i]])
            while(nums[i] === nums[i+1]){
                i++ // duplicate removal
            }
        }
    }
    dfs(0,[])
    return ret
}
Copy code

131. Split palindrome string

analysis

  1. This is a variant combination problem, because the arrangement order has been determined, as long as it is cut
  2. Therefore, in the traversal process, only when the substring meets the palindrome requirements can it be cut, and then go down, otherwise it is better to cut it off
  3. The determination of palindrome substring can be simply realized with left and right double pointers
var partition = function(s) {
    const ret = []
    // Determine whether it is a palindrome substring
    function isValid(s) {
        if(s.length === 1) return true // Only one character
        let l = 0,r = s.length-1
        while(l<r){
            if(s[l] !== s[r]) return false
            l++
            r--
        }
        return true

    }
    const dfs = (start,arr) => {
        if(start === s.length){
            ret.push(arr)
            return 
        }
       let temp =''
        for(let i =start;i<s.length;i++){
            temp+=s[i]
            if(isValid(temp)){
                dfs(i+1,[...arr,temp])
            }
        }
    }
    dfs(0,[])
    return ret
};
Copy code

93. Restore IP address

analysis

  1. This question is similar to 131. Split palindrome string Similar
  2. Here is also the segmentation string, but the judgment condition is that each segment must meet the valid IP address, but the shelf is the same
  3. There are also many judgment conditions here. As long as the qualified conditions are counted, many branches can be cut off
var restoreIpAddresses = function (s) {
  const ret = [];

  function isValid(s) {
    if (s.length > 1 && s[0] == 0) return false; // Cannot start with 0
    if (s >= 1 << 8) return false; // To be between [0255]
    return true;
  }

  const dfs = (start, arr) => {
    if (arr.length === 4 && start !== s.length) return; // It has been divided into four points, but it's not finished yet
    if (start === s.length) {
      if (arr.length === 4) {
        ret.push(arr.join("."));
      }
      // Whether divided into four or not, they left
      return;
    }

    let str = "";
    for (let i = start; i < s.length && i < start + 3; i++) {
      str += s[i];
      if (isValid(str)) {
        dfs(i + 1, [...arr, str]);
      }
    }
  };
  dfs(0, []);
  return ret;
};

Copy code

112. Path sum

analysis

  1. The path is on the root leaf complete route and the sum is target
  2. dfs sequence traversal can go on
  3. Time complexity {O(n)}O(n)
 var hasPathSum = function(root, targetSum) {
    let ret = false
    const dfs = (root,sum) => {
        if(ret || !root) return  // As long as one road goes through, there is no need to go anywhere else
        sum += root.val
        if(!root.left && !root.right && sum  === targetSum) {
                ret = true
                return 
        }
        if(root.left) dfs(root.left,sum)
        if(root.right) dfs(root.right,sum)
    }
    dfs(root,0)
    return ret
};
Copy code

113. Path sum II

analysis

  1. The root - leaf path is still found, but this time save all the qualified paths found
  2. Time complexity {O(n)}O(n)
 var pathSum = function(root, targetSum) {
    const ret = []
    const dfs = (root,arr,sum) => {
        if(!root) return 
        sum+=root.val
        arr = [...arr,root.val]
        if(!root.left && !root.right && sum == targetSum){
            ret.push(arr)
        }
        if(root.left) dfs(root.left,[...arr],sum)
        if(root.right) dfs(root.right,[...arr],sum)
    }
    dfs(root,[],0)
    return ret
};
Copy code

437. Path sum III

analysis

  1. The path found this time can be any start end node in the tree,;
  2. However, the path must be downward, that is, it cannot look like a.left - a - a.right, which is actually a limiting condition to reduce the difficulty
  3. So it's just the same top-down traversal, but if you encounter a path that meets the requirements, you still have to continue to traverse to the leaf node
  4. And 112. Path sum And 113. Path sum II The biggest difference is that the path this time does not limit the starting point and ending point;
  5. Without limiting the end point, I can record once in the traversal process as long as the targetSum is met, up to the leaf node position, and there is no need to judge again at the leaf node
  6. Without limiting that the starting point is the root node, you can take any node as the starting point, that is, when you need to traverse the whole tree as the starting point, go down to find the path;
  7. Time complexity {O(nlogn)}O(nlogn)
var pathSum = function (root, targetSum) {
  let ret = 0;
  // This is a dfs for finding paths and with any root node
  const dfs = (root, sum) => {
    if (!root) return;
    sum += root.val;
    if (sum === targetSum) ret++;
    if (!root.left && !root.right) return; // The leaf node is over
    if (root.left) dfs(root.left, sum);
    if (root.right) dfs(root.right, sum);
  };

  //   This is traversing the whole tree and then continuing down
  const outer = (root) => {
    if (!root) return;
    dfs(root, 0);
    outer(root.left);
    outer(root.right);
  };
  outer(root);
  return ret;
};

Copy code

51. Queen n

Reference: leetcode-cn.com/problems/n-...

Analysis -- directly find the chessboard that meets the requirements

  1. The row is the depth of tree recursion, and the column is the width of each layer. The backtracking method is used for dfs traversal of the tree
  2. The whole process requires three parts. Traverse the tree in a backtracking way, find the qualified node chessboard[row][col], and convert the qualified two-dimensional array into a qualified string array
  3. Time complexity {O(n*logn)}O(n * logn)
var solveNQueens = function (n) {
  const ret = [];
  // 1. The actual walking process of Queen n -- backtracking tree
  const dfs = (row, chessboard) => {
    if (row === n) {
      // It has reached the leaf node--
      //   However, chessboard is a two-dimensional array, which cannot be push ed in casually. It needs to be copied deeply
      ret.push(getStrChessboad(chessboard));
      return;
    }
    // Each line starts from 0 - n-1, and then goes back if it doesn't meet the requirements
    for (let col = 0; col < n; col++) {
      if (isValid(row, col, chessboard)) {
        // If the chessboard[row][col] meets the requirements, it is a road
        chessboard[row][col] = "Q";
        dfs(row + 1, chessboard);
        chessboard[row][col] = "."; // Go back
      }
    }
  };

  // Judge whether the current node meets the requirements of Queen N -- note that [0,n-1] here is calculated from left to right
  function isValid(row, col, chessboard) {
    // Same column
    for (let i = 0; i < row; i++) {
      if (chessboard[i][col] === "Q") {
        return false;
      }
    }
    // 45 'tilt from left to right
    for (let i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
      if (chessboard[i][j] === "Q") {
        return false;
      }
    }
    // 135 ` tilt from right to left
    for (let i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
      if (chessboard[i][j] === "Q") {
        return false;
      }
    }
    // If it is not the same column or left and right slashes, the requirements are met
    return true;
  }

  //  Convert the N Queen of a two-dimensional array into a one-dimensional array string
  function getStrChessboad(chessboard) {
    const ret = [];
    chessboard.forEach((row) => {
      let str = "";
      row.forEach((item) => {
        str += item;
      });
      ret.push(str);
    });
    return ret;
  }

  const chessboard = new Array(n).fill([]).map(() => new Array(n).fill("."));
  dfs(0, chessboard);
  return ret;
};
Copy code

52. N Queen II

analysis

  1. Problems and 51. Queen n It's basically the same, but the value is changed from the complete N-queen scheme to just knowing a few
  2. Therefore, the third part of the conversion can be deleted directly and then copied directly
var totalNQueens = function (n) {
  let ret = 0;

  const dfs = (row, chessboard) => {
    if (row === n) {
      ret++;
      return;
    }

    for (let col = 0; col < n; col++) {
      if (isValid(row, col, chessboard)) {
        chessboard[row][col] = "Q";
        dfs(row + 1, chessboard);
        chessboard[row][col] = ".";
      }
    }

    function isValid(row, col, chessboard) {
      for (let i = 0; i < row; i++) {
        if (chessboard[i][col] === "Q") return false;
      }

      for (let i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
        if (chessboard[i][j] === "Q") return false;
      }

      for (let i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
        if (chessboard[i][j] === "Q") return false;
      }
      return true;
    }
  };
  const chessboard = new Array(n).fill([]).map(() => new Array(n).fill("."));
  dfs(0, chessboard);
  return ret;
};

Copy code

@Analysis

  1. The backtracking process is very simple, but is there a better way to handle the judgment condition isValid
  2. In the first question, we want to create an instance of Queen N, so we need to use an array. Now we don't need a specific queen N, so we can show queen N in other forms without the form of array
  3. Three binary bits col, DLR and DRL are used to represent the values on the column, starting from the left 45 ° and starting from the right 135 ° respectively
  4. col here is easy to understand, because the value of i in each row will not change when the row to be judged
  5. For dlr, the bit corresponding to binary is tilted, and only such a value conforms to 45 ` tilt; Similarly, drl is the same Q Q . . . . . . Q . . . . . . Q . . . . . . Q . . . . .
  6. therefore
var totalNQueens = function (n) {
  let ret = 0;
  const dfs = (r, col, dlr, drl) => {
    if (r === n) {
      ret++;
      return;
    }
    for (let i = 0; i < n; i++) {
      // The current coordinate is converted to the value corresponding to the binary bit
      const _col = 1 << i;
      const _dlr = 1 << (r + i); // This indicates that the value of i in other lines should be 1 < < (r + i) at the current r, so we set such a value to try other values to see if they meet the requirements
      const _drl = 1 << (n - i + r);
      if ((col & _col) || (dlr & _dlr) || (drl & _drl)) continue; // As long as one is true,
      dfs(r + 1, col | _col, dlr | _dlr, drl | _drl);
    }
  };
  dfs(0, 0, 0, 0);
  return ret;
};

If there are friends who need to get free information, You can click here to get the information!

Topics: Java Back-end Programmer architecture