回溯小结
对于排列/组合/集合,无论形式如何变,无非就是穷举所有解,合理的剪枝以降低算法的复杂度,在这类问题中,我们关注的都是树枝上的值!
顺序有关排列数,顺序无关组合数,一共两种类型 排除重复的结果t,就要避免相同的数在同一层出现,可以通过排序且当前元素不等于上一个元素来判断
if ( i!= k && nums[ i] == nums[ i- 1 ] ) {
continue ;
}
元素可以重复的话用swap函数和used数组来控制 最重要的就是backtrack函数中的k,以及每次循环的i是从哪里开始的,决定是否可以重复(i从当前层k开始,说明前边的元素就不能再使用了,是组合数形式,i从0开始,说明元素交换顺序是两种情况,是排列数形式) 适当的使用剪枝函数,可以降低时间复杂度
length、length()、size()区别
length是数组的属性 length() 是字符串的方法 size() 是集合Lits的方法 List<Integer> t = new ArrayList<Integer>();
左边是一个声明,是一个泛型,List是一个接口定义了列表基本行为 ArrayList 是Java集合框架中的一个类 通过 new 关键字创建了一个 ArrayList 对象,可以存储整数类型的元素。 类可以实现(implement)一个或多个接口,从而保证类将实现接口中定义的方法。 接口可以实现多继承 List<List<Integer>> res = new ArrayList<List<Integer>>();
在嵌套情况下,后边new不需要两个ArrayListlist的一些基础操作,add,remove, 固定位置的add
list.clear() 清空list中的所有元素 list.remove(list.size() - 1) 移除最后一个元素 新学到一种数据结构方式List,track.addLast(i); track.removeLast();
// 右边这部分其实也要写成new LinkedList< List< Integer>> ( ) ; 但是这里用了[ 钻石运算符] ( https://www.zhihu.com/question/594493047)
List< List< Integer>> res = new LinkedList<> ( ) ;
// 记录回溯算法的递归路径
LinkedList< Integer> track = new LinkedList<> ( ) ;
51. N 皇后/ 52. N 皇后 II
class Solution {
List < List < String > > res = new ArrayList < > ( ) ;
public List < List < String > > solveNQueens ( int n) {
int [ ] a = new int [ n] ;
for ( int j = 0 ; j < n; a[ j] = j, j++ ) ;
List < String > board = new ArrayList < > ( ) ;
for ( int i = 0 ; i < n; i++ ) {
StringBuilder sb = new StringBuilder ( ) ;
for ( int j = 0 ; j < n; j++ ) {
sb. append ( '.' ) ;
}
board. add ( sb. toString ( ) ) ;
}
permutation ( a, 0 , board) ;
return res;
}
void permutation ( int [ ] a, int k, List < String > board) {
if ( k== a. length) {
res. add ( new ArrayList < > ( board) ) ;
return ;
}
int n = a. length;
for ( int i= k; i< n; i++ ) {
if ( ! isValid ( board, k, a[ i] ) ) {
continue ;
}
swap ( a, k, i) ;
StringBuilder sb = new StringBuilder ( board. get ( k) ) ;
sb. setCharAt ( a[ k] , 'Q' ) ;
board. set ( k, sb. toString ( ) ) ;
permutation ( a, k+ 1 , board) ;
StringBuilder sc = new StringBuilder ( board. get ( k) ) ;
sc. setCharAt ( a[ k] , '.' ) ;
board. set ( k, sc. toString ( ) ) ;
swap ( a, i, k) ;
}
}
void swap ( int a[ ] , int k, int i) {
int temp = a[ k] ;
a[ k] = a[ i] ;
a[ i] = temp;
}
boolean isValid ( List < String > board, int row, int col) {
int n = board. size ( ) ;
for ( int i= row- 1 , j= col+ 1 ; i>= 0 && j< n; i-- , j++ ) {
if ( board. get ( i) . charAt ( j) == 'Q' ) {
return false ;
}
}
for ( int i= row- 1 , j= col- 1 ; i>= 0 && j>= 0 ; i-- , j-- ) {
if ( board. get ( i) . charAt ( j) == 'Q' ) {
return false ;
}
}
return true ;
}
}
78. 子集
我的答案,是通过一个二叉树,其中左枝是选择,右枝是不选择,遍历这棵树
class Solution {
List < Integer > t = new ArrayList < Integer > ( ) ;
List < List < Integer > > res = new ArrayList < List < Integer > > ( ) ;
public List < List < Integer > > subsets ( int [ ] nums) {
backtrack ( nums, 0 ) ;
return res;
}
void backtrack ( int [ ] nums, int k) {
if ( k== nums. length) {
res. add ( new ArrayList < Integer > ( t) ) ;
return ;
}
backtrack ( nums, k+ 1 ) ;
t. add ( nums[ k] ) ;
backtrack ( nums, k+ 1 ) ;
t. remove ( t. size ( ) - 1 ) ;
}
}
这是答案给出的另外一种方法,很巧妙,和全排列同样的代码,唯一的区别是backtrack(nums, i + 1); 的时候不再是k了而是i+1,通过这个方式就可以控制,1在第一轮的时候被选择,在第二轮的时候就直接认为不选择1。
class Solution {
List < List < Integer > > res = new LinkedList < > ( ) ;
LinkedList < Integer > track = new LinkedList < > ( ) ;
public List < List < Integer > > subsets ( int [ ] nums) {
backtrack ( nums, 0 ) ;
return res;
}
void backtrack ( int [ ] nums, int start) {
res. add ( new LinkedList < > ( track) ) ;
for ( int i = start; i < nums. length; i++ ) {
track. addLast ( nums[ i] ) ;
backtrack ( nums, i + 1 ) ;
track. removeLast ( ) ;
}
}
}
做完这俩题好像懂了为什么要分组合/子集树 排列数两种类型了
以拍照为例,子集树是确定谁来合影,排列数是确定合影时的站位顺序
子集:元素的位置不重要,所以只需要对当前位置元素的选或者不选遍历即可,顺序无关 排列数:不重复情况下,在每个位置k上全部元素都要遍历一遍,顺序有关
77. 组合
class Solution {
List < Integer > t = new ArrayList < Integer > ( ) ;
List < List < Integer > > res = new ArrayList < List < Integer > > ( ) ;
public List < List < Integer > > combine ( int n, int k) {
backtrack ( n, 1 , k) ;
return res;
}
void backtrack ( int n, int m, int k) {
if ( t. size ( ) == k) {
res. add ( new ArrayList < Integer > ( t) ) ;
return ;
}
for ( int i = m; i<= n; i++ ) {
t. add ( i) ;
backtrack ( n, i+ 1 , k) ;
t. remove ( t. size ( ) - 1 ) ;
}
}
}
90. 子集 II
带重复元素,要不就是先排序然后判断相邻的元素是否相同 或者每次循环的时候判断这个值是否出现过
class Solution {
List< Integer> t = new ArrayList< Integer> ( ) ;
List< List< Integer>> res = new ArrayList< List< Integer>> ( ) ;
public List< List< Integer>> subsetsWithDup( int[ ] nums) {
Arrays.sort( nums) ;
backtrack( nums,0) ;
return res;
}
void backtrack( int[ ] nums,int k) {
res.add( new ArrayList< Integer> ( t)) ;
for( int i = k; i< nums.length; i++) {
if( i!= k && nums[ i] == nums[ i-1] ) {
continue ;
}
t.add( nums[ i] ) ;
backtrack( nums,i+1) ;
t.remove( t.size( ) -1) ;
}
}
}
40. 组合总和 II
第一次运行的时候超出了时间限制,我以为是Arrays.sort函数复杂度高,没想到原来是需要剪枝!!!回溯结束会自己停止,但是如果超时那么就需要考虑某一枝是否值得遍历
class Solution {
List< Integer> t = new ArrayList< Integer> ( ) ;
List< List< Integer>> res = new ArrayList< List< Integer>> ( ) ;
public List< List< Integer>> combinationSum2( int[ ] candidates, int target) {
Arrays.sort( candidates) ;
backtrack( candidates,0,target) ;
return res;
}
void backtrack( int[ ] nums,int k,int target) {
int sum = t.stream( ) .mapToInt( Integer::intValue) .sum( ) ;
if( sum == target) {
res.add( new ArrayList< Integer> ( t)) ;
return ;
}
if( sum> target) {
return ;
}
for( int i = k; i< nums.length; i++) {
if( i!= k && nums[ i] == nums[ i-1] ) {
continue ;
}
t.add( nums[ i] ) ;
backtrack( nums,i+1,target) ;
t.remove( t.size( ) -1) ;
}
}
}
47. 全排列 II
最开始用swap方法做了半天,但是发现swap和sort方法相冲,所以换成了used来记录放置元素重复使用 还有一个点是调试才懂的,就是要求 nums[i]==nums[i-1] 的时候直接continue,这里其实需要规定此时它们不在同一评估层次,如果在同一层就需要continue,如果在不同层,类似于112的情况,此时尽管第二个1与前边重复,但是还需要遍历到
class Solution {
List< Integer> t = new ArrayList< Integer> ( ) ;
List< List< Integer>> res = new ArrayList< List< Integer>> ( ) ;
boolean[ ] used;
// swap会把之前的排序打乱,所以不能用,只能用used
public List< List< Integer>> permuteUnique( int[ ] nums) {
Arrays.sort( nums) ;
used = new boolean[ nums.length] ;
backtrack( nums,0) ;
return res;
}
void backtrack( int[ ] nums,int k) {
if( k== nums.length) {
res.add( new ArrayList< Integer> ( t)) ;
return ;
}
for( int i = 0 ; i< nums.length; i++) {
if( used[ i] ) continue ;
if( i!= 0 && nums[ i] == nums[ i-1] && used[ i-1] != true) {
continue ;
}
used[ i] = true ;
t.add( nums[ i] ) ;
backtrack( nums,k+1) ;
t.remove( t.size( ) -1) ;
used[ i] = false ;
}
}
}
39. 组合总和
class Solution {
List< Integer> t = new ArrayList< Integer> ( ) ;
List< List< Integer>> res = new ArrayList< List< Integer>> ( ) ;
public List< List< Integer>> combinationSum( int[ ] candidates, int target) {
backtrack( candidates,0,target) ;
return res;
}
void backtrack( int[ ] candidates,int k,int ret) {
if( k== candidates.length) {
if( ret == 0 ) {
res.add( new ArrayList< Integer> ( t)) ;
return ;
}
return ;
}
int num = ret/candidates[ k] ;
for ( int i = 0 ; i<= num; i++) {
for( int j = 0 ; j< i; j++) {
t.add( candidates[ k] ) ;
}
backtrack( candidates,k+1,ret-i*candidates[ k] ) ;
for( int j = 0 ; j< i; j++) {
t.remove( t.size( ) -1) ;
}
}
}
}