回溯算法(完结)

167 阅读5分钟

韩老魔高光时刻镇楼哈哈哈哈哈哈哈哈哈。

好了,今天再看一些其余类型的题,回溯算法相关的题就基本看完了。

字符串的排列

输入一个字符串(有重复字符串的排列组合),打印出该字符串中字符的所有排列。你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。题目来源:leetcode.157

输入:s = "abc"
输出:["abc","acb","bac","bca","cab","cba"]

这道题其实就是包含重复数字的全排列,只不过数组变成字母了,直接看代码吧。

function permutation(goods) {
    const result = []
    goods = goods.split('').sort()
    function loop(path,used) {
        if (path.length === goods.length) {
            result.push(path.slice().join(''))
            return
        }
        for(let i = 0;i<goods.length;i++) {
            if(used[i]) continue
            if(i>0 && goods[i] === goods[i-1] && !used[i-1]) continue
            path.push(goods[i])
            used[i] = true
            loop(path,used)
            path.pop()
            used[i] = false
        }
    }
    loop([],[])
    return result
};

把给定的字符串直接切割成数组然后再解题,这道题就变成了带重复数字数组的全排列了。记得保存结果的时候再转成字符串就行了,其他的没什么好说的了。

无重复字符串的排列组合

无重复字符串的排列组合。编写一种方法,计算某字符串的所有排列组合,字符串每个字符均不相同。题目来源:leetcode

 输入:S = "qwe"
 输出:["qwe", "qew", "wqe", "weq", "ewq", "eqw"]

emmm这道题基本上道题没什么区别,就是不包含相同字符了。

function permutation(S) {
    const result = []
    const arr = S.split('')
    function loop(path,used) {
        if (path.length === arr.length) {
            result.push(path.slice().join(''))
            return
        }
        for(let i = 0; i<arr.length; i++) {
            if(used[i]) continue
            path.push(arr[i])
            used[i] = true
            loop(path,used)
            path.pop()
            used[i] = false
        }
    }
    loop([],[])
    return result
}

感觉这道题没什么好说的,就是数组的全排列,咱们接着往下看吧。

顺次数

我们定义「顺次数」为:每一位上的数字都比前一位上的数字大 1 的整数。 请你返回由 [low, high] 范围内所有顺次数组成的 有序 列表(从小到大排序)。题目来源:leetcode.1291

输出:low = 1000, high = 13000
输出:[1234,2345,3456,4567,5678,6789,12345]

这道题我们先理解什么是顺次数,通俗来讲就是连续的数字组成的数,这个条件就可以充当我们的剪枝条件,然后再找复合题解的跳出条件,显然是low<题解<high符合这个判断的解就是题解,然后就跟着思路写就行了。

function sequentialDigits(low, high) {
    const result = []
  function loop(path,start) {
    const num = Number(path.join(''))
    if (num > high) return
    if (low <= num && num <= high) {
      result.push(num)
    }
    for (let i=start; i<10; i++) {
      if (!path.length || (i-path[path.length-1] === 1)) {
        path.push(i)
        loop(path, i+1)
        path.pop()
      }
    }
  }
  loop([],1)
  result.sort((a,b) => a-b)
  return result
}

这道题难点是剪枝函数的判断写法,其他的基本没什么要注意的,这道题有很多其他更优的解法,因为最近在刷回溯算法所以用回溯写了。

复原IP地址

给定一个只包含数字的字符串,复原它并返回所有可能的 IP 地址格式。 有效的 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 '.' 分隔。

题目来源:leetcode.93

示例 1:
  输入:s = "25525511135"
  输出:["255.255.11.135","255.255.111.35"]
示例 2:
  输入:s = "0000"
  输出:["0.0.0.0"]

首先判断题解目标条件:由四个有效判断组成且正好消耗完完所有的字符,符合条件的组合就存起来,如果已经有四个有效判断了但是字符还有剩余,那么这个组合就不符合题解,就没必要往下了。

然后判断剪枝条件:在每一次选择的时候判断剪枝条件,符合的放进去,不符合的给裁剪掉,题中每个片段应该是长度为1到3、大于等于0小于等于255的、而且不能是以0开头的比如0xx、0x,不符合的选择就裁剪掉。

然后就按照思路去写代码。

function restoreIpAddresses(s) {
    const result = []
    function loop(path, start) {
        if (path.length === 4 && path.join('') === s) { // 判断每一种组合的长度和是否耗尽了字符 如果是就把题解存起来
            result.push(path.join('.'))
            return
        }
        if (path.length > 4) return // 如果组合长度大于4 说明结果不符合 就执行跳出
        for (let i=1; i<4; i++) {
            const str = s.substring(start,start+i) // 拿到每次选择的片段
            if (start + i - 1 >= s.length) continue // 如果指针start越界 就跳出
            if (str > 255) continue // 片段大于255也不符合 
            if (str.length > 1 && str.startsWith('0')) continue // 片段是以0开头的也不符合
            path.push(str) // 选择符合条件的片段
            loop(path, start+i)
            path.pop()
        }
    }
    loop([],0)
    return result
}

这里面我们借助了一个指针start来选择每次片段的长度,当指针大于给定字符的长度时就越界了这种就不再往下进行了。

这道题相比之前几道是有点难度的,大家做题之前先梳理一下思路,然后根据思路开始写代码,不要求能一边写对,刚开始都是一边写一边调试然后调整代码的。

分割回文串

给定一个字符串s,将s分割成一些子串,使每个子串都是回文串。返回s所有可能的分割方案。

题目来源:leetcode.131

示例 1:
  输入: "aab"
  输出:
  [  ["aa","b"],
    ["a","a","b"]
  ]
示例 2:
  输入:s = "a"
  输出:[["a"]]

这道题先得理解什么是回文串,以及怎么判断一个字符串是不是回文串。

回文串:当一个字符串正反读没有区别的时候,这个字符串就是回文串。

判断一个字符串是不是回文串的方法:

var isPalindrome = function(s) {
    return s === s.split('').reverse().join('')
}

然后判断题解目标条件:分割成不同的字串,消耗完所有字符。

剪枝条件:字串为回文串。

剩下的其实就是求给定字符所有字串了,然后选择符合条件的字串,最后判断是否符合目标题解。

function partition(s) {
    const result = []
    function loop(path, start) {
        if (path.join('') === s) {
            result.push(path.slice())
            return
        }
        for (let i=1; i<=s.length - start; i++) { // i<=s.length - start 限制剩余字串的长度 以防start超界
            const str = s.substring(start, start+i)
            if (!isPalindrome(str)) continue // 判断是否为回文串
            path.push(str)
            loop(path, start + i)
            path.pop()
        }
    }
    loop([],0)
    return result
}
var isPalindrome = function(s) {
    return s === s.split('').reverse().join('')
}

这道题和上边ip地址的有点像,思路都差不多,上一道没问题的话这道题也不是问题。

字母大小写全排列

给定一个字符串S,通过将字符串S中的每个字母转变大小写,我们可以获得一个新的字符串。返回所有可能得到的字符串集合。题目来源:leetcode.784

示例 1:输入:s = "a1b2"
输出:["a1b2", "a1B2", "A1b2", "A1B2"]
示例 2:输入: s = "3z4"
输出: ["3z4","3Z4"]

 直接看代码

function letterCasePermutation(s) {
    const result = []
    function loop(path,start) {
        if (path.length === s.length) { // 设置目标题解条件
            result.push(path.slice())
            return
        }
        if(isNaN(s[start])) { // 判断指针当前字符是否为字母
            loop(`${path}${s[start].toLowerCase()}`, start+1) // 选择+隐式回溯
            loop(`${path}${s[start].toUpperCase()}`, start+1)
        } else {
            loop(`${path}${s[start]}`, start+1)
        }
    }
    loop('',0)
    return result
}

这里和一般的回溯有点区别就是循环和回溯的步骤都变成隐形的了,设置好目标题解的条件,用指针start从左向右遍历就相当于for循环,撤销的步骤隐藏在了选择的时候,每一次选择的步骤中都是用上一步的path然后拼接这一次的选择s[start].toLowerCase()上一步path的值是不会变的所以这里相当于隐式回溯了,然后通过dfs遍历出所有的解,把达到目标条件的解存起来。

括号生成

数字n代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。题目来源:leetcode.22

示例1:输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]
示例 2:输入:n = 1
输出:["()"]

想解这道题要先想明白有效的括号组合怎么判断,首先我们观察有效的括号组合左括号和右括号数目一定一样并且一个有效的括号组合里面从左向右无论取几个字符,里面左括号的数量一定是大于等于右括号的数量的,因为每有一个右括号必定有一个左括号跟它对应。我们根据这个就能写出判断有效括号组合的条件。

function generateParenthesis(n) {
    if (n === 1) return ['()']
    const result = []
    const loop = (path, left, right) => {
        if (path.length === 2*n) { // 设置目标题解条件
            result.push(path.slice())
            return
        }
        if (right > left) return // 不符合有效括号组合的进行剪枝
        if (left < n) { // 只要左括号小于给定的n就可以选
            loop(`${path}(`, left+1, right) // 继续递归做下一步选择
        }
        if (right < n) { // 同理只要右括号小于给定的n也可以选
            loop(`${path})`, left, right+1) // 继续递归做下一步选择
        }
    }
    loop('',0,0)
    return result
}

这里的撤销选择也是隐式的,通过两个计数变量计算左右括号的数量,然后dfs递归遍历出所有的解,把不符合目标题解的解给裁剪掉就可以了。

电话号码的字母组合

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

示例 1:输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
示例 2:输入:digits = ""
输出:[]
示例 3:输入:digits = "2"
输出:["a","b","c"]

先明确思路,首先需要一个数字对应字母的一个映射map,从给定的数字中拿到每个数字对应的字母,然后第一个数字有三种或四种选择,第二个数字有三种或四种选择....最后层层递归下去,找到所有的组合。

拿‘23’来举例,上图:

递归的每一层循环的字母数组是不一样的,第一层是['a','b','c'],第二层就变成了['d','e','f'],所以解题的时候每一次递归要取相应的字母数组拿来循环取值,来看代码。

function letterCombinations(digits) {
    if (!digits.length) return []
    const result = []
    const numMap = { // 数字字母映射map
        2: ['a','b','c'],
        3: ['d','e','f'],
        4: ['g','h','i'],
        5: ['j','k','l'],
        6: ['m','n','o'],
        7: ['p','q','r','s'],
        8: ['t','u','v'],
        9: ['w','x','y','z']
    }
    const numArr = digits.split('')
    function loop(path, start) {
        if (path.length === digits.length) { // 达到目标条件
            result.push(path.slice())
            return
        }
        if (start > numArr.length-1) return // 指针越界就跳出
        const letterArr = numMap[numArr[start]] // 取到每一层循环的字母数组
        for (let i=0; i<letterArr.length; i++) {
            loop(`${path}${letterArr[i]}`, start+1) // 选择 包含了隐式撤销 然后进行递归
        }
    }
    loop('',0)
    return result
}

这道题难点在于每次递归循环中用来选择字符的数组是不一样的,这点能顺利解决的话这道题就迎刃而解了。

回溯算法到这里就告一段落了,刷题一路任重而道远,加油。