算法基础——回溯算法

回溯算法

回溯算法的理解

回溯和递归是分不开的(就像迭代和递推一样),递归的过程中隐藏着回溯,回溯或递归的本质就是暴力搜索,用于解决排列、组合、切割、子集、棋盘等即使嵌套for循环(迭代法)也难以解决的问题,比如要嵌套多少层for是未知的。
这些问题都可以抽象成一个树形结构,即N叉树,一个问题不断地有很多个小问题的分支路径,回溯的思想就是遍历完一条路径之后,返回到上个问题并恢复现场。回溯属于DFS。
N叉树的深度由递归来处理,树的每层宽度用for迭代处理,回溯法就是递归和迭代的组合使用,即原本全用迭代需要嵌套N层for循环,这个过程由递归解决,我们只需要写一层for循环,递归N次即可。虽然递归或利用值传递都可以隐式地回溯,但迭代部分的全局变量就必须手动回溯了。

回溯算法的模板

主体就是一个暴力搜索的递归函数,通常没有返回值,少数情况有返回值,比如只需找到一个解。

void backtracking(参数列表) {
	if(终止条件){
		收集结果;
		return;
	}
	for(集合元素){
		处理当前元素;
		对当前元素进行递归;
		回溯操作;
	}
	return;
}

参数列表通常较多,可以先写着,随用随加,由于返回是void,所以参数必然有结果集,参数过多时可改成全局变量;终止条件一般是遇到叶子结点,即一条路径到头了,当然构建树的叶子结点的条件要看具体任务;集合元素通常为父结点的所有子结点,即兄弟姐妹;回溯操作并非是必须的或者显式的,有些任务不需要,则纯递归就可以隐式地完成,上述任务通常结果集是全局的(包括引用传递),每次递归都会影响,故一条路径走完返回时需要回溯恢复现场,所以才叫回溯法。
由于没有返回值,所以其他不符合条件的情况即使不剪枝提前终止,最后返回也会因为恢复现场而不影响其他结果。

回溯过程的细节

首先变量如果要参与递归,要么是全局变量,要么在最开始作为参数传进来,二者的交集传入全局变量也视为传参,只是如果是引用传递可以保存变化在全局变量而不占用返回值。
因此有三种情况:全局变量(不传参),引用传递,值传递。
全局变量(传参)包含在后两者中。
全局变量(不传参)和引用传递本质都是所有递归函数操作同一个变量,故必须手动回溯。注意引用传递必须接收const对象,index+1属于临时对象无法使用。
值传递则如果手动操作则需要搭配手动回溯,如下的path,如果是操作写在传参里则不需要,如下的index,因为传入的是临时副本,回溯时也会自动销毁

void backtracking(int index, string digits, vector<int> path) {
	/*...*/
	path.push_back(i);
	backtracking(index+1, digits, path);
	path.pop_back();
}

值传递会产生额外的拷贝操作,空间时间效率都低,大量数据易超时,尽量不用。

回溯算法的优化(剪枝)

回溯算法的优化通常只能从剪枝下手,不然也不会用回溯暴力搜索了,除了额外终止条件结束当前路径的剪枝,大部分回溯的剪枝都可以通过优化for循环的条件,即减少每层for循环的次数,当然太复杂分开写应该也是可以的,一般写法层数越深,for循环的次数越少,从树形结构上看,是一个往左斜的N叉树,当然具体看集合元素的逻辑。
在这里插入图片描述

记忆化搜索

递归搜索(回溯)+保存计算结果=记忆化搜索
一些问题的回溯过程中会出现重复递归的情况,即之前这段递归已经计算过,之后其他路径也遇到同样的递归段,导致重复计算。
例如打家劫舍问题用回溯写:

int dfs(int i) { // i表示房屋下标,最小为i=0
    if (i < 0) return 0;
    int res = max(dfs(i - 1), dfs(i - 2) + nums[i]);
    return res;
}

每个房屋的状态为0和1,每次递归都要递归两种状态,故时间复杂度为2的n次方。看着像二叉树,但这种树的分支有很多是重复的,所以可以优化成记忆化搜索:

vector<int> memo(n, -1); // 初始化记忆数组,打家劫舍结果不可能为负数
int dfs(int i) { // i表示房屋下标,最小为i=0
    if (i < 0) return 0;
    if (memo[i] != -1) return memo[i]; // 如果memo[i]被搜索过直接返回
    else return memo[i] = max(dfs(i - 1), dfs(i - 2) + nums[i]); // 否则计算赋值并返回
}

可以发现,max()计算和memo赋值过程只发生在dfs()结束后从底往上归的过程中,递的过程并不涉及计算赋值,通常这类问题分支也是确定的,我们也知道底在哪里(回溯法更多是用来解决那些底未知、分支未知的问题),那么是否可以去掉递的过程,留下归的过程,直接从底往上递推计算呢?
记忆化搜索通常可以转换成递推(即动态规划的递推公式):

dfs(i) = max(dfs(i - 1), dfs(i - 2) + nums[i]); // 递归
dp[i] = max(dp[i – 1], dp[i – 2] + nums[i]); // 递推

将递归函数dfs换成dp数组,递归改成循环,递归的边界为dp数组的初始值,同时为了避免数组出现负数下标,可以让i从2开始,前面的使用数组初始值这里递归的边界条件是i<0返回0,故初始化为0即可。
动态规划就是把自上而下(递归)的记忆化搜索改成自下而上(递推)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值