根据前几个专题的学习,现在来一些比较综合的题进行练习
题目一:
思路:
根据之前写的那道求子集的题步骤一样,只是多了异或这一步,那我们完全可以先求出所有子集,然后再遍历子集,求得每一个子集异或的结果,进行相加
代码1:
class Solution {
//所有子集异或的和
public int sum=0;
//结果集
public List<List<Integer>> ret=new ArrayList<>();
//其中一个子集
public List<Integer> path=new ArrayList<>();
public void dfs(int[] nums,int k){
//添加子集
ret.add(new ArrayList<>(path));
for(int i=k;i<nums.length;i++){
path.add(nums[i]);
dfs(nums,i+1);
//恢复现场
path.remove(path.size()-1);
}
}
public int subsetXORSum(int[] nums) {
//先求出所有子集
dfs(nums,0);
//遍历每个子集
for (List<Integer> x:ret){
//该子集的异或和
int ans=0;
for (int a:x){
ans^=a;
}
//加上该子集异或和
sum+=ans;
}
return sum;
}
}
但是我们没必要用二维数组和一维数组求存储结果集和子集,因为这道题不需要我们返回这些,只需要求得每个子集的异或和,那我们完全可以在求子集的过程中就进行异或操作,减少时间复杂度和空间复杂度
其中一维数组恢复现场用的是remove,但我们异或就可以利用2个相同的数会消消乐来进行恢复现场
代码2(优化):
class Solution {
public int sum=0;
//该子集的异或和
public int path=0;
public void dfs(int[] nums,int k){
//加上该子集的异或和
sum+=path;
for(int i=k;i<nums.length;i++){
//异或该子集的元素
path^=nums[i];
dfs(nums,i+1);
//恢复现场
path^=nums[i];
}
}
public int subsetXORSum(int[] nums) {
dfs(nums,0);
return sum;
}
}
题目二:
思路:
跟之前写的全排列I前面思路几乎一样,但多了重复数字这一前提,使得按照之前的方法会重复一些全排列
最简单的方法就是还是按照之前的写法写,但是最后用一个哈希set来去重即可,虽然时间复杂度和空间复杂度都会比较高,但是思路很简单,几乎照搬之前的代码
代码1:
class Solution {
public List<List<Integer>> ret=new ArrayList<>();
public List<Integer> path=new ArrayList<>();
//哈希表和布尔数组一样的作用,可互换
Map<Integer,Integer> hash=new HashMap<>();
public void dfs(int[] nums){
//如果排完了
if(path.size()==nums.length){
ret.add(new ArrayList<>(path));
return;
}
for(int i=0;i<nums.length;i++){
//如果查哈希表发现没用过
if(hash.getOrDefault(i,0)==0){
path.add(nums[i]);
//更改为用过
hash.put(i,1);
dfs(nums);
//恢复现场
hash.put(i,0);
path.remove(path.size()-1);
}
}
}
public List<List<Integer>> permuteUnique(int[] nums) {
dfs(nums);
//用哈希set去重
Set<List<Integer>> set=new HashSet<>(ret);
//去完重再转回二维数组
ret=new ArrayList<>(set);
return ret;
}
}
虽然思路很简单,但是还是消耗比较大,不够优秀
所以就来思考如何不使用哈希set,在排列的时候就进行对应的剪枝,减少set的消耗
以[1,1,1,2]这个数组来举例
还是先画出决策树
从第一次选第一格的数字时,就发生了剪枝,那就是如果出现相同的数字,就要判断一下,如果前面用过该数字,后面相同的就不能使用了,但问题是数组不一定有序,所以我们要先排序
第一个前面没有数字,但第一个一定是不用剪枝的,所以把第一个也加入条件
但每次循环都从第一个开始,但第一个如果已经用过,那么就不能再用了,所以前提是没使用过的不用剪枝
综上有三个条件,没使用过&&(如果是第一个||如果前面的数字不等于后面的数字||两个数字相等用最前面没用过的),如果满足这个条件就进入递归,否则就不进入(剪枝)
当然也可以换成满足条件就剪枝,不满足条件就进入递归,只需要将上面的或者和并且进行互换,否定和肯定进行互换即可
代码2:
class Solution {
List<List<Integer>> ret=new ArrayList<>();
List<Integer> path=new ArrayList<>();
boolean[] check;
public void dfs(int[] nums){
//如果排完了
if(path.size()==nums.length){
ret.add(new ArrayList<>(path));
//剪枝
return;
}
for(int i=0;i<nums.length;i++){
//满足条件的就不剪枝
if(check[i]==false&&(i==0||nums[i-1]!=nums[i]||check[i-1]==false)){
path.add(nums[i]);
//标记为使用过
check[i]=true;
dfs(nums);
//恢复现场
check[i]=false;
path.remove(path.size()-1);
}
}
}
public List<List<Integer>> permuteUnique(int[] nums) {
//先排序
Arrays.sort(nums);
check=new boolean[nums.length];
dfs(nums);
return ret;
}
}
题目三:
思路:
老样子,先画决策树
跟之前的操作大同小异,只是这里如何建立映射关系,哈希表是一一映射,这里是一对多映射,可以创建一个字符串数组,每一个数字下标对应其所能表示的字符串
然后就按照之前全排列的步骤写就行
代码:
class Solution {
//结果集
List<String> ret =new ArrayList<>();
//每一个排列
StringBuffer sb=new StringBuffer();
//创建映射关系
String[] ss=new String[]{null,null,"abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
public void dfs(String s,int k){
//如果排完了
if(sb.length()==s.length()){
ret.add(sb.toString());
return;
}
//找到对应映射的字符串
String str=ss[s.charAt(k)-'0'];
//遍历字符串
for(int i=0;i<str.length();i++){
//添加该字符串的第i个
sb.append(str.charAt(i));
//往后排列下一个
dfs(s,k+1);
//恢复现场
sb.deleteCharAt(sb.length()-1);
}
}
public List<String> letterCombinations(String digits) {
//如果为空字符串
if(digits.equals("")){
return ret;
}
dfs(digits,0);
return ret;
}
}
注意这里判断是否是空字符串可以用digits.equals("")或者digits.length()==0来判断,不能用digits=="",因为这个是比较引用地址,肯定是false,编程语言还学了其他的比如python就容易写出来,不要犯语法错误
题目四:
思路:
可能第一反应是跟之前有一道题很像,就是选或者不选,比如第一个选“(”或者不选“(”, 选“)”或者不选“)”,按照这种思路来画决策树,就可以将所有括号组成排列完
但是又排列的太过了,题目要求是有效的括号组合,什么是有效的括号组合
第一个显而易见,第二个证明也不难,就是不能出现“())(”这种情况,这种情况第二对的括号就反了
按照之前简单的选或者不选的决策树,有些无效的括号组合就添加进来了,所以我们就要进行剪枝
主要剪的就是左括号的数量必须大于等于右括号,换而言之就是添加右括号之前必须有待匹配的左括号存在
那么我们就需要两个全局变量left和right来记录左右括号的剩余数量,结束条件那就是当排列长度为括号对数的两倍时就停止
代码:
class Solution {
//结果集
List<String> ret=new ArrayList<>();
//每一个排列
StringBuffer sb=new StringBuffer();
//左括号和右括号剩余数量
int left=0;
int right=0;
public void dfs(int n){
//如果排完了
if(sb.length()==n*2){
ret.add(sb.toString());
return;
}
//如果还有左括号
if(left>0){
//加上左括号
sb.append("(");
//剩余数量减1
left--;
//排下一个空
dfs(n);
//恢复现场
sb.deleteCharAt(sb.length()-1);
left++;
}
//如果前面出现了左括号
if(left<right){
//加上右括号
sb.append(")");
right--;
//排下一个空
dfs(n);
//恢复现场
sb.deleteCharAt(sb.length()-1);
right++;
}
}
public List<String> generateParenthesis(int n) {
//更新数量
left=right=n;
dfs(n);
return ret;
}
}
题目五:
思路:
还是大同小异的全排列类型的题目,其中[2,1]和[1,2]是同一种,所以要进行剪枝,因此传参的时候要传遍历的位置
画决策树,然后就可以写代码了
代码:
class Solution {
//结果集
List<List<Integer>> ret = new ArrayList<>();
//每一种排列
List<Integer> path = new ArrayList<>();
public void dfs(int n, int k, int cur) {
//如果排完了
if (path.size() == k) {
ret.add(new ArrayList<>(path));
return;
}
for (int i = cur; i <= n; i++) {
path.add(i);
dfs(n, k, i + 1);
//恢复现场
path.removeLast();
}
}
public List<List<Integer>> combine(int n, int k) {
dfs(n, k, 1);
return ret;
}
}
题目六:
思路:
跟之前子集那道题大同小异,之前是选或者不选,这道题是加或者减
还是画决策树,然后写代码
代码1(全局变量):
class Solution {
//结果
int count = 0;
//和
int sum = 0;
public void dfs(int[] nums, int target, int cur) {
//如果数字全部用完了并且符合目标和
if (cur == nums.length && sum == target) {
count++;
return;
}
//如果还有数字没用完
if (cur < nums.length) {
//加
sum += nums[cur];
dfs(nums, target, cur + 1);
sum -= nums[cur]; //恢复现场
//减
sum -= nums[cur];
dfs(nums, target, cur + 1);
sum += nums[cur]; //恢复现场
}
}
public int findTargetSumWays(int[] nums, int target) {
dfs(nums, target, 0);
return count;
}
}
这道题也可以利用方法传参的方式来自动恢复现场,将和以参数传递
代码2(方法传参):
class Solution {
// 结果
int count = 0;
// 和
int sum = 0;
public void dfs(int[] nums, int target, int cur, int path) {
// 如果数字全部用完了并且符合目标和
if (cur == nums.length && path == target) {
count++;
return;
}
// 如果还有数字没用完
if (cur < nums.length) {
// 加
dfs(nums, target, cur + 1, path + nums[cur]);
// 减
dfs(nums, target, cur + 1, path - nums[cur]);
}
}
public int findTargetSumWays(int[] nums, int target) {
dfs(nums, target, 0, 0);
return count;
}
}
这道题第二种方式时间会更快一点,但大部分都是忽略不计的,两种方法都行
题目七:
思路:
题意很简单,跟之前的也大同小异,如果按照之前的方法,那么会出现顺序不同但结果是同一个的情况,有一个很暴力无脑的方法 ,就是添加的时候排好序再添加,最后再用哈希set去重就行
代码(暴力):
class Solution {
List<List<Integer>> ret=new ArrayList<>();
List<Integer> path=new ArrayList<>();
public void dfs(int[] nums,int target,int sum){
//如果加过了就不加了
if(sum>target){
return;
}
//如果刚刚好
if(sum==target){
//先排序再添加
List<Integer> tmp=new ArrayList<>(path);
Collections.sort(tmp);
ret.add(tmp);
return;
}
//进行添加
for(int i=0;i<nums.length;i++){
path.add(nums[i]);
dfs(nums,target,sum+nums[i]);
//恢复现场
path.removeLast();
}
}
public List<List<Integer>> combinationSum(int[] candidates, int target) {
dfs(candidates,target,0);
//用哈希set去重
Set<List<Integer>> set=new HashSet<>(ret);
ret=new ArrayList<>(set);
return ret;
}
}
其中数组的排序是Arrays.sort(),而顺序表的排序是Collections.sort()
这种方法很好想,不需要花心思剪枝,但是时间空间复杂度肯定也不低
如果要剪枝的话也很容易,先画决策树
(注:第二层3+5等于8,不是等于5,画错了)
之前的题是不能重复选,所以数组位置都是加1,这道题可以重复选,所以位置不用加1
剪枝有两种情况,第一种就是数组位置之前的都剪掉,第二种就是和大于目标和时,直接return
代码1:
class Solution {
List<List<Integer>> ret=new ArrayList<>();
List<Integer> path=new ArrayList<>();
public void dfs(int[] nums,int target,int sum,int cur){
//如果加过了就不加了
if(sum>target){
return;
}
//如果刚刚好
if(sum==target){
ret.add(new ArrayList<>(path));
return;
}
//进行添加
for(int i=cur;i<nums.length;i++){
path.add(nums[i]);
dfs(nums,target,sum+nums[i],i);
//恢复现场
path.removeLast();
}
}
public List<List<Integer>> combinationSum(int[] candidates, int target) {
dfs(candidates,target,0,0);
return ret;
}
}
决策树也不唯一,除此之外还有一种画法
那就是按照选多少个的思路来画决策树
比如从2开始,可以选0到4个2,因为第5个2就超了
然后在每个分支下,去选n个3,同理在0个2的基础上可以选0到2个3,以此类推
代码2:
class Solution {
int aim;
List<Integer> path;
List<List<Integer>> ret;
public List<List<Integer>> combinationSum(int[] nums, int target) {
path = new ArrayList<>();
ret = new ArrayList<>();
aim = target;
dfs(nums, 0, 0);
return ret;
}
public void dfs(int[] nums, int pos, int sum) {
if (sum == aim) {
ret.add(new ArrayList<>(path));
return;
}
if (sum > aim || pos == nums.length)
return;
// 枚举 nums[pos] 使⽤多少个
for (int k = 0; k * nums[pos] + sum <= aim; k++) {
if (k != 0)
path.add(nums[pos]);
dfs(nums, pos + 1, sum + k * nums[pos]);
}
// 恢复现场
for (int k = 1; k * nums[pos] + sum <= aim; k++) {
path.remove(path.size() - 1);
}
}
}
题目八:
思路:
其中数字不用变,如果字母的话就有两种选择,转大写或者小写
然后结束条件还是排的长度等于原字符串的长度就说明排完了,就添加到结果集
恢复现场就删除最后的元素即可
代码:
class Solution {
List<String> ret = new ArrayList<>();
StringBuffer path = new StringBuffer();
public void dfs(String s, int cur) {
//结束条件
if (cur == s.length()) {
String ss = path.toString();
ret.add(ss);
return;
}
char ch = s.charAt(cur);
//如果当前字符是字母
if (Character.isLetter(ch)) {
//选择大写
path.append(Character.toUpperCase(ch));
dfs(s, cur + 1);
path.deleteCharAt(path.length() - 1);
//选择小写
path.append(Character.toLowerCase(ch));
dfs(s, cur + 1);
path.deleteCharAt(path.length() - 1);
}
//如果是数字
if (Character.isDigit(ch)) {
path.append(ch);
dfs(s, cur + 1);
path.deleteCharAt(path.length() - 1);
}
}
public List<String> letterCasePermutation(String s) {
dfs(s, 0);
return ret;
}
}
题目九:
思路:
还是先画出决策树
两种情况要剪枝,第一种是之前用过的不能用,第二种是不满足题目优美的条件就剪掉,每次填入一个空,用Boolean类型的数组记录使用情况,这道题没说要记录所有路径情况,所以可以不用建立path数组,只用返回有多少种即可,每次回溯完要恢复现场
代码:
class Solution {
//记录有多少种
int count = 0;
//记录数字使用情况
boolean[] check;
public void dfs(int n, int cur) {
//如果当前全部排完了
if (cur == n + 1) {
count++;
return;
}
//枚举1到n个数
for (int j = 1; j <= n; j++) {
//如果这个数没用过
if (check[j] == false) {
//如果这个数符合优美的条件
if (j % cur == 0 || cur % j == 0) {
//修改使用情况
check[j] = true;
//去填下一个空
dfs(n, cur + 1);
//恢复现场
check[j] = false;
}
}
}
}
public int countArrangement(int n) {
//因为下标是从1开始,所以要加1
check = new boolean[n + 1];
//有n个数,从第1个空开始填
dfs(n, 1);
return count;
}
}
题目十:
思路:
很经典的一道递归回溯的题,题意就是返回一个二维数组,一维数组记录每一种的解法,一维数组里的字符串表示放置情况,每一个字符串代表每一行的放置情况,Q代表有皇后,.表示空格
先画决策树
也就是从第0行开始,每个格子去尝试
剪枝有三种情况,第一种就是同列有皇后,第二种就是主对角线有皇后,第三种就是副对角线有皇后(因为我们是一行一行去枚举的,所以不包括同行有皇后的情况)
那么接下来就是如何实现剪枝
我们还是根据之前的方法,用boolean类型的数组去记录是否有皇后,如果false就代表没有皇后,这个位置可以放,true就代表有皇后,这个位置不能放
因为有三种剪枝情况,所以可以创建三个Boolean类型的数组
其中col记录列的情况,checkDig1记录主对角线,checkDig2记录副对角线
其中列很好写,有n列就创建长度为n的数组
主对角线如下
以(0,0)为原点,创建坐标系,很容易知道每条主对角线的斜率是1(因为是45度),根据关系可以写成表达式:y=kx+b,其中k=1,也就是y=x+b
然后进行移项,y-x=b,所以每一条主对角线上的点y-x都是一个定值,因此我们就可以用boolean数组的下标y-x来代表这条主对角线有没有皇后
但是当y<x时,会出现负数,而数组下标又没有负数,所以我们要对两边进行同加
其中负数最小的情况是当y=0,x=n-1(有n列,但是是从0开始,所以最大为n-1列)
此时y-x=-n+1,所以为了让所有情况都为正数,就可以加上n(当然也可以加上n-1,使得刚好为0,无非是浪不浪费一个格子而已,只要足够就行)
因此就用y-x+n来表示主对角线皇后的情况
同理副对角线
斜率为-1,经过移项得到x+y=b,因为是加法,所以不出现负数的情况,就不用同加了
最后确定三个数组长度
列数组:有n列,所以长度为n
主对角线数组:当row=n-1,col=0,row-col+n=n-1-0+n=2*n-1,长度为2n刚好
(如果要做到极致的不浪费,那么就是row-col+n-1=2*n-2,长度为2n-1刚好)
副对角线数组:当row=n-1,col=n-1,row+col=n-1+n-1=2*n-2,所以长度2n足够
(如果要做到极致的不浪费,那么长度为2n-1刚好)
因此当这一个格子对应的三个Boolean数组都为false才能放皇后,这时进行修改记录情况,去找下一行,找完要恢复现场
结束条件就是当到了第n行时,就说明全都正确放完了,没到第n行的全部都失败了,此时将棋盘的放置情况添加即可
代码:
class Solution {
//结果集
List<List<String>> ret=new ArrayList<>();
//记录是否有皇后,col:列,checkDig1:主对角线,checkDig2:副对角线
boolean[] col,checkDig1,checkDig2;
//模拟棋盘
char[][] ch;
public void dfs(int row,int n){
//如果到了第n行
if(row==n){
//把这种解法全部放进去
List<String> path=new ArrayList<>();
for(int i=0;i<n;i++){
path.add(new String(ch[i]));
}
ret.add(path);
return;
}
//遍历row行的第n格
for(int i=0;i<n;i++){
//如果这个格子没有发生皇后冲突
if(col[i]==false&&checkDig1[row-i+n-1]==false&&checkDig2[row+i]==false){
//可以放进去
ch[row][i]='Q';
//修改记录情况
col[i]=checkDig1[row-i+n-1]=checkDig2[row+i]=true;
//去下一行
dfs(row+1,n);
//恢复现场
ch[row][i]='.';
col[i]=checkDig1[row-i+n-1]=checkDig2[row+i]=false;
}
}
}
public List<List<String>> solveNQueens(int n) {
//有n列,所以长度为n足够
col=new boolean[n];
//当row=n-1,col=0,row-col+n-1=2*n-2,所以长度2n-1刚好
checkDig1=new boolean[2*n-1];
//当row=n-1,col=n-1,row+col=2*n-2,所以长度2n-1刚好
checkDig2=new boolean[2*n-1];
//模拟n*n的棋盘
ch=new char[n][n];
//初始化棋盘
for(int i=0;i<n;i++){
Arrays.fill(ch[i],'.');
}
//从第0行开始遍历,到第n行结束
dfs(0,n);
return ret;
}
}
题目十一:
思路:
跟上一道的n皇后比较类似,都是棋盘类,但是n皇后剪枝主要是行,列,主副对角线,而数独是行,列,九宫格
所以行列大致类似的思路,而实现九宫格剪枝是主要问题
先给棋盘画出下标
总共有9个九宫格,我们以每3行每3列作为一块,就变成0,1,2三“行”,和0,1,2三“列”
这样[0][0]就对应左上角的九宫格,以此类推
而0-2这三列/3都等于0,3-5这三列/3都等于1,6-8这三列/3都等于2,因此就可以通过/3来判断是哪一个九宫格了,列如此,反过来行也是如此
而之前n皇后只需要记录有没有一个就行,所以是一维数组
而数独还有记录是哪一个数字出现了,所以还要多一维
因此
行:[9][10](9行,每行有9个数,所以长度10就是1对应1下标,浪费了0这一小格,如果要做到极致的不浪费,也可以为[9][9],那么就是1对应0下标)
列:[9][10](9列,每列有9个数,所以长度为10就是1对应1下标,浪费了0这一小格,如果要做到极致的不浪费,也可以为[9][9],那么就是1对应0下标)
九宫格:[3][3][10](3行3列个九宫格,每个九宫格有9个数,所以长度为10就是1对应1,浪费了0这一小格,如果要做到极致的不浪费,也可以为[3][3][9],那么就是1对应0下标)
那么接下来就遍历整个棋盘,如果空的就不管,有数字就去三个数组里看看有没有出现重复的,有就直接返回false,没有就继续遍历,最后遍历完了还没有返回false,就说明是有效的数读,返回true
代码:
class Solution {
//行
boolean[][] row=new boolean[9][10];
//列
boolean[][] col=new boolean[9][10];
//九宫格
boolean[][][] grid=new boolean[3][3][10];
public boolean isValidSudoku(char[][] board) {
//遍历棋盘
for(int i=0;i<9;i++){
for(int j=0;j<9;j++){
//如果不是空格,是数字
if(board[i][j]!='.'){
//拿到这个格子的数字
int num=board[i][j]-'0';
//如果出现过了
if(row[i][num]||col[j][num]||grid[i/3][j/3][num]){
return false;
}else{//修改记录情况
row[i][num]=col[j][num]=grid[i/3][j/3][num]=true;
}
}
}
}
//说明是有效的数独
return true;
}
}
题目十二:
思路:
跟上一题比较类似,只不过这道题需要我们自己将数独填完,其中需要进行决策和回溯,而决策的时候需要进行一些剪枝,其中剪枝就使用我们上一道题判断是否有效来实现
第一步先将原本棋盘上数字的出现情况进行修改,也就是初始化
第二步就去遍历棋盘,从第0行第0列开始,一行一行的去填
如果该行和该列的这一格是空的,就说明要填,我们就从1-9进行尝试,也就是决策,而有些数我们通过判断就可以排除掉,这时就进行了剪枝,然后往后进行遍历,其中如果是最后一列,就要换行,反之,就往下一列去遍历
而如果有数,就说明原本题目就已经填过了,我们不用修改,直接去填后面的
其中结束条件那就是到了第9行,说明整个棋盘都填完了,那么就结束了,可以返回
但其中有些决策是错误的,导致后面填不下去了,即无解了,说明前面填错了,我们要让之前填数的知道,所以要有返回值false,而如果接收到返回值false,就说明这里填错了,要重新填,那么就要恢复现场,将该格置空,并修改出现情况
其中有唯一一个正确解会到达结束条件,那么结束条件就要返回true,让前面的都知道这么填是对的,不用修改了,于是又一层一层返回true
最后就解出了数独
代码:
class Solution {
boolean[][] row = new boolean[9][10];
boolean[][] col = new boolean[9][10];
boolean[][][] grid = new boolean[3][3][10];
public boolean dfs(char[][] board, int i, int j) {
//如果到了第9行,说明已经解完数独了
if (i == 9) {
return true;
}
//如果这格是空的
if (board[i][j] == '.') {
//尝试填数1-9
for (int n = 1; n <= 9; n++) {
//如果这个数填进去没有发生冲突
if (row[i][n] == false && col[j][n] == false && grid[i / 3][j / 3][n] == false) {
//修改出现情况
row[i][n] = col[j][n] = grid[i / 3][j / 3][n] = true;
//填数
board[i][j] = (char) (n + '0');
//如果是该行的最后一列
if (j == 8) {
//去下一行继续填数,如果下一行也能够正确填完,说明之前的数没填错
if(dfs(board, i + 1, 0)){
//返回给上一次填数的时候,说明这里填对了
return true;
}else{//说明下一行出现填不下去的情况,说明前面填错了
//恢复现场
row[i][n] = col[j][n] = grid[i / 3][j / 3][n] = false;
board[i][j]='.';
}
} else {//如果不是该行的最后一列
//去填该行的下一列,如果下一列也能够正确填完,说明之前的数没填错
if(dfs(board, i, j + 1)){
//返回给上一次填数的时候,说明这里填对了
return true;
}else{//说明下一列出现填不下去的情况,说明前面填错了
//恢复现场
row[i][n] = col[j][n] = grid[i / 3][j / 3][n] = false;
board[i][j]='.';
}
}
}
}
//出现无解的情况了,说明之前没填对,要告诉之前填数的决策是错的
return false;
} else {//如果是原本棋盘就有的数
//如果是该行的最后一列
if (j == 8) {
//如果后面能够正确填数,说明之前没填错
if(dfs(board, i + 1, 0)){
return true;
}
//后面填不下去了,说明之前填错了
return false;
} else {//如果不是该行的最后一列
//如果后面能够正确填数,说明之前没填错
if(dfs(board, i, j + 1)){
return true;
}
//后面填不下去了,说明之前填错了
return false;
}
}
}
public void solveSudoku(char[][] board) {
//先初始化原本棋盘数字出现情况
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
if (board[i][j] != '.') {
int num = board[i][j] - '0';
row[i][num] = col[j][num] = grid[i / 3][j / 3][num] = true;
}
}
}
//从第0行第0列开始
dfs(board, 0, 0);
}
}
题目十三:
思路:
这种属于矩阵内的搜索,类似于走迷宫,做决策的过程如下
而画出决策树如下
所以这时候就需要先去遍历整个数组, 找到起点再去走,而起点也不止一个,需要找到正确的起点
然后走的方向是上下左右,去找字符串的下一个字符
因此我们的dfs功能是给定一个点,去找字符串包括cur位置之后的字符串
因此参数为dfs(char[][] board,int i,int j,int cur,String word),其中board是原数组,ij是坐标,cur是要找的字符串的cur位置,word是要找的字符串
其中上下左右会出现边界情况,因此需要作出界判断,没出界才能走
但是会有一个细节,那就是不能走重复的路
因此我们需要一个记录数组,来记录走过的情况
那么结束条件就是当cur遍历完了字符串,就说明找完了,返回true,同时也要告诉上一次决策是对的,反之,要返回false,跟数独那个一个思想
但还有一个优化的点,那就是鸽巢原理,如果字符数组所有字符都没有字符串长的话,那么肯定是不能的,因此可以先判断一下
代码1:
class Solution {
//原数组的行和列
int row,col;
//记录数组
boolean[][] check;
public boolean dfs(char[][] board,int i,int j,int cur,String word){
//结束条件
if(cur==word.length()){
return true;
}
//如果当前是要找的字符
if(board[i][j]==word.charAt(cur)&&check[i][j]==false){
//标记该位置走过了
check[i][j]=true;
//往上
if(i>0){
if(dfs(board,i-1,j,cur+1,word)){
return true;
}
}
//往下
if(i+1<row){
if(dfs(board,i+1,j,cur+1,word)){
return true;
}
}
//往右
if(j+1<col){
if(dfs(board,i,j+1,cur+1,word)){
return true;
}
}
//往左
if(j>0){
if(dfs(board,i,j-1,cur+1,word)){
return true;
}
}
//恢复现场
check[i][j]=false;
//说明此时无解,之前决策错了
return false;
}else{//说明当前不是要找的字符
//告诉之前的决策错了
return false;
}
}
public boolean exist(char[][] board, String word) {
//求原数组的行数和列数
row=board.length;
col=board[0].length;
//鸽巢原理
if(word.length()>row*col){
return false;
}
//特判
if(row==1&&col==1){
if(board[0][0]==word.charAt(0)){
return true;
}
return false;
}
//创建记录数组
check=new boolean[row][col];
//遍历数组找起点
for(int i=0;i<row;i++){
for(int j=0;j<col;j++){
//找到一个起点
if(board[i][j]==word.charAt(0)){
if(dfs(board,i,j,0,word)){
//说明决策对了
return true;
}
}
}
}
//说明无解,找不到
return false;
}
}
但这里会出现字符数组只有一个的情况,那么起点就是终点,没有上下左右可以选,正常情况下认为是无解了,但其实这里是已经找完了,所以要特判一下
还有另外一个思路
刚刚那个dfs我们假设为给定一个点,将该点与字符串cur位置的字符进行比较
这回我们还可以假设为给定一个点,去上下左右找字符串cur位置的字符
然后还有上下左右的查找,我们之前是用四个dfs,比较冗杂,如果出现八个方向,那么要写八个dfs,非常多,因此有一个方式进行修改
我们可以用两个数组来存偏移量,并建立映射关系
比如上图,dx[0]dy[0]就对应右,dx[1]dy[1]就对应左,如此类推,这样就可以用一个for循环来实现上下左右
代码2:
class Solution {
int row, col;
boolean[][] check;
//上下左右映射关系
int[] dx = { 0, 0, 1, -1 };
int[] dy = { 1, -1, 0, 0 };
public boolean dfs(char[][] board, int i, int j, int cur, String word) {
//找完了
if (cur == word.length()) {
return true;
}
//上下左右去找
for (int k = 0; k < 4; k++) {
int x = dx[k], y = dy[k];
//不出界&&没走过&&是要找的字符
if (i + x < row && i + x >= 0 && j + y < col && j + y >= 0 && !check[i + x][j + y]
&& board[i + x][j + y] == word.charAt(cur)) {
//修改记录情况
check[i + x][j + y] = true;
if (dfs(board, i + x, j + y, cur + 1, word)) {//在当前位置的上下左右去找下一个字符
return true;
}
//恢复现场
check[i + x][j + y] = false;
}
}
//上下左右都没有,说明之前决策错了
return false;
}
public boolean exist(char[][] board, String word) {
row = board.length;
col = board[0].length;
//鸽巢原理
if (row * col < word.length()) {
return false;
}
check = new boolean[row][col];
//遍历数组找起点
for (int i = 0; i < row; i++) {
for (int j = 0; j < col; j++) {
//找到一个起点
if (board[i][j] == word.charAt(0)) {
//修改记录情况
check[i][j] = true;
//在当前位置的上下左右去找word的第二个字符
if (dfs(board, i, j, 1, word)) {
return true;
}
//恢复现场
check[i][j] = false;
}
}
}
//无解
return false;
}
}
题目十四:
思路:
跟上一道题差不多类型的,都是走迷宫,所以我们只需要先遍历数组,然后找起点,上下左右去寻找,因为也是不能走重复的路,所以需要使用一个记录数组
而这一道题主要独特的是不需要递归出口,按理来说大部分dfs一开始都有一个结束条件,这道题不需要,只需要在一开始更新结果就行,如果上下左右都不能走,那么也就自然return了
代码:
class Solution {
//结果
int max=0;
//上下左右映射关系
int[] dx={0,0,1,-1};
int[] dy={1,-1,0,0};
//记录数组
boolean[][] check;
int row,col;
public void dfs(int[][] grid,int i,int j,int sum){
//更新结果
if(sum>max){
max=sum;
}
//上下左右去找
for(int k=0;k<4;k++){
int x=dx[k],y=dy[k];
if(i+x>=0&&i+x<row&&j+y>=0&&j+y<col&&check[i+x][j+y]==false&&grid[i+x][j+y]!=0){
//修改记录情况
check[i+x][j+y]=true;
dfs(grid,i+x,j+y,sum+grid[i+x][j+y]);
//恢复现场
check[i+x][j+y]=false;
}
}
}
public int getMaximumGold(int[][] grid) {
row=grid.length;
col=grid[0].length;
check=new boolean[row][col];
//遍历数组找起点
for(int i=0;i<row;i++){
for(int j=0;j<col;j++){
//找到其中一个起点
if(grid[i][j]!=0){
check[i][j]=true;
dfs(grid,i,j,grid[i][j]);
check[i][j]=false;
}
}
}
//返回结果
return max;
}
}
题目十五:
思路:
也还是走迷宫类型的题,还是先找到起点,然后去上下左右遍历,直到走到终点,这时就需要检查是否符合题目要求,是合法的走法
比较暴力的检查方法就是去遍历记录数组,如果除了-1之外都是true,说明都走过了,此时结果+1,反之则不加
还有一种方法要更好一点,那就是在遍历数组找起点的时候,顺便统计出从起点到终点不漏不重复的走需要多少步,然后dfs传参要多传一个当前步数,这样子到终点时只需要判断当前步数是否等于所需步数,即可知道是否是合法的走法
其他地方都与前两道走迷宫的题几乎相同
代码1:
class Solution {
int row,col;
int ret=0;
int[] dx={0,0,1,-1};
int[] dy={1,-1,0,0};
boolean[][] check;
//暴力检查是否是合法的走法
public boolean checkright(int[][] grid,boolean[][] checkarr){
for(int i=0;i<row;i++){
for(int j=0;j<col;j++){
//如果是能走的但没走过
if(grid[i][j]!=-1&&checkarr[i][j]==false){
//说明不是合法的
return false;
}
}
}
//说明合法
return true;
}
public void dfs(int[][] grid,int i,int j){
//如果走到终点了
if(grid[i][j]==2){
//如果是合法的
if(checkright(grid,check)){
//结果+1
ret++;
}
return;
}
//上下左右
for(int k=0;k<4;k++){
int x=i+dx[k],y=j+dy[k];
if(x>=0&&x<row&&y>=0&&y<col&&check[x][y]==false&&grid[x][y]!=-1){
//修改记录情况
check[x][y]=true;
dfs(grid,x,y);
//恢复现场
check[x][y]=false;
}
}
}
public int uniquePathsIII(int[][] grid) {
row=grid.length;
col=grid[0].length;
check=new boolean[row][col];
//遍历数组找起点
for(int i=0;i<row;i++){
for(int j=0;j<col;j++){
if(grid[i][j]==1){
//修改记录情况
check[i][j]=true;
dfs(grid,i,j);
//返回结果
return ret;
}
}
}
//照顾编译器
return ret;
}
}
代码2(更优):
class Solution {
int row,col;
//结果
int ret=0;
//上下左右映射关系
int[] dx={0,0,1,-1};
int[] dy={1,-1,0,0};
//记录数组
boolean[][] check;
//所需步数
int step;
public void dfs(int[][] grid,int i,int j,int count){
//如果走到终点了
if(grid[i][j]==2){
//如果是合法的走法
if(step==count){
//结果+1
ret++;
}
return;
}
//上下左右
for(int k=0;k<4;k++){
int x=i+dx[k],y=j+dy[k];
if(x>=0&&x<row&&y>=0&&y<col&&check[x][y]==false&&grid[x][y]!=-1){
//修改记录情况
check[x][y]=true;
dfs(grid,x,y,count+1);
//恢复现场
check[x][y]=false;
}
}
}
public int uniquePathsIII(int[][] grid) {
row=grid.length;
col=grid[0].length;
check=new boolean[row][col];
//起点的位置
int bx=0,by=0;
for(int i=0;i<row;i++){
for(int j=0;j<col;j++){
//统计需要的步数
if(grid[i][j]==0){
step++;
}else if(grid[i][j]==1){
//记录起点的位置并修改记录情况
check[i][j]=true;
bx=i;
by=j;
}
}
}
//加上起点和终点的两步
step+=2;
//从起点开始,当前已经走了起点,步数为1步
dfs(grid,bx,by,1);
return ret;
}
}
总结:
至此所有的综合练习全部写完,应该对dfs有了更深刻的感受,其中重点是恢复现场,剪枝,画决策树,确定结束条件
以及走迷宫类型的题,可以用映射关系来记录上下左右,同理也可以记录八个方向
还有对角线和九宫格的写法
基本也就是暴搜,思路不算太难,主要要有代码实现能力,能将决策树的思路转化为代码
接下来还有floodfill和记忆化搜索两个小专题,学完就算大体地学完了递归,搜索和回溯这个大专题了,进行加油!