回溯算法详解及例题分析1

回溯法往往和递归,解空间树有关,当我们分析一道题时,发现其包含子问题,往往可以建立一颗“树”解决问题。回溯法的本质是深度优先遍历,例如树的三种遍历方式,也可以视为采用回溯法
这样说还是有些抽象,下面举一些用回溯法解题的常见题型:

  • 排列问题
  • 组合问题
  • 询问一共有多少种方式实现某目标

当我们选择使用回溯法时,该题型往往有以下特点:

  • 有若干可选项
  • 子问题(可采用递归)
  • 询问所有方式是什么
    我们仔细观察会发现,其特点与动态规划非常接近,如子问题,其实动态规划是在回溯法的基础上进行了改进,优化了时间复杂度,因此有些能用回溯法解决的问题,也可以选择使用动态规划算法,先举出回溯法例题:
    例如:
    leetcode 17
    给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

示例:

输入:“23”
输出:[“ad”, “ae”, “af”, “bd”, “be”, “bf”, “cd”, “ce”, “cf”].
说明:
尽管上面的答案是按字典序排列的,但是你可以任意选择答案输出的顺序。
根据题目描述,我们可以从中分析出递归过程:
在这里插入图片描述
根据图示可以看出,这道题的本质是树的应用,具有以下特点:

  • 具有若干可选项,即一个数字对应多个字母
  • 递归子结构
  • 回溯遍历
  • 询问所有可行解
    根据此思路,写出以下代码:
class Solution {
public:
//对应关系
	string letters[10] = {  " ","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
	//递归函数digits代表数字串,index为解析的第index个字符数字,s保存字符串,res为返回值
	void _letterCombinations(string digits, int index, string s, vector<string>&res)
	{
	//终止条件
		if (index == digits.size())
		{
			res.push_back(s);
		}
		
		char c = digits[index];
		//有letters[c-'0'].size()种选择方式
		for (int i = 0; i < letters[c-'0'].size(); i++)
		{
			_letterCombinations(digits, index + 1, s + letters[c - '0'][i], res);
		}

	}
	vector<string> letterCombinations(string digits) {
		vector<string> res;
		if (digits.size() == 0)
			return res;
		_letterCombinations(digits, 0, "",res);
		return res;
	}
};

再来看一道题:
给定一个只包含数字的字符串,复原它并返回所有可能的 IP 地址格式。

示例:

输入: “25525511135”
输出: [“255.255.11.135”, “255.255.111.35”]

乍一看我们很难发现其中的递归,但是当我们仔细分析之后,每次确定一个.的位置之后,再继续确定下一个位置,而每一个.可选三个位置,即1,2,3三位数,因此我们可以分析出递归树大概的结构

class Solution {
public:
	//string s 要分割的字符串, int index 第index个‘.’
	//string result,保存最终分割串 vector<string>res返回值
	void _restoreIpAddresses(string s, int index, string result, vector<string>&res)
	{
		//递归终止条件
		if (index == 0&&s.empty())
			res.push_back(result);
		else
		{
		//三种插入选择
			for (int i = 1; i <=3; i++)
			{
				if (s.size() >= i && isValid(s.substr(0, i)))
				{
					if (index == 1)
						_restoreIpAddresses(s.substr(i), index - 1, result + s.substr(0, i), res);
					else
					{
						_restoreIpAddresses(s.substr(i), index - 1, result + s.substr(0, i) + '.', res);
					}
				
				}
					
			}
			
		}
	}
	bool isValid(string s)
	{
		//判断是否为0-255数字
		if (s.empty() || s.size() > 1 && s[0] == '0')
			return false;
		return  atoi(s.c_str())<=255&& atoi(s.c_str())>=0;
	}
	vector<string> restoreIpAddresses(string s) {
		vector<string> res;
		if (s.size() == 0||s.size()>12)
			return res;
		_restoreIpAddresses(s, 4, "", res);
		return res;
	}
};

如果你没有发现上述两道题的共同特性,再来看一题:
给定一个没有重复数字的序列,返回其所有可能的全排列。

示例:

扫描二维码关注公众号,回复: 9078893 查看本文章

输入: [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]
这道题也具有子问题和递归结构:
在这里插入图片描述
上代码:

class Solution {
public:
	vector<bool>visited;
	void _permute(vector<int>& nums, int index, vector<int>& s, vector<vector<int>>&res)
	{
		if (index == nums.size())
		{
			res.push_back(s);
			return;
		}
		for (int i = 0; i < nums.size(); i++)
		{
			if (!visited[i])
			{
				s.push_back(nums[i]);
				visited[i] = true;
				_permute(nums, index + 1, s, res);
				//回溯过程,返回上一步
				s.pop_back();
				visited[i] = false;
			}
		}
	}
	vector<vector<int>> permute(vector<int>& nums) {
		vector<vector<int>>res;
		if (nums.size() == 0)
			return res;
		vector<int> s;
		visited = vector<bool>(nums.size(), false);
		_permute(nums, 0, s, res);
        return res;
	}
};

以上三题都使用了通用“模板”,仔细观察就能发现,其中最重要的就是有
1 递归
2 有限可选项
3 输出所有方式
至于回溯法的优化:剪枝,下一次再和大家分享

发布了49 篇原创文章 · 获赞 198 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/qq_36696486/article/details/89138967
今日推荐