力扣精选题——单词拆分(dp+dfs+dfs(备忘录)+bfs+bfs(备忘录))

这道题好在哪里呢?
对我来说,这道题可以让我尝试5种解法:dp,dfs,dfs+ 备忘录,bfs,bfs+备忘录
所以这道题可以纳入精选里面。

题目链接

在这里插入图片描述

dp

使用dp是最容易让人理解的。

// 方法一:dp
    private static boolean fun1(String s, List<String> dict) {
        boolean[] dp = new boolean[s.length() + 1];
        for (int i = 1; i <= s.length(); i ++) {
            // 枚举k的值
            for (int k = 0; k <= i; k ++) {
                // 如果往前截取全部字符串,我们直接判断子串[0, i - 1]
                // 是否存在于字典wordDict中即可
                if (k == i) {
                    if (dict.contains(s.substring(0, i))) {
                        dp[i] = true;
                        continue;
                    }
                }
                // 递推公式
                dp[i] = dp[i - k] && dict.contains(s.substring(i - k, i));
                // 如果dp[i]为true,说明前i个字符结果拆解可以让他的所有字串都存在于字典wordDict中,直接终止内层循环,不用再计算dp[i]了
                if (dp[i]) break;
            }
        }
        return dp[s.length()];
    }
  • 它的逻辑思路是什么呢?
  • 首先dp的原因是一事物现在的状态与过去的状态有关联
    • 所以我们要从最开始标记状态,记录到一个数据结构中存储起来
    • 有些情况,只用几个中间变量即可,这种是什么情况呢?
      • 比如现在的状态只与前两个状态有关,那么就没有必要存那么多,反正也用不上
  • 回到这个题
  • 首先dp[0] 是false,因为这个表示的是一个空字符串,而字典wordDict中是没有空字符串的
  • 所以从dp[1]开始即可
  • 先看状态转移方程(这个是关键)
  • dp[i] = dp[i - k] && dict.contains(s.substring(i - k, i));
    • 以i为边界,将s分为两部分
    • s的左边被k分为两部分
    • 分别判断这两部分是否符合条件
    • 对于k左边,用的是dp数组判断
    • 对于k右边,用的是是否存在于字典中判断
      • 原因在于dp数组存的是从下标0开始的,而不是从中间开始的(这句话只辅助理解)
  • 需要注意的是:
  • 在k等于i时,表示的是i之前作为一个子串看字典中有没有
  • 还有一点就是:
  • boolean[] dp = new boolean[s.length() + 1];
  • 在开辟空间的时候,是len + 1,原因在于:0表示的是空,始终是false,对于字符串的第一个下标,就是1了,所以最后一个位置的下标就该是len,总长度自然是len + 1
    做dp,最重要的是要抽象,不能着眼于细枝末梢,要脱离是关键。
  • 还有两点,留给大家自己琢磨(还是那句话要有一定高度):
  • 分别是以下两部分代码在代码里的作用,最关键的点在于怎么想问题才能把它加到逻辑里面:
if (dict.contains(s.substring(0, i)))  {
	dp[i] = true;
	continue;
}if (dp[i]) break;

上面的dp方法,有些地方都很有讲究,值得细细琢磨

dfs

// 方法二 dfs
class Func2 {
    public boolean wordBreak(String s, List<String> wordDict) {
        return dfs(s, wordDict, 0);
    }

    // start表示的是从字符串s的哪个位置开始
    public boolean dfs(String s, List<String> wordDict, int start) {
        // 字符串中的所有字符都遍历完了,就是到叶子节点了
        // 说明字符串s可以拆分成在字典中出现的单词,直接返回true
        if (start == s.length())    return true;
        // 开始拆分字符串s
        for (int i = start + 1; i <= s.length(); i ++) {
            // 如果截取的子串不在字典中,继续截取更大的子串
            if (!wordDict.contains(s.substring(start, i))) continue;
            // 如果截取的子串在字典中,继续剩下的拆分
            // 如果剩下的可以拆分成在字典中出现的单词,直接返回true
            // 如果不能则继续截取更大的子串判断
            if (dfs(s, wordDict, i)) return true;
        }
        return false;
    }
}
  • for (int i = start + 1; i <= s.length(); i ++)
  • 这个式子不好一下子推出了,我是这样做的:
    • 靠逻辑写了个for (int i = start; i < s.length(); i ++)
    • 然后,调试,运行,就发现有漏洞没有考虑到,进行修补,就可以了。
    • 那为什么i从start + 1开始呢?
      • 因为要不然s.substring(start, i)就会是一个空字符串,所以这个函数其实是左闭右开的。
    • 那为什么i <= s.length()呢,这不就超了吗
      • 也是上面那个原因

dfs + 备忘录

public boolean wordBreak(String s, List<String> wordDict) {
        return dfs(s, wordDict, new HashSet<>(), 0);
    }

    // start表示的是从字符串s的哪个位置开始
    public boolean dfs(String s, List<String> wordDict, Set<Integer> indexSet, int start) {
        // 字符串都拆分完了,返回true
        if (start == s.length())    return true;
        for (int i = start + 1; i <= s.length(); i ++) {
            // 如果已经判断过,则直接跳过,防止重复判断
            if (indexSet.contains(i))   continue;
            // 截取子串,判断是否在字典中
            if (wordDict.contains(s.substring(start, i))) {
                if (dfs(s, wordDict, indexSet, i))
                    return true;
                // 标记为已判断过
                indexSet.add(i);
            }
        }
        return false;
    }
  • 值得注意的是:
    • 备忘录存的是下标
    • 比如:存了个7,他的意思是:下标为7之后的子串是不符合题意的
      • 以后7之前的子串即使有多种排列方式,一旦碰到7,就没必要继续比了,直接返回
      • 因为后面的是不符合题意的

BFS

public boolean wordBreak(String s, List<String> wordDict) {
        // 这里为了提高效率,把list转为set,因为set的查找效率要比list高
        Set<String> setDict = new HashSet<>(wordDict);
        // 记录当前层开始遍历字符串s的位置
        Queue<Integer> queue = new LinkedList<>();
        queue.add(0);
        int length = s.length();
        while (!queue.isEmpty()) {
            int index = queue.poll();
            // 如果字符串都遍历完了,自己返回true
            if (index == length)    return true;
            for (int i = index + 1; i <= length; i ++) {
                if (setDict.contains(s.substring(index, i)))
                    queue.add(i);
            }
        }
        return false;
    }
  • 注意点是:
  • 使用队列存储符合题意的下标,不断加进去,同时也不断推出
  • 这个容易理解

bfs + 备忘录

public boolean wordBreak(String s, List<String> wordDict) {
        // 这里为了提高效率,把list转化为set,因为set的查找效率比list高
        Set<String> setDict = new HashSet<>(wordDict);
        // 记录当前层开始遍历字符串s的位置
        Queue<Integer> queue = new LinkedList<>();
        queue.add(0);
        int length = s.length();
        // 记录访问过的位置,减少重复判断
        boolean[] visited = new boolean[length];
        while (!queue.isEmpty()) {
            int index = queue.poll();
            // 如果字符串都遍历完了,直接返回true
            if (index == length)    return true;
            if (visited[index])     continue;
            // 标记访问过
            visited[index] = true;
            for (int i = index + 1; i <= length; i ++) {
                if (setDict.contains(s.substring(index, i)))
                    queue.add(i);
            }
        }
        return false;
    }
  • 一定要自己写,否则不知道自己有多菜!多写
  • 这里的备忘录同样存储的是:
    • 例:存储的是7
    • 意思是:
      • 7后面的子串是不会符合题意的,把他存储起来,以后碰到7直接continue
      • 这里的7在抽象逻辑上表示的不是下标7,而是以7开头到结尾的子串的意思。
      • 更进一步解释就是:7后面的子串已经被纳入到计算中了,不用再次纳入一次
      • 反正要求的是能或不能,并不是都是那些个
      • 所以这里可以直接continue
  • 我们会发现:
    • 这些逻辑稍稍一变,就是一个新题型了
  • 总结:
    • 要掌握数据结构使用
    • 抽象到逻辑层面,不要局限在细节里面,否则你钻不出来的
    • 然后再修改你的逻辑细节
  • 目前我的水平也就这个火候,还需多努力!
  • 要时时回顾,过一阵子再回来看,就会比上一次的理解更深入一点
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值