LeetCode 题集:回溯法和递归(二)字符串相关问题

本文介绍 LeetCode 题集中,使用回溯法(递归)解决字符串相关的问题。

LeetCode 其他有关回溯法的问题:
LeetCode 题集:回溯法和递归(一)数组相关问题
LeetCode 题集:回溯法和递归(三)矩阵相关问题


131. Palindrome Partitioning(分割回文串)


问题描述

LeetCode 131 问题描述

思路与代码


本题思路与数组类问题相似,根据回溯法的思想,从起始位置开始,遍历下一个回文子串的结束位置,然后更新起始位置,递归完成即可。

代码如下:

class Solution:
    def partition(self, s: str) -> List[List[str]]:
        list_partition = []
        partition = []

        def is_palindrome(i: int, j: int):
            """
            判断子串是否为回文串
            :param i:  子串起点
            :param j:  子串终点
            :return:  是否为回文串
            """

            while i < j:
                if s[i] != s[j]:
                    return False

                i, j = i + 1, j - 1

            return True

        def backtrack(pos: int):
            """
            回溯法的递归函数
            :param pos:  当前起始位置
            :return:  无
            """

            if pos >= len(s):
                list_partition.append(partition.copy())
                return

            # 从当前位置遍历下一个回文子串
            for i in range(pos, len(s)):
                if is_palindrome(i=pos, j=i):
                    partition.append(s[pos: i + 1])
                    backtrack(pos=i + 1)
                    partition.pop()

        backtrack(pos=0)

        return list_partition

运行效果:
LeetCode 131 运行效果 1

算法可以进一步优化,即使用动态规划进行预处理,将每个子串是否为回文串提前计算出来,提高整体效率。

LeetCode 131 官方题解 动态规划预处理
LeetCode 131 官方题解
优化后的代码如下:

class Solution:
    def partition(self, s: str) -> List[List[str]]:
        list_partition = []
        partition = []

        # 动态规划预处理
        n = len(s)
        mat_is_pld = [[True for _ in range(n)] for _ in range(n)]
        for i in range(n - 1, -1, -1):
            for j in range(i + 1, n):
                mat_is_pld[i][j] = (s[i] == s[j]) and mat_is_pld[i + 1][j - 1]

        def backtrack(pos: int):
            """
            回溯法的递归函数
            :param pos:  当前起始位置
            :return:  无
            """

            if pos >= len(s):
                list_partition.append(partition.copy())
                return

            # 从当前位置遍历下一个回文子串
            for i in range(pos, len(s)):
                if mat_is_pld[pos][i]:
                    partition.append(s[pos: i + 1])
                    backtrack(pos=i + 1)
                    partition.pop()

        backtrack(pos=0)

        return list_partition

运行效果:
LeetCode 131 运行效果 2

显而易见的是,事先存储回文子串信息,会增加内存(空间)消耗。


132. Palindrome Partitioning II(分割回文串 II)


问题描述

LeetCode 132 问题描述

思路与代码


本着 Palindrome Partitioning II 是 Palindrome Partitioning 的变体的思维方式,笔者在第一时间选择修改前一题的代码以求解本题,并且去掉了一些变体问题中不必要的存储:

class Solution:
    def minCut(self, s: str) -> int:
        if len(s) == 1:
            return 0

        # 动态规划预处理
        n = len(s)
        mat_is_pld = [[True for _ in range(n)] for _ in range(n)]
        for i in range(n - 1, -1, -1):
            for j in range(i + 1, n):
                mat_is_pld[i][j] = (s[i] == s[j]) and mat_is_pld[i + 1][j - 1]

        def backtrack(pos: int, num_sub: int, min_num_sub: int) -> int:
            """
            回溯法的递归函数

            :param pos:  当前起始位置
            :param num_sub:  当前已切割的子串数
            :param min_num_sub:  当前的最小切割子串数

            :return: min_num_sub:  当前的最小切割子串数
            """

            if pos >= len(s):
                min_num_sub = min(num_sub, min_num_sub)
                return min_num_sub

            # 从当前位置遍历下一个回文子串
            # for i in range(pos, len(s)):
            for i in range(len(s) - 1, pos - 1, -1):
                if mat_is_pld[pos][i]:
                    if num_sub + 1 >= min_num_sub:  # 若当前已切割的子串数超过历史最小值,则直接剪枝
                        break

                    num_sub += 1
                    min_num_sub = backtrack(pos=i + 1, num_sub=num_sub, min_num_sub=min_num_sub)
                    num_sub -= 1

            return min_num_sub

        min_cut = backtrack(pos=0, num_sub=0, min_num_sub=n) - 1

        return min_cut

提交运行后,发现超时:
LeetCode 132 超时算例

于是很乖巧地去请教官方题解,恍然大悟,因为本题不是求所有切割方式,而是最小切割次数,即不是要找到全部解集,而是只要一个最优解,因此动态规划才是更优的方法。事实证明,思维定势要不得呀!

LeetCode 132 官方题解

LeetCode 132 官方题解 1
LeetCode 132 官方题解 2

代码如下:

class Solution:
    def minCut(self, s: str) -> int:
        # 动态规划预处理:字串是否为回文串
        n = len(s)
        mat_is_pld = [[True for _ in range(n)] for _ in range(n)]
        for i in range(n - 1, -1, -1):
            for j in range(i + 1, n):
                mat_is_pld[i][j] = (s[i] == s[j]) and mat_is_pld[i + 1][j - 1]

        # 动态规划求解:最少切割次数
        list_min_cut = [i for i in range(n)]  # 前 i 个字符组成的子串的最少切割次数
        for i in range(n):
            if mat_is_pld[0][i]:
                list_min_cut[i] = 0

            else:
                for j in range(i):
                    if mat_is_pld[j + 1][i]:
                        list_min_cut[i] = min(list_min_cut[i], list_min_cut[j] + 1)

        return list_min_cut[n - 1]

运行效果:
LeetCode 132 运行效果 1

在官网的所有提交记录中,还有更优的方法,该方法也采用动态规划,不同点在于:

  • 对两种特殊情况直接输出结果:无需切割(整体为回文串);只需切割 1 次
  • 将动态规划预处理的部分与求解部分合二为一
  • 在循环过程中,对奇数长度和偶数长度两种情况的回文子串分别处理,通过由中心点向两侧扩展的方式高效切割回文串,以更新动态规划的中间结果列表

代码如下:

class Solution:
    def minCut(self, s: str) -> int:
        # special case 1: 无需切割
        if s == s[:: -1]:
            return 0

        # special case 2: 只需切割 1 次
        for i in range(1, len(s)):
            if s[i:] == s[: i - 1: -1] and s[: i] == s[i - 1:: -1]:
                return 1

        dp = [i for i in range(-1, len(s))]  # dp[i] 表示到第 i 位之前的子串的最小切割次数
        for i in range(len(s)):
            t = 0
            while i - t >= 0 and i + t < len(s) and s[i - t] == s[i + t]:  # 奇数长度的回文串
                dp[i + t + 1] = min(dp[i + t + 1], dp[i - t] + 1)
                t = t + 1

            t = 0
            while i - t >= 0 and i + t + 1 < len(s) and s[i - t] == s[i + t + 1]:  # 偶数长度的回文串
                dp[i + t + 2] = min(dp[i + t + 2], dp[i - t] + 1)
                t = t + 1
        return dp[-1]

运行效果:
LeetCode 132 运行效果 2


22. Generate Parentheses(括号生成)


问题描述

LeetCode 22 问题描述

思路与代码


本题的目的是穷举出所有可行组合,因此很容易想到回溯法,要点在于右括号不能出现在左括号的左边,即每当加入一个右括号时,其左侧的左括号数量需要多于右括号。

具体代码如下:

class Solution:
    def generateParenthesis(self, n: int) -> List[str]:
        list_group = []

        def backtrack(pos: int, num_left: int, num_right: int, group: str):
            if pos == 2 * n:
                list_group.append(group)
                return

            if num_left < n:  # 左括号的数量未用完,还可以继续添加左括号
                group += '('
                backtrack(pos=pos + 1, num_left=num_left + 1, num_right=num_right, group=group)
                group = group[: -1]

            if num_left > num_right:  # 已使用的左括号数量多于右括号,则可以添加右括号
                group += ')'
                backtrack(pos=pos + 1, num_left=num_left, num_right=num_right + 1, group=group)
                group = group[: -1]

        backtrack(pos=0, num_left=0, num_right=0, group='')

        return list_group

运行效果还不错:
LeetCode 22 运行效果 1

此外,笔者在题解中看到了一种动态规划的方法,也比较有启发:

LeetCode 22 题解 动态规划

LeetCode 22 题解 1
LeetCode 22 题解 2

代码如下:

class Solution:
    def generateParenthesis(self, n: int) -> List[str]:
        if n == 0:
            return []

        total_list = list()
        total_list.append([None])    # 0 组括号时记为 None
        total_list.append(["()"])    # 1 组括号只有一种情况

        for i in range(2, n+1):    # 开始计算 i 组括号时的括号组合
            list_tmp = []
            for j in range(i):    # 开始遍历 p q ,其中 p + q = i - 1 , j 作为索引
                now_list1 = total_list[j]    # p = j 时的括号组合情况
                now_list2 = total_list[i-1-j]    # q = (i - 1) - j 时的括号组合情况
                for k1 in now_list1:
                    for k2 in now_list2:
                        if not k1:
                            k1 = ""
                        if not k2:
                            k2 = ""
                        el = "(" + k1 + ")" + k2
                        list_tmp.append(el)    # 把所有可能的情况添加到 list_tmp 中
            total_list.append(list_tmp)    # list_tmp 就是 i 组括号的所有情况,添加到 total_list 中,继续求解 i = i + 1 的情况
        return total_list[n]

运行效果:
LeetCode 22 运行效果 2

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值