题目

类型:深度优先搜索

image.png

解题思路

题意拆解:如何判断某个 s = words[i] 是否为「连接词」
例如 s = abc,其可能的 item 组合为 a 和 bc。
判断单个字符串是否为连接词可使用动态规划求解:
定义 f[i] 为考虑 s 的前 i 个字符(令下标从 1 开始),能够切分出的最大 item 数的个数。

这里之所以采用「记录 f[i] 为最大分割 item 数(int 类型动规数组)」,而不是「记录 f[i] 为是否可由多个 item 组成(bool 类型动规数组)」,是因为每个 s = words[i] 至少可由自身组成,采用 bool 记录状态的话,最终 f[n] 必然为 True,需要额外处理最后一个状态,干脆记录最大分割数量好了。此时如果 s 为「连接词」必然有 f[n] > 1。

假设 f[i] 可由 f[j] 转移而来(其中 j < i ),那么能够转移的充要条件为 f[j] != 0 且子串 s[(j + 1)..i] 在 words 出现过。

通过「字符串哈希」方式来优化判断某个子串是否存在于 words 中。在判断每个 s = words[i] 是否为为连接词前,先对 words进行遍历,预处理每个 words[i] 的哈希值,并存入 HashSet 中,这样将「判断某个子串是否存在于 words」的问题转化为「判断某个数值是否存在于 Set 当中」。又由于 在计算某个子串 s 的哈希值时,是从前往后处理每一位的 s[i],因此在转移 f[i] 时,我们期望能够从前往后处理子串,这是常规的从 [0, i - 1] 范围内找可转移点 f[j] 无法做到的。
所以我们调整转移逻辑为:从 f[i] 出发,枚举范围 [i + 1, n] ,找到可由 f[i] 所能更新的状态 f[j] ,并尝试使用 f[i] 来更新 f[j] 。转移方程为:

连接词 - 图2

能够转移的前提条件为 f[i] 为有效值,且子串 s[(i + 1), j] 在 words 出现过。

由于字符串哈希会产生哈希碰撞,在计算哈希值的时候,修改了一下哈希计算方式(额外增加了一个 OFFSET)

  1. class Solution {
  2. Set<Long> set = new HashSet<>();
  3. int P = 131, OFFSET = 128;
  4. public List<String> findAllConcatenatedWordsInADict(String[] words) {
  5. for (String s : words) {
  6. long hash = 0;
  7. for (char c : s.toCharArray()) hash = hash * P + (c - 'a') + OFFSET;
  8. set.add(hash);
  9. }
  10. List<String> ans = new ArrayList<>();
  11. for (String s : words) {
  12. if (check(s)) ans.add(s);
  13. }
  14. return ans;
  15. }
  16. boolean check(String s) {
  17. int n = s.length();
  18. int[] f = new int[n + 1];
  19. Arrays.fill(f, -1);
  20. f[0] = 0;
  21. for (int i = 0; i <= n; i++) {
  22. if (f[i] == -1) continue;
  23. long cur = 0;
  24. for (int j = i + 1; j <= n; j++) {
  25. cur = cur * P + (s.charAt(j - 1) - 'a') + OFFSET;
  26. if (set.contains(cur)) f[j] = Math.max(f[j], f[i] + 1);
  27. }
  28. if (f[n] > 1) return true;
  29. }
  30. return false;
  31. }
  32. }