递归算法能够解决的一个典型问题,是具有树形结构的问题,当我们发现一个问题与一个更小的问题之间存在递推的关系时,递归关系呈现出来的是一个树形结构
树形问题:分析问题时,发现解决问题的思路本质是一棵树的形状
- 回溯算法是在树形图上的深度优先遍历
- 回溯的产生是由于深度优先遍历的回退,造成了状态重置或撤销选项
解决回溯问题的第一步是画出树形图
回溯的本质是DFS,回溯只是DFS产生的现象
- 本质上是遍历的算法,全程使用一份状态变量去搜索状态空间里的所有状态
- 具有DFS的自叶子到根的特点

17. 电话号码的字母组合

通过分析,'23’可分解为以下树形

得到以下推断过程:
得到以下分析:
设,digits代表数字字符串,s(digits)是digits所能代表的字母字符串,,letter( digits[i] ) 表示第 i 个数字能表示的所有字母
则有:
-
s( digits[0…n-1] )
= letter( digits[0] ) + s( digits[1…n-1] )
= letter( digits[0] ) + letter( digits[1] ) + s( digits[2…n-1] )
= …
因此,不难得出如下思路步骤
思路步骤:
-
每次从digits中取出一个数字字符,找到其对应的字母字符串并循环
-
以当前的字母字符串为基础,从digits中取出下一个数字字符,找出字母字符串,递归取出所有数字字符并将当前字符与之前的字符串进行拼接 (s( digits[0…n-1] ) = letter( digits[0] ) + s( digits[1…n-1] ))
-
直到构成的解 s 长度与digits长度相同 (因为有多少数字组成的每一个字母字符串就有多少位,因此只要s的长度等于digits的长度),就说明完成了一个字母组合,存入list
-
递归上述步骤,直到所有第一个数字对应的字母字符串的所有字母组合完毕
Java代码实现如下:
/**
* @Author: antigenMHC
* @Date: 2020/3/27 11:47
* @Version: 1.0
**/
public class test {
public static void main(String[] args) {
List<String> strings = new Solution().letterCombinations("23");
for (String s : strings) {
System.out.println(s);
}
}
}
class Solution {
private String[] letterTable = new String[]{
"", //0
"", //1
"abc", //2
"def", //3
"ghi", //4
"jkl", //5
"mno", //6
"pqrs", //7
"tuv", //8
"wxyz", //9
};
private List<String> lists = new LinkedList<>();
//返回值为digits所能表示的所有字母字符串组成的list
public List<String> letterCombinations(String digits) {
find(digits, 0, "");
return lists;
}
//index表示每次处理的数字在数字字符串中的位置
//该函数每次处理一位数字!
//s表示index之前已经转换了的数字,即 s ( digits[0, index-1] )
//寻找和digits[index]匹配的字母字符串,获得 letter( digits[index] )
//s ( digits[0, index-1] ) + letter( digits[index] ) = s ( digits[0, index] ) 即得到了一个新解
private void find(String digits, int index, String s){
//到达digits末尾,生成一个字母字符串
if(s.length() == digits.length()) {
lists.add(s);
return;
}
char nowDigits = digits.charAt(index); //获取对应数字字符
assert (nowDigits >= '2' && nowDigits <= '9');
String letters = letterTable[nowDigits-'0']; //提取数字对应的字母字符串
//遍历digits对应的字符串,每次得到一个字母,然后继续向下搜索,找出其后后面所有数字的组合
for(int i=0; i<letters.length(); i++){
//s+letters.charAt(i), 基于当前字母组合继续向下寻找
//digits有多少数字,最终的每一个字母字符串就有多少位,因此只需要将数字对应的字母字符串中的每一个字母取出来,与前面的拼接,然后继续对下一个数字
//进行字母字符的求解即可
find(digits, index+1, s+letters.charAt(i));
}
return;
}
}
上述代码画出图来可以很好地理解
通过递归调用来寻找答案的过程也称为回溯,回溯法是暴利解法的一个主要实现手段
从树的形状来看和结果来看,结果的个数大致为 3n (n为树深度),所以时间复杂度为 O(2n)
接下来看几个回溯法能够处理的问题
1.排列问题
46. 全排列

继续先分析出树形结构,如下

可以发现思路如下:
排列 ( nums[0…n-1] ) = {取出一个数字} + 排列 ( nums[0…n-1 - 这个数字] )
排列问题中,元素之间是相互冲突的,因此需要判断和回溯(使用了一个元素并存入list中后,去掉这个元素,选择下一个元素)
Java代码实现如下:
class Solution {
private List<List<Integer>> reList = new ArrayList<>();
//一个List<Integer>为一个排列
public List<List<Integer>> permute(int[] nums) {
if(nums.length==0) return reList;
find(nums, 0, new ArrayList<>());
return reList;
}
//index表示第index个元素,l表示保存了index个元素的排列
//当传进index+1时,l就获得一个index+1个元素的排列
private void find(int[] nums, int index, List<Integer> l){
//至多组成nums.length的长度的一个排列,当index==nums.length时说明生成了一个排列到l中
//保存l到reList
if(index == nums.length){
//保存当前生成的这个排列
reList.add(new ArrayList<>(l));
return;
}
//遍历每一个数字,找到需要添加的第index+1个数字
for(int i=0; i<nums.length; i++){
//l中不存在这个数字,则添加
if(!l.contains(nums[i])){
l.add(nums[i]);
find(nums, index+1, l);
//在排列问题中,元素之间是相互冲突的,因此在回溯时需要对l所存的内容也进行回溯
//回退一个元素,不然l中就始终存在nums[i]了,nums[i]会被重复使用
//回溯状态和变量!
l.remove(l.size()-1);
}
}
return;
}
}
排列问题中:一个数字在不同排列中可以出现多次,因此每次递归 i 从0开始查找所有可能
2.组合问题
77. 组合

同样,先分析出其树形结构

在组合问题中,前一次被取用元素在下一次中不再取用
因此,只需要对排列问题稍加修改即可
Java代码实现如下:
class Solution {
private List<List<Integer>> reList = new ArrayList<>();
private List<Integer> tmp = new ArrayList<>();
private List<Boolean> used = new ArrayList<Boolean>();
private int pre = -1;
public List<List<Integer>> combine(int n, int k) {
if(k==0 || n==0) return reList;
List<Integer> list = new ArrayList<>();
for(int i=0; i<=n; i++){
list.add(i);
used.add(false);
}
find(list, k, 1);
return reList;
}
private void find(List<Integer> n, int k, int index){
if(tmp.size() == k){
reList.add(new ArrayList<>(tmp));
return;
}
//组合问题基本模板
for(int i=index; i<n.size(); i++){
tmp.add(n.get(i));
//前一个分支把第i个数及以前的数字的所有情况都考虑了进去,因此从i+1个元素开始考虑
find(n, k, i+1);
//回溯,探寻弹出的元素可以组成的其他组合
//回溯算法中基本上都要
pre = tmp.remove(tmp.size()-1);
}
}
}
其实,通过上面的树形图,可以很容易发现,分支4是不需要的,假如我们的递归深度很深的话,那最后一个分支会占用大量的时空复杂度,因此我们可以将其剪去,这就是经典的剪枝思想

对于n=4,k=2的情况而言,根本不需要去考虑取4,即最后一个元素的情况,因此可以尝试将取4的情况 “剪掉”
对于分析发现:
- 每次循环,储存组合的list中还有 k-list.size()个空位
- 所以 [i … n] 中至少要有 k-list.size() 个元素来填补list的空位,于是 i 如何求呢?
- k-l.size() = 1时, i(最大) = n = n-1+1, [n…n]就有1个元素刚好填补
- k-l.size() = 2时, i(最大) = n-1= n-2+1, [n-1…n] 就有2个元素刚好填补
- k-l.size() = 3时, i(最大) = n-2 = n-3+1, [n-2…n] 3个元素刚好填补
- 于是推导得 i <= n-(k-l.size())+1 <= n,i 不能大于 n-(k-l.size())+1 ,会导致需要填补的元素变多,空位不足
由此,获得剪枝后的Java代码如下:
class Solution {
private List<List<Integer>> reList = new LinkedList<>();
public List<List<Integer>> combine(int n, int k) {
if(k==0 || n==0 || k>n) return reList;
find(n, new ArrayList<>(), k, 1);
return reList;
}
private void find(int n, List<Integer> l, int k, int start){
if(l.size() == k){
reList.add(new ArrayList<>(l));
return;
}
// i<=n-(k-l.size())+1 剪枝条件
// list中还有k-l.size()个空位
// 所以 [i .... n] 中至少要有 k-l.size() 个元素填补,因此问题为 i 取多少时[i .... n]有k-l.size() 个元素?
// k-l.size() == 1时, i(最大)==n==n-1+1, [n...n]就有1个元素刚好填补
// k-l.size() == 2时, i(最大)==n-1==n-2+1, [n-1...n] 就有2个元素刚好填补
// k-l.size() == 3时, i(最大)==n-2==n-3+1, [n-2...n] 3个元素刚好填补
//因此 i <= n-(k-l.size())+1 <= n
//i最大等于n-(k-l.size())+1,因为如果i比n-(k-l.size())+1还大的话,list中就没有足够的空位了
for(int i=start; i<=n-(k-l.size())+1; i++){
l.add(i);
//前一个分支把第i个数及以前的数字的所有情况都考虑了进去,因此从i+1个元素开始考虑
find(n, l, k, i+1);
l.remove(l.size()-1);
}
}
}
class Solution {
private Set<List<Integer>> set = new HashSet<>();
private List<List<Integer>> reList = new LinkedList<>();
private List<Integer> tmp = new ArrayList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
if(candidates.length == 0) return reList;
find(candidates, target, 0, 0);
reList.addAll(set);
return reList;
}
private void find(int[] candidates, int tartget, int index, int sum){
if(sum > tartget) return;
if(sum==tartget || index>candidates.length){
set.add(new ArrayList<>(tmp));
return;
}
//从index开始,每一位都重复添加,看是否与target相等
for(int i=index; i<candidates.length; i++){
tmp.add(candidates[i]);
//传入i,重复相加
find(candidates, tartget, i, sum+candidates[i]);
tmp.remove(tmp.size()-1);
}
}
}
class Solution {
private Set<List<Integer>> set = new HashSet<>();
private List<List<Integer>> reList = new LinkedList<>();
private List<Integer> tmp = new ArrayList<>();
private Set<Integer> canSet = new HashSet<>();
private int pre = -1;
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates); //排序,相邻大小数挨在一起
find(candidates, target, 0, 0);
reList.addAll(set);
return reList;
}
//每个数只能选择一次, 不允许同级出现同样大小的元素,但不同层可以
private void find(int[] candidates, int tartget, int index, int sum){
if(sum > tartget) return;
if(sum==tartget){
set.add(new ArrayList<>(tmp));
return;
}
for(int i=index; i<candidates.length; i++){
//用pre记录删除了的元素,表示已经使用过,不再使用,剪枝
if(pre == candidates[i]) continue;
tmp.add(candidates[i]);
//一个数只能用一次,i+1
find(candidates, tartget, i+1, sum+candidates[i]);
pre = tmp.remove(tmp.size()-1);
}
}
}
class Solution {
private Set<List<Integer>> reSet = new HashSet<>();
private List<Integer> tmp = new ArrayList<>();
private int[] arr = new int[]{1,2,3,4,5,6,7,8,9};
private boolean[] used = new boolean[]{false,false,false,false,false,false,false,false,false};
public List<List<Integer>> combinationSum3(int k, int n) {
find(n,k,0, 0);
List<List<Integer>> reList = new ArrayList<>(reSet);
return reList;
}
private void find(int target, int k, int index, int sum){
if(sum > target || tmp.size() > k) return;
if(sum == target && tmp.size()==k){
reSet.add(new ArrayList<>(tmp));
return;
}
for(int i=index; i<9; i++){
if(!used[i]){
tmp.add(arr[i]);
used[i] = true;
find(target, k, i, sum+arr[i]);
used[i] = false;
tmp.remove(tmp.size()-1);
}
}
}
}
组合题规律总结
当题干包含:
-
时,使用Set集合去重 -
使用辅助数字判断哪个数字已经被使用过,在生成一个组合后将数字和存储组合的列表回溯 (如216,面试题08) -
循环时 i 以传入第index位基位,递归时重复传入当前 i 表示可以重复使用 (如39题) -
循环时 i 以传入第index位基位,递归时传入 i+1,表示前 i 个数字已经存在当前组合中 (这种情况下可以考虑剪枝提高效率) (如40题)
二维平面上的回溯法
比如最常见的迷宫问题
79. 单词搜索

简单分析树形如下:

基于传入的坐标,对当前坐标的四周进行试探,当满足条件是进入坐标,继续试探,被试探过的坐标应该标记为visited,当该坐标四周试探完成不满足条件后,应该将vitised回溯为未访问状态,以便让其相邻位置进行试探
class Solution {
//设置偏移量数组的技巧在二维数组问题中经常使用
//4个方位,每个一维数组中的第一个元素代表startX的移动,第二个元素代表startY的移动
private int map[][] = new int[][]{ {-1,0}, //上
{0,1}, //右
{1,0}, //下
{0,-1}}; //左
private Boolean[][] visited;
public boolean exist(char[][] board, String word) {
if(board.length==0 || word==null) return false;
visited = new Boolean[board.length][board[0].length];
for(int i=0; i<board.length; i++){
for(int j=0; j<board[i].length; j++){
visited[i][j] = false;
}
}
for(int i=0; i<board.length; i++){
for(int j=0; j<board[i].length; j++){
//每次从[i][j]出发,寻找相应的字符串
if (searchWord(board, word, 0, i, j)) {
return true;
}
}
}
return false;
}
//每次查找word的第index个元素
//从board[startX][startY]开始,寻找word[index...word.length()]
private boolean searchWord(char[][]board, String word, int index, int startX, int startY){
//当前所处的board位置是否与字符串最后一位相同
if(index == word.length()-1){
return board[startX][startY] == word.charAt(index);
}
//元素相等时,继续搜索
if(board[startX][startY] == word.charAt(index)){
//设置为已经访问过该元素
visited[startX][startY] = true;
//从startX,startY开始朝四个方向出发进行搜索
for(int i=0; i<4; i++){
int newX = startX + map[i][0];
int newY = startY + map[i][1];
//newX,newY不能越界 && board[newX][newY]没有被访问过
if((newX>=0 && newX<board.length && newY>=0 && newY<board[0].length) &&
!visited[newX][newY]){
if (searchWord(board, word, index+1, newX, newY)) {
return true;
}
}
}
//基于[startX][startY]找遍4个方向都找不到的话,放弃[startX][startY]位置,置位false
visited[startX][startY] = false;
}
return false;
}
}
FloodFill算法(洪泛填充算法)
FloodFill的过程其实就是 DFS 的过程
200. 岛屿数量

我们需要找到与一个陆地直接相邻,即同属于一个岛屿的其它陆地(递归寻找),进行标记,这样在寻找下一个岛屿的时候才不会重复。
从某个位置开始向外扩散,直到没有相连的陆地,这个过程就是FloodFill的过程
这里的 FloodFill算法 不需要进行状态回溯,因为当递归未结束时找到的所有陆地一定属于了一个岛屿,如果回溯了的话就说明不属于这个岛屿了
class Solution {
private boolean[][] visited;
private int[][] map = new int[][]{{-1,0},{0,1},{1,0},{0,-1}};
private int islands;
public int numIslands(char[][] grid) {
if(grid.length==0) return 0;
visited = new boolean[grid.length][grid[0].length];
for (int i = 0; i < grid.length; i++) {
for(int j = 0; j < grid[i].length; j++){
visited[i][j] = false;
}
}
for (int i = 0; i < grid.length; i++) {
for(int j = 0; j < grid[i].length; j++){
//FloodFill :本质就是一次DFS
//递归终止条件隐含在这,只有陆地才会归入岛屿
//再次循环时,对于同属于这块岛屿的其它陆地就不会再进行查找
if(grid[i][j] == '1' && !visited[i][j]){
islands++;
findIslands(grid, i, j);
}
}
}
return islands;
}
private void findIslands(char[][] grid, int startX, int startY){
for(int i=0; i<4 ;i++) {
//不仅标识是否被访问,同时也标识已经属于一个岛屿了
visited[startX][startY] = true;
int newX = startX+map[i][0];
int newY = startY+map[i][1];
if(newX>=0 && newX<grid.length && newY>=0 && newY<grid[0].length && !visited[newX][newY]){
if(grid[newX][newY] == '1'){
findIslands(grid, newX, newY);
}
}
//这里不需要状态回溯,因为当递归未结束时找到的所有陆地一定属于了一个岛屿,如果回溯了的话就说明不属于这个岛屿了
}
}
}
这里有另外一种更简洁的解法
//一次dfs找到一个岛屿
class Solution {
public static void dfs(char[][] grid, int i, int j) {
if(i < 0 || i >= grid.length || j < 0 || j >= grid[0].length || grid[i][j] != '1') return;
else grid[i][j] = '2'; // 将找到的 '1' 标注为 2,防止便利时再被找到
dfs(grid, i, j+1); // 搜索上下左右的 '1'
dfs(grid, i-1, j);
dfs(grid, i+1, j);
dfs(grid, i, j-1);
}
public static int numIslands(char[][] grid) {
int count = 0;
for (int i = 0; i < grid.length; i++) {
for (int j = 0; j < grid[0].length; j++) {
if(grid[i][j] == '1') {
++count;
dfs(grid, i, j);
}
}
}
return count;
}
}
130. 被围绕的区域

只要与边界上的O相邻的所有O都不会被反转成X,因此可以认为是一个反向的FloodFill,从边界上的每一个O开始递归,将递归到的所有为O的位置标记为true (在对grid遍历时对于X的位置就可以标记为true)。最后将不为true的位置(即不与边界相连)上的所有元素转换成X
class Solution {
int[][] map = new int[][]{{-1,0},{0,1},{1,0},{0,-1}};
boolean[][] visited;
public void solve(char[][] board) {
if(board.length == 0) return;
visited = new boolean[board.length][board[0].length];
for (int i = 0; i < board.length; i++) {
for(int j=0; j<board[i].length; j++){
visited[i][j] = false;
}
}
//找出与边界相连的所有O,以及所有X,标记为true
for (int i = 0; i < board.length; i++) {
for(int j=0; j<board[i].length; j++){
if(board[i][j]=='X') visited[i][j] = true;
//从边界开始进行dfs
if(board[i][j]=='O' && (i==0 || j==0 || i==board.length-1 || j==board[i].length-1)){
dfs(board, i, j);
}
}
}
//没有被标记的就是没有与边界相邻的O了
for (int i = 0; i < visited.length; i++) {
for(int j=0; j<visited[i].length; j++){
if(!visited[i][j]){
board[i][j] = 'X';
}
}
}
}
public void dfs(char[][] board, int startX, int startY){
for(int i=0; i<4; i++){
visited[startX][startY] = true;
int newX = startX+map[i][0];
int newY = startY+map[i][1];
if(newX>0 && newY>0 && newX<board.length && newY<board[0].length && !visited[newX][newY]){
if(board[newX][newY] == 'O'){
dfs(board, newX, newY);
}
}
}
}
}


对于N皇后问题,n个皇后摆放在nn的棋盘格中,使得横,竖和两个对角线方向均不会同时出现两个皇后
以4皇后为例,44的范围,每一行都应该有一个皇后,不然其中一行没有皇后,则说明另外一行至少有两个皇后,即违背题意。
思路:
每一次在一行中摆放一个皇后,看是否能摆放,如果不能摆放则回到上一行,重新摆放上一行皇后的位置,直到在四行中每一行都摆放了皇后

于是问题就变成了如何快速判断皇后位置是否合法:
- 竖向:col[i] 表示第 i 列已经被占用
- 对角线1:dia1[i] 表示对角线1中第 i 个元素被占用
- 对角线2:dia2[i] 表示对角线2中第 i 个元素被占用
对对角线1而言

发现对角线上的横纵坐标相加是相同的,于是用dia1[x+y] 表示第 i 个对角线被占用很合适
对对角线2而言

对角线上的横纵坐标相减的值是一样的,但是由于是从-3开始的,于是使用 i-j+n-1使其从下标0开始,即 dia2[x-y+n-1]
Java代码实现如下:
class Solution {
//存放每一行皇后摆放在第几列的位置
//row[0] 存放 第一行中皇后所在的位置
//row[1] 存放 第二行中皇后所在的位置
private List<Integer> row = new ArrayList<>();
//纵方向是否冲突
private List<Boolean> col = new ArrayList<>();
//两个对角线方向是否冲突
private List<Boolean> dia1 = new ArrayList<>();
private List<Boolean> dia2 = new ArrayList<>();
private List<List<String>> reList = new ArrayList<>();
//n皇后的一个解是一个字符串组成的大小为n的列表
public List<List<String>> solveNQueens(int n) {
for(int i=0; i<n; i++){
col.add(false);
}
for(int i=0; i<2*n-1; i++){
dia1.add(false);
dia2.add(false);
}
find(n, 0);
return reList;
}
//index: 第几行
//在n皇后问题中,摆放第index行的皇后所处位置
private void find(int n, int index){
//index == n,说明得到了一个n皇后问题的解
if(index == n){
//增加一个n皇后问题的解,getOne生成一个.和Q组成的字符串列表
reList.add(getOne(n));
return;
}
for(int i=0; i<n; i++){
//尝试将第index行的皇后摆放在第i列
//如果在纵,以及两个对角线方向都不冲突的话,进行摆放
if(!col.get(i) && !dia1.get(index+i) && !dia2.get(index-i+n-1) ){
row.add(i);
//将对应的列,及两个对角线置位true
col.set(i, true);
dia1.set(index+i, true);
dia2.set(index-i+n-1, true);
//尝试下一行
find(n, index+1);
//回溯
col.set(i, false);
dia1.set(index+i, false);
dia2.set(index-i+n-1, false);
row.remove(row.size()-1);
}
}
}
//将每一行转换为.,并在每一行的相应位置改为Q
private List<String> getOne(int n){
List<String> re = new ArrayList<>();
//n*n的大小
char[][] arr = new char[n][n];
//每个位置初始化为.
for(int i=0; i<n; i++){
for(int j=0; j<n; j++){
arr[i][j] = '.';
}
}
//在每一行的皇后位置置为Q
for(int i=0; i<n; i++){
//row代表每一行中的皇后应该存在的列位置
arr[i][row.get(i)] = 'Q';
//每一行形成的String存入re中
re.add(String.valueOf(arr[i]));
}
return re;
}
}
37. 解数独

这题和N皇后问题有异曲同工之妙,数独问题的特殊点在于其33的方格,因此我们定义一个[3][3][10]的三维数组,来表示在一个33的方格中,1~9这9个数字是否被使用过 (定义为10仅仅是为了方便)
详细思路见如下Java代码:
class Solution {
//[i][j]表示第[i]行的已经使用了数字[j]
private boolean[][] rows = new boolean[9][10];
private boolean[][] cols = new boolean[9][10];
//表示在一个3*3的方块,数字[j]已经被使用过
private boolean[][][] blocks = new boolean[3][3][10];
public void solveSudoku(char[][] board) {
//初始化不可使用范围
for(int i=0; i<board.length; i++){
for(int j=0; j<board[i].length; j++){
//转数字
int num = board[i][j] - '0';
if(num<=9 && num>=1){
//在行上已经使用
rows[i][num] = true;
//在列上已经使用
cols[j][num] = true;
//3*3方格的对应位置上已经有了num这个数字
blocks[i/3][j/3][num] = true;
}
}
}
find(board, 0, 0);
}
private boolean find(char[][] board, int row, int col){
//这个边界条件很重要!搜寻完一行以后row++进行下一行的搜寻,最后row到达最后一行才搜寻结束,返回true就不会进行回溯
if(col == board[0].length){
col=0;
row++;
if(row==board.length)
return true;
}
//是空则尝试填充, 否则跳过,继续尝试下一个位置
if(board[row][col] == '.'){
for(int num=1; num<10; num++){
//是否可以使用
boolean used = !(rows[row][num] || cols[col][num] || blocks[row/3][col/3][num]);
if(used){
rows[row][num] = true;
cols[col][num] = true;
blocks[row/3][col/3][num] = true;
//存入
board[row][col] = (char)('0'+num);
//完全替换完成后返回true,会一直向上返回,但不再回溯,避免回溯成原来的亚子
if(find(board, row, col+1)) return true;
//回溯
rows[row][num] = false;
cols[col][num] = false;
blocks[row/3][col/3][num] = false;
board[row][col] = '.';
}
}
}else
return find(board, row, col+1);
return false;
}
}
本文深入探讨了递归和回溯算法,通过电话号码的字母组合、全排列、组合问题等实例,阐述了如何利用树形结构分析问题,并提供了相关Java代码实现。文章还涉及了FloodFill算法及其在岛屿数量和被围绕区域问题中的应用,以及N皇后和解数独问题的解决方案,展示了回溯法在解决组合和排列问题中的核心思想。



503

被折叠的 条评论
为什么被折叠?



