【数据结构与算法】——回溯法

回溯法在解决问题时的每一步都尝试所有可能的选项,最终找出所有可行的方案
回溯法非常适合解决由多个步骤组成的问题,并且每个步骤都有多个选项
在某一步选择了其中一个选项之后,就进入下一步,然后会面临新的选项
就这样重复选择,直至到达最终的状态
用回溯法解决问题的过程可以形象地用一个树形结构表示,求解问题的每个步骤可以看作树中的一个节点,如果在某一步有n个可能的选项,每个选项是树中的一条边,然后经过这些边就可以到达该节点的n个子节点
在采用回溯法解决问题时如果到达树形结构的叶节点,就找到了问题的一个解。
如果希望找到更多的解,那么还可以回溯到它的父节点再尝试父节点其他的选项。
如果父节点所有可能的选项都已经尝试过,那么再回溯到父节点的父节点以尝试它的其他选项,这样逐层回溯到树的根节点
因此,采用回溯法解决问题的过程实质上是在树形结构中从根节点开始进行深度优先遍历
通常,回溯法的深度优先遍历用递归代码实现
如果在前往某个节点时对问题的解的状态进行了修改,那么在回溯到它的父节点时要记得清除相应的修改
由于回溯法是在所有选项形成的树上进行深度优先遍历,如果解决问题的步骤较多或每个步骤都面临多个选项,那么遍历整棵树将需要较多的时间。如果明确知道某些子树没有必要遍历,那么在遍历的时候应该避开这些子树以优化效率
通常将使用回溯法时避免遍历不必要的子树的方法称为剪枝

回溯法适用于解决由多个步骤组成的问题,每一步都会面临多个选项,选择其中一个选项后会进入下一步,又会同样面临许多选项,就这样重复进行选择,直至达到最终的状态,这样就会找到问题的一个解;如果需要找到问题的其他解,可以回溯到上一个步骤进行选择,以便选择其他选项,如果该步骤的所有选项已经全部被遍历,可以再回溯至该步骤的上一个步骤进行选择,直至回溯到初始状态。为了避免遍历无用路径,可以使用剪枝方法来优化效率;另外,如果在前往某一步时对问题解的状态进行了修改,回溯的时候需要清除相应修改。

集合的组合、排列
组合与元素的顺序无关
排列与元素的顺序相关
在这里插入图片描述

在这里插入图片描述
等递归函数执行完成之后,函数helper也执行完成,接下来将回到前一个数字的函数调用处继续执行
在res中添加的是sublist的一个拷贝,而不是sublist本身,这是因为接下来还需要修改sublist以便得到其他的子集,同时避免已经添加到res中的子集被修改。在res中添加sublist的拷贝可以避免不必要的修改

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
思路:避免重复的组合的方法是当在某一步决定跳过某个值为m的数字时,跳过所有值为m的数字
为了方便跳过后面所有值相同的数字,可以将集合中的所有数字排序,把相同的数字放在一起,这样方便比较数字。当决定跳过某个值的数字时,可以按顺序扫描后面的数字,直到找到不同的值为止
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
排列
在这里插入图片描述
思路:生成全排列的过程,就是交换输入集合中元素的顺序以得到不同的排列
如果输入的集合中有n个元素,那么生成一个全排列需要n步,当生成排列的第1个数字时会面临n个选项,即n个数字都有可能成为排列的第1个数字。生成排列的第1个数字之后接下来生成第2个数字,此时面临n-1个选项,即剩下的n-1个数字都有可能成为第2个数字。然后以此类推,直到生成最后一个数字,此时只剩下1个数字,也就只有一个选项,看起来解决这个问题可以分成n步,而且每一步都面临若干选项,这是典型的适用回溯法的场景

public List<List<Integer>> permute(int[] nums) {
	List<List<Integer>> result = new LinkedList<>();
	helper(nums, 0, result);
	return result;
}

public void helper(int[] nums, int i, List<List<Integer>> result) {
	if (i == nums.length) {
		List<Integer> permutation = new LinkedList<>();
		for (int num : nums) {
			permutation.add(num);
		}
		result.add(permutation);
	} else {
		for (int j = i; j < nums.length; ++j) {
			swap(nums, i, j);
			helper(nums, i + 1, result);
			swap(nums, i, j);
		}
	}
}

private void swap(int[] nums, int i, int j) {
	if (i != j) {
		int temp = nums[i];
		nums[i] = nums[j];
		nums[j] = temp;
	}
}

当函数helper生成排列的下标为i时,下标从0到i-1的数字都已经选定,但数组nums中下标从i到n-1的数字(假设数组的长度为n)都有可能放到排列的下标为i的位置,因此函数helper中有一个for循环逐一用下标为i的数字交换它后面的数字。这个for循环包含下标为i的数字本身,这是因为它自己也能放在排列下标为i的位置。交换之后接着调用递归函数生成排列中下标为i+1的数字。由于之前已经交换了数组中的两个数字,修改了排列的状态,在函数退出之前需要清除对排列状态的修改,因此再次交换之前交换的两个数字
当下标i等于数组nums的长度时,排列的每个数字都已经产生了,nums中保存了一个完整的全排列,于是将全排列复制一份并添加到返回值result中。最终result中包含所有的全排列
假设数组nums的长度为n,当i等于0时,递归函数helper的for循环执行n次,当i等于1时for循环执行n-1次,以此类推,当i等于n-1时,for循环执行1次。因此全排列的时间复杂度为O(n!)
在这里插入图片描述

在这里插入图片描述
不交换之前交换的元素
如果集合中有重复的数字,那么交换集合中重复的数字得到的全排列是同一个全排列。例如,交换【1,1,2】中的两个数字1并不能得到新的全排列
下面采用回溯法的思路来解决这个问题。当处理到全排列的第i个数字时,如果已经将某个值为m的数字交换为排列的第i个数字那么再遇到其他值为m的数字就跳过。
helper中使用了一个HashSet,用来保存已经交换到排列下标为i的位置的所有值。只有当一个数值之前没有被交换到第i位时才做交换,否则直接跳过
在这里插入图片描述

适用回溯法的问题的一个特征是问题可能有很多个解,并且题目要求列出所有的解。如果题目只是要求计算解的数目,那么可能需要运用动态规划

在这里插入图片描述
在这里插入图片描述
思路:当处理到字符串中的某个字符时,如果包括该字符在内后面还有n个字符,那么此时面临n个选项,即分割出长度为1的字符串(只包含该字符)、分割出长度为2的子字符串(即包含该字符及它后面的一个字符),以此类推,分割出长度为n的字符串(即包含该字符在内的后面的所有字符)。由于题目要求分割出来的每个字符串都是回文,因此需要逐一判断这n个字符串是不是回文,只有回文字符串才是符合条件的分割。分割出一段回文字符串之后,接着分割后面的字符串
在这里插入图片描述
使用LinkedList会超时
在这里插入图片描述
在这里插入图片描述

一个IP地址被3个’.'字符分隔成4段,每段是从0到255之间的一个数字。另外,除”0“本身外,其他数字不能以’0’开头

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值