文章目录
一、找出所有子集的异或总和再求和
一个数组的 异或总和 定义为数组中所有元素按位 XOR 的结果;如果数组为 空 ,则异或总和为 0 。
例如,数组 [2,5,6] 的 异或总和 为 2 XOR 5 XOR 6 = 1 。
给你一个数组 nums ,请你求出 nums 中每个 子集 的 异或总和 ,计算并返回这些值相加之 和 。
注意:在本题中,元素 相同 的不同子集应 多次 计数。
数组 a 是数组 b 的一个 子集 的前提条件是:从 b 删除几个(也可能不删除)元素能够得到 a 。
示例 1:
输入:nums = [1,3]
输出:6
解释:[1,3] 共有 4 个子集:
-
空子集的异或总和是 0 。
-
1\] 的异或总和为 1 。
-
1,3\] 的异或总和为 1 XOR 3 = 2 。 0 + 1 + 3 + 2 = 6
-
利用上一专题中第二题的方法二进行解题,按本题要求我们记录path的方法应该是直接进行异或,这样每次进入dfs时不仅得到一个正确的子集,而且直接就是该子集的异或结果,这时直接令sum+=path;
-
而本题恢复现场的方法值得注意,我们直接让path再次异或原来的元素就可以让该元素的痕迹消失,也就完成了恢复现场。
代码实现
java
class Solution
{
int path;
int sum;
public int subsetXORSum(int[] nums)
{
dfs(nums, 0);
return sum;
}
public void dfs(int[] nums, int pos)
{
sum += path;
for(int i = pos; i < nums.length; i++)
{
path ^= nums[i];
dfs(nums, i + 1);
path ^= nums[i]; // 恢复现场
}
}
}
二、全排列 II
给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
示例 1:
输入:nums = [1,1,2]
输出:
\[1,1,2\], \[1,2,1\], \[2,1,1\]
示例 2:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
解题思路
- 全排列的进阶版,解题思路与原版一样,但是本题出现了重复的元素,我们需要处理一下可能会出现的重复的排列数。
- 数组存在重复的数会导致在dfs的同一层(也就是全排列的同一位上的数)中若选择了相等的数就会导致重复的全排列,如果是不同层(也就是全排列的不同位)的选择就没事。
- 那么我们可以先对nums排序,使重复的数字放在一块,便于后续操作。在同一层的选择会出现问题,那也就是在for循环中选择了相等的数,且这两个挨着的数中前者没有被使用过(used[i-1]==false),那么此时就判定不能再使用nums[i]了。
代码实现及解析
java
class Solution {
boolean[] used;
List<List<Integer>> ret;
List<Integer> path;
public List<List<Integer>> permuteUnique(int[] nums) {
Arrays.sort(nums);//先对nums排序,使重复的数字放在一块,便于后续操作
int n=nums.length;
used=new boolean[n];
ret=new ArrayList<>();
path=new ArrayList<>();
dfs(nums);
return ret;
}
void dfs(int[] nums){
if(path.size()==nums.length){//得到一个全排列
ret.add(new ArrayList<>(path));//拷贝副本
return;
}
for(int i=0;i<nums.length;i++){//循环进行该位数字的不同选择
//used[i]==false:该位数字不能被使用过 &&
//(i==0:i为首位元素,就不用检查其重复性了 ||
//nums[i]!=nums[i-1]:i不为首元素,则检查其是否与前一个元素相同||
//used[i-1]==true:相同也没事,只要它俩不在同一层,也就是不代表同一位数)
if(used[i]==false&&(i==0||nums[i]!=nums[i-1]||used[i-1]==true)){//条件满足才可以使用该数字
path.add(nums[i]);
used[i]=true;
dfs(nums);
path.remove(path.size()-1);
used[i]=false;
}
}
}
}
总结
复习解题思路和代码注释
三、括号生成
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
示例 1:
输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]
示例 2:
输入:n = 1
输出:["()"]
解题思路
- 当n=3时,共6个位置(对应之前学过的几位数),每个位置有两种选择:左括号或右括号,正是进行了这样的一个思维转换才使得我们画出了决策图,进而使用dfs算法解题
决策图:

什么叫有效的括号组合?
满足两点:
- 左括号总数==右括号总数
- 从左开始到任意位置的区间内所得括号组合中左括号数量均>=右括号(很巧妙)
- 只要满足以上两点,该括号组合一定是有效的,所以反过来只要在生成括号组合的过程中始终满足第二点,并且在最终使第一点成立就可得出有效的括号组合
代码实现及解析
java
class Solution {
List<String> ret;
StringBuilder path;
int leftNum,rightNum;//实时记录左、右括号数量
int n;
public List<String> generateParenthesis(int _n) {
n=_n;
ret=new ArrayList<>();
path=new StringBuilder();
dfs();
return ret;
}
void dfs(){
if(rightNum==n){//递归出口:当右括号数量已达上限,则说明我们已经得到一个有效括号组合(毕竟不合格的已经被剪枝剪掉了,leftNum且始终保持>=rightNum)
ret.add(path.toString());
return;
}
//一层递归的不同选择(if语句作为剪枝操作):
if(leftNum<n){//当左括号数量未达上限,该位置可以选左括号
path.append("("); leftNum++;
dfs();
path.deleteCharAt(path.length()-1);//恢复现场
leftNum--;
}
if(rightNum<leftNum){//当rightNum仍不及leftNum(这样保持leftNum>=rightNum,可使括号组合一直为有效),该位置可选右括号
path.append(")"); rightNum++;
dfs();
path.deleteCharAt(path.length()-1);
rightNum--;
}
}
}
总结
复习解题思路和代码注释
四、组合
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
你可以按 任何顺序 返回答案。
解题思路
-
该题与之前【子集】那题一样,所以使用dfs解题
-
该题为【组合/子集】类问题,该种问题中通常会出现"(1,2)和(2,1)是一样的"这种特征,当我们使用之前的"多层递归代表不同数位,每层递归内多个选择代表单个数位上不同的选择"这种方法来解题时要注意(可以结合决策图思考):
java
path.add(nums[i]);
//进入下一层递归:
dfs(i+1);//pos=i+1
//下一层递归时要从i+1位置开始进行选择(递归中的for循环要从pos开始),
//因为i+1位置之前的元素在之前的某个情况中已经枚举过了,
//这次还使用的话虽然顺序不同,但在子集问题中仍属重复(相当于是你不要管现在存在不存在,而是这种组合之前存在过了,那这次就不应该再枚举了)
- 用以上这种思路来理解【子集】那题的方法二会更好
代码实现及解析
java
class Solution {
List<List<Integer>> ret;
List<Integer> path;
int n,k;
public List<List<Integer>> combine(int _n, int _k) {
n=_n; k=_k;
ret=new ArrayList<>();
path=new ArrayList<>();
dfs(1);
return ret;
}
void dfs(int pos){
if(path.size()==k){//枚举了k个数就可以了
ret.add(new ArrayList<>(path));
return;
}
for(int i=pos;i<=n;i++){//该位置可以有pos~n这些选择
path.add(i);
dfs(i+1);//但为了不重复,下一个位置就要从i+1开始(如解题思路中所讲)
path.remove(path.size()-1);
}
}
}
总结
复习解题思路和后面两个代码注释
五、目标和
给你一个非负整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 '+' 或 '-' ,然后串联起所有整数,可以构造一个 表达式 :
例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1" 。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。
示例 1:
输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 3 。
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3
解题思路
- 我们这几次使用的暴搜(dfs)解题方法在这种题目上也可以应用,也就是针对于"做多次选择,每次选择都有多种选项"这种特征的题目,且本题的做法与之前【子集】那道题目针对于每个元素进行"选与不选"的抉择的做题方法有很大的相似之处,只不过本题是"针对于每个数选择:加还是减"。
代码实现及解析
java
class Solution {
int count;//记录有效表达式的数目
int target;
public int findTargetSumWays(int[] nums, int _target) {
target=_target;
dfs(nums,0,0);
return count;
}
void dfs(int[] nums,int pos,int path){
if(pos==nums.length){//pos到头了,完成了一次完整的表达式定义
if(path==target) count++;//如果表达式结果path为目标值,则计数+1
return;
}
//选择加法:
dfs(nums,pos+1,path+nums[pos]);//在参数中更新path,并进行下一位数的选择,使用参数维护path,方便恢复现场操作
//选择减法:
dfs(nums,pos+1,path-nums[pos]);//在参数中更新path,并进行下一位数的选择
}
}
总结
复习解题思路这种单变量path(本题是做为记录表达式加和的变量)确实也适合使用参数进行传递并记录,方便恢复现场,不用再使用全局变量了,可以去代码里看一下这个操作
六、 组合总和
给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
解题思路
方法一:
-
想一下与【子集】那题的联系,【子集】那题是"某元素选与不选",该题是"某元素选几个(0个、1个、2个......)",因为本题允许同一个数字无限制重复被选取。
-
记住组合/子集类题目的该种做法
方法一恢复现场放在下面(先看代码,不然不知道在说什么)的原因:
- 这个恢复现场的做法不是为了更换本层的选择,而是为了回溯过去之后方便更换上一层dfs中的组合选择,由此与之前的做法不一样(以前都是恢复现场->进入下一个循环->由此而换一个属于本层的选择),可能一下子反应不过来。
方法二:
- 使用的方法与【子集】那题的方法二是一样的:
第二种方法就是对于子集问题的结果我们可以发现其结果可以分类:不含有元素的(空集)、只包含1个元素的、包含两个元素的...
所以我们可以分组进行处理,0元素的、有一个元素的... ,但是代码实现不是想当然地写的,我们可以在dfs中用for循环对每个元素进行遍历,这样来做出不同的选择,但每固定一个数(pos位置)仍需要直接进入下一位数(pos+1)的dfs,而且dfs中的for循环是从形参pos开始的,不然会导致子集重复,这样我们发现每次进入dfs其实都是一个结果。
记住组合/子集类题目的该种做法
代码实现及解析
方法一:
java
//方法一:
class Solution {
List<List<Integer>> ret;
List<Integer> path;
int[] nums;
int target;
public List<List<Integer>> combinationSum(int[] candidates, int _target) {
nums=candidates;
target=_target;
ret=new ArrayList<>();
path=new ArrayList<>();
dfs(0,0);
return ret;
}
void dfs(int pos,int sum){
if(sum==target){//一旦该子集的和为目标值,就记录,并返回(再往下加就要>target了)
ret.add(new ArrayList<>(path));
return;
}
if(pos==nums.length||sum>target) return;//pos遍历到头了就必须要回溯了,或者sum都超过target了也没必要再往下递归了
for(int i=0;(i*nums[pos]+sum)<=target;i++){//循环选择nums[pos]这个元素使用几个,直到>target
if(i!=0) path.add(nums[pos]);
//固定了一个数之后就去下一位:
dfs(pos+1,sum+i*nums[pos]);
//本来dfs回溯之后到了这里是要对path进行恢复现场的(将nums[pos]数量的使用更换),但是我们发现不恢复正好满足nums[pos]的数量递增1的要求
}
//但是要意识到for循环结束后,本层dfs就要结束而向上回溯了,也就是要回到上一个pos对nums[pos-1]位置元素的数量进行更换了,
//那这个时候就要将残留下来的这些nums[pos]清除掉了
for(int i=1;(i*nums[pos]+sum)<=target;i++){//但要注意i要从1开始,因为原来i==0的时候并没有add
path.remove(path.size()-1);
}
}
}
方法二:
java
//方法二:
class Solution {
List<List<Integer>> ret;
List<Integer> path;
int[] nums;
int target;
public List<List<Integer>> combinationSum(int[] candidates, int _target) {
nums=candidates;
target=_target;
ret=new ArrayList<>();
path=new ArrayList<>();
dfs(0,0);
return ret;
}
void dfs(int pos,int sum){
if(sum==target){//一旦该子集的和为目标值,就记录
ret.add(new ArrayList<>(path));
return;
}
if(pos==nums.length||sum>target) return;//pos遍历到头了就必须要回溯了,sum都超过target了也没必要再往下递归了
for(int i=pos;i<nums.length;i++){
path.add(nums[i]);
dfs(i,sum+nums[i]);//这种子集问题,本来pos传参时应传i+1,但是该题允许一个元素无限制地使用,所以下一位数仍可使用i位置元素
path.remove(path.size()-1);//恢复现场(sum作为参数,回溯过程中就自动恢复现场了)
}
}
}
总结
可以先复习方法二的解题思路和代码注释,再复习方法一的解题思路和代码注释
七、N皇后
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。

解题思路
- 一般思路:对于每个Queen,它的位置有多种选择,使用dfs解题,不可放Queen放地方进行剪枝就行了,但是这样太麻烦了,每个Queen的放置都要遍历整个矩阵来寻找位置。
优化:
-
我们发现按照规则所有Queen只能放在不同行上面,所有我们可以针对每一行进行多次选择,每次选择都有"该行中Queen应放在哪个位置"这样的多个选项,使用dfs解题。
-
在dfs过程中我们面临一个问题:有些位置是不可以放置Queen的,所有我们要对这些位置进行标记,但是如果像以前一样做个check矩阵,遍历所有需标记的位置进行标记就又太麻烦了。
对于check数组的优化:
- 规则是Queen位置的一整行(这个不用担心了因为dfs逻辑就是分行进行放置Queen的)、一整列都不可再放了,所以我们可以搞个col[n]数组,矩阵有几列,数组大小就为几,这样直接来标记一整列为不可放置。
- 那对角线的标记也有好方法,将对角线放到数学的直角坐标系中可以发现其可分别表示为y=-x+b(副对角线)、y=x+b(主对角线),所以在同一对角线上的坐标的x+y(副对角线)或y-x(主对角线)相同(它们都等于b),所以以这样的方式直接将整个对角线标记。
(这个推导过程没有那么复杂,只是我用文字描述的话确实讲得比较多而已)
代码实现及解析
java
class Solution {
List<List<String>> ret;
char[][] path;//以后对矩阵操作还是要建一个二维数组,这样好操作,转String什么的都好说,不使用这种二维数组会使对矩阵操作变得复杂
int n;
boolean[] checkCol;//column:列
boolean[] checkDiag1;//主对角线(x+y=b)
boolean[] checkDiag2;//副对角线(y-x=b) diagonal:对角线
public List<List<String>> solveNQueens(int _n) {
ret=new ArrayList<>();
n=_n;
path=new char[n][n];
checkCol=new boolean[n];
checkDiag1=new boolean[2*n];
checkDiag2=new boolean[2*n];
//对棋盘path进行初始化:
for(int i=0;i<n;i++){
Arrays.fill(path[i],'.');
}
dfs(0);//从下标为0的那一行开始进行选择
return ret;
}
void dfs(int row){
if(row==n){//已对每一行Queen的位置进行了选择
List<String> tmp=new ArrayList<>();
//将字符型二维数组path进行转化:
for(int i=0;i<n;i++){
tmp.add(new String(path[i]));
}
ret.add(tmp);
return;
}
for(int j=0;j<n;j++){
if(!checkCol[j]&&!checkDiag1[row+j]&&!checkDiag2[j-row+n]){//该位置不能被标记为true
path[row][j]='Q';
checkCol[j]=checkDiag1[row+j]=checkDiag2[j-row+n]=true;
dfs(row+1);//去下一行进行选择
//恢复现场:
path[row][j]='.';
checkCol[j]=checkDiag1[row+j]=checkDiag2[j-row+n]=false;//注意Diag2(副对角线)的定位使用的是y-x+n,防止下标为负
}
}
}
}
总结
这种带矩阵的题目可能第一反应是使用bfs算法,但是要记住本题是符合"多次选择,每次都有多种选项"的特征的,所以使用dfs算法,接下来有几题也是这样的复习解题思路和代码注释
八、解数独
编写一个程序,通过填充空格来解决数独问题。
数独的解法需 遵循如下规则:
数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图)
数独部分空格内已填入了数字,空白格用 '.' 表示。


解题思路
- 跟上题相似,使用dfs解题,对每个空格进行选择,每个空格有多个选项
那么本题的重点还是如何剪枝了:
- 行和列像上题一样,搞个数组去定位每行、每列就行,不过本题还需标识哪行哪列的具体到哪一个数不能放置了,所以我们将数组的维度升高,变为二维数组,这样既可定位到行、列,又可以定位到具体的数
- 而题目中要求的大方格的标记方法也简单,将每个大方格整体像二维数组那样进行下标的标记(0、1、2),然后对每个小方格的坐标进行计算便可得到大方格的坐标:(x/3,y/3),那么像上面那样我们也需定位到具体的数,所以将check大方格的数组定义为三维数组即可
代码实现及解析
java
class Solution {
boolean[][] checkRow,checkCol;
boolean[][][] checkGrid;//检查大方格内是否有重复的数字
public void solveSudoku(char[][] board) {
checkRow=new boolean[9][10];
checkCol=new boolean[9][10];
checkGrid=new boolean[3][3][10];//单个一维数组的大小扩展为10是为了使其下标与数字适配
//初始化check表:
for(int i=0;i<9;i++){
for(int j=0;j<9;j++){
if(board[i][j]!='.'){//遇到数字就将其行、列、大方格内的对应状态标记
int num=board[i][j]-'0';
checkRow[i][num]=checkCol[j][num]=checkGrid[i/3][j/3][num]=true;
}
}
}
dfs(board);
}
boolean dfs(char[][] board){
/*
对下面两个最外层for循环的解释:dfs进来就是要对一个位置进行数字放置的多种选择的,
但由题可得该位置上可能已经就有数字了,所以我们进入dfs之后重新遍历一遍矩阵,直到遇到第一个空格
我们再在这个位置上进行填写。
也可以进dfs后直接进行判断:是空格就填写,不是就直接对坐标传参(pos)去下一层dfs像以前一样,
但是在这样的二维矩阵中i、j可不是一直++的,我们要控制i、j的正确走向是很麻烦的,所以不建议这样写
而且会多出来函数栈帧的开销
*/
for(int i=0;i<9;i++){
for(int j=0;j<9;j++){
if(board[i][j]=='.'){//此时找到了一个空白位置,可以对此位置进行数字放置的多种选择
for(int num=1;num<=9;num++){
if(!checkRow[i][num]&&!checkCol[j][num]&&!checkGrid[i/3][j/3][num]){
board[i][j]=(char)(num+'0');
checkRow[i][num]=checkCol[j][num]=checkGrid[i/3][j/3][num]=true;
if(dfs(board)) return true;//如果dfs返回了true,说明已经将所有位置都已放上了合适的数,就不要再去下面更改此次的选择而去换其他的数尝试了,直接return true
//恢复现场(然后进入下一层循环去更改此次的选择而去换其他的数尝试了):
board[i][j]='.';
checkRow[i][num]=checkCol[j][num]=checkGrid[i/3][j/3][num]=false;
}
}
//for循环结束了,说明这个位置压根就找不到一个合适的数放入,
//如果能找到合适数字填入该位置,那就会一直在里面dfs下去,直到将所有位置填完而解决此数独问题
//直接返回并给个失败的信号,不要再往下一个位置继续尝试了(而是回到上一个位置换一个数尝试):
return false;
}
}
}
return true;//最外层的两个大循环都结束了,说明此次遍历了所有的位置都不是空格(也就是已将所有位置都放上了合适的数了),直接给个成功的信号,回溯回去之后不要再换数字去尝试了
}
}
总结
复习解题思路和代码注释
九、单词搜索
给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中"相邻"单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

解题思路
- 读完题目就知道是使用dfs解题,只不过本题要寻找不同的合适的起点进行尝试,那就遍历矩阵一个一个找呗
- 一步一步地在矩阵中走,每走一步之后(肯定不是瞎走,哪个位置与字母匹配才去哪)下一步就有四个放向的不同选择。但是千万不要以为是不同的字母代表不同次的选择,用个pos来标记匹配到了哪个字母就行
代码实现及解析
java
class Solution {
boolean[][] visited;
int m,n;
char[] word;
//偏移量数组:
int[] dx={0,0,1,-1};
int[] dy={1,-1,0,0};
public boolean exist(char[][] board, String _word) {
m=board.length;
n=board[0].length;
visited=new boolean[m][n];
word=_word.toCharArray();
//遍历矩阵来寻找所有合适的起点(与第一个字母匹配的位置),从该起点开始搜索,使用dfs进行字母的一一匹配
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(board[i][j]==word[0]){
visited[i][j]=true;
if(dfs(board,i,j,1)) return true;//如果这个起点的尝试已成功了,就不用换其他起点尝试了
visited[i][j]=false;//恢复现场,选择下一个起点进行尝试
}
}
}
return false;//以所有起点开始的尝试均失败,不存在该单词
}
boolean dfs(char[][] board,int i,int j,int pos){
if(pos==word.length){
return true;
}
for(int k=0;k<4;k++){//尝试四个方向的路径
int x=i+dx[k],y=j+dy[k];
if(x>=0&&x<m&&y>=0&&y<n&&!visited[x][y]&&board[x][y]==word[pos]){
visited[x][y]=true;
if(dfs(board,x,y,pos+1)) return true;//如果此次的路径尝试已成功了,就不用换其他路径尝试了
visited[x][y]=false;//恢复现场,换其他路径尝试
}
}
return false;//四个方向都失败,所以该位置不行,回溯上去换路径
}
}
总结
复习解题思路给dfs设置返回值从而达到剪枝的目的的技巧应熟记,也就是当此次的路径尝试已经成功/失败(收到了dfs的返回值)就直接停止进行其他的dfs尝试,达到剪枝的目的(如果这句话不太理解在说什么就去结合代码思考)