《剑指 Offer》专项突破版 - 面试题85、86 和 87 : 使用回溯法解决其他类型的问题(C++ 实现)

目录

前言

面试题 85 : 生成匹配的括号

面试题 86 : 分割回文子字符串

面试题 87 : 恢复 IP 地址



前言

除了可以解决与集合排列、组合相关的问题,回溯法还能解决很多算法面试题。如果解决一个问题需要若干步骤,并且每一步都面临若干选项,当在某一步做了某个选择之后前往下一步仍然面临若干选项,那么可以考虑尝试用回溯法解决。通常,回溯法可以用递归的代码实现。

适用回溯法的问题的一个特征是问题可能有很多个解,并且题目要求列出所有的解。如果题目只是要求计算解的数目,或者只需要求一个最优解(通常是最大值或最小值),那么可能需要运用动态规范


面试题 85 : 生成匹配的括号

题目

输入一个正整数 n,请输出所有包含 n 个左括号和 n 个右括号的组合,要求每个组合的左括号和右括号匹配。例如,当 n 等于 2 时,有两个符合条件的括号组合,分别是 "(())" 和 "()()"。

分析

如果输入 n,那么生成的括号组合包含 n 个左括号和 n 个右括号。因此生成这样的组合需要 2n 步,每一步生成一个括号。每一步都面临两个选项,既可能生成左括号也可能生成右括号。由此看来,这个问题很适合采用回溯法解决。

在生成括号组合时需要注意每一步都要满足限制条件。

  1. 第 1 个限制条件是左括号或右括号的数目不能超过 n 个

  2. 第 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();
        }
    }
};
  • 24
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值