刷题时间:2021.7.26-2021.7.28
回溯法
回溯法又称为试探法,但当探索到某一步时,发现原先选择达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为 回溯法。
分治算法(归并排序)
将一个规模为N的问题分解为K个规模较小的子问题,这些子问题相互独立且与原问题性质相同。求出子问题的解后进行合并,就可得到原问题的解。
一般步骤:1.分解,将要解决的问题划分成若干规模较小的同类问题;
2.求解,当子问题划分得足够小时,用较简单的方法解决;
3.合并,按原问题的要求,将子问题的解逐层合并构成原问题的解。
1、LeetCode78子集
方法一:回溯法
思路:利用回溯方法生成子集,即对于每个元素,都有试探放入或不放入集合中的两个选择:
选择放入该元素,递归的进行后续元素的选择,完成放入该元素后续所有元素的试探;之后将其拿出,即再进行一次选择不放入该元素,递归的进行后续元素的选择,完成不放入该元素后续所有元素的试探。
本来选择放入,再选择一次不放入的这个过程,称为回溯试探法。
class Solution {
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> result = new ArrayList<>();//存储最终结果
List<Integer> item = new ArrayList<Integer>();//回溯时,产生各个子集的数组
generate(0,nums,item,result);//计算各个子集
return result;
}
public void generate(int i,int[] nums,List<Integer> item,List<List<Integer>> result){
//找到所有以item开头的所有子集
result.add(new ArrayList<Integer>(item));
//因为java传的是引用,如果直接把item添加进去,之后item变化添加进去的对象也会发生变化
for(int j=i;j<nums.length;j++){
item.add(nums[j]);
generate(j+1,nums,item,result);
item.remove(item.size()-1);//删除最后一个元素
}
}
}
若输入为[1,2,3],则输出为[[],[1],[1,2],[1,2,3],[1,3],[2],[2,3],[3]]。
方法二:位运算法
思路:若一个集合有三个元素A, B, C,则3个元素有2^3 = 8种组成集合的方式,用0-7表示这些集合。
A元素为100=4;B元素为010=2; C元素为001= 1。如构造某一集合,即使用A,B,C对应的三个整数与该集合对应的整数做&运算,当为真时,将该元素push进入集合。
class Solution {
public List<List<Integer>> subsets(int[] nums) {
List<Integer> item = new ArrayList<Integer>();
List<List<Integer>> result = new ArrayList<List<Integer>>();
int all_set = 1 << nums.length;//1<<n即为2^n
for (int i = 0; i < all_set; i++) {//遍历所有集合
item.clear();
for (int j = 0; j < nums.length; j++) {
if ((i & (1 << j)) != 0) {//(1<<j)即为构造nums数组的第j个元素,若(i&(1<<j)为真则nums[j]放入item
item.add(nums[j]);
}
}
result.add(new ArrayList<Integer>(item));
}
return result;
}
}
若输入为[1,2,3],则输出为[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]。
2、LeetCode90子集 II
LeetCode官方题解
在递归时,若发现没有选择上一个数,且当前数字与上一个数相同,则可以跳过当前生成的子集。
class Solution {
List<Integer> item = new ArrayList<Integer>();
List<List<Integer>> result = new ArrayList<List<Integer>>();
public List<List<Integer>> subsetsWithDup(int[] nums) {
Arrays.sort(nums);
dfs(false, 0, nums);
return result;
}
public void dfs(boolean choosePre, int cur, int[] nums) {
if (cur == nums.length) {
result.add(new ArrayList<Integer>(item));
return;
}
dfs(false, cur + 1, nums);
if (!choosePre && cur > 0 && nums[cur - 1] == nums[cur]) {
return;
}
item.add(nums[cur]);
dfs(true, cur + 1, nums);
item.remove(item.size() - 1);
}
}
若输入为[1,2,2],则输出为[[],[2],[2,2],[1],[1,2],[1,2,2]]。
3、LeetCode40组合总和 II
思路:在搜索回溯过程中进行剪枝操作:递归调用时,计算已选择元素的和sum,若sum >target,不再进行更深的搜索,直接返回。
LeetCode官方题解
public class Solution {
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
int len = candidates.length;
List<List<Integer>> res = new ArrayList<>();
if (len == 0) {
return res;
}
Arrays.sort(candidates);
Deque<Integer> path = new ArrayDeque<>(len);
dfs(candidates, len, 0, target, path, res);
return res;
}
//begin从候选数组的 begin 位置开始搜索,target表示剩余,这个值一开始等于 target,path从根结点到叶子结点的路径
private void dfs(int[] candidates, int len, int begin, int target, Deque<Integer> path, List<List<Integer>> res) {
if (target == 0) {
res.add(new ArrayList<>(path));
return;
}
for (int i = begin; i < len; i++) {
// 大剪枝:减去candidates[i]小于0,减去后面的candidates[i+1]、candidates[i+2]肯定也小于0,因此用 break
if (target - candidates[i] < 0) {
break;
}
// 小剪枝:同一层相同数值的结点,从第2个开始,候选数更少,结果一定发生重复,因此跳过,用continue
if (i > begin && candidates[i] == candidates[i - 1]) {
continue;
}
path.addLast(candidates[i]);
// 因为元素不可以重复使用,这里递归传递下去的是 i + 1 而不是 i
dfs(candidates, len, i + 1, target - candidates[i], path, res);
path.removeLast();
}
}
}
4、LeetCode22括号生成
思路:递归生成所有可能,递归需要限制条件:
1.左括号与右括号的数量,最多放置n个。
2.若左括号的数量<=右括号数量,不可进行放置右括号的递归。
class Solution {
public List<String> generateParenthesis(int n) {
List<String> result = new ArrayList<>();
generate("",n,n,result);
return result;
}
//生成字符串item,当前还可以放置左括号的数量left,右括号的数量right
void generate(String item, int left,int right,List<String> result){
if(left == 0 && right == 0){
result.add(item);
return;
}
if(left > 0){//左括号还有剩余
generate(item+'(',left-1,right,result);
}
if(left < right){//右括号剩余比左括号多
generate(item+')',left,right-1,result);
}
}
}
5、LeetCode51 N皇后
思路:利用递归对棋盘的每一行放置皇后,放置时,按列顺序寻找可以放置皇后的列,若可以放置皇后,将皇后放置该位置,并更新mark标记数组,递归进行下一行的皇后放置;当该次递归结束后,恢复mark数组,并尝试下一个可能放皇后的列。当递归可以完成N行的N个皇后放置,则将该结果保存并返回。
LeetCode评论代码
class Solution {
public List<List<String>> solveNQueens(int n) {
List<List<String>> result = new ArrayList<>();
char[][] chess = new char[n][n];
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
chess[i][j] = '.';
}
}
dfs(0, chess, result);//从第0行开始放置皇后
return result;
}
private static void dfs(int row, char[][] chess, List<List<String>> result) {
if (row == chess.length) {//所有行填满,添加结果
result.add(construct(chess));
return;
}
for (int column = 0; column < chess.length; column++) {//对当前行尝试放置每一列
if (valid(chess, row, column)) {
chess[row][column] = 'Q';//放置皇后
dfs(row + 1, chess, result);//放下一行
chess[row][column] = '.';//回溯
}
}
}
private static boolean valid(char[][] chess, int row, int column) {
//因为是一行一行往下放,所以只有列和斜对角出现攻击,行不会被攻击
for (int i = 0; i < row; i++) {//列攻击检查
if (chess[i][column] == 'Q') {
return false;
}
}
//看右上角是否被攻击,即从当前位置的上一行,当前列的右边,也就是右上角第一个斜对角位置开始检查。每次在上一个的检查的位置上上移一行,右移一列,也就是下一个斜对角
for (int i = row -1, j = column+1; i >= 0 && j < chess.length; i--, j++) {
if (chess[i][j] == 'Q') {//右上角是否有皇后
return false;
}
}
//和右上角一样进行左上角的检查。i代表被检查行,j代表被检查的列。i和j组成一个斜对角的坐标
for (int i = row -1, j = column -1; i >= 0 && j >= 0; i--, j--) {
if (chess[i][j] == 'Q') {
return false;
}
}
return true;
}
private static List<String> construct(char[][] chess) {//将数组转换为List<String>来保存最终结果
List<String> path = new ArrayList<>();
for (int i = 0; i < chess.length; i++) {
path.add(new String(chess[i]));
}
return path;
}
}
6、LeetCode315计算右侧小于当前元素的个数
思路:在归并两排序数组时,当需要将前一个数组元素的指针i指向的元素插入时,对应的count[j], 即为指向后一个数组的指针j的值。
LeetCode官方题解
class Solution {
private int[] index;
private int[] temp;
private int[] tempIndex;
private int[] ans;
public List<Integer> countSmaller(int[] nums) {
this.index = new int[nums.length];
this.temp = new int[nums.length];
this.tempIndex = new int[nums.length];
this.ans = new int[nums.length];
for (int i = 0; i < nums.length; ++i) {
index[i] = i;
}
int l = 0, r = nums.length - 1;
mergeSort(nums, l, r);
List<Integer> list = new ArrayList<Integer>();
for (int num : ans) {
list.add(num);
}
return list;
}
public void mergeSort(int[] a, int l, int r) {
if (l >= r) {
return;
}
int mid = (l + r) >> 1;
mergeSort(a, l, mid);
mergeSort(a, mid + 1, r);
merge(a, l, mid, r);
}
public void merge(int[] a, int l, int mid, int r) {
int i = l, j = mid + 1, p = l;
while (i <= mid && j <= r) {
if (a[i] <= a[j]) {
temp[p] = a[i];
tempIndex[p] = index[i];
ans[index[i]] += (j - mid - 1);
++i;
++p;
} else {
temp[p] = a[j];
tempIndex[p] = index[j];
++j;
++p;
}
}
while (i <= mid) {
temp[p] = a[i];
tempIndex[p] = index[i];
ans[index[i]] += (j - mid - 1);
++i;
++p;
}
while (j <= r) {
temp[p] = a[j];
tempIndex[p] = index[j];
++j;
++p;
}
for (int k = l; k <= r; ++k) {
index[k] = tempIndex[k];
a[k] = temp[k];
}
}
}