在回溯①中我们介绍了利用回溯解决组合排列问题,并总结了回溯问题的统一模板。
除了排列组合外,回溯还可以解决分割与子集问题。
- 分割问题:一个字符串按一定规则有几种切割方式
- 子集问题:一个N个数的集合里有多少符合条件的子集
接下来我们一起来看看分割问题和子集问题如何转换为N叉树并利用模板解决吧!
分割问题
LeetCode-131.分割回文串
给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。
示例:
输入: s = "aab"
输出: [["a","a","b"],["aa","b"]]
上图展示了如何将这个问题转化为树形结构,可以得出:
- 终止条件是候选集合为空
- for循环中,即进入当前层之后,要干的是分割,如果分割出来的是回文,则进入下一层,如果不是就可以直接跳到下一层是否是回文;
let res = []
/**
* @param {string} string 字符串
* @returns 是否回文串
*/
function isPalindrome(string) {
let left = 0;
let right = string.length - 1;
for (left; left < right; left++, right--) {
if (string[left] !== string[right]) return false;
}
return true;
}
/**
* @param {string} candidates
* @param {string[]} curSplitCombine
* @returns
*/
function backtracing(candidates, curSplitCombine) {
if (!candidates.length) {
res.push([...curSplitCombine]);
return;
}
for (let i = 0; i < candidates.length; i++) {
//分割
const splitStr = candidates.slice(0, i + 1);
//判断是否是回文
if (!isPalindrome(splitStr)) continue;
curSplitCombine.push(splitStr);
backtracing(candidates.slice(i + 1), curSplitCombine);
curSplitCombine.pop();
}
}
/**
* @param {string} s
* @return {string[][]}
*/
var partition = function (s) {
res = [];
backtracing(s, []);
return res;
};
LeetCode-93.复原 IP 地址
给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s 中插入 '.' 来形成。
一个有效的IP地址需要满足下列条件:
- 包含四个整数,用
.分隔 - 每个整数介于
[0,255]之间,且不能以0作为前导
实际上与上题相似,我们在每次分之前先判断分出的结果是否符合条件即可;
终止条件也十分好想,因为IP是由四个整数组成的,因此只需要切割三次就可以了;当然我们在收集结果的时候,要判断剩余的子串是否满足条件;
let res = [];
function checkIpBlock(blockStr) {
if (!blockStr.length) return false;
if (parseInt(blockStr) > 255) return false;
if (blockStr.length > 1 && blockStr[0] === '0') return false;
return true;
}
/**
* @param {string} candidates
* @param {*} curCombin
* @param {*} cutCount
* @returns {void}
*/
function backtracing(candidates, curCombin, cutCount) {
//终止条件 切了3次
if (cutCount === 3) {
//剩余子串不满足Ip要求直接返回
if (!checkIpBlock(candidates)) return;
//所有条件都满足,收集这组结果
let combine = "";
curCombin.forEach((str) => {
combine += str + ".";
})
combine += candidates;
res.push(combine);
return;
}
for (let i = 0; i < candidates.length; i++) {
//剪枝 切割长度超出3位
if (i >= 3) return;
const subStr = candidates.slice(0, i + 1);
if (!checkIpBlock(subStr)) {
continue;
}
//切割次数加一
cutCount++;
//收集切割出的串
curCombin.push(subStr);
//递归切割剩余子串
backtracing(candidates.slice(i + 1), curCombin, cutCount);
//回溯
curCombin.pop();
cutCount--;
}
return;
}
/**
* @param {string} s
* @return {string[]}
*/
var restoreIpAddresses = function (s) {
res = [];
backtracing(s, [], 0);
return res;
};
子集问题
LeetCode-78.子集
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)
示例
输入: nums = [1,2,3]
输出: [[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
想想子集与组合有什么关联?长度在[0,nums.length]的所有组合就是子集
在求组合时我们在达到某个长度后收集结果,而求子集时每个节点都要收集;
知道了这个代码就很好实现了:
let res = []
/**
* @param {number[]} candidates 候选数字集合
* @param {number[]} curCombin 当前已选组合
*/
function backtracing(candidates, curCombin) {
//结果的收集放在最前面
res.push([...curCombin]);
if (!candidates.length) {
return;
}
for (let i = 0; i < candidates.length; i++) {
curCombin.push(candidates[i]);
backtracing(candidates.slice(i + 1), curCombin)
curCombin.pop();
}
return;
}
/**
* @param {number[]} nums
* @return {number[][]}
*/
var subsets = function (nums) {
res = []
backtracing(nums, [], 0);
return res;
};
LeetCode-90.子集 II
给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。
这道题需要注意重复元素,而去重的方法也很简单:提前排序
let res = []
/**
*
* @param {number[]} candidates 候选数字集合
* @param {number[]} curCombin 当前组合
*/
function backtracing(candidates, curCombin) {
//每次进来都是一个正确的子集,收集
res.push([...curCombin]);
//终止条件
if (!candidates.length) {
return;
}
for (let i = 0; i < candidates.length; i++) {
//如果这次的待选数字与上次相同,那么说明以该数字开头的所有组合都在之前收集过了
if (i && candidates[i] === candidates[i - 1]) {
continue;
}
//记录
curCombin.push(candidates[i]);
//递归记录
backtracing(candidates.slice(i + 1), curCombin);
//回溯
curCombin.pop();
}
return;
}
/**
* @param {number[]} nums
* @return {number[][]}
*/
var subsetsWithDup = function (nums) {
res = [];
nums.sort((a, b) => a - b);
backtracing(nums, []);
return res;
};
排序的目的是为了让相同的元素出现在同一层!
LeetCode-491.递增子序列
给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。
示例:
输入: nums = [4,6,7,7]
输出: [[4,6],[4,6,7],[4,6,7,7],[4,7],[4,7,7],[6,7],[6,7,7],[7,7]]
- 终止条件:候选集合为空
- 收集条件:已选组合长度大于等于2
- 逻辑:
- 每次选择一个元素,要判断这个元素是否满足递增,否则就跳过;
- 此外,对于一个元素其向下递归会获得其所有可能的递增子序列,如果在选择这个数字的这一层中有重复元素,那么这个重复元素不需要再向下寻找了;
let res = []
/**
* @param {number[]} candidates 候选数字集合
* @param {number[]} curCombin 当前组合
* @param {Set} used 使用过的数字
*/
function backtracing(candidates, curCombine) {
if (curCombine.length >= 2) {
res.push([...curCombine]);
}
if (!candidates.length) {
return;
}
let layerUsed = []
for (let i = 0; i < candidates.length; i++) {
if (curCombine.length && candidates[i] < curCombine[curCombine.length - 1]) {
continue;
}
if (layerUsed.includes(candidates[i])) { continue; }
layerUsed.push(candidates[i]);
curCombine.push(candidates[i]);
backtracing(candidates.slice(i + 1), curCombine);
curCombine.pop();
}
return;
}
/**
* @param {number[]} nums
* @return {number[][]}
*/
var findSubsequences = function (nums) {
res = []
backtracing(nums, []);
return res;
};
其他回溯可解决问题
LeetCode-332.重新安排行程
描述:
给你一份航线列表 tickets ,其中 tickets[i] = [fromi, toi] 表示飞机出发和降落的机场地点。请你对该行程进行重新规划排序。
所有这些机票都属于一个从 JFK(肯尼迪国际机场)出发的先生,所以该行程必须从 JFK 开始。如果存在多种有效的行程,请你按字典排序返回最小的行程组合。
- 例如,行程
["JFK", "LGA"]与["JFK", "LGB"]相比就更小,排序更靠前。
假定所有机票至少存在一种合理的行程。且所有的机票 必须都用一次 且 只能用一次。
思考: 这一题实际上是图相关的题目,但实际上回溯就是在深度优先搜索的基础上实现的;
首先题目需要的是一个最小排序的形成,那么我们可以提前将tickets排序,这样只要回溯过程中获得一个结果就可以结束了(刚好我们之前二叉树学习过如何中断递归)
其次是我们需要构建好数据结构:题目所给数据结构位:tickets = [["JFK","SFO"],["JFK","ATL"],["SFO","ATL"],["ATL","JFK"],["ATL","SFO"]],而图相关问题一般我们需要使用邻接矩阵/邻接表
这里我们选择邻接表,因为邻接表更方便构造并且取值等操作更加方便:目标结构{JFK:[SFO,ATL],SFO:[ATL],ATL:[JFK,SFO]},构造好后还需要排序
根据题目描述:
- 终止条件为所有的票被使用完
- 返回值,由于我们需要中断循环,因此返回
true|false代表是否找到合理的行程; - 参数:我们需要知道已经使用过的票数
usedCount以及这次递归的起始位置是哪from - 逻辑:
- 每次递归遍历的是候选集合,这里的候选集合应该是当前起始位置的可达位置:
ticketsMap[from] - 在选择完一张票后,应该及时的删除该机票
- 每次递归遍历的是候选集合,这里的候选集合应该是当前起始位置的可达位置:
始发站已经确认!
let line = ["JFK"];
/**
* @typedef {object} ArrayObject
* @property {string[]} cityList
*/
/**
* 超时了,应该使用 hierholzer欧拉回路算法,但是回溯思路正确
* @param {object} ticketsMap 机票邻接表
* @param {string[]} curLine 当前航线
* @param {string} from 起始地
* @param {number} usedCount 使用过的机票
*/
function backtracing(ticketsMap, curLine, from, usedCount, totalCount) {
//如果所有机票都用完了,则结束,题目中已告知所有机票都应该被使用
if (totalCount === usedCount) {
line = [...curLine];
return true;
}
//当前from可到达的其他城市
const cityList = ticketsMap[from];
//如果无可达城市,且机票没用完,则该路不通
if (!cityList) return false;
for (let i = 0; i < cityList.length; i++) {
//下一站
const nextCity = cityList[i];
//收集航线信息
curLine.push(nextCity);
//删除该机票
cityList.splice(i, 1);
//递归收集航线
const hasLine = backtracing(ticketsMap, curLine, nextCity, usedCount + 1, totalCount);
//终端递归,找到了一条能够用完所有机票的航线,由于已经排序过了,找到的第一个就是字母排序最小的
if (hasLine) return true;
//回溯
cityList.splice(i, 0, nextCity)
curLine.pop();
}
return false;
}
/**
* @param {string[][]} tickets
* @return {string[]}
*/
var findItinerary = function (tickets) {
line = ["JFK"]
//构建邻接表
let ticketsMap = {};
for (let ticket of tickets) {
const [from, to] = ticket;
if (!ticketsMap[from]) {
ticketsMap[from] = []
}
ticketsMap[from].push(to);
}
//将邻接表中的每一个进行排序
for (let city in ticketsMap) {
ticketsMap[city].sort()
};
backtracing(ticketsMap, line, "JFK", 0, tickets.length);
return line;
};
这个解法没啥问题,就是超时了😅,应该使用欧拉回路算法;但是我还没有学,就先暂且搁置,等到了图专题再来解决;
LeetCode-51.N 皇后
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n ,返回所有不同的 **n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。
let res = []
//判断是否是同一列
function isSameColumn(pose, curPos) {
for (let i = 0; i < curPos.length; i++) {
if (curPos[i] === pose) {
return true;
}
}
return false;
}
//判断是否是同一斜线
function isSameSlash(pose, curPos, curRow) {
for (let i = 0; i < curPos.length; i++) {
//行差
const offset = Math.abs(curRow - i);
if (curPos[i] + offset === pose || curPos[i] - offset === pose) {
return true;
}
}
return false;
}
//获取某行的布局,n是列数, qPos是皇后的列下标
function getLayout(n, qPos) {
let rowRes = ''
for (let j = 0; j < n; j++) {
if (j === qPos) {
rowRes += 'Q';
} else {
rowRes += '.';
}
}
return rowRes;
}
/**
*
* @param {number} n N皇后
* @param {number} curRow 当前的行数
* @param {number[]} curPos 当前已经存放的位置,列下标
* @param {number[]} curRes 当前收集的结果集
*/
function backtracing(n, curRow, curPos, curRes) {
if (curRow === n) {
res.push([...curRes])
return;
}
for (let i = 0; i < n; i++) {
//是否相同列
if (isSameColumn(i, curPos)) {
continue;
}
//是否相同斜线
if (isSameSlash(i, curPos, curRow)) {
continue;
}
//放置Q
let rowRes = getLayout(n, i);
curRes.push(rowRes);
curPos.push(i)
curRow++;
//递归下一行
backtracing(n, curRow, curPos, curRes);
curRow--;
curPos.pop();
curRes.pop();
}
return;
}
/**
* @param {number} n
* @return {string[][]}
*/
var solveNQueens = function (n) {
res = []
backtracing(n, 0, [], []);
return res;
};