目录
前言
除了可以解决与集合排列、组合相关的问题,回溯法还能解决很多算法面试题。如果解决一个问题需要若干步骤,并且每一步都面临若干选项,当在某一步做了某个选择之后前往下一步仍然面临若干选项,那么可以考虑尝试用回溯法解决。通常,回溯法可以用递归的代码实现。
适用回溯法的问题的一个特征是问题可能有很多个解,并且题目要求列出所有的解。如果题目只是要求计算解的数目,或者只需要求一个最优解(通常是最大值或最小值),那么可能需要运用动态规范。
面试题 85 : 生成匹配的括号
题目:
输入一个正整数 n,请输出所有包含 n 个左括号和 n 个右括号的组合,要求每个组合的左括号和右括号匹配。例如,当 n 等于 2 时,有两个符合条件的括号组合,分别是 "(())" 和 "()()"。
分析:
如果输入 n,那么生成的括号组合包含 n 个左括号和 n 个右括号。因此生成这样的组合需要 2n 步,每一步生成一个括号。每一步都面临两个选项,既可能生成左括号也可能生成右括号。由此看来,这个问题很适合采用回溯法解决。
在生成括号组合时需要注意每一步都要满足限制条件。
-
第 1 个限制条件是左括号或右括号的数目不能超过 n 个。
-
第 2 个限制条件是括号的匹配原则,即在任意步骤中已经生成的右括号的数目不能超过左括号的数目。例如,如果在已经生成 "()" 之后再生成第 3 个括号,此时第 3 个括号只能是左括号不能是右括号。如果第 3 个是右括号,那么组合变成 "())",由于右括号的数目超过左括号的数目,之后不管怎么生成后面的括号,这个组合的左括号和右括号都不能匹配。
代码实现:
class Solution {
public:
vector<string> generateParenthesis(int n) {
vector<string> result;
string parenthesis;
dfs(n, n, result, parenthesis);
return result;
}
private:
void dfs(int left, int right, vector<string>& result, string& parenthesis) {
if (left == 0 && right == 0)
{
result.push_back(parenthesis);
return;
}
if (left > 0)
{
parenthesis.push_back('(');
dfs(left - 1, right, result, parenthesis);
parenthesis.pop_back();
}
if (left < right)
{
parenthesis.push_back(')');
dfs(left, right - 1, result, parenthesis);
parenthesis.pop_back();
}
}
};
在上述代码中,递归函数 dfs 的参数 left 表示还需生成左括号的数目,参数 right 表示还需要生成右括号的数目。每生成一个左括号,参数 left 减 1;每生成一个右括号,参数 right 减 1。当参数 left 和 right 都等于 0 时,一个完整的括号组合已经生成。
当生成一个括号时,只要已经生成的左括号的数目少于 n 个(即参数 left 大于 0)就可能生成一个左括号;只要已经生成的右括号的数目少于已经生成的左括号的数目(即参数 left 小于 right)就可能生成一个右括号。
面试题 86 : 分割回文子字符串
题目:
输入一个字符串,要求将它分割成若干子字符串,使每个子字符串都是回文。请列出所有可能的分割方法。例如,输入 "google",将输出 3 种符合条件的分割方法,分别是 ["g", "o", "o", "g", "l", "e"]、["g", "oo", "g", "l", "e"] 和 ["goog", "l", "e"]。
分析:
当处理到字符串中的某个字符时,如果包括该字符在内后面还有 n 个字符,那么此时面临 n 个选项,即分割出长度为 1 的子字符串(只包含该字符)、分割出长度为 2 的子字符串(即包含该字符以及它后面的一个字符),以此类推,分割出长度为 n 的子字符串(即包含该字符在内的后面的所有字符)。由于题目要求分割出来的每个子字符串都是回文,因此需要逐一判断这个 n 个子字符串是不是回文,只有回文子字符串才是符合条件的分割。分割出一段回文子字符串之后,接着分割后面的字符串。
例如,输入字符串 "google",假设处理到第 1 个字符 'g'。此时包括字符 'g' 在内后面一共有 6 个字符,所以此时面临 6 个选项,即可以分割出 6 个以字符 'g' 开头的子字符串,分别为 "g"、"go"、"goo"、"goog"、"googl" 和 "google",其中只有 "g" 和 "goog" 是回文子字符串。分割出 "g" 和 "goog" 这两个回文子字符串之后,再用同样的方法分割后面的字符串。
解决这个问题同样需要很多步,每一步分割出一个回文子字符串。如果处理到某个字符时包括该字符在内后面有 n 个字符,就面临 n 个选项。这也是一个典型的适用回溯法的场景。
代码实现:
class Solution {
public:
vector<vector<string>> partition(string s) {
vector<vector<string>> result;
vector<string> palindrome;
dfs(s, 0, result, palindrome);
return result;
}
private:
void dfs(string& s, int index, vector<vector<string>>& result, vector<string>& palindrome) {
if (index == s.size())
{
result.push_back(palindrome);
return;
}
for (int i = index; i < s.size(); ++i)
{
if (isPalinedrome(s, index, i))
{
palindrome.push_back(s.substr(index, i - index + 1));
dfs(s, i + 1, result, palindrome);
palindrome.pop_back();
}
}
}
bool isPalinedrome(string& s, int left, int right) {
while (left < right)
{
if (s[left] != s[right])
return false;
++left;
--right;
}
return true;
}
};
面试题 87 : 恢复 IP 地址
题目:
输入一个只包含数字的字符串,请列出所有可能恢复出来的 IP 地址。例如,输入字符串 "10203040",可能恢复出 3 个 IP 地址,分别为 "10.20.30.40"、"102.0.30.40" 和 "10.203.0.40"。
分析:
一个 IP 地址被 3 个 '.' 字符分隔成 4 段,每段是从 0 到 255 之间的一个数字。另外,除 "0" 本身外,其他数字不能以 '0' 开头。例如,"10.203.0.40" 是一个有效的 IP 地址,但 "10.203.04.0" 却不是有效的 IP 地址,这是因为第 3 个数字 "04" 以 '0' 开头。
代码实现:
class Solution {
public:
vector<string> restoreIpAddresses(string s) {
vector<string> result;
vector<string> segments;
dfs(s, 0, result, segments);
return result;
}
private:
void dfs(string& s, int index, vector<string>& result, vector<string>& segments) {
if (index == s.size() && segments.size() == 4)
{
result.push_back(segments[0] + '.' + segments[1] + '.'
+ segments[2] + '.' + segments[3]);
return;
}
if (index == s.size() || segments.size() == 4)
return;
int num = 0;
for (int i = index; i < index + 3 && i < s.size(); ++i)
{
num = num * 10 + s[i] - '0';
if (num > 255 || (i != index && s[index] == '0'))
break;
segments.push_back(s.substr(index, i - index + 1));
dfs(s, i + 1, result, segments);
segments.pop_back();
}
}
};