From b2904bc0bf1b93f7ec6e6c0e82649e1bd0a7f5ae Mon Sep 17 00:00:00 2001 From: yk Date: Thu, 21 Mar 2024 20:24:11 +0800 Subject: [PATCH] dfs --- ...2\345\275\222-\345\233\236\346\272\257.md" | 790 ++++++++++++++++-- .../\344\275\215\350\277\220\347\256\227.md" | 2 + 2 files changed, 707 insertions(+), 85 deletions(-) diff --git "a/content/posts/algorithm/trick/\346\232\264\345\212\233\351\200\222\345\275\222-\345\233\236\346\272\257.md" "b/content/posts/algorithm/trick/\346\232\264\345\212\233\351\200\222\345\275\222-\345\233\236\346\272\257.md" index 0e23c9d..6127ec7 100644 --- "a/content/posts/algorithm/trick/\346\232\264\345\212\233\351\200\222\345\275\222-\345\233\236\346\272\257.md" +++ "b/content/posts/algorithm/trick/\346\232\264\345\212\233\351\200\222\345\275\222-\345\233\236\346\272\257.md" @@ -1,14 +1,69 @@ --- -title: '暴力递归-回溯' +title: '暴力递归-dfs&回溯' date: 2022-10-09T20:49:13+08:00 -lastmod: 2024-03-07 +lastmod: 2024-03-27 series: [trick] categories: [algorithm] --- -## 排列组合(permutation and combination) +## 概念 -一些经验: +刚开始学习算法的时候,看了某大佬讲解回溯算法和 dfs 的区别: + +- **dfs** 关注点在 「节点」 +- **回溯** 关注点在 「树枝」 + +对应的代码如下: + +```js +// DFS 算法把「做选择」「撤销选择」的逻辑放在 for 循环外面 +var dfs = function (root) { + if (root == null) return + // 做选择 + console.log('我已经进入节点 ' + root + ' 啦') + for (var i in root.children) { + dfs(root.children[i]) + } + // 撤销选择 + console.log('我将要离开节点 ' + root + ' 啦') +} + +// 回溯算法把「做选择」「撤销选择」的逻辑放在 for 循环里面 +var backtrack = function (root) { + if (root == null) return + for (var i in root.children) { + // 做选择 + console.log('我站在节点 ' + root + ' 到节点 ' + root.children[i] + ' 的树枝上') + backtrack(root.children[i]) + // 撤销选择 + console.log('我将要离开节点 ' + root.children[i] + ' 到节点 ' + root + ' 的树枝上') + } +} +``` + +以上代码不要死记硬背,更不要被唬到了,想想一下二叉树的递归序,一切就都了然了 --- **不过是多叉树没明着写出来所有分支,改用了一个 for 循环罢了**。 + +--- + +后面在 B 站看了左神的算法课,左神说过这么一句话:国外根本就不存在什么回溯的说法,就是暴力递归。不知道此话的真假,抱着怀疑的态度,我问了下 chatGPT: + +> 本质区别 +> +> - dfs 就是沿着树的深度尽可能远的搜索,直到到达叶节点 +> - 回溯则是一种搜索策略,发现路径不符合条件或者找到解则返回上一步继续尝试其他路径,直到遍历完所有可能的路径或者找到解 + + + +dfs 在遍历树、图等等场景应用非常广泛,而回溯算法则更善于解决「排列组合」类的问题(排列有序,组合无序),本文主要练习一下回溯算法。 + +--- + +### 回溯练习-排列组合 + + -> 其实这种回溯,与 dfs 很相像,只不过回溯是遍历树枝,而 dfs 是遍历节点。 -> backtrack 中 for 循环以外都是节点操作位置,for 循环内是树枝的操作位置 - -另外涉及到递归都需要注意终止条件哦 👻。 - -### [39.组合总和](https://leetcode.cn/problems/combination-sum/) - -关键点: - -1. 无重复元素 -2. 元素可以重复选。 +#### lc.39 组合总和 ```js /** @@ -38,22 +84,35 @@ categories: [algorithm] * @return {number[][]} */ var combinationSum = function (candidates, target) { + /** + * 无重复元素,所以不用剪枝 + * 可以被重复选取,那么就是类似这么个树 + * 1 2 3 + * 1 2 3 1 2 3 1 2 3 + * ..... ..... .... + */ const res = [] const track = [] let sum = 0 - const backtrack = start => { + /** + * 仍然不要忘记定义递归 + * 输入:层级 level,输出 void; 在递归过程中把可行解塞入 res + * 结束条件,这道题比较明显 sum === target + */ + const backtrack = level => { if (sum === target) { - res.push([...track]) + res.push([...track]) // 注意拷贝一下 return } - if (sum > target) return - for (let i = start; i < candidates.length; ++i) { - const v = candidates[i] - sum += v - track.push(v) - backtrack(i) - sum -= v + if (sum > target) return // 结束条件不要忘了~ + // level 在这里的含义是1.在 level 层, 2.在候选数据 [level...end] 区间内做选择,通过保证元素的相对顺序,来避免重复的组合 + // 如果每次都从 0 开始,那么 可能会产生 [1,2] [2,1] 这样重复的组合,不过这对排列来说,是有用的 + for (let i = level; i < candidates.length; ++i) { + track.push(candidates[i]) + sum += candidates[i] + backtrack(i) // 可重复使用,i + 1 则是在下一层排除了自己 track.pop() + sum -= candidates[i] } } backtrack(0) @@ -61,39 +120,38 @@ var combinationSum = function (candidates, target) { } ``` -### [40.组合总和 II](https://leetcode.cn/problems/combination-sum-ii/) +此题完全弄懂之后,排列组合就都是纸老虎了。 -关键点: +#### lc.40 组合总和 II -1. 有重复元素 -2. 不可以重复选择 -3. 要求 不能包含重复的组合 + ```js /** + * 与 lc.39 唯二不同,1.有重复元素,2.不能复用元素 * @param {number[]} candidates * @param {number} target * @return {number[][]} */ var combinationSum2 = function (candidates, target) { - candidates.sort((a, b) => a - b) const res = [] const track = [] let sum = 0 - const backtrack = start => { + candidates.sort((a, b) => a - b) + const backtrack = level => { if (sum === target) { res.push([...track]) return } - for (let i = start; i < candidates.length; ++i) { - if (sum + candidates[i] > target) continue - if (i > start && candidates[i] === candidates[i - 1]) continue - const v = candidates[i] - track.push(v) - sum += v - backtrack(i + 1) + if (sum > target) return + for (let i = level; i < candidates.length; ++i) { + // 因为元素有重复的,所以需要先进行 「排序」,然后进行剪枝 + if (i > level && candidates[i] === candidates[i - 1]) continue + track.push(candidates[i]) + sum += candidates[i] + backtrack(i + 1) // 不能复用元素,下一层 level 不能包扩 i 自身 + sum -= candidates[i] track.pop() - sum -= v } } backtrack(0) @@ -101,14 +159,41 @@ var combinationSum2 = function (candidates, target) { } ``` -### [77.组合](https://leetcode.cn/problems/combinations/) +#### lc.216 组合总和 III -这道题没有直接说出是否有重复元素,是否可以复选需要我们自己审题提炼。 +```js +/** + * @param {number} k + * @param {number} n + * @return {number[][]} + */ +var combinationSum3 = function (k, n) { + const res = [] + const track = [] + let sum = 0 + const backtrack = level => { + if (sum === n && track.length === k) { + res.push([...track]) + return + } + if (sum > n) return + for (let i = level; i < 10; ++i) { + if (track.length > k) continue + track.push(i) + sum += i + backtrack(i + 1) + track.pop() + sum -= i + } + } + backtrack(1) + return res +} +``` -关键点: +#### lc.77 组合 -1. 无重复元素 -2. 不可以复选 + ```js /** @@ -119,12 +204,12 @@ var combinationSum2 = function (candidates, target) { var combine = function (n, k) { const res = [] const track = [] - const backtrack = start => { + const backtrack = level => { if (track.length === k) { res.push([...track]) return } - for (let i = start; i <= n; ++i) { + for (let i = level; i <= n; ++i) { track.push(i) backtrack(i + 1) track.pop() @@ -135,12 +220,13 @@ var combine = function (n, k) { } ``` -### [78.子集](https://leetcode.cn/problems/subsets/) +索然无味的一题~ -关键点: +--- + +#### lc.78 子集 -1. 无重复元素 -2. 不可复选 + ```js /** @@ -148,13 +234,21 @@ var combine = function (n, k) { * @return {number[][]} */ var subsets = function (nums) { - const res = [] + // root + // 1 2 3 + // 2 3 3 + // 3 + const res = [[]] const track = [] - const backtrack = start => { - res.push([...track]) + const backtrack = level => { + // res.push([...track]) 在这里加入 是另一种无需 res 提前 [[]] + // 此处是在「节点」操作区 if (track.length === nums.length) return - for (let i = start; i < nums.length; ++i) { + for (let i = level; i < nums.length; ++i) { track.push(nums[i]) + // 在这里加入 需要 res 需要提前加一个空 [] + // 此处是 「树枝」操作区 + res.push([...track]) backtrack(i + 1) track.pop() } @@ -164,14 +258,9 @@ var subsets = function (nums) { } ``` -> 这道题的递归结束条件,看似没有设置,其实是通过 `start` 来控制的,若 `start >= nums.length` 是不会进入 `for` 循环的,也就结束的递归 - -### [90.子集 II](https://leetcode.cn/problems/subsets-ii/) +#### lc.90 子集 II -关键点: - -1. 有重复元素 -2. 不可重复选择 + ```js /** @@ -179,14 +268,14 @@ var subsets = function (nums) { * @return {number[][]} */ var subsetsWithDup = function (nums) { - nums.sort((a, b) => a - b) const res = [] const track = [] - const backtrack = start => { + nums.sort((a, b) => a - b) + const backtrack = level => { res.push([...track]) - if (track.length === nums.length) return - for (let i = start; i < nums.length; ++i) { - if (i > start && nums[i] === nums[i - 1]) continue + if (level === nums.length) return + for (let i = level; i < nums.length; ++i) { + if (i > level && nums[i] === nums[i - 1]) continue track.push(nums[i]) backtrack(i + 1) track.pop() @@ -197,12 +286,16 @@ var subsetsWithDup = function (nums) { } ``` -### [46.全排列](https://leetcode.cn/problems/permutations/) +--- -关键点: +#### lc.46 全排列 -1. 无重复元素 -2. 不可复选 + + +排列和组合最大的区别是: + +- 组合无序,[1,2]和 [2,1] 是同一个组合,所以需要 level 来控制 +- 排序有序,所以每次都是从 level-0 开始,但是不能重复使用元素,就需要一个 「used」(可以为一个简单的 boolean[]数组,也可以为一个栈) 来进行剪枝操作 ```js /** @@ -212,20 +305,19 @@ var subsetsWithDup = function (nums) { var permute = function (nums) { const res = [] const track = [] - const uesd = [] + const used = [] const backtrack = () => { if (track.length === nums.length) { res.push([...track]) return } for (let i = 0; i < nums.length; ++i) { - const v = nums[i] - if (uesd.includes(v)) continue - track.push(v) - uesd.push(v) + if (used[i]) continue // 剪枝 + track.push(nums[i]) + used[i] = true backtrack() track.pop() - uesd.pop() + used[i] = false } } backtrack() @@ -233,12 +325,9 @@ var permute = function (nums) { } ``` -### [47.全排列 II](https://leetcode.cn/problems/permutations-ii/) +#### lc.47 全排列 II -关键点: - -1. 有重复元素 -2. 不可复选 + ```js /** @@ -246,18 +335,18 @@ var permute = function (nums) { * @return {number[][]} */ var permuteUnique = function (nums) { - nums.sort((a, b) => a - b) const res = [] const track = [] const used = [] + nums.sort((a, b) => a - b) const backtrack = () => { if (track.length === nums.length) { res.push([...track]) return } for (let i = 0; i < nums.length; ++i) { - if (used[i]) continue - if (i > 0 && nums[i] === nums[i - 1] && !used[i - 1]) continue // 新的剪枝逻辑 就是 保证相同元素的顺序固定不变 + if (used[i]) continue // 去除自身剪枝 + if (i > 0 && nums[i] === nums[i - 1] && !used[i - 1]) continue // 关键 track.push(nums[i]) used[i] = true backtrack() @@ -269,3 +358,534 @@ var permuteUnique = function (nums) { return res } ``` + +上方 `!used[i-1]` 是为了去除重复的排列,当同一层前后两个元素相同时,如果前一个元素没有使用,那么就 continue,这样做的结果就是会让 `[2,2',2'']` 这样的数组保持固定的顺序,即 2 一定在 2' 前,2' 一定在 2'' 前。如果改为 `used[i-1]` 也能得到去重的效果,就是固定顺序为 2'' -> 2' -> 2,但是剪枝的效率会大大折扣,可以参考 labuladong 大佬的示意图理解。 + +
+ + +
+ +### 经典题 + +#### lc.51 N 皇后 + +```js +/** + * @param {number} n + * @return {string[][]} + */ +var solveNQueens = function (n) { + // 这道题是二维的 track,所以得先初始化一个二维棋盘再说 + // 同时类似组合,需要 level 来控制深度;又似排列,需要每次都遍历每一行的每一个 + const res = [] + const board = Array.from(Array(n), () => Array(n).fill('.')) + const backtrack = level => { + if (level === n) { + res.push([...board.map(item => item.join(''))]) + return + } + for (let i = 0; i < n; ++i) { + //剪枝操作 + if (!isValid(board, level, i)) continue + board[level][i] = 'Q' + backtrack(level + 1) + board[level][i] = '.' + } + } + backtrack(0) + return res +} +/** + * 判断是否合格,因为是从上往下铺的,所以只判断左上,上,右上即可 + */ +function isValid(board, row, col) { + for (let i = 0; i < row; ++i) { + if (board[i][col] === 'Q') return false + } + for (let i = row - 1, j = col - 1; i >= 0; --i, --j) { + if (board[i][j] === 'Q') return false + } + for (let i = row - 1, j = col + 1; i >= 0; --i, ++j) { + if (board[i][j] === 'Q') return false + } + return true +} +``` + + + +#### lc.698 划分为 k 个相等的子集 + +给你输入一个数组 nums 和一个正整数 k,请你判断 nums 是否能够被平分为元素和相同的 k 个子集。 + +典中典,也是深入理解回溯的绝佳好题,必会必懂。 + +```js +// lc.416 分割两个等和子集,可以用动态规划去做,本文是 k 个子集,得上 dfs +// lc.78 是求出所有子集,这道题是固定了子集的数量,去分配 +/** + * @param {number[]} nums + * @param {number} k + * @return {boolean} + */ +var canPartitionKSubsets = function (nums, k) { + if (k > nums.length) return false + const sum = nums.reduce((a, b) => a + b) + if (sum % k !== 0) return false + + const bucketTarget = sum / k + const used = [] + // 定义递归:输入 k 号桶, 每个数字不能重复使用,所以从 level 层开始选择 + // 输出:k 号桶,是否应该把 nums[level] 加入进来 + const backtrack = (k, level, bucketSum) => { + if (k === 0) return true // 所有桶装满了 + // 一个桶装满了,继续下一个桶 + if (bucketSum === bucketTarget) { + return backtrack(k - 1, 0, 0) // 从 0 层 0 sum 重新开始累加和 + } + for (let i = level; i < nums.length; ++i) { + if (used[i]) continue // 剪枝,被用过啦 + if (nums[i] + bucketSum > bucketTarget) continue // 这个数装不得,装了就超载~ + used[i] = true + bucketSum += nums[i] + if (backtrack(k, level + 1, bucketSum)) return true // 递归下一个数字是否加入桶 + used[i] = false + bucketSum -= nums[i] + } + return false + } + return backtrack(k, 0, 0) +} +/** + * 上方代码,逻辑上是没有问题的,但是效率低下,跑力扣的测试用例会超时 + * + * 优化自然是可以想到 memo 缓存,再就是我没想到的 used 改为位运算[捂脸]。。。 + */ +var canPartitionKSubsets = function (nums, k) { + if (k > nums.length) return false + const sum = nums.reduce((a, b) => a + b) + if (sum % k !== 0) return false + + const bucketTarget = sum / k + let used = 0 // 使用位图技巧 + const backtrack = (k, level, bucketSum, memo) => { + if (k === 0) return true + if (bucketSum === bucketTarget) { + const nextBucket = backtrack(k - 1, 0, 0, memo) + memo.set(used, nextBucket) + return nextBucket + } + if (memo.has(used)) { + // 避免冗余计算 + return memo.get(used) + } + for (let i = level; i < nums.length; ++i) { + // 判断第 i 位是否是 1 + if (((used >> i) & 1) === 1) { + continue // nums[i] 已经被装入别的桶中 + } + if (nums[i] + bucketSum > bucketTarget) continue + used |= 1 << i // 将第 i 位置为 1 + bucketSum += nums[i] + if (backtrack(k, level + 1, bucketSum, memo)) return true + used ^= 1 << i // 使用异或运算将第 i 位恢复 0 + bucketSum -= nums[i] + } + return false + } + return backtrack(k, 0, 0, new Map()) +} +``` + +这道题,我是觉得挺有难度的。。。位运算俺着实没想到啊 😭 + + + +> 推荐阅读:[「labuladong 球盒模型」](https://labuladong.online/algo/practice-in-action/two-views-of-backtrack-2/) + +#### lc.22 括号生成 + +```js +/** + * @param {number} n + * @return {string[]} + */ +var generateParenthesis = function (n) { + // 当出现右括号数量 > 左括号数量时,无效 + const res = [] + const track = [] + const backtrack = (l, r) => { + if (l > r) return + if (l < 0 || r < 0) return + if (l === 0 && r === 0) { + res.push([...track].join('')) + return + } + /** 之前可选择的是很多个,这里就两个直接摊开写方便 */ + track.push('(') + backtrack(l - 1, r) + track.pop() + track.push(')') + backtrack(l, r - 1) + track.pop() + } + backtrack(n, n) + return res +} +``` + +#### lc.37 解数独 hard + +建议先做岛屿类问题,再做此题。 + +```js +/** + * 根据题意,一开始很容易写出这样子的代码,但是此刻 dfs 是什么意思呢?应该是一个探测的过程 + * 也就是说 -- 此处有回溯!与岛屿类问题 flood fill 算法不同的是:flood fill 它直接就 flush 掉了找到的陆地 + * 而此处的 dfs 是在不断探测的,是要走回头路的,所以这两个 for 循环是在 dfs 之内的,即: + * dfs() {for(for(选择 dfs 撤销选择))} + */ +var solveSudoku = function (board) { + for (let i = 0; i < m; ++i) { + for (let j = 0; j < n; ++j) { + if (board[i][j] === '.') { + for (let k = 1; k <= 9; ++k) { + dfs(grid, i, j, k.toString()) + } + } + } + } +} +/** 开奖 */ +/** + * @param {character[][]} board + * @return {void} Do not return anything, modify board in-place instead. + */ +var solveSudoku = function (board) { + dfs(board) +} +function dfs(grid) { + for (let i = 0; i < 9; ++i) { + for (let j = 0; j < 9; ++j) { + if (grid[i][j] === '.') { + for (let k = 1; k <= 9; ++k) { + // 重点是剪枝 + if (!isValid(grid, i, j, k.toString())) continue + grid[i][j] = k.toString() + if (dfs(grid)) return true // 找到一种可行解 直接结束 + grid[i][j] = '.' + } + return false // 9 个数字都不行 + } + } + } + return true // 遍历完没有返回 false,说明找到了一组合适棋盘位置了 +} +function isValid(grid, row, col, tryVal) { + for (let i = 0; i < 9; ++i) { + if (grid[row][i] === tryVal) return false + if (grid[i][col] === tryVal) return false + } + + const startRow = Math.floor(row / 3) * 3 + const startCol = Math.floor(col / 3) * 3 + for (let i = startRow; i < startRow + 3; ++i) { + for (let j = startCol; j < startCol + 3; ++j) { + if (grid[i][j] === tryVal) return false + } + } + + return true +} +``` + +#### lc.200 岛屿的数量 + +首先考验的是 dfs 遍历二维数组的能力。 + +```js +/** + * 在二维矩阵中的 dfs --- for{for{dfs}} + */ +function dfs(matrix, i, j, visited) { + if (i < 0 || j < 0 || i >= m || j >= n) return + /** 进节点 */ + if (visited[i][j]) return + visited[i][j] = true + /** 上下左右遍历 */ + dfs(matrix, i - 1, j) + dfs(matrix, i + 1, j) + dfs(matrix, i, j - 1) + dfs(matrix, i, j + 1) + /** 出节点 */ +} + +/** 此外有方向数组的技巧 */ +const dirs = [ + [-1, 0], + [1, 0], + [0, -1], + [0, 1] +] +// 把上方的四个 dfs 配合 dirs 改为 for 循环即可 +for (var d of dirs) { + const next_i = i + d[0] + const next_j = j + d[1] + dfs(matrix, next_i, next_j, visited) +} +``` + +```js +/** + * @param {character[][]} grid + * @return {number} + */ +var numIslands = function (grid) { + let res = 0 + const m = grid.length + const n = grid[0].length + const visited = Array.from(Array(m), () => Array(n).fill(false)) + for (let i = 0; i < m; ++i) { + for (let j = 0; j < n; ++j) { + // 从某个陆地节点开始 detect + if (grid[i][j] === '1') { + res++ + dfs(grid, i, j, visited) + } + } + } + return res +} +function dfs(grid, i, j, visited) { + const m = grid.length + const n = grid[0].length + if (i < 0 || j < 0 || i >= m || j >= n) return + if (grid[i][j] === '0') return + grid[i][j] = '0' // 淹没土地 + dfs(grid, i - 1, j, visited) + dfs(grid, i + 1, j, visited) + dfs(grid, i, j - 1, visited) + dfs(grid, i, j + 1, visited) +} +``` + +> 这种“淹掉岛屿”的 dfs 算法有自己的名字 --- [「经典的 Flood fill 算法」](https://zh.wikipedia.org/wiki/Flood_fill),这样可以不用维护 visited 数组。如果题目要求不能修改原数组,那么还是用 visited 去做,就此题而言具体就是多两步操作,一个是在 res++ 前判断是否 visit 过,另一个就是在每次 dfs 前判断是否 visit 过。 + +_另外此题还可以使用 BFS 和 并查集 解决_ + + + +#### lc.695 岛屿的最大面积 + +```js +/** + * @param {number[][]} grid + * @return {number} + */ +var maxAreaOfIsland = function (grid) { + let maxArea = 0 + const m = grid.length + const n = grid[0].length + for (let i = 0; i < m; ++i) { + for (let j = 0; j < n; ++j) { + if (grid[i][j] === 1) { + maxArea = Math.max(maxArea, dfs(grid, i, j)) + } + } + } + return maxArea +} +// 继续 flood fill +function dfs(grid, i, j) { + const m = grid.length + const n = grid[0].length + if (i < 0 || j < 0 || i >= m || j >= n) return 0 + if (grid[i][j] === 0) return 0 + grid[i][j] = 0 + return dfs(grid, i - 1, j) + dfs(grid, i + 1, j) + dfs(grid, i, j - 1) + dfs(grid, i, j + 1) + 1 +} +``` + + + +#### lc.1020 飞地的数量 + +```js +/** + * @param {number[][]} grid + * @return {number} + */ +var numEnclaves = function (grid) { + let res = 0 + const m = grid.length + const n = grid[0].length + // 先把四个边界的淹没掉 + for (let i = 0; i < m; ++i) { + dfs(grid, i, 0) + dfs(grid, i, n - 1) + } + for (let i = 0; i < n; ++i) { + dfs(grid, 0, i) + dfs(grid, m - 1, i) + } + for (let i = 0; i < m; ++i) { + for (let j = 0; j < n; ++j) { + if (grid[i][j] === 1) { + res++ + } + } + } + return res +} +function dfs(grid, i, j) { + const m = grid.length + const n = grid[0].length + if (i < 0 || j < 0 || i >= m || j >= n) return + if (grid[i][j] === 0) return + grid[i][j] = 0 + dfs(grid, i - 1, j) + dfs(grid, i + 1, j) + dfs(grid, i, j - 1) + dfs(grid, i, j + 1) +} +``` + +#### lc.1254 统计封闭岛屿的数目 + +```js +/** + * @param {number[][]} grid + * @return {number} + */ +var closedIsland = function (grid) { + let res = 0 + const m = grid.length + const n = grid[0].length + for (let i = 0; i < m; ++i) { + dfs(grid, i, 0) + dfs(grid, i, n - 1) + } + for (let j = 0; j < n; ++j) { + dfs(grid, 0, j) + dfs(grid, m - 1, j) + } + for (let i = 0; i < m; ++i) { + for (let j = 0; j < n; ++j) { + if (grid[i][j] === 0) { + res++ + dfs(grid, i, j) + } + } + } + return res +} +function dfs(grid, i, j) { + const m = grid.length + const n = grid[0].length + if (i < 0 || j < 0 || i >= m || j >= n) return + if (grid[i][j] === 1) return + grid[i][j] = 1 + dfs(grid, i - 1, j) + dfs(grid, i + 1, j) + dfs(grid, i, j - 1) + dfs(grid, i, j + 1) +} +``` + +#### lc.1905 统计子岛屿 + +这道题需要稍微思考一下,同样大小的 grid,统计 grid2 中的子岛屿,直接遍历是不太好操作的,得先排除掉非子岛屿,然后再统计。什么是非子岛屿,那就是在 grid 中是陆地,但是在 grid1 中是海洋,直接淹掉,这样最后再遍历的时候剩下的就都是子岛屿啦。 + +```js +/** + * @param {number[][]} grid1 + * @param {number[][]} grid2 + * @return {number} + */ +var countSubIslands = function (grid1, grid2) { + let res = 0 + const m = grid2.length + const n = grid2[0].length + for (let i = 0; i < m; ++i) { + for (let j = 0; j < n; ++j) { + if (grid2[i][j] === 1 && grid2[i][j] !== grid1[i][j]) { + dfs(grid2, i, j) + } + } + } + for (let i = 0; i < m; ++i) { + for (let j = 0; j < n; ++j) { + if (grid2[i][j] === 1) { + res++ + dfs(grid2, i, j) + } + } + } + return res +} +function dfs(grid, i, j) { + const m = grid.length + const n = grid[0].length + if (i < 0 || j < 0 || i >= m || j >= n) return + if (grid[i][j] === 0) return + grid[i][j] = 0 + dfs(grid, i - 1, j) + dfs(grid, i + 1, j) + dfs(grid, i, j - 1) + dfs(grid, i, j + 1) +} +``` + +这道题自然也是可以使用 「并查集」来解决,后序并查集章节会详细使用。 + +--- + +> 请注意,本文所有的回溯函数,都使用了闭包的特性,如果不使用闭包,把需要的参数变为回溯函数的入参即可。 + +我亦无他,唯手熟尔!peace~ diff --git "a/content/posts/algorithm/z-others/\344\275\215\350\277\220\347\256\227.md" "b/content/posts/algorithm/z-others/\344\275\215\350\277\220\347\256\227.md" index ff63740..301adfc 100644 --- "a/content/posts/algorithm/z-others/\344\275\215\350\277\220\347\256\227.md" +++ "b/content/posts/algorithm/z-others/\344\275\215\350\277\220\347\256\227.md" @@ -100,6 +100,8 @@ public static int findOdd(int[] arr) { ### 进阶:一组数,只有 2 个数出现了一次,其他都是偶数次,找出这 2 个数 +lc.260 + ```java /** * 根据上一题可以很容易得到 a^b 的值,问题在于怎么找到这两个数字