回溯(backtrack)


(JAVA版本)

1 回溯法思路

常用于遍历列表所有子集,是 DFS 深度搜索一种,一般用于全排列,穷尽所有可能,遍历的过程实际上是一个决策树的遍历过程。时间复杂度一般 O(N!),它不像动态规划存在重叠子问题可以优化,回溯算法就是纯暴力穷举,复杂度一般都很高。
思路:核心就是从选择列表里做一个选择,然后一直递归往下搜索答案,如果遇到路径不通,就返回来撤销这次选择。

伪代码结构

		int[] result = new int[size];
		void backtrack(选择列表,路径): {
		    if 满足结束条件: {
		        result.add(路径)
		        return
		    }
		    for (选择 : 选择列表): {
		        做选择
		        backtrack(选择列表,路径)
		        撤销选择
		    }
		}

说明
在后面的题目中,我们会频繁的看到visited[]以及start变量,有些朋友可能会疑惑什么时候使用 visited 数组,什么时候使用 start 变量?总结如下:

  • 排列问题,讲究顺序(即 [2, 2, 3] 与 [2, 3, 2] 视为不同列表时),需要记录哪些数字已经使用过,此时用visited 数组;
  • 组合问题,不讲究顺序(即 [2, 2, 3] 与 [2, 3, 2] 视为相同列表时),需要按照某种顺序搜索,此时使用start 变量。

2 常见题目

2.1 (lee-78) 子集

给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。可以按 任意顺序 返回解集。
思路:回溯法

输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]

(1)按任意顺序返回

	/**
	 * 1.按任意顺序返回
	 */
	public ArrayList<ArrayList<Integer>> subsets1(int[] S) {  
		ArrayList<ArrayList<Integer>> res  = new ArrayList<>();
        LinkedList<Integer> track = new LinkedList<>();
        back(res,S,0,track);
		return res;        
    }  

	private void back(ArrayList<ArrayList<Integer>> res,int[] S, int start, LinkedList<Integer> track) {
		res.add(new ArrayList<>(track));
		for(int i = start;i < S.length;i++) {
        	track.add(S[i]); //更新状态
        	back(res,S,i+1,track); //回溯
        	track.removeLast(); //回退状态
        }       
	}

(2)按一定顺序返回

	/**
	 * 2.按一定顺序返回
	 */
	public List<List<Integer>> subsets(int[] nums) {
		List<List<Integer>> res = new ArrayList<>();
		for(int i = 0;i <= nums.length ;i++) {  //i是某个子集内的元素个数
			backtrack(res,0,new ArrayList<Integer>(),nums,i); 
		}
		return res;	
    }
	
	private void backtrack(List<List<Integer>> res, int first, ArrayList<Integer> cur, int[] nums,int k) {
		if(cur.size()== k) {
			res.add(new ArrayList<>(cur));
			return ;
		}
		for(int i = first;i < nums.length;i++) {
			cur.add(nums[i]);
			backtrack(res,i+1, cur, nums,k);
			cur.remove(cur.size()-1); // 删除最后添加的一个数
		}
	}
	
	public static void main(String[] args) {
		Subsets s = new Subsets();
		Scanner in = new Scanner(System.in);
		String[] str = in.nextLine().split(" ");
		int[] nums  = new int[str.length];
		for(int i = 0;i <nums.length;i++) {
			nums[i] = Integer.parseInt(str[i]);
		}
		List<List<Integer>> res = s.subsets(nums);
		System.out.println(res);
	}

2.2 (lee-90) 子集2

给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。

输入:nums = [1,2,2]
输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Scanner;

public class Subsets2 {

	/**
	 * 思路:重复的解不添加进去,并用一个数组visited标记某个元素是否被访问过
	 */
	public List<List<Integer>> subsetsWithDup(int[] nums) {
		List<List<Integer>> res = new ArrayList<>();
		Arrays.sort(nums); //先将数组排序
		List<Integer> temp = new ArrayList<>();
		for(int i= 0;i <= nums.length;i++) {
			backtrack(res,temp,new boolean[nums.length],nums,0,i);
		}
		return res;
    }

	/** 
	 * @param res 所有子集
	 * @param temp 
	 * @param visited 标记元素
	 * @param nums 
	 * @param start
	 * @param size
	 */
	private void backtrack(List<List<Integer>> res, List<Integer> temp, boolean[] visited, int[] nums, int start, int size) {
		if(temp.size() == size) {
			res.add(new ArrayList<>(temp));
			return;
		}
		for(int i = start; i <nums.length;i++) {
			if(i != 0  && nums[i] == nums[i-1] && !visited[i-1]) {  //若发现没有选择上一个数,且当前数字与上一个数相同且前一个数没有被标记过,则可以跳过当前生成的子集。。
				continue;
			}
			temp.add(nums[i]);
			visited[i] = true;
			backtrack(res, temp, visited, nums, i+1, size); //根据与前面元素是否重复,来决定start的取值,也就是开始遍历的位置
			visited[i] = false;
			temp.remove(temp.size()-1);
		}	
	}
	
	public static void main(String[] args) {
		Subsets2 s = new Subsets2();
		Scanner in = new Scanner(System.in);
		String[] str = in.nextLine().split(" ");
		int[] nums  = new int[str.length];
		for(int i = 0;i <nums.length;i++) {
			nums[i] = Integer.parseInt(str[i]);
		}
		List<List<Integer>> res = s.subsetsWithDup(nums);
		System.out.println(res);
	}
}

2.3 (lee-46) 全排列

给定一个没有重复数字的序列,返回其所有可能的全排列。 可以 按任意顺序 返回答案。

输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

	/**
	 * 思路:需要用一个visited数组记录已经选择过的元素,满足条件的结果才进行返回
	 * @param nums
	 * @return
	 */
	public List<List<Integer>> permute(int[] nums) {
		List<List<Integer>> res = new ArrayList<>();
		List<Integer> permute = new ArrayList<>();
		boolean[] visited = new boolean[nums.length];
		backtrack(res,permute,visited,nums);
		return res;
    }

	/**
	 * @param res 返回结果
	 * @param permute 
	 * @param visited 记录以及选择过的元素
	 * @param nums 数组
	 */
	private void backtrack(List<List<Integer>> res, List<Integer> permute, boolean[] visited, int[] nums) {
		if(permute.size() == nums.length) { //满足结束条件
			res.add(new ArrayList<>(permute));
			return ;
		}
		for(int i = 0;i < visited.length;i++) {
			if(visited[i]) {
				continue;
			}
			visited[i] = true; //做选择
			permute.add(nums[i]); 
			backtrack(res, permute, visited, nums);
			visited[i] = false;//撤销选择	
			permute.remove(permute.size() - 1); 		
		}
	}

2.4 (lee-47) 全排列2

给定一个可包含重复数字的序列,返回所有不重复的全排列。
时间复杂度为 O(n×n!)
空间复杂度 O(n)

输入:nums = [1,1,2]
输出:[[1,1,2], [1,2,1], [2,1,1]]

	public List<List<Integer>> permuteUnique(int[] nums) {
		 List<List<Integer>> res = new ArrayList<>();
		 List<Integer> permute = new ArrayList<>();
		 Arrays.sort(nums);
		 boolean[] visited  = new boolean[nums.length];
		 backtrack(res,permute,visited,nums);
		 return res;
				
	 }

	private void backtrack(List<List<Integer>> res, List<Integer> permute, boolean[] visited, int[] nums) {
		if(permute.size() == nums.length) {
			res.add(new ArrayList<>(permute));
			return;
		}
		for(int i = 0;i <nums.length;i++) {
			if(i != 0 && nums[i] == nums[i-1] && !visited[i-1]) { //若发现没有选择上一个数,且当前数字与上一个数相同且前一个数没有被标记过,则可以跳过当前生成的子集。
				continue;
			}
			if(visited[i]) {
				continue;
			}
			visited[i] = true; //做选择
			permute.add(nums[i]);
			backtrack(res, permute, visited, nums);
			visited[i] = false;//撤销选择
			permute.remove(permute.size() - 1);
		}
	}

2.5 (lee-39) 组合总和

给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的数字可以无限制重复被选取
所有数字(包括 target)都是正整数。
解集不能包含重复的组合。

输入:candidates = [2,3,6,7], target = 7,
所求解集为:[ [7], [2,2,3] ]

(1)写法一

	/**
	 * (-------------写法一 -------------)
	 * [2,3,6,7],7, res = [[7],[2,2,3]]
	 * 思路:画树形图,深度优先遍历,回溯剪枝
	 * 1.以 target 根结点 ,创建一个分支时做减;
	 * 2.每一个箭头表示:从父亲结点的数值减去边上的数值,得到孩子结点的数值。边的值就是题目中给出的 candidate 数组的每个元素的值;
	 * 3.减到 0 或者负数的时候停止,即:结点 0 和负数结点成为叶子结点;
	 * 4.所有从根结点到结点 0 的路径(只能从上往下,没有回路)就是题目要找的一个结果列表。
	 * 
	 * 以target=7为例,结果集应该为{{2,2,3},{7}},但是以上步骤会出现重复路径,比如{2,3,2},{3,2,2},由于题目不要求顺序输出,因此需要去重,也就是剪枝。
	 * 去重思路:在搜索的过程中去重,每一次搜索的时候设置下一轮搜索的起点start;
	 * 
	 * @param candidates
	 * @param target
	 * @return
	 */
	public List<List<Integer>> combinationSum(int[] candidates, int target) {
		List<List<Integer>> res = new ArrayList<>();
		List<Integer> combinPath = new ArrayList<>();
		backtrack(res,combinPath,target,candidates,0);
		return res;
    }

	/**
	 * 
	 * @param res 结果集
	 * @param combin 从根节点到叶子节点的路径列表
	 * @param target 每减去一个元素,目标值变小
	 * @param candidates 候选数组
	 * @param start 搜索起点
	 */
	private void backtrack(List<List<Integer>> res, List<Integer> combinPath, int target, int[] candidates, int start) {
		// target 为负数和 0 的时候不再产生新的叶子结点
		if(target < 0 ) {
			return;
		}
		if(target == 0) {
			res.add(new ArrayList<>(combinPath));
			return;
		}
		for(int i = start;i < candidates.length;i++) {//从 start 开始搜索
			combinPath.add(candidates[i]); 
			System.out.println("递归之前 => " + combinPath + ",剩余 = " + (target - candidates[i]));
			
			backtrack(res, combinPath, target-candidates[i], candidates,i);// 注意:由于每一个元素可以重复使用,下一轮搜索的起点依然是 i,这里非常容易弄错
			
			combinPath.remove(combinPath.size() - 1);
			System.out.println("递归之后 => " + combinPath);
		}
	}

(2)写法二

	/**
	 *  (-------------写法二 -------------)
	 *  剪枝提速:如果 target 减去一个数得到负数,那么减去一个更大的树依然是负数,同样搜索不到结果;
	 *  基于这个想法,我们可以对输入数组进行排序,排序是为了提高搜索速度,添加相关逻辑达到进一步剪枝的目的。
	 *  可以打印递归前后的输出查看***
	 * @param candidates
	 * @param target
	 * @return
	 */
	public List<List<Integer>> combinationSum1(int[] candidates, int target) {
		List<List<Integer>> res = new ArrayList<>();
		List<Integer> combinPath = new ArrayList<>();
		Arrays.sort(candidates); // 排序是剪枝的前提
		backtrack1(res,combinPath,candidates,target,0);
		return res;
		
	}
		
	private void backtrack1(List<List<Integer>> res, List<Integer> combinPath, int[] candidates, int target, int start) {
		// 由于进入更深层的时候,小于 0 的部分被剪枝,因此递归终止条件值只判断等于 0 的情况
		if(target == 0) {
			res.add(new ArrayList<>(combinPath));
			return;
		}
		for(int i = start ;i < candidates.length;i++) {
			if(target < candidates[i]) { // 重点理解这里剪枝,前提是候选数组已经有序
				break;
			}
			combinPath.add(candidates[i]);
			System.out.println("递归之前 => " + combinPath + ",剩余 = " + (target - candidates[i]));

			backtrack1(res, combinPath, candidates, target-candidates[i], i);
			
			combinPath.remove(combinPath.size() - 1);
			System.out.println("递归之后 => " + combinPath);
		}
	}

2.6 (lee-40) 组合总和2

给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的 每个数字在每个组合中只能使用一次
说明
所有数字(包括目标数)都是正整数。
解集不能包含重复的组合。

输入: candidates = [2,5,2,1,2], target = 5,
输出: [ [1,2,2], [5] ]

时间复杂度:O(n*2^n)
空间复杂度:O(n)

	/**
	 * 思路:画树形图,深度遍历,剪枝去重
	 * 去重的思路:由于题目要求每个数字在每个组合中只能使用一次并且解集不能包含重复组合,
	 * [2,5,2,1,2] T = 5, res = [[1,2,2],[5]]
	 * @param candidates
	 * @param target
	 * @return
	 */
	public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        List<List<Integer>> res = new ArrayList<>();
        List<Integer> path = new ArrayList<>();
        Arrays.sort(candidates); //排序避免重复解
        backtrack(res,path,candidates,target,0);
		return res;
    }

	/**
	 * 
	 * @param res
	 * @param path
	 * @param candidates
	 * @param target
	 * @param start
	 */
	private void backtrack(List<List<Integer>> res, List<Integer> path, int[] candidates, int target, int start) {
		if(target == 0) {
			res.add(new ArrayList<>(path));
			return;
		}
		for(int i = start;i < candidates.length;i++) { //for循环遍历从start开始的数,选一个数进入下一层递归。 
			if(target < candidates[i]) { // 剪枝:如果当前candidates数组的和已经大于目标target,没必要枚举了,直接return
				break;
			}
			if(i > start && candidates[i] == candidates[i-1]){ //避免子路径重复,如出现两个[1,2,2]:如果从start开始的数有连续出现的重复数字,跳过该数字continue
                continue;
            }
			path.add(candidates[i]);
			backtrack(res, path, candidates, target - candidates[i], i+1); //注意为避免一个组合里出现重复使用的情况所以在进入下一层递归时要用i+1,从i之后的数中选择接下来的数,避免出现[1,1,1,1,1]类似这样
			path.remove(path.size() - 1);			
		}
		
	}

2.7 (lee-93) 复原IP地址

给定一个只包含数字的字符串,用以表示一个 IP 地址,返回所有可能从 s 获得的 有效 IP 地址 。你可以按任何顺序返回答案。有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 ‘.’ 分隔。
例如:“0.1.2.201” 和 “192.168.1.1” 是 有效 IP 地址,但是 “0.011.255.245”、“192.168.1.312” 和 “192.168@1.1” 是 无效 IP 地址。

输入:s = “25525511135”
输出:[“255.255.11.135”,“255.255.111.35”]
输入:s = “010010”
输出:[“0.10.0.10”,“0.100.1.0”]

	/**
	 * 思路:由于要找出所有可能复原出的 IP 地址,使用回溯的方法,对所有可能的字符串分隔方式进行搜索,并筛选出满足要求的作为答案。
	 * 回溯套路
	 * @param s
	 * @return
	 */
	public List<String> restoreIpAddresses(String s) {
		List<String> res = new ArrayList<>();
		List<String> path = new ArrayList<>();
		backtrack(res,path,s,0);
		return res;
    }

	private void backtrack(List<String> res, List<String> path, String s, int start) {
		//终止条件
		if(path.size() > 4) { //IP超过四段终止
			return;
		}
		if(path.size() >= 4 && start != s.length()) { //如果等于4段但不是原字符串也终止
			return;
		}
		if(path.size() == 4) { //如果等于4段并且组合起来是原字符串则添加到结果集里面
			res.add(String.join(".", path));
			return;
		}
		for(int i = start; i < s.length();i++) {
			//选择条件:如果字符串在0-255之外则跳过;如果字符串存在01这种情况也跳过继续
			String str = s.substring(start,i+1);
			if(str.length() > 1 && str.startsWith("0") || str.length() > 3) {
				continue;
			} 
			int val = Integer.valueOf(str);
			if(val < 0 || val > 255) {
				continue;
			}
			path.add(str); //做选择
			backtrack(res, path, s, i+1);
			path.remove(path.size() - 1); //撤销选择
		}
		
	}

2.8 (lee-17) 电话号码的字母组合

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

输入:digits = “23”
输出:[“ad”,“ae”,“af”,“bd”,“be”,“bf”,“cd”,“ce”,“cf”]
输入:digits = “2”
输出:[“a”,“b”,“c”]
输入:digits = “”
输出:[ ]

	public List<String> letterCombinations(String digits) {
		List<String> res = new ArrayList<>();			
		if(digits.length() == 0) {
			return res;
		}
		Map<Character,String> map = new HashMap<>();
		map.put('2', "abc");
		map.put('3', "def");
		map.put('4', "ghi");
		map.put('5', "jkl");
		map.put('6', "mno");
		map.put('7', "pqrs");
		map.put('8', "tuv");
		map.put('9', "wxyz");
		
		backtrack(res,digits,map,new StringBuilder(),0);
		return res;
    }
    /**
	 * 
	 * @param res 结果集
	 * @param digits 输入的String数字串
	 * @param map 存储字符和字符串的映射
	 * @param cur 当前读入的内容
	 * @param start 
	 */
	private void backtrack(List<String> res, String digits,Map<Character, String> map, StringBuilder cur ,int start) {
		if(cur.length() == digits.length()) { //结束条件:当长度够了就添加结果
			res.add(cur.toString());
			return;
		}
		char c = digits.charAt(start); //根据当前数字选择字符
		String letters = map.get(c);//字符对应的字符串
		for(int i  = 0;i <letters.length();i++) {
			cur.append(letters.charAt(i));
			backtrack(res, digits,map, cur ,start+1);
			cur.deleteCharAt(start);
		}
	}

2.9 (lee-131) 分割回文串

给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。回文串 是正着读和反着读都一样的字符串。

输入:s = “aab”
输出:[[“a”,“a”,“b”],[“aa”,“b”]]
输入:s = “a”
输出:[[“a”]]

	public List<List<String>> partition(String s) {
		List<List<String>> res = new ArrayList<>();
		List<String> path = new ArrayList<>();
		backtrack(res,path,s,0);
		return res;
    }

	/**
	 * 回溯
	 * @param res
	 * @param path
	 * @param s
	 * @param start
	 */
	private void backtrack(List<List<String>> res, List<String> path, String s, int start) {
		if(start == s.length()) { //注意是start
			res.add(new ArrayList<>(path));
			return;
		}
		for(int i = start;i < s.length();i++) {
			String str = s.substring(start,i+1);
			if(!isPalindrome(str)) {
				continue;
			}
			path.add(str);
			backtrack(res, path, s, i+1);  //注意
			path.remove(path.size() - 1);
		}
		
	}

	/**
	 * 判断是否是回文串
	 * @param s
	 * @return
	 */
	private boolean isPalindrome(String s) {
		if(s == null || s.length() <= 1) {
			return true;
		}
		int left = 0;
		int right = s.length() - 1;
		while(left < right) {
			if(s.charAt(left) != s.charAt(right)) {
				return false;
			}
			left++;
			right--;
		}
		return true;
	}

2.10 (lee-79) 单词搜索

(lee-JZ12)矩阵中的路径
请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一格开始,每一步可以在矩阵中向左、右、上、下移动一格。
如果一条路径经过了矩阵的某一格,那么该路径不能再次进入该格子。例如,在下面的3×4的矩阵中包含一条字符串“abcced”的路径(路径中的字母用加粗标出)。
在这里插入图片描述
但矩阵中不包含字符串“abfb”的路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入这个格子。

输入:board = [[“a”,“b”],[“c”,“d”]], word = “abcd”
输出:false
输入:board = [[“A”,“B”,“C”,“E”],[“S”,“F”,“C”,“S”],[“A”,“D”,“E”,“E”]], word = “ABCCED”
输出:true

	/**
	 * dfs + 回溯(重要)
	 */
	private int m,n;
	private int[][] direction  = {{1,0},{-1,0},{0,1},{0,-1}};
	
	public boolean exist(char[][] board,String word) {
		m = board.length;
		n = board[0].length;
		boolean[][] visited = new boolean[m][n];
		for(int i = 0;i < m;i++) {
			for(int j = 0;j < n;j++) {
				if(dfs(0,i,j,visited,board,word)){
					return true;
				}
			}
		}
		return false;
	}

	private boolean dfs(int start, int r, int c, boolean[][] visited, char[][] board, String word) {
		//递归结束条件:返回true ---- 字符串以全部匹配
		if(start == word.length()) { 
			return true;
		}
		//递归结束条件:返回false ---- 如果行或列索引越界 || 当前矩阵元素与目标字符不同 || 当前矩阵元素已访问过   说明此路不通
		if(r < 0 || r >= m || c < 0 || c >= n || board[r][c] != word.charAt(start) || visited[r][c]) {
			return false;
		}
		visited[r][c] = true; 
		for(int[] d : direction) {
			if(dfs(start + 1, r + d[0], c + d[1], visited, board, word)) {
				return true;
			}
		}		
		visited[r][c] = false;		
		return false;
	}
	

2.11 (lee-200) 岛屿数量

给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。此外,你可以假设该网格的四条边均被水包围。
思路:DFS 通过深度搜索遍历可能性

输入:grid = [
[“1”,“1”,“1”,“1”,“0”],
[“1”,“1”,“0”,“1”,“0”],
[“1”,“1”,“0”,“0”,“0”],
[“0”,“0”,“0”,“0”,“0”]
]
输出:1

	public int numIslands(char[][] grid) {
		if(grid.length==0) {
			return 0;
		}
		int total = 0;
		int row = grid.length;
		int col = grid[0].length;
		for(int i = 0;i < row;i++) {
			for(int j = 0;j < col;j++) {
				if(grid[i][j]=='1') {
					dfs(grid,i,j);
					total++;				
				}
			}
		}
		return total;
    }

	/**
	 * 深度搜索DFS
	 * @param grid 二维网格-岛屿
	 * @param x 
	 * @param y
	 * @return
	 */
	private void dfs(char[][] grid, int x, int y) {
		if(x < 0 || x >= grid.length || y < 0 || y >= grid[0].length || grid[x][y] != '1' ) {
			return;
		}
		grid[x][y] = '0';
		dfs(grid,x-1,y);
		dfs(grid,x+1,y);
		dfs(grid,x,y-1);
		dfs(grid,x,y+1);
	}

2.12 (lee-JZ13)机器人的运动范围

地上有一个m行n列的方格,从坐标 [0,0] 到坐标 [m-1,n-1] 。一个机器人从坐标 [0, 0] 的格子开始移动,它每次可以向左、右、上、下移动一格(不能移动到方格外),也不能进入行坐标和列坐标的数位之和大于k的格子。例如,当k为18时,机器人能够进入方格 [35, 37] ,因为3+5+3+7=18。但它不能进入方格 [35, 38],因为3+5+3+8=19。请问该机器人能够到达多少个格子?

输入:m = 2, n = 3, k = 1
输出:3
输入:m = 3, n = 1, k = 0
输出:1

思路:DFS
首先,我们可以把行坐标和列坐标的数位之和大于k的格子看成是障碍物,标注为1,其他可达的地方标注为0。
其次,因为机器人可以向上下左右移动一格,我们可以将其缩减为向下和向右两个方向移动,因为当我们从起点向这两个方向移动时,则新加入的空方格都可以由上方或左方的格子移动一步得到,而且当k值越大,连通的空方格的区域越多。
因此状态转移方程可以写成:visited[i][j] = visited[i-1][j] || visited[i][j-1] visited[i][j]表示可达或者不可达
最后,使用一个变量res来记录可达的格子的数量
初始条件:visited[i][j] = 0 可达
再说,如何计算数位和?
我们每次将数 X%10 取余的操作得到X的个位数,然后再将X/10,相当于>>一位,删除个位数,不断重复直到 X = 0 时结束。
时间复杂度:O(mn) 其中 m 为方格的行数, n 为方格的列数。一共有 O(mn)个状态需要计算,每个状态递推计算的时间复杂度为 O(1),所以总时间复杂度为 O(mn).
空间复杂度:O(m
n) 需要 O(mn)大小的结构来记录每个位置是否可达。

public class RobotMovingCount {		
	// DFS 	
	int res = 0;
	public int movingCount(int m, int n, int k) {
		if(k == 0) {
			return 1;
		}
		boolean[][] visited = new boolean[m][n]; //初始化默认为false		
		dfs(visited,0,0,k);
		return res;
	}
		
	private void dfs(boolean[][] visited, int x, int y, int k) {
		int m = visited.length;
		int n = visited[0].length;
		//越界情况和遇到障碍的情况
		if(x >= m || x < 0 || y >= n || y < 0 || get(x)+get(y) > k || visited[x][y]) {
			return ;
		}
		visited[x][y] = true; //未遍历
		res++;
		dfs(visited, x+1, y, k); //往下走一格
		dfs(visited, x, y+1, k); //往右走一格
	}
	
	//位数和
	private int get(int x) {
		int res = 0;
		while(x != 0) {
			res += x % 10;
			x /= 10;
		}
		return res;
	}
		
	public static void main(String[] args) {
		Scanner in = new Scanner(System.in);
		int m = in.nextInt();
		int n = in.nextInt();
		int k = in.nextInt();
		RobotMovingCount rmc = new RobotMovingCount();
		int res = rmc.movingCount(m, n, k);
		System.out.print(res);
	}
}

2.13 (lee-JZ38) 字符串的排列

(lee-JZ38)字符串的排列
返回String[]类型:输入一个字符串,打印出该字符串中字符的所有排列。你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。
(NC-JZ27)字符串的排列:返回ArrayList类型。输入一个字符串,长度不超过9(可能有字符重复),字符只包括大小写字母。

输入:s = “abc”
输出:[“abc”,“acb”,“bac”,“bca”,“cab”,“cba”]

public class Permutation {
	/*
	 * 思路:回溯法
	 * 与lee-46和47类似题目,都是求全排列
	 * 要求不能重复,因此使用47中无重复的排列
	 */
	public ArrayList<String> permutation(String s) {	
 		ArrayList<String> res = new ArrayList<>();
		StringBuilder sb = new StringBuilder();
		Arrays.sort(s.toCharArray());    //按字典序时
		boolean[] visited  = new boolean[s.length()];
		backtrack(res,sb,visited,s);
		return res;
	}

	private void backtrack(ArrayList<String> res, StringBuilder sb, boolean[] visited, String s) {
		if(sb.length() == s.length()) {
			res.add(sb.toString());
			return;
		}
		for(int i = 0;i < visited.length;i++) {
			if(i != 0 && s.charAt(i)==s.charAt(i-1) && !visited[i-1]) { //若发现没有选择上一个字符,且当前字符与上一个字符相同且前一个字符没有被标记过,则可以跳过当前生成的子集。
				continue;
			}
			if(visited[i]) {
				continue;
			}
			visited[i] = true; //做选择
			sb.append(s.charAt(i));
			backtrack(res, sb, visited, s);
			visited[i] = false;//撤销选择
			sb.deleteCharAt(sb.length() - 1);
		}
	}
	
	public static void main(String args[]) {
		Permutation p = new Permutation();
		Scanner in = new Scanner(System.in);
		String s = in.next();
		ArrayList<String>  res = p.permutation(s);
		System.out.print(res);
				
	}
}

3 练习链接

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值