回溯专场
1、全排列、组合、子集
1、字符串全排列、组合问题
1.1 字典序解法(求下一个字典序数、字符串)字典序、接受重复
//O(n*n!)
// 利用字典序来解决全排列问题,时间占用最少!
public static ArrayList<String> permutation_dictory(String str) {
ArrayList<String> res = new ArrayList<String>();
if(str.length() == 0) return res;
char [] array = str.toCharArray();
Arrays.sort(array);
String s = new String(array);
res.add(str);
while(true){
s = nextString(s);
if(!s.equals("-1"))
res.add(s);
else
break;
}
return res;
}
public static String nextString(String str){
char [] array = str.toCharArray();
int length = str.length();
int i = length-2;
for(; i>=0 && array[i] >= array[i+1]; i--);
if(i == -1) return "-1";
int j = length-1;
for(; j>=0 && array[j] <= array[i]; j--);
char tmp = array[i];
array[i] = array[j];
array[j] = tmp;
for(int a=i+1, b=length-1; a<b;a++,b--){
tmp = array[a];
array[a] = array[b];
array[b] = tmp;
}
return new String(array);
}
1.2 递归回溯法(不能去重,也不能保证字典序)
有重复时:
static HashSet<String> set = new HashSet<String>();
static HashMap<Character, Integer> map;
public static ArrayList<String> permutation_dfs(String str) {
char[] chars = str.toCharArray();
StringBuilder sb = new StringBuilder();
int n = chars.length;
ArrayList<String> list = new ArrayList<String>();
// 记录每个字符出现的次数: 去重
map = new HashMap<>();
for (char c : chars){
map.put(c, map.getOrDefault(c, 0)+1);
}
dfs(list, sb, chars); //针对重复字符出现的回溯方法
dfs_2(list, sb, chars);//非重复
System.out.println(list);
Collections.sort(list); //字典序输出
System.out.println(list);
return list;
}
public static void dfs(List<String> list, StringBuilder sb, char[] chars){
if(sb.length() == chars.length){
if(!set.contains(sb.toString())){
set.add(sb.toString());
list.add(sb.toString());
}
return;
}
for (char c: chars){
if(map.get(c) > 0){ // 记录每个字符出现的次数: 去重
sb.append(c);
map.put(c, map.getOrDefault(c, 0)-1);
dfs(list, sb, chars);
map.put(sb.charAt(sb.length()-1), map.getOrDefault(sb.charAt(sb.length()-1),0)+1);
sb.deleteCharAt(sb.length()-1);
}
}
}
无重复时:
在这里插入代码片
1、全排列、组合、子集
public static void dfsSubset(int[] nums, List<List<Integer>> list, List<Integer> ans, int index) {
list.add(new ArrayList<>(ans));
for (int i = index; i < nums.length; i++) {
ans.add(nums[i]);
dfsSubset(nums, list, ans,index+1);
ans.remove(ans.size()-1);
}
}
public static void dfsArrangement(int[] nums, List<List<Integer>> list, List<Integer> ans){
if(nums.length == ans.size()){
list.add(new ArrayList<>(ans));
return;
}
for(int num : nums)
{
if(ans.contains(num))
continue;
ans.add(num);
dfsArrangement(nums, list, ans);
ans.remove(ans.size() - 1);
}
}
public static void dfsCombination(int k, int[] nums, List<List<Integer>> list, List<Integer> ans, int index){
if(k == ans.size())
list.add(new ArrayList<>(ans));
for (int i = index; i < nums.length; i++) {
ans.add(nums[i]);
dfsCombination(k, nums, list, ans, i+1);
ans.remove(ans.size() - 1);
}
}
2、跳台阶问题
二级目录
1、由0生成一个数(加一或乘2)
三种方法解决:
def dfs(num):
if(num == 0):
return 0
elif(num % 2 != 0):
return 1 + dfs(num - 1)
else:
return 1 + min(dfs(int(num/2)), dfs(num-1))
def dp(n):
if n <= 3:
return n
else:
dp = [0 for i in range(n + 1)]
dp[1], dp[2], dp[3] = 1, 2, 3
for i in range(4, len(dp)):
if i % 2 != 0:
dp[i] = 1 + dp[i-1]
else:
dp[i] = 1 + min(dp[i-1], dp[int(i / 2)])
return dp[n]
def erjinzhi(n):
m = 0
for i in range(2, len(bin(n))):
if bin(n)[i].__eq__(1):
m += 1
return m+len(bin(n))-2-1
3、字符串回溯
3.1 生成括号
public static void dfsGenerateParent(int left, int right, List<String> list, int n,StringBuilder sb){
if(left < 0 || right < 0)
return;
if(right < left)
return;
if (left == 0 && right == 0){
list.add(sb.toString());
return;
}
sb.append('(');
dfsGenerateParent(left - 1, right, list, n, sb);
sb.deleteCharAt(sb.length()-1);
sb.append(')');
dfsGenerateParent(left, right-1, list, n, sb);
sb.deleteCharAt(sb.length()-1);
}
3.2 字符串在另一个字符串以子序列形式出现的次数
在这里插入代码片
3.3 大小写字母的全排列
public static void dfs(String s, StringBuilder sb, List<String> list, int index){
if(index > s.length())
return;
if(sb.length() == s.length()){
list.add(sb.toString());
return;
}
for (int i = index; i < s.length(); i++) {
char c = s.charAt(i);
if(Character.isDigit(c)){
sb.append(c);
}
else {
char[] chars = new char[]{c,Character.isUpperCase(c)? Character.toLowerCase(c) : Character.toUpperCase(c)};
for(Character character : chars){
sb.append(character);
dfs(s, sb, list, i+1);
while (sb.length() > 0 && Character.isDigit(s.charAt(sb.length()-1))){
sb.deleteCharAt(sb.length()-1);
}
sb.deleteCharAt(sb.length()-1);
}
}
}
}
3.4 判断括号合法性
在这里插入代码片
4、八皇后、解数独
在这里插入代码片
https://zhuanlan.zhihu.com/p/388681117
4、N数之和问题(凑零钱、和为target、Nsum)
1、Nsum
1.1 2Sum(数组中只存在一组唯一解)的三种解法。
public static int[] twoSum1(int[] nums,int target){
int n = nums.length;
for (int i = 0; i < n; i++) {
for (int j =i+1; j < n; j++) {
if(target == nums[i] + nums[j])
return new int[]{i, j};
}
}
return new int[]{-1,-1};
}
public static int[] twoSum2(int[] nums, int target){
int n = nums.length;
HashMap<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < n; i++) {
map.put(nums[i], i);
}
//以空间换时间,先放进hashmap中,再检索。
//双层for循环常用优化方法!!!
for (int i = 0; i < n; i++) {
int j = target - nums[i];
if(map.containsKey(j) && map.get(j) != i)
return new int[]{i, map.get(j)};
}
return new int[]{-1, -1};
}
public static int[] twoSum3(int[] nums, int target){
Arrays.sort(nums);
//先排序,后用双指针
int n = nums.length;
int left = 0, right = n-1;
while (left < right){
int sum = nums[left] + nums[right];
if(sum > target)
right--;
else if (sum < target)
left++;
else if (sum == target)
return new int[]{left, right};
}
return new int[]{-1,-1};
}
1.2 Nsum(存在多组且重复解得数组,且target > 0)的回溯去重解法
public static void dfs(int N, int target, int[] nums, List<List<Integer>> list, int index, List<Integer> res,HashSet<List<Integer>> set){
if(target < 0)
return;
if(target == 0 && res.size() == N){
List<Integer> repeat = new ArrayList<>(res);
Collections.sort(repeat);
if(!set.contains(repeat)){
list.add(new ArrayList<>(res));
set.add(repeat);
}
return;
}
for (int i = index; i < nums.length; i++) {
res.add(nums[i]);
dfs(N, target-nums[i], nums, list, i+1, res,set);
res.remove(res.size()-1);
}
}
1.3 目标和(用正负号组合数组中的数得到target)
int count = 0;
public int findTargetSumWays(int[] nums, int target) {
backtrack(nums, target, 0, 0);
return count;
}
public void backtrack(int[] nums, int target, int index, int sum) {
if (index == nums.length) {
if (sum == target) {
count++;
}
} else {
backtrack(nums, target, index + 1, sum + nums[index]);
backtrack(nums, target, index + 1, sum - nums[index]);
}
}
2、组合为target,求list
1.1 、组合总数为target(数组不重复)
题目描述:
public List<List<Integer>> combinationSum(int[] candidates, int target) {
int len = candidates.length;
List<List<Integer>> res = new ArrayList<>();
if (len == 0) {
return res;
}
//排序是剪枝的前提
Arrays.sort(candidates);
List<Integer> path = new ArrayList<>();
dfs(candidates, 0, target, path, res);
return res;
}
private void dfs(int[] candidates, int index,int target, List<Integer> arr, List<List<Integer>> list) {
// 由于进入更深层的时候,小于 0 的部分被剪枝,因此递归终止条件值只判断等于 0 的情况
if (target == 0) {
list.add(new ArrayList<>(arr));
return;
}
for (int i = index; i < candidates.length; i++) {
// 重点理解这里剪枝,前提是候选数组已经有序,
if (target - candidates[i] < 0) {
break;
}
arr.add(candidates[i]);
dfs(candidates, i, target - candidates[i], arr, list);
arr.remove(arr.size() - 1);
}
}
3、组合为target,求最小或最大凑法
1、数目不限量,求凑齐钱的最少钱币数目
public static int coinChange(int[] coins, int amount) {
if (amount < 1) {
return 0;
}
int n = coinChange(coins, amount, new int[amount]);
System.out.println(n);
return n;
}
private static int coinChange(int[] coins, int rem, int[] count) {
if (rem < 0) {
return -1;
}
if (rem == 0) {
return 0;
}
if (count[rem - 1] != 0) {
return count[rem - 1];
}
int min = Integer.MAX_VALUE;
for (int coin : coins) {
int res = coinChange(coins, rem - coin, count);
if (res >= 0 && res < min) {
min = 1 + res;
}
}
count[rem - 1] = (min == Integer.MAX_VALUE) ? -1 : min;
return count[rem - 1];
}
2、数目不限量,求凑齐钱有多少种方法
public int change(int amount, int[] coins) {
int[] dp = new int[amount + 1];
dp[0] = 1;
for (int coin : coins) {
for (int i = coin; i <= amount; i++) {
dp[i] += dp[i - coin];
}
}
return dp[amount];
}