剪枝
排除那些不符合条件的分支。提高程序的运行效率。
回溯:
一层层递归,尝试搜素答案,
- 找到答案:返回结果,尝试其他的分支
- 找不到答案:返回上一层,尝试其他分支
回溯模版:
result = [];
function backtrack (path, list) {
if (满足条件) {
result.push(path);
return
}
for () {
// 单层逻辑
backtrack (path, list)
// 撤销选择 重置状态
}
}
回溯四部曲:
- 回溯参数
- 终止条件
- 单层递归逻辑
- 选择其他分支(撤销选择 重置状态)
473. 火柴拼正方形
(medium)
你将得到一个整数数组 matchsticks ,其中 matchsticks[i] 是第 i 个火柴棒的长度。你要用 所有的火柴棍 拼成一个正方形。你 不能折断 任何一根火柴棒,但你可以把它们连在一起,而且每根火柴棒必须 使用一次 。
如果你能使这个正方形,则返回 true ,否则返回 false 。
示例 1:
输入: matchsticks = [1,1,2,2,2]
输出: true
解释: 能拼成一个边长为2的正方形,每边两根火柴。
示例 2:输入: matchsticks = [3,3,3,3,4]
输出: false
解释: 不能用所有火柴拼成一个正方形。提示:
1 <= matchsticks.length <= 15
1 <= matchsticks[i] <= 108
- 思路 :排序nums数组,减少回溯的次数。不断尝试将nums中的元素放入4个桶中,如果都能放的下,则能拼成正方形
js:
//例子:[1,2,2,2,1]
var makesquare = function (nums) {
function backtrack(i, nums, edge, bucket) {
if (i >= nums.length) {//递归结束条件
return true;
}
for (let j = 0; j < 4; j++) {//循环4个桶
if (bucket[j] + nums[i] > edge) {//这个桶装不下 继续找下一个桶
continue;
}
bucket[j] += nums[i];//将当前元素加入桶中
if (backtrack(i + 1, nums, edge, bucket)) {//索引i加1 继续递归下一个nums中的元素
return true;//下一个元素能放进桶中
}
bucket[j] -= nums[i];//回溯状态
}
return false;//循环结束都没放进合适的桶 那不能构成正方形
}
if (nums.length < 4) {//nums长度小于4 直接不能构成正方形
return false;
}
let sum = 0;
for (let i = 0; i < nums.length; i++) {
sum += nums[i];
}
if (sum % 4) {//nums的和不能整除4 不能构成正方行
return false;
}
nums.sort((a, b) => b - a);//排序nums
let bucket = Array(4).fill(0);//准备4个桶
return backtrack(0, nums, sum / 4, bucket);//传入nums元素的索引i,nums,一个边长,和桶bucket
};
52. N皇后 II
(hard)
n 皇后问题 研究的是如何将 n 个皇后放置在 n × n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n ,返回 n 皇后问题 不同的解决方案的数量。
示例 1:
输入:n = 4
输出:2
解释:如上图所示,4 皇后问题存在两个不同的解法。
示例 2:输入:n = 1
输出:1提示:
1 <= n <= 9
方法1.位运算
js:
var totalNQueens = function (n) {
if (n < 1) return
let count = 0;
dfs(n, 0, 0, 0, 0)
return count
//n:皇后的数量
//row:当前行
//cols:放置皇后的位置
//diag1:可以攻击的左倾斜对角线
//diag2:可以攻击的右倾斜对角线
function dfs(n, row, cols, diag1, diag2) {
if (row >= n) {//递归终止 统计解法
count += 1;
return
}
//~(cols | diag1 | diag2):攻击的位置合起来 取反之后,1的位置就是可以放置皇后的位置
//(1 << n) - 1:从右向左,大于n的位置都变成0
//(~(cols | diag1 | diag2)) & ((1 << n) - 1):从右向左,可以放置皇后的位置,大于n的位置都变成0
let bits = (~(cols | diag1 | diag2)) & ((1 << n) - 1)
while (bits) {
let p = bits & -bits//取到从右向左第一个1
bits = bits & (bits - 1)//去掉从右向左第一个1
//列和两个对角线合上不可以放置的二进制位
dfs(n, row + 1, cols | p, (diag1 | p) << 1, (diag2 | p) >>> 1)
}
}
};
79. 单词搜索
(medium)
给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
示例 1:
输入:board = [[“A”,“B”,“C”,“E”],[“S”,“F”,“C”,“S”],[“A”,“D”,“E”,“E”]], word = “ABCCED”
输出:true
示例 2:
输入:board = [[“A”,“B”,“C”,“E”],[“S”,“F”,“C”,“S”],[“A”,“D”,“E”,“E”]], word = “SEE”
输出:true
示例 3:
输入:board = [[“A”,“B”,“C”,“E”],[“S”,“F”,“C”,“S”],[“A”,“D”,“E”,“E”]], word = “ABCB”
输出:false提示:
m == board.length
n = board[i].length
1 <= m, n <= 6
1 <= word.length <= 15
board 和 word 仅由大小写英文字母组成进阶:你可以使用搜索剪枝的技术来优化解决方案,使其在 board 更大的情况下可以更快解决问题?
-
思路:从上到下,左到右遍历网格,每个坐标递归调用
check(i, j, k)
函数,i,j表示网格坐标,k表示word的第k个字符,如果能搜索到第k个字符返回true,否则返回false,check函数的终止条件有2种情况- 如果i,j位置的字符和字符串位置k的字符不相等,则这条搜索路径搜索失败 返回false
- 如果搜索到了字符串的结尾,则找到了网格中的一条路径,这条路径上的字符正好可以组成字符串s
两种情况都不满足则把当前网格节点加入
visited
数组,
visited
表示节点已经访问过了,然后顺着当前网格坐标的四个方向继续尝试,如果没找到k开始的子串,则回溯状态
visited[i] [j] = false
,继续后面的尝试。 -
复杂度分析:时间复杂度
O(MN⋅3^L)
,M,N 为网格的长度与宽度,L 为字符串 word 的长度,第一次调用
check
函数的时候,进行4个方向的检查,其余坐标的节点都是3个方向检查,走过来的分支不会反方向回去,所以
check
函数的时间复杂度是
3^L
,而网格有
M*N
个坐标,且存在剪枝,所以最坏的情况下时间复杂度是
O(MN⋅3^L)
。空间复杂度是
O(MN)
,
visited
数组空间是
O(MN)
,
check
递归栈的最大深度在最坏的情况下是
O(MN)
方法1:回溯
Js:
var exist = function(board, word) {
const h = board.length, w = board[0].length;//网格的长和宽
const directions = [[0, 1], [0, -1], [1, 0], [-1, 0]];//方向数组
const visited = new Array(h);//标记是否访问过的数组
for (let i = 0; i < visited.length; ++i) {//初始化visited数组
visited[i] = new Array(w).fill(false);
}
const check = (i, j, s, k) => {//检查从网格i,j出发是否能搜索到0-k的字符组成的子串
//如果i,j位置的字符和第k个的字符不相等,则这条搜索路径搜索失败 返回false
if (board[i][j] != s.charAt(k)) {
return false;
//如果搜索到了字符串的结尾,则找到了网格中的一条路径,这条路径上的字符正好可以组成字符串s
} else if (k == s.length - 1) {
return true;
}
visited[i][j] = true;//标记i,j被访问过了
let result = false;
for (const [dx, dy] of directions) {//向i,j的四个方向继续尝试寻找
let newi = i + dx, newj = j + dy;
if (newi >= 0 && newi < h && newj >= 0 && newj < w) {//新的坐标位置合法检查
if (!visited[newi][newj]) {//新的坐标不能存在于visited中,也就是不能是访问过的
const flag = check(newi, newj, s, k + 1);//继续检查新的坐标
if (flag) {//如果在网格中找到了字符串 则跳过循环
result = true;
break;
}
}
}
}
visited[i][j] = false;//回溯状态
return result;//返回结果
}
for (let i = 0; i < h; i++) {
for (let j = 0; j < w; j++) {
const flag = check(i, j, word, 0);
if (flag) {
return true;
}
}
}
return false;
};
17. 电话号码的字母组合
(medium)
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例 1:
输入:digits = “23”
输出:[“ad”,“ae”,“af”,“bd”,“be”,“bf”,“cd”,“ce”,“cf”]
示例 2:输入:digits = “”
输出:[]
示例 3:输入:digits = “2”
输出:[“a”,“b”,“c”]提示:
0 <= digits.length <= 4
digits[i] 是范围 [‘2’, ‘9’] 的一个数字。
方法1.dfs+回溯
- 思路:深度优先遍历,遍历函数传入每一层形成的字符串和一个指向字符的位置指针,打给你指针的位置到达字符串的结尾时,将形成的字符串加入结果数组,递归的每一层遍历这一层的数字对应的字符,然后传入新的字符,指针向后移动一次,不断递归
-
复杂度:时间复杂度
O(3^m * 4^n)
,m,n分别是三个字母和四个字母对应的数字个数。空间复杂度
O(m+n)
,递归栈的深度,最大为
m+n
js:
//输入:digits = "23"
//输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
var letterCombinations = (digits) => {
if (digits.length == 0) return [];
const res = [];
const map = {//建立电话号码和字母的映射关系
2: "abc",
3: "def",
4: "ghi",
5: "jkl",
6: "mno",
7: "pqrs",
8: "tuv",
9: "wxyz",
};
const dfs = (curStr, i) => {//curStr是递归每一层的字符串,i是扫描的指针
if (i > digits.length - 1) {//边界条件,递归的出口
res.push(curStr); //其中一个分支的解推入res
return; //结束递归分支,进入另一个分支
}
const letters = map[digits[i]]; //取出数字对应的字母
for (const l of letters) {
//进入不同字母的分支
dfs(curStr + l, i + 1); //参数传入新的字符串,i右移,继续递归
}
};
dfs("", 0); // 递归入口,传入空字符串,i初始为0的位置
return res;
};
方法2.bfs
- 思路:用队列广度优先遍历,先循环数字数组,然后取出对应的字母,与当前层的字符串组成新的字符串加入队列,遍历完成之后,队列的最后一层就是解。
-
复杂度:时间复杂度
O(3^m * 4^n)
,m,n分别是三个字符和四个字母对应的数组个数。空间复杂度
O(3^m * 4^n)
,队列的空间大小,最大为
3^m * 4^n
js:
var letterCombinations = (digits) => {
if (digits.length == 0) return [];
const map = {
2: "abc",
3: "def",
4: "ghi",
5: "jkl",
6: "mno",
7: "pqrs",
8: "tuv",
9: "wxyz",
};
const queue = [];
queue.push("");
for (let i = 0; i < digits.length; i++) {//循环数字的每个字符
const levelSize = queue.length; //当前层的节点个数
for (let j = 0; j < levelSize; j++) {
const curStr = queue.shift(); //当前层的字符串
const letters = map[digits[i]];//获取数字对应的字母字符
for (const l of letters) {
queue.push(curStr + l); //新生成的字符串入列
}
}
}
return queue; //最后一层生成的字符串就是解
};
51. N 皇后
(hard)
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。
示例 1:
输入:n = 4
输出:[[“.Q…”,“…Q”,“Q…”,“…Q.”],[“…Q.”,“Q…”,“…Q”,“.Q…”]]
解释:如上图所示,4 皇后问题存在两个不同的解法。
示例 2:输入:n = 1
输出:[[“Q”]]提示:
1 <= n <= 9
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/n-queens
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
方法1.回溯
- 思路:从上到下,从左到右遍历棋盘,准备好三个set分别记录列和两个对角线可以攻击到的坐标,尝试在每个空位放置皇后,放置之后更新三个可以攻击到的set坐标,然后继续下一层遍历,完成下一层之后,尝试回溯当前层,也就是撤销当前层放置的皇后,同时撤销三个可以攻击到的set坐标,不断回溯,直到遍历完成,找到所有可能的解。
-
复杂度分析:时间复杂度:
O(N!)
,其中 N 是皇后数量,由于每个皇后必须位于不同列,因此已经放置的皇后所在的列不能放置别的皇后。第一个皇后有 N 列可以选择,第二个皇后最多有 N-1列可以选择…。空间复杂度:
O(N)
,其中 N 是皇后数量,空间复杂度主要取决于递归调用层数、记录每行放置的皇后列下标的数组以及三个集合,递归调用层数不会超过 N,数组的长度为 N,每个集合的元素个数都不会超过 N。
js:
const solveNQueens = (n) => {
const board = new Array(n);
for (let i = 0; i < n; i++) {
board[i] = new Array(n).fill('.');//生成board
}
const cols = new Set(); // 列集,记录出现过皇后的列
const diag1 = new Set(); // 正对角线集
const diag2 = new Set(); // 反对角线集
const res = [];//结果数组
const backtrack = (row) => {
if (row == n) {//终止条件
const stringsBoard = board.slice();
for (let i = 0; i < n; i++) {//生成字符串
stringsBoard[i] = stringsBoard[i].join('');
}
res.push(stringsBoard);
return;
}
for (let col = 0; col < n; col++) {
// 如果当前点的所在的列,所在的对角线都没有皇后,即可选择,否则,跳过
if (!cols.has(col) && !diag1.has(row + col) && !diag2.has(row - col)) {
board[row][col] = 'Q'; // 放置皇后
cols.add(col); // 记录放了皇后的列
diag2.add(row - col); // 记录放了皇后的正对角线
diag1.add(row + col); // 记录放了皇后的负对角线
backtrack(row + 1);
board[row][col] = '.'; // 撤销该点的皇后
cols.delete(col); // 对应的记录也删一下
diag2.delete(row - col);
diag1.delete(row + col);
}
}
};
backtrack(0);
return res;
};
36. 有效的数独
(medium)
请你判断一个 9 x 9 的数独是否有效。只需要 根据以下规则 ,验证已经填入的数字是否有效即可。
数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3×3 宫内只能出现一次。(请参考示例图)注意:
一个有效的数独(部分已被填充)不一定是可解的。
只需要根据以上规则,验证已经填入的数字是否有效即可。
空白格用 ‘.’ 表示。示例 1:
输入:board =
[[“5”,“3”,“.”,“.”,“7”,“.”,“.”,“.”,“.”]
,[“6”,“.”,“.”,“1”,“9”,“5”,“.”,“.”,“.”]
,[“.”,“9”,“8”,“.”,“.”,“.”,“.”,“6”,“.”]
,[“8”,“.”,“.”,“.”,“6”,“.”,“.”,“.”,“3”]
,[“4”,“.”,“.”,“8”,“.”,“3”,“.”,“.”,“1”]
,[“7”,“.”,“.”,“.”,“2”,“.”,“.”,“.”,“6”]
,[“.”,“6”,“.”,“.”,“.”,“.”,“2”,“8”,“.”]
,[“.”,“.”,“.”,“4”,“1”,“9”,“.”,“.”,“5”]
,[“.”,“.”,“.”,“.”,“8”,“.”,“.”,“7”,“9”]]
输出:true
示例 2:输入:board =
[[“8”,“3”,“.”,“.”,“7”,“.”,“.”,“.”,“.”]
,[“6”,“.”,“.”,“1”,“9”,“5”,“.”,“.”,“.”]
,[“.”,“9”,“8”,“.”,“.”,“.”,“.”,“6”,“.”]
,[“8”,“.”,“.”,“.”,“6”,“.”,“.”,“.”,“3”]
,[“4”,“.”,“.”,“8”,“.”,“3”,“.”,“.”,“1”]
,[“7”,“.”,“.”,“.”,“2”,“.”,“.”,“.”,“6”]
,[“.”,“6”,“.”,“.”,“.”,“.”,“2”,“8”,“.”]
,[“.”,“.”,“.”,“4”,“1”,“9”,“.”,“.”,“5”]
,[“.”,“.”,“.”,“.”,“8”,“.”,“.”,“7”,“9”]]
输出:false
解释:除了第一行的第一个数字从 5 改为 8 以外,空格内其他数字均与 示例1 相同。 但由于位于左上角的 3×3 宫内有两个 8 存在, 因此这个数独是无效的。提示:
board.length == 9
board[i].length == 9
board[i][j] 是一位数字(1-9)或者 ‘.’
方法1:回溯
- 思路:准备行、列、3 * 3小方块,三个哈希表或者set或者9 * 9的二维数组,都可以,只要能判重复即可,从上到下,从左到右循环,依次检查行、列、3 * 3小方块中是否有重复的数字,如果有则返回false,然后更新哈希表或者set。
-
复杂度分析:时间复杂度:
O(1)
,数独共有 81 个单元格,每个单元格遍历一次即可。空间复杂度:
O(1)
,数独的大小固定,因此哈希表的空间也是固定的。
Js:
var isValidSudoku = function(board) {
// 方向判重
let rows = {};//行
let columns = {};//列
let boxes = {};//3*3小方块
// 遍历数独
for(let i = 0;i < 9;i++){
for(let j = 0;j < 9;j++){
let num = board[i][j];
if(num != '.'){//遇到有效的数字
let boxIndex = parseInt((i/3)) * 3 + parseInt(j/3);// 子数独序号
if(rows[i+'-'+num] || columns[j+'-'+num] || boxes[boxIndex+'-'+num]){//重复检测
return false;
}
// 方向 + 数字 组成唯一键值,若出现第二次,即为重复
// 更新三个对象
rows[i+'-'+num] = true;
columns[j+'-'+num] = true;
boxes[boxIndex+'-'+num] = true;
}
}
}
return true;
};
46. 全排列
(medium)
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
示例 1:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
示例 2:输入:nums = [0,1]
输出:[[0,1],[1,0]]
示例 3:输入:nums = [1]
输出:[[1]]提示:
1 <= nums.length <= 6
-10 <= nums[i] <= 10
nums 中的所有整数 互不相同
- 思路:准备path数组,存放每一个回溯递归的分支中的数字排列,调用回溯函数 传入nums,nums长度,used数组,used表示已经使用的数字,回溯函数中循环nums中的数,每层循环将nums中的元素加入path中,然后递归调用回溯函数,调用完成之后,回溯之前的状态,当path数组的长度和nums的长度相同就找到了一种排列。
-
复杂度:时间复杂度
O(n*n!)
。空间复杂度
O(n)
,递归栈深度
js:
var permute = function(nums) {
const res = [], path = [];
backtracking(nums, nums.length, []);//调用回溯函数 传入nums,nums长度,used数组
return res;
function backtracking(n, k, used) {
if(path.length === k) {//递归终止条件
res.push(Array.from(path));
return;
}
for (let i = 0; i < k; i++ ) {
if(used[i]) continue;//已经使用过了就跳过本轮循环
path.push(n[i]);
used[i] = true;
backtracking(n, k, used);//递归
path.pop();//回溯 将push进的元素pop出来 然后标记成未使用 继续其他分支
used[i] = false;
}
}
};
22. 括号生成
(medium)
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
示例 1:
输入:n = 3
输出:[“((()))”,“(()())”,“(())()”,“()(())”,“()()()”]
示例 2:输入:n = 1
输出:[“()”]提示:
1 <= n <= 8
方法1:暴力
复杂度分析:时间复杂度
O(2^2n*n)
,字符串的长度为
2n
,每个位置有两种选择,选择左或者右括号,验证字符串是否有效复杂度
O(n)
,剪枝之后会优化,最坏的情况是
O(2^2n*n)
。空间复杂度
O(n)
,递归次数最多2n
方法2.递归dfs
-
思路:采用递归,终止条件是字符串的长度等于
2n
,递归函数传入构建的字符串,左右括号剩余多少,每个位置有两种选择,选择左或者右括号,这里可以进行剪枝优化,只有右括号的保有数量大于左括号的保有数量,才能选右括号,否则肯定不能构成有效括号
Js:
const generateParenthesis = (n) => {
const res = []; // 输出的结果数组
const generate = (str, left, right) => {
if (str.length == 2 * n) { // 字符串构建完成
res.push(str); // 将字符串加入res
return; // 结束当前递归(结束当前搜索分支)
}
if (left > 0) { // 只要左括号有剩,可以选它,继续递归做选择
generate(str + '(', left - 1, right);
}
if (right > left) { // 右括号的保有数量大于左括号的保有数量,才能选右括号
generate(str + ')', left, right - 1);
}
};
generate('', n, n); // 递归的入口,初始字符串是空字符串,初始括号数量都是n
return res;
};
方法3.回溯
- 思路:当左括号剩下的多,说明字符串中的左括号数量少于右括号,不合法,对字符串尝试添加左括号,然后回溯,尝试添加右括号,然后尝试回溯
Js:
var generateParenthesis = function(n) {
if (n == 0) return []
const res = []
let track = []
backtrack(n, n, track, res)
return res
function backtrack(left, right, track, res) {
// 数量小于0,不合法
if (left < 0 || right < 0) return
// 若左括号剩下的多,说明不合法
if (right < left) return
// 所有括号用完,得到合法组合
if (left == 0 && right == 0) {
res.push(track.join(''))
return
}
// 尝试添加左括号
track.push('(')
//这个地方一定要注意 需要拷贝一份track,也就是采用[...track], 不然会影响其他分支
backtrack(left - 1, right, [...track], res)
track.pop()
// 尝试添加右括号
track.push(')')
backtrack(left, right - 1, [...track], res)
track.pop()
}
};
37. 解数独
(hard)
编写一个程序,通过填充空格来解决数独问题。
数独的解法需 遵循如下规则:
数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3×3 宫内只能出现一次。(请参考示例图)
数独部分空格内已填入了数字,空白格用 ‘.’ 表示。示例 1:
输入:board = [[“5”,“3”,“.”,“.”,“7”,“.”,“.”,“.”,“.”],[“6”,“.”,“.”,“1”,“9”,“5”,“.”,“.”,“.”],[“.”,“9”,“8”,“.”,“.”,“.”,“.”,“6”,“.”],[“8”,“.”,“.”,“.”,“6”,“.”,“.”,“.”,“3”],[“4”,“.”,“.”,“8”,“.”,“3”,“.”,“.”,“1”],[“7”,“.”,“.”,“.”,“2”,“.”,“.”,“.”,“6”],[“.”,“6”,“.”,“.”,“.”,“.”,“2”,“8”,“.”],[“.”,“.”,“.”,“4”,“1”,“9”,“.”,“.”,“5”],[“.”,“.”,“.”,“.”,“8”,“.”,“.”,“7”,“9”]]
输出:[[“5”,“3”,“4”,“6”,“7”,“8”,“9”,“1”,“2”],[“6”,“7”,“2”,“1”,“9”,“5”,“3”,“4”,“8”],[“1”,“9”,“8”,“3”,“4”,“2”,“5”,“6”,“7”],[“8”,“5”,“9”,“7”,“6”,“1”,“4”,“2”,“3”],[“4”,“2”,“6”,“8”,“5”,“3”,“7”,“9”,“1”],[“7”,“1”,“3”,“9”,“2”,“4”,“8”,“5”,“6”],[“9”,“6”,“1”,“5”,“3”,“7”,“2”,“8”,“4”],[“2”,“8”,“7”,“4”,“1”,“9”,“6”,“3”,“5”],[“3”,“4”,“5”,“2”,“8”,“6”,“1”,“7”,“9”]]
解释:输入的数独如上图所示,唯一有效的解决方案如下所示:
提示:
board.length == 9
board[i].length == 9
board[i][j] 是一位数字或者 ‘.’
题目数据 保证 输入数独仅有一个解
- 思路:循环行和列,尝试在每个位置放置1-9,并检验合法性,包括行、列、3 * 3方块的合法性,如果合法继续循环,直到找到一个合法的解,如果不合法,则回溯状态,并继续尝试其他的可能性
- 复杂度分析:同36题
js:
var solveSudoku = function(board) {
function isValid(row, col, val, board) {
let len = board.length
// 行中的数字不能重复
for(let i = 0; i < len; i++) {
if(board[row][i] === val) {
return false
}
}
// 列中的数字不能重复
for(let i = 0; i < len; i++) {
if(board[i][col] === val) {
return false
}
}
let startRow = Math.floor(row / 3) * 3
let startCol = Math.floor(col / 3) * 3
//方块中的数字不能重复
for(let i = startRow; i < startRow + 3; i++) {
for(let j = startCol; j < startCol + 3; j++) {
if(board[i][j] === val) {
return false
}
}
}
return true
}
function backTracking() {//回溯函数
for(let i = 0; i < board.length; i++) {
for(let j = 0; j < board[0].length; j++) {//循环行和列
if(board[i][j] !== '.') continue
for(let val = 1; val <= 9; val++) {//尝试在当前单元格放置1-9
if(isValid(i, j, `${val}`, board)) {//判断放置数字的合法性
board[i][j] = `${val}`//放置数字
if (backTracking()) {//合法返回ture
return true
}
board[i][j] = `.`//不合法回溯状态
}
}
return false//1-9的数字都不合法,返回false
}
}
return true//全部可能性都尝试完成 返回true 说明有解
}
backTracking()
return board
};
77. 组合
(medium)
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
你可以按 任何顺序 返回答案。
示例 1:
输入:n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
示例 2:输入:n = 1, k = 1
输出:[[1]]提示:
1 <= n <= 20
1 <= k <= n
-
思路:回溯函数传入n,k和选择的元素位置startIndex,在每层递归中,从startIndex开始循环到
n - (k - path.length) + 1
的位置,将这些数加入path,然后startIndex加1,继续递归函数进入下一个分支,完成调用之后回溯状态,当path的长度等于k的时候终止这层分支,加入结果中。 -
复杂度:时间复杂度:
O(C(n, k) * k)
,枚举结果总数为
C(n, k)
,每次得到一个结果需要
O(k)
时间。空间复杂度:
O(n)
,最大是n层递归栈。
js:
const combine = (n, k) => {
const res = [];
const helper = (startIndex, path) => { //startIndex表示搜索的起点位置 path是每条分支的一个组合)
if (path.length == k) {
res.push(path.slice()); //需要拷贝一份 避免受其他分支的影响
return;
}
for (let i = startIndex; i <= n - (k - path.length) + 1; i++) {//剪枝
path.push(i); //加入path
helper(i + 1, path); //下一层递归
path.pop(); //回溯状态
}
};
helper(1, []); //递归入口
return res;
}
视频讲解:
传送门