leetcode 139. 单词拆分 python思路详解

leetcode 139. 单词拆分

首先先阅读一下题干:

给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。 说明:

• 拆分时可以重复使用字典中的单词。
• 你可以假设字典中没有重复的单词。

示例 1: 输入: s = “leetcode”,wordDict = [“leet”, “code”] 输出: true 解释: 返回 true 因为 “leetcode” 可以被拆分成 “leet code”。
示例 2: 输入: s = “applepenapple”, wordDict = [“apple”, “pen”] 输出: true 解释: 返回 true 因为 “applepenapple” 可以被拆分成 “apple pen apple”。
#注意你可以重复使用字典中的单词。
示例 3: 输入: s = “catsandog”, wordDict = [“cats”, “dog”, “sand”, “and”, “cat”] 输出: false

递归思路

首先这个题拿到手看得时候,第一个能诞生的思路是使用递归,将一个长字符串一步一步的剪下前几个位置的部分,将剩下的部分重复递归。

递归解决问题的思路是分别找到循环体和结束条件,本题里的循环体就是按顺序遍历字符串的每个字符,判断从开头到此个字符这个单词是否在字典中,如果在,将剩余的字符串递归循环,如果不在继续遍历下一个字符。

结束条件是:1.分解到某一步时,如果剩余字符串的前几个字母在字典中不存在,例如示例1中,如果给定s=“leetacode”,第一步将leet拆解出来,将"acode"递归进入,此时a与任何字母组合都不在字典中,我们可以认为此种字符串操作方式false,所以返回false返回上一层,尝试其他拆解方式;2. 第二种情况是,剩余的字符串,这个整体就是字典中的某个单次,例如示例1中,s=“leetcode”,第一步将leet拆解出来,code进入递归,code这个整体在字典中,此时我们可以认为这个单词是可以被拆分的。

按照如上思路,写出python代码:

class Solution:
    def wordBreak(self, s: str, wordDict: List[str]) -> bool:
        return self.wordb(s, wordDict)

    def wordb(self, s, words):
    	#结束情况2
        if s in words: return True

        for i in range(1, len(s)):
            # 遍历体,检查前i个字符是否在字典内
            # 如果在的话,将剩下的部分送入递归循环
            if s[:i] in words:
                res = self.wordb(s[i:], words)
                if res == False:
                    continue
                else:
                    return True
        return False

使用了递归方式以后,提交代码发现此时超时,那么肯定有更省时间的解法(递归的方式解决问题通常都不是最优解)。

动态规划(DP)思路:

如何能判断是否使用动态规划思路来解题呢? 在递归思路中我们是从左往右思考的,我们先找到第一个在字典中的单词,然后再找到下一个,直到字符串结尾。那么反过来,我们从后往前思考一下,一个长度为n的字符串最后一个部分假设是从m处到结尾这个部分在字典中,那么s是否可以被拆分的问题就变成了s[:m]是否可被拆分的问题了。以此类推下去我们就可以写出一个一个动态规划的思路,从后往前遍历,找到第一个在字典里的词以后,假设此时这个位置是j,此时dp[n] = dp[j],既n个字符的字符串能否被拆解等于前j个字符串是否能被拆解,因为此时n-j这部分的字符串在字典中。

DP代码:

class Solution:
    def wordBreak(self, s: str, wordDict: List[str]) -> bool:
        dp = [False] * (len(s) + 1)
        dp[0] = True
        if s in wordDict: return True
        for i in range(1, len(s) + 1):
            # 从第一个到最后一个字符
            for j in range(i):
                # i之前的第一个到i个字符
                if dp[j] and s[j:i] in wordDict:
                    dp[i] = True
                    break
        return dp[len(s)]

这里再提一下动态规划的基本步骤:

  1. 设置储存空间。对于本题,我需要存储的是以从开头到第i个字符的字符串是否能够被拆分,只有一个变量i所以储存空间是一个一维矩阵,也就是dp[i]。如果是需要存储上下坐标的话,例如从i到j,dp[i][j], 这也就是一个二维矩阵。
  2. 赋初始值。动态规划问题是使用空间换时间的思路,并且是将n的问题,逆推到1的问题,所以要将所有的初始值手动赋予。首先所有的值默认是False,然后第一个dp[0]的含义是从0到0也就是空,我们将这种特殊情况赋值为True(其实是用不上的)。类似于硬币问题中,我们会将1,2,5的值放为1,这都是赋予初始值的步骤内容。
  3. 动态规划方程。第三步是将动态规划方程放入循环中,循环的选择因题目不同,本题中我们的储存空间存的是从开头到第i是否能被拆解,那么肯定要从头遍历一遍,所以一层循环是必须的;然后判断值是T or F,有两种情况,第一种,如果从开头到i的字符串整体就在字典中,那么直接就是True,第二种从后往前找到第一个j使,j到i这个字符串s[j:i]在字典中,此时dp[i] = dp[j]。这两种情况构成了一个动态规划方程:

dp[i] = { True if dp[:i] in dict 或者 dp[j] if dp[j:i] in dict }

你可能感兴趣的:(leetcode)