回溯算法-深度优先遍历

1 回溯

什么问题适合用回溯

回溯适合解决排列组合的问题。例如从集合A中选出哪些元素组成的集合B(找出子集B),使得B可行/求可行的B个数/最佳方案/所有方案。

回溯的目的是遍历所有组合的可能性。比如有3个元素,如何遍历出所有组合方式(所有子集)?

回溯与深度优先遍历

回溯和深度优先遍历很类似。回溯的关键是画出回溯路径,即回溯树

回溯与递归

简单的递归不能剪枝。只能一递到底。
回溯=递归+剪枝。

回溯与枚举

枚举的难点是需要枚举出所有情况。因此有的问题不能枚举。

例如下面的例题-最多可达的换楼请求,用枚举的话刚好可以用一个二进制数对应一种情况 (哪一位为1就表示选中),则可以用枚举。
例如从n个不同的数里选m个,如果m已知,可以用m层for循环遍历。否则就不能枚举。

2 回溯问题解决思路

步骤

  1. 画出回溯路径,确定哪些地方需要检验。
  2. 确定可行性检验条件,方法(如何判断可行)
  3. 确定状态变量,前进和回溯的时候如何更新状态变量。
    如常见的状态变量是有一个 当前已选元素集合,前进的时候添加元素,回溯的时候移除元素。

解决方式

方式1:
先选一个元素,检验。接着选下一个,循环。

特点:

  1. 适合有序选取。因为会同时遍历到ABC和BCA这几种情况,不过可以通过剪枝排除重复情况。
  2. 对每一个点进行检验。而不仅仅是叶子节点。
  3. 需要维护数组,表示到达这个点的时候剩余可供选择的点。比较麻烦

方式2:
对于每一个元素,要么选择,要么不选。每一层分出两条枝。

特点:

  1. 适合无序选取,因为是从第一个元素往后判断,元素A必定在B前面。不会出现BA
  2. 仅仅对叶子节点判断
  3. 不需要维护数组来表示当前可用选项。始终是两个选项(选、不选)。

方法1适合可以重复选的情况,此时没有第3条限制,即不用维护数组。方法2适合不能重复选的情况,每个元素要么选要么不选,也可以用于可以重复选的情况。

优化(剪枝)

有时候某一点往后的所有节点都不能满足要求。就要从这点剪掉(return)

例如生成括号的例题,当左括号大于一半或者小于右括号的时候就不满足。
例如组合题中,如果剩余选项+已选元素数量<要求数量,则没必要再往下了,因为即使把所有剩余选项假如已选集合,也不满足要求。

代码范式

方式2的代码大概长这样

Class Solution{
	定义一些参数在外面,省的dfs参数太多。
	int n;
	int ans

	public int Solution(String[] args){
		初始化一些参数,包括用于记录每个一个节点的状态的参数,以及统计参数(方案对应的因变量)
		this.n=n;
		
		调用dfs,主要参数是层数,其他参数可以放在外面,也可以放在这里。
		dfs(0);//0表示在第0层。

		返回
		return ans;
	}
	public void dfs(int pos){
		当到达最后一层的时候,判断可行性,更新统计参数,returnif(pos ==n){
			if(Fessible()){
				update(ans);
			}
			return;
		}
		
		如果接受,更新哪些状态变量
		dfs(pos+1);
		恢复状态变量
		
		如果不接受,更新哪些状态变量
		dfs(pos+1);
		恢复状态变量
	}
}

3 回溯例题–最多可达的换楼请求

在这里插入图片描述
在这里插入图片描述

问题解析:
给定集合requests,从中选出若干元素,满足每个数字在元素第一列出现的次数等于第二列出现的次数,求元素最多的方案。

回溯路径

这里是找无序子集,选择方式2。

接受
不接受
接受
不接受
接受
不接受
请求1
请求2
请求2
请求n
请求n
请求n
请求n
检验
检验
检验
检验

可行性检验

使用数组delta表示当前时刻/当前状态/当前节点,每个建筑的人数变化。delta[k]=m表示第k栋建筑增加了m个人。
使用ans表示所求的最大子集B的大小。

可行性检验:
若delta所有元素都为0,则可行,更新ans。
若delta所有元素不都为0,不可行。若可行,

前进和回溯的过程

前进的时候更新delta,即出去人的建筑delta–,进来人的建筑delta++。

回溯的时候回复delta。即出去人的建筑delta++,进来人的建筑delta++。

代码

注意用到的状态变量:delta,cnt。delta表示当前各个建筑物的人数增加情况,cnt表示当前选择的方案数量。

class Solution {
    int ans=0;//最佳选择方案数
    int n;//建筑数
    int m;//方案总数

    
    int[] delta;
    int cnt=0; //被选择方案数量
    public int maximumRequests(int n, int[][] requests) {
        this.n=n;
        this.m = requests.length;
        this.delta = new int[n];

        dfs(requests, 0);
        
        return ans;
    }
    public void dfs(int[][] requests, int pos){
        if (pos== m){
            if(fessible(delta)){
                ans = Math.max(ans,cnt);
            }
            return;
        }
        //接受
        int[] curReq = requests[pos];
        delta[curReq[0]]++;
        delta[curReq[1]]--;
        cnt++;
        dfs(requests, pos+1);
        delta[curReq[1]]++;
        delta[curReq[0]]--;
        cnt--;
		
		//不接受
        dfs(requests, pos+1);

    }
    public boolean fessible(int[] delta){
        for(int item: delta){
            if(item!=0){
                return false;
            }
        }
        return true;
    }
}

优化:
这里使用了fessible函数判断方案可行性,fessible函数内部有循环,增加了时间复杂度。O(2^m*n)
实际上可以维护一个状态zero变量,表示delta数组中0的数量。实时更新。O(2^m)

剪枝

对于每一个分支,都要等到最后才知道是否可行。因此不能剪枝。

另一种解法:枚举

用二进制数mask表示选取状态。如mask=00001表示仅仅选择方案1.遍历mask,从0000到11111(m个1。对每一个mask,判断此时的可行性,更新ans。

判断可行性的方式是把mask转为delta(如mask=00101表示:delta[requests[0][0]]++,delta[requests[0][1]]–,delta[requests[2][0]]++,delta[requests[2][1]]–,),并用上面的fessible函数判断delta。

4 其他题目

括号生成

在这里插入图片描述
在这里插入图片描述
解法:同样用方法2.每一次可以选择左括号或者右括号。当到达最后一层即第2n层时,判断有效性。
技巧:使用brace 表示 (左括号数-右括号数) 的值。

优化:可以剪枝,如果当前状态是:左括号少于右括号即brace<0/左括号数量大于n即brace>剩余层数。不再继续。

组合总和

在这里插入图片描述
说明:从集合中选出若干元素(可以重复选),总和等于target。

解法:像是方法1和方法2的结合。
每次选择一个元素,判断此时和是否大于等于target(停止条件),判断和是否等于target(目标),接着选下一个元素。
需要时刻判断
不用维护当前选项,因为可以重复选。

构造的回溯树(以3个元素为例):

A
B
C
A
B
C
B
C
C
开始
A,
B
C
A
B
C
B
C
C
,,,...

注意A的下面可以是ABC,B的下面只有BC,这是为了避免重复(AB和BA)。

也可以构造的回溯树(以3个元素为例):

不选p
选p
不选p
选p
不选p
选p
开始
p=1
p=0
p=2
*p=1
p=1
p=0
.....

p是0到n的索引,如果选p对应元素就不移动p,否则p++。表示集合索引。例如点方案是{B}

组合总和2

题目:和上一题的区别是:每个元素只能使用一次,元素可能相同。具有相同的元素集合不能返回多次。
如输入 {2,2} target=2. 需要输出{{2}},而不是 {{2}, {2}}

分析:
如果直接用方法2,即依次判断每个元素是否需要,元素相同的组合会算多次。还要进一步去重。
把输入转换成哈希表。相当于每个元素只能用有限次。
其实上一题相当于这一题的特殊情况:每一个元素可用无限次。

因此一个可行的方法 是维护这个哈希表。在进入之前增加判断条件(value>=1),进入前更新表,回溯时恢复表。

组合

在这里插入图片描述
用方法2即可,重点是可以剪枝:
一、如果当前元素数量list.size=要求数量k,则已找到,返回(显然)
二、如果当前元素数量+剩余可选数量<要求数量。 则不可能再找到,返回。curList.size()+(n-pos+1)<k

第二个剪枝尤其重要。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值