跳到主要内容

40 篇博文 含有标签「动态规划」

查看所有标签

· 阅读需 5 分钟

1、题干

给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。

回文字符串 是正着读和倒过来读一样的字符串。

子字符串 是字符串中的由连续字符组成的一个序列。

具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。

 

示例 1:

输入:s = "abc"
输出:3
解释:三个回文子串: "a", "b", "c"

示例 2:

输入:s = "aaa"
输出:6
解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa"

 

提示:

  • 1 <= s.length <= 1000
  • s 由小写英文字母组成

2、解法1-暴力解法

双指针 ij 枚举所有子串的起点和终点,遍历 ij 区间段内的字符判断其是否回文。

这种方式时间复杂度是 O(n3)O(n^3),复杂度太高,如果数据量再大点可能会超时。其中双指针枚举的时间复杂度是 O(n2)O(n^2),回文判断的时间复杂度是 O(n)O(n)。解法2就是在此基础上优化了回文判断的复杂度。


3、代码

var countSubstrings = function (s) {
function isPal(s, l, r) {
for (let i = l; i < (l + r) / 2; i++) {
if (s[i] !== s[l + r - i - 1]) return false;
}
return true
}

let count = 0;
for (let i = 0; i < s.length; i++) {
for (let j = i + 1; j <= s.length; j++) {
if (isPal(s, i, j)) count++;
}
}

return count;
};

4、复杂度

  • 时间复杂度:O(n3)O(n^3)
  • 空间复杂度:O(1)O(1)

5、执行结果

  • 执行用时: 308 ms
  • 内存消耗: 38.4 MB

6、解法2-双指针

该解法是解法1的优化版本。先使用双指针 ij 枚举所有子串的起点和终点,同时分别按顺序和逆序累加所有遍历过的字符得到字符串 s1s2,判断是否回文只需对 s1s2 判等即可。这里将回文判断的时间复杂度从 O(n)O(n) 优化到 O(1)O(1),但整体空间复杂度从 O(1)O(1) 升到 O(n)O(n),算是空间换时间。


7、代码

var countSubstrings = function (s) {
let count = 0;
for (let i = 0; i < s.length; i++) {
let s1 = '', s2 = '';
for (let j = i; j < s.length; j++) {
s1 += s[j], s2 = s[j] + s2;
if (s1 === s2) count++;
}
}

return count;
};

8、复杂度

  • 时间复杂度:O(n2)O(n^2)
  • 空间复杂度:O(n)O(n)

9、执行结果

  • 执行用时: 196 ms
  • 内存消耗: 44.3 MB

这里的执行用时仍然比较高,是因为字符串拼接属于耗时操作。


10、解法3-中心扩展

枚举所有可能的回文中心 s[i]s[i]、s[i + 1],若回文子串长度为奇数则其中心为 s[i],回文子串长度为偶数则其中心为 s[i]、s[i + 1];以中心向左右两边扩展,即左边界 l 减一右边界 r 加1,如果 s[l]s[r] 相等则回文数加1。


11、代码

var countSubstrings = function (s) {
let count = 0;
for (let i = 0; i < s.length; i++) {
for (let l = i, r = i; l >= 0 && s[l] === s[r]; l--, r++) count++;
for (let l = i, r = i + 1; l >= 0 && s[l] === s[r]; l--, r++) count++;
}
return count;
};

12、复杂度

  • 时间复杂度:O(n2)O(n^2)
  • 空间复杂度:O(1)O(1)

13、执行结果

image.png

· 阅读需 5 分钟

1、题干

给定一个字符串 s ,请计算这个字符串中有多少个回文子字符串。

具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。

 

示例 1:

输入:s = "abc"
输出:3
解释:三个回文子串: "a", "b", "c"

示例 2:

输入:s = "aaa"
输出:6
解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa"

 

提示:

  • 1 <= s.length <= 1000
  • s 由小写英文字母组成

 

注意:本题与主站 70 题相同:https://leetcode-cn.com/problems/palindromic-substrings/ 

2、解法1-暴力解法

双指针 ij 枚举所有子串的起点和终点,遍历 ij 区间段内的字符判断其是否回文。

这种方式时间复杂度是 O(n3)O(n^3),复杂度太高,如果数据量再大点可能会超时。其中双指针枚举的时间复杂度是 O(n2)O(n^2),回文判断的时间复杂度是 O(n)O(n)。解法2就是在此基础上优化了回文判断的复杂度。


3、代码

var countSubstrings = function (s) {
function isPal(s, l, r) {
for (let i = l; i < (l + r) / 2; i++) {
if (s[i] !== s[l + r - i - 1]) return false;
}
return true
}

let count = 0;
for (let i = 0; i < s.length; i++) {
for (let j = i + 1; j <= s.length; j++) {
if (isPal(s, i, j)) count++;
}
}

return count;
};

4、复杂度

  • 时间复杂度:O(n3)O(n^3)
  • 空间复杂度:O(1)O(1)

5、执行结果

  • 执行用时: 308 ms
  • 内存消耗: 38.4 MB

6、解法2-双指针

该解法是解法1的优化版本。先使用双指针 ij 枚举所有子串的起点和终点,同时分别按顺序和逆序累加所有遍历过的字符得到字符串 s1s2,判断是否回文只需对 s1s2 判等即可。这里将回文判断的时间复杂度从 O(n)O(n) 优化到 O(1)O(1),但整体空间复杂度从 O(1)O(1) 升到 O(n)O(n),算是空间换时间。


7、代码

var countSubstrings = function (s) {
let count = 0;
for (let i = 0; i < s.length; i++) {
let s1 = '', s2 = '';
for (let j = i; j < s.length; j++) {
s1 += s[j], s2 = s[j] + s2;
if (s1 === s2) count++;
}
}

return count;
};

8、复杂度

  • 时间复杂度:O(n2)O(n^2)
  • 空间复杂度:O(n)O(n)

9、执行结果

  • 执行用时: 196 ms
  • 内存消耗: 44.3 MB

这里的执行用时仍然比较高,是因为字符串拼接属于耗时操作。


10、解法3-中心扩展

枚举所有可能的回文中心 s[i]s[i]、s[i + 1],若回文子串长度为奇数则其中心为 s[i],回文子串长度为偶数则其中心为 s[i]、s[i + 1];以中心向左右两边扩展,即左边界 l 减一右边界 r 加1,如果 s[l]s[r] 相等则回文数加1。


11、代码

var countSubstrings = function (s) {
let count = 0;
for (let i = 0; i < s.length; i++) {
for (let l = i, r = i; l >= 0 && s[l] === s[r]; l--, r++) count++;
for (let l = i, r = i + 1; l >= 0 && s[l] === s[r]; l--, r++) count++;
}
return count;
};

12、复杂度

  • 时间复杂度:O(n2)O(n^2)
  • 空间复杂度:O(1)O(1)

13、执行结果

image.png

· 阅读需 5 分钟

1、题干

给定一个字符串 s ,请计算这个字符串中有多少个回文子字符串。

具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。

 

示例 1:

输入:s = "abc"
输出:3
解释:三个回文子串: "a", "b", "c"

示例 2:

输入:s = "aaa"
输出:6
解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa"

 

提示:

  • 1 <= s.length <= 1000
  • s 由小写英文字母组成

 

注意:本题与主站 647 题相同:https://leetcode-cn.com/problems/palindromic-substrings/ 

2、解法1-暴力解法

双指针 ij 枚举所有子串的起点和终点,遍历 ij 区间段内的字符判断其是否回文。

这种方式时间复杂度是 O(n3)O(n^3),复杂度太高,如果数据量再大点可能会超时。其中双指针枚举的时间复杂度是 O(n2)O(n^2),回文判断的时间复杂度是 O(n)O(n)。解法2就是在此基础上优化了回文判断的复杂度。


3、代码

var countSubstrings = function (s) {
function isPal(s, l, r) {
for (let i = l; i < (l + r) / 2; i++) {
if (s[i] !== s[l + r - i - 1]) return false;
}
return true
}

let count = 0;
for (let i = 0; i < s.length; i++) {
for (let j = i + 1; j <= s.length; j++) {
if (isPal(s, i, j)) count++;
}
}

return count;
};

4、复杂度

  • 时间复杂度:O(n3)O(n^3)
  • 空间复杂度:O(1)O(1)

5、执行结果

  • 执行用时: 308 ms
  • 内存消耗: 38.4 MB

6、解法2-双指针

该解法是解法1的优化版本。先使用双指针 ij 枚举所有子串的起点和终点,同时分别按顺序和逆序累加所有遍历过的字符得到字符串 s1s2,判断是否回文只需对 s1s2 判等即可。这里将回文判断的时间复杂度从 O(n)O(n) 优化到 O(1)O(1),但整体空间复杂度从 O(1)O(1) 升到 O(n)O(n),算是空间换时间。


7、代码

var countSubstrings = function (s) {
let count = 0;
for (let i = 0; i < s.length; i++) {
let s1 = '', s2 = '';
for (let j = i; j < s.length; j++) {
s1 += s[j], s2 = s[j] + s2;
if (s1 === s2) count++;
}
}

return count;
};

8、复杂度

  • 时间复杂度:O(n2)O(n^2)
  • 空间复杂度:O(n)O(n)

9、执行结果

  • 执行用时: 196 ms
  • 内存消耗: 44.3 MB

这里的执行用时仍然比较高,是因为字符串拼接属于耗时操作。


10、解法3-中心扩展

枚举所有可能的回文中心 s[i]s[i]、s[i + 1],若回文子串长度为奇数则其中心为 s[i],回文子串长度为偶数则其中心为 s[i]、s[i + 1];以中心向左右两边扩展,即左边界 l 减一右边界 r 加1,如果 s[l]s[r] 相等则回文数加1。


11、代码

var countSubstrings = function (s) {
let count = 0;
for (let i = 0; i < s.length; i++) {
for (let l = i, r = i; l >= 0 && s[l] === s[r]; l--, r++) count++;
for (let l = i, r = i + 1; l >= 0 && s[l] === s[r]; l--, r++) count++;
}
return count;
};

12、复杂度

  • 时间复杂度:O(n2)O(n^2)
  • 空间复杂度:O(1)O(1)

13、执行结果

image.png

· 阅读需 8 分钟

1、题干

给你一个下标从 0 开始的二维整数数组 questions ,其中 questions[i] = [pointsi, brainpoweri] 。

这个数组表示一场考试里的一系列题目,你需要 按顺序 (也就是从问题 0 开始依次解决),针对每个问题选择 解决 或者 跳过 操作。解决问题 i 将让你 获得  pointsi 的分数,但是你将 无法 解决接下来的 brainpoweri 个问题(即只能跳过接下来的 brainpoweri 个问题)。如果你跳过问题 i ,你可以对下一个问题决定使用哪种操作。

  • 比方说,给你 questions = [[3, 2], [4, 3], [4, 4], [2, 5]] :
    • 如果问题 0 被解决了, 那么你可以获得 3 分,但你不能解决问题 1 和 2 。
    • 如果你跳过问题 0 ,且解决问题 1 ,你将获得 4 分但是不能解决问题 2 和 3 。

请你返回这场考试里你能获得的 最高 分数。

 

示例 1:

输入:questions = [[3,2],[4,3],[4,4],[2,5]]
输出:5
解释:解决问题 0 和 3 得到最高分。
- 解决问题 0 :获得 3 分,但接下来 2 个问题都不能解决。
- 不能解决问题 1 和 2
- 解决问题 3 :获得 2 分
总得分为:3 + 2 = 5 。没有别的办法获得 5 分或者多于 5 分。

示例 2:

输入:questions = [[1,1],[2,2],[3,3],[4,4],[5,5]]
输出:7
解释:解决问题 1 和 4 得到最高分。
- 跳过问题 0
- 解决问题 1 :获得 2 分,但接下来 2 个问题都不能解决。
- 不能解决问题 2 和 3
- 解决问题 4 :获得 5 分
总得分为:2 + 5 = 7 。没有别的办法获得 7 分或者多于 7 分。

 

提示:

  • 1 <= questions.length <= 105
  • questions[i].length == 2
  • 1 <= pointsi, brainpoweri <= 105

动态规划没学明白,周赛坑了一小时没写出来,可能我智力有问题吧

13375743a4d4704fbb667f6be417fd85.gif

2、解法1-记忆化搜索

  • 每个问题都有解决和跳过两种选择
  • 对于任意问题 ii 如果解决则得分叠加下一道可解的题即 dfs(i + 1 + questions[i][1]) + questions[i][0])
  • 如果跳过则得分与下一道题相同即 dfs(i + 1)
  • 题目要求最高分数因此取二者的最大值,如此深度遍历下去直至结束
  • 由于每个问题都有2种状态,因此时间复杂度接近 O(2n)O(2^n),为了避免超时使用哈希表记录已搜索过的题目后续直接返回

一般动态规划的题都能用暴力穷举(DFS、BFS、回溯等)解决,如果实在想不到动态规划的解题思路,可以尝试暴力穷举+记忆化。


3、复杂度

  • 时间复杂度:不使用记忆化时为 O(2n)O(2^n),使用记忆化后无法确定
  • 空间复杂度:O(n)O(n)

4、代码

var mostPoints = function (questions) {
const n = questions.length, visited = new Map();
function dfs(idx) {
if (idx > n - 1) return 0;
if (visited.has(idx)) return visited.get(idx);
const [score, skip] = questions[idx];
return visited.set(idx, Math.max(dfs(idx + 1), dfs(idx + 1 + skip) + score)).get(idx);
}
return dfs(0);
};

5、执行结果

执行用时: 360 ms 内存消耗: 91.8 MB


6、解法2-顺序DP

  • 对于任意问题 ii 而言 dp[i]dp[i] 表示完成 [0,i][0,i] 题能获得的最高分,另外每个问题都有解决和跳过两种选择
  • 如果解决问题 ii 则其得分 dp[i]+questions[i][0]dp[i] + questions[i][0] 可以转移给下一道可解的题 jj 使用(其中 j=questions[i][1]+i+1j = questions[i][1] + i + 1),转移时 dp[j]dp[j] 取二者最大值即 dp[j]=Math.max(dp[i]+questions[i][0],dp[j])dp[j] = Math.max(dp[i] + questions[i][0], dp[j])
  • 如果跳过问题 ii 则其得分 dp[i]dp[i] 可以转移给下一道题 i+1i + 1 使用,转移时 dp[i+1]dp[i + 1] 取二者最大值即 dp[i+1]=Math.max(dp[i],dp[i+1]);dp[i + 1] = Math.max(dp[i], dp[i + 1]);

注意:\ 一般动态规划按以上思路遍历下去最终返回 dpdp 数组末尾元素即可,但这里会出现一种特殊情况:最高分在 dpdp 数组中的索引可能大于 questions.lengthquestions.length。 例如用例:[[25,1],[44,3],[91,1],[12,3],[93,3]] 的长度为5,而最高分出现在 dp[8]dp[8]

因此使用变量 max 在动态规划过程中记录所有得分中的最高分并返回,最后AC了。

所以最后注意动态规划过程中处理好最高得分数组越界的问题就能AC


7、复杂度

  • 时间复杂度:O(n)O(n)
  • 空间复杂度:O(n)O(n)

8、代码

var mostPoints = function (questions) {
const n = questions.length, dp = new Array(n).fill(0);
let max = 0;
for (let i = 0; i < n; i++) {
dp[i + 1] = Math.max(dp[i], dp[i + 1] || 0);
const j = questions[i][1] + i + 1;
dp[j] = Math.max(dp[i] + questions[i][0], dp[j] || 0);
max = Math.max(dp[i + 1], dp[j], max);
}
return max;
};

9、执行结果

执行用时: 280 ms 内存消耗: 69.8 MB


10、解法3-逆序DP

看了些资料才发现,逆序DP也是一种解题方法,并不是特例

  • 逆序DP中状态转移是相反的,即将靠后问题的得分转移给前面的问题,对于任意问题 ii 而言 dp[i]dp[i] 表示完成 [i,n1][i,n-1] 题能获得的最高分
  • 如果选择跳过问题 ii,则将下一题得分 dp[i+1]dp[i + 1] 转移给 dp[i]dp[i]
  • 如果选择解决问题 ii,则将下一可解题得分 dp[questions[i][1]+i+1]dp[questions[i][1] + i + 1] 转移给 dp[i]dp[i],并叠加当前题得分 questions[i][0]questions[i][0]
  • 依次处理所有问题,最后返回 dp[0]dp[0]

11、复杂度

  • 时间复杂度:O(n)O(n)
  • 空间复杂度:O(n)O(n)

12、代码

var mostPoints = function (questions) {
const n = questions.length, dp = new Array(n).fill(0);
for (let i = n - 1; i > -1; i--) {
dp[i] = Math.max(dp[i + 1] || 0, questions[i][0] + (dp[questions[i][1] + i + 1] || 0));
}
return dp[0];
};

13、执行结果

执行用时: 176 ms 内存消耗: 65 MB

· 阅读需 4 分钟

1、题干

给你一个整数 n,请你帮忙统计一下我们可以按下述规则形成多少个长度为 n 的字符串:

  • 字符串中的每个字符都应当是小写元音字母('a', 'e', 'i', 'o', 'u'
  • 每个元音 'a' 后面都只能跟着 'e'
  • 每个元音 'e' 后面只能跟着 'a' 或者是 'i'
  • 每个元音 'i' 后面 不能 再跟着另一个 'i'
  • 每个元音 'o' 后面只能跟着 'i' 或者是 'u'
  • 每个元音 'u' 后面只能跟着 'a'

由于答案可能会很大,所以请你返回 模 10^9 + 7 之后的结果。

 

示例 1:

输入:n = 1
输出:5
解释:所有可能的字符串分别是:"a", "e", "i" , "o" 和 "u"。

示例 2:

输入:n = 2
输出:10
解释:所有可能的字符串分别是:"ae", "ea", "ei", "ia", "ie", "io", "iu", "oi", "ou" 和 "ua"。

示例 3:

输入:n = 5
输出:68

 

提示:

  • 1 <= n <= 2 * 10^4

2、解题思路

根据题意可以列举出 字符串长度跟结果数的关系

  • 1个字母时只有5种情况:"a", "e", "i" , "o" , "u"
  • 2个字母时有10种情况:"ae", "ea", "ei", "ia", "ie", "io", "iu", "oi", "ou" , "ua"

实际上 结果数就是尾字母的个数

  • 1个字母时尾字母个数:"a" * 1, "e" * 1, "i" * 1 , "o" * 1 , "u" * 1,总数为5
  • 2个字母时尾字母个数:"a" * 3, "e" * 2, "i" * 2 , "o" * 1 , "u" * 2,总数为10

因此,使用数组 c 按字典序存储尾字母 "a", "e", "i" , "o" , "u" 的数量,然后根据题中给出的字母组合关系,可以推导出下一轮的尾字母数量 nc : nc = [c[1] + c[2] + c[4], c[0] + c[2], c[1] + c[3], c[2], c[2] + c[3]]


根据递推公式计算出2个字符以内的情况符合正确结果:

  • 1个字母时尾字母个数为:[1, 1, 1, 1, 1]
  • 2个字母时尾字母个数为:[3, 2, 2, 1, 2]

3、代码

var countVowelPermutation = function (n) {
const MOD = 1e9 + 7;
let c = [1, 1, 1, 1, 1];

for (let i = 2; i <= n; i++) {
c = [(c[1] + c[2] + c[4]) % MOD, (c[0] + c[2]) % MOD, (c[1] + c[3]) % MOD, c[2] % MOD, (c[2] + c[3]) % MOD];
}

return c.reduce((a, k) => a + k, 0) % MOD;
};

4、复杂度

  • 时间复杂度:O(n)O(n)
  • 空间复杂度:O(1)O(1)

5、执行结果

image.png

· 阅读需 4 分钟

1、题干

路径 被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。

路径和 是路径中各节点值的总和。

给定一个二叉树的根节点 root ,返回其 最大路径和,即所有路径上节点值之和的最大值。

 

示例 1:

输入:root = [1,2,3]
输出:6
解释:最优路径是 2 -> 1 -> 3 ,路径和为 2 + 1 + 3 = 6

示例 2:

输入:root = [-10,9,20,null,null,15,7]
输出:42
解释:最优路径是 15 -> 20 -> 7 ,路径和为 15 + 20 + 7 = 42

 

提示:

  • 树中节点数目范围是 [1, 3 * 104]
  • -1000 <= Node.val <= 1000

 

注意:本题与主站 124 题相同: https://leetcode-cn.com/problems/binary-tree-maximum-path-sum/

2、解题思路

深度遍历树上所有节点,对每个节点求最大路径和即可。

3、代码

var maxPathSum = function (root) {
let res = root.val;
function dfs(node) {
if (!node) return -Infinity;
const maxSum1 = dfs(node.left), maxSum2 = dfs(node.right);
const maxSum = Math.max(node.val, maxSum1 + node.val, maxSum2 + node.val);
res = Math.max(res, maxSum, maxSum1, maxSum2, maxSum1 + maxSum2 + node.val);
return maxSum;
}
return dfs(root), res;
};

关键代码说明

  • 求每个节点的最大路径和,且路径不重复的关键
    • const maxSum = Math.max(node.val, maxSum1 + node.val, maxSum2 + node.val);
    • 这行代码用于求当前节点下的最大路径和maxSum,并将其返回给父节点用于计算父节点下的最大路径和
    • Math.max取最大值时包含当前节点的值、当前节点值+左子节点最大路径和、当前节点值+右子节点最大路径和
    • 即最大路径和一定要包含当前节点的值,还可能叠加某个子节点最大路径和,也可能不叠加子节点最大路径和
    • 因此dfs函数逐级返回时,由下至上形成了一条不分叉的路径,并能逐级向上返回子节点的最大路径和
  • 求整棵树最大路径和res的关键
    • res = Math.max(res, maxSum, maxSum1, maxSum2, maxSum1 + maxSum2 + node.val);
    • 在每个节点求最大路径和时,res取其自身以及maxSum1maxSum2node.val三者任意组合的路径和中的最大值
    • maxSum1maxSum2node.val三者任意组合求和时,至少取1个,且取2个时不能是maxSum1 + maxSum2,因为这个组合不构成路径

4、执行结果

1.png

· 阅读需 4 分钟

1、题干

路径 被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。

路径和 是路径中各节点值的总和。

给定一个二叉树的根节点 root ,返回其 最大路径和,即所有路径上节点值之和的最大值。

 

示例 1:

输入:root = [1,2,3]
输出:6
解释:最优路径是 2 -> 1 -> 3 ,路径和为 2 + 1 + 3 = 6

示例 2:

输入:root = [-10,9,20,null,null,15,7]
输出:42
解释:最优路径是 15 -> 20 -> 7 ,路径和为 15 + 20 + 7 = 42

 

提示:

  • 树中节点数目范围是 [1, 3 * 104]
  • -1000 <= Node.val <= 1000

 

注意:本题与主站 124 题相同: https://leetcode-cn.com/problems/binary-tree-maximum-path-sum/

2、解题思路

深度遍历树上所有节点,对每个节点求最大路径和即可。

3、代码

var maxPathSum = function (root) {
let res = root.val;
function dfs(node) {
if (!node) return -Infinity;
const maxSum1 = dfs(node.left), maxSum2 = dfs(node.right);
const maxSum = Math.max(node.val, maxSum1 + node.val, maxSum2 + node.val);
res = Math.max(res, maxSum, maxSum1, maxSum2, maxSum1 + maxSum2 + node.val);
return maxSum;
}
return dfs(root), res;
};

关键代码说明

  • 求每个节点的最大路径和,且路径不重复的关键
    • const maxSum = Math.max(node.val, maxSum1 + node.val, maxSum2 + node.val);
    • 这行代码用于求当前节点下的最大路径和maxSum,并将其返回给父节点用于计算父节点下的最大路径和
    • Math.max取最大值时包含当前节点的值、当前节点值+左子节点最大路径和、当前节点值+右子节点最大路径和
    • 即最大路径和一定要包含当前节点的值,还可能叠加某个子节点最大路径和,也可能不叠加子节点最大路径和
    • 因此dfs函数逐级返回时,由下至上形成了一条不分叉的路径,并能逐级向上返回子节点的最大路径和
  • 求整棵树最大路径和res的关键
    • res = Math.max(res, maxSum, maxSum1, maxSum2, maxSum1 + maxSum2 + node.val);
    • 在每个节点求最大路径和时,res取其自身以及maxSum1maxSum2node.val三者任意组合的路径和中的最大值
    • maxSum1maxSum2node.val三者任意组合求和时,至少取1个,且取2个时不能是maxSum1 + maxSum2,因为这个组合不构成路径

4、执行结果

1.png

· 阅读需 4 分钟

1、题干

给你一个 不含重复 单词的字符串数组 words ,请你找出并返回 words 中的所有 连接词

连接词 定义为:一个完全由给定数组中的至少两个较短单词(不一定是不同的两个单词)组成的字符串。

 

示例 1:

输入:words = ["cat","cats","catsdogcats","dog","dogcatsdog","hippopotamuses","rat","ratcatdogcat"]
输出:["catsdogcats","dogcatsdog","ratcatdogcat"]
解释:"catsdogcats" 由 "cats", "dog" 和 "cats" 组成;
"dogcatsdog" 由 "dog", "cats" 和 "dog" 组成;
"ratcatdogcat" 由 "rat", "cat", "dog" 和 "cat" 组成。

示例 2:

输入:words = ["cat","dog","catdog"]
输出:["catdog"]

 

提示:

  • 1 <= words.length <= 104
  • 1 <= words[i].length <= 30
  • words[i] 仅由小写英文字母组成。 
  • words 中的所有字符串都是 唯一 的。
  • 1 <= sum(words[i].length) <= 105

常规解法用字典树处理,详情可以看官解。 补充:这个解法有漏洞,调整用例肯定还会超时

2、解题思路

这里采用一种非常规解法,创建哈希集合set并加入所有单词,然后遍历所有单词并判断该单词是否由set中的元素组成,判断过程是利用递归对每个单词进行分段,如果set包含所有分段后的子串则说明其是连接词。

然后写出了下面这段代码,结果通过用例43/44,最后一个用例超时。主要是因为这个用例的特殊性,导致递归检查函数的执行次数暴涨。

var findAllConcatenatedWordsInADict = function (words) {
const set = new Set(words);
if (set.has("")) set.delete("");

function dfs(word, start) {
let s = '';
for (let i = start; i < word.length; i++) {
s += word[i];
if (set.has(s) && dfs(word, i + 1)) return true;
}
return set.has(s) && start;
}

return words.reduce((acc, cur) => (dfs(cur, 0) && acc.push(cur), acc), []);
};

由于该用例的特殊性,尝试添加预检策略绕过一些特殊用例:

  • 1、字符串数组按长度升序排序,由于长单词肯定由短单词组成,意味着如果前面的短单词无法组合出当前单词的特征,就说明其不是连接词
  • 2、预检函数,取出单词中所有连续的两个字符(最短连接子串),检查更短的单词是否包含或能组合出这个最短连接子串,如果为否则说明其不是连接词

3、完整代码

var findAllConcatenatedWordsInADict = function (words) {
words.sort((a, b) => a.length - b.length);
const set = new Set(words);
if (set.has('')) set.delete('');

function dfs(word, start) {
let s = '';
for (let i = start; i < word.length; i++) {
s += word[i];
if (set.has(s) && dfs(word, i + 1)) return true;
}
return set.has(s) && start;
}

const headSet = new Set(), tailSet = new Set(), compSet = new Set();
function preCheck(word) {
let found = true, comps = [];
for (let i = 0; i < word.length - 1; i++) {
const s = word[i] + word[i + 1];
if (!compSet.has(s) && !(tailSet.has(s[0]) && headSet.has(s[1]))) found = false;
comps.push(s);
}
headSet.add(word[0]), tailSet.add(word[word.length - 1]);
for (const c of comps) compSet.add(c);
return found;
}

return words.reduce((acc, cur) => {
if (!preCheck(cur)) return acc;
if (dfs(cur, 0)) acc.push(cur);
return acc;
}, []);
};

4、执行结果

1.png

· 阅读需 2 分钟

1、题干

给你一个整数数组 arr 和一个整数 difference,请你找出并返回 arr 中最长等差子序列的长度,该子序列中相邻元素之间的差等于 difference

子序列 是指在不改变其余元素顺序的情况下,通过删除一些元素或不删除任何元素而从 arr 派生出来的序列。

 

示例 1:

输入:arr = [1,2,3,4], difference = 1
输出:4
解释:最长的等差子序列是 [1,2,3,4]。

示例 2:

输入:arr = [1,3,5,7], difference = 1
输出:1
解释:最长的等差子序列是任意单个元素。

示例 3:

输入:arr = [1,5,7,8,5,3,4,2,1], difference = -2
输出:4
解释:最长的等差子序列是 [7,5,3,1]。

 

提示:

  • 1 <= arr.length <= 105
  • -104 <= arr[i], difference <= 104

头回见到用哈希表做dp table,记录下。

代码

/**
* @param {number[]} arr
* @param {number} d
* @return {number}
*/
var longestSubsequence = function (arr, d) {
let max = 1;
const dp = {};
for (let n of arr) {
dp[n] = (dp[n - d] || 0) + 1;
max = Math.max(max,dp[n]);
}

return max;
};

· 阅读需 4 分钟

1、题干

有些数的素因子只有 3,5,7,请设计一个算法找出第 k 个数。注意,不是必须有这些素因子,而是必须不包含其他的素因子。例如,前几个数按顺序应该是 1,3,5,7,9,15,21。

示例 1:

输入: k = 5

输出: 9

1、解题思路

一开始没想到动态规划、三指针、步进法这些方案,看了题解还是觉得这些规律或方案有点难想到。我的第一反应是:这个问题好像可以用BFS+Set去重来解决。

1.1、转换思路

根据这个方向,实际上可以把问题转换成:已知一棵树的根是1,第一层节点是根节点与 3、5、7相乘的结果,第二层节点是第一层节点与 3、5、7相乘的结果,以此类推; 求这棵树中按广度遍历的第k个子节点的值。于是写下了这段代码:

var getKthMagicNumber = function(k) {
let nums = [1];

while (nums.length) {
const set = new Set();

for (let i = 0; i < nums.length; i++) {
k--;
if (!k) return nums[i];
set.add(3 * nums[i]);
set.add(5 * nums[i]);
set.add(7 * nums[i]);
}

nums = [...set];
}
};

1.2、测试出错

写完之后测试出错,是因为忽略了一个很重要的条件:按顺序排列。在上述代码中用于遍历树的 nums 代表的是每一层节点,它始终是单调递增的。虽然每一层节点都单调递增,但是随着层数增加,下一层的节点却有可能小于上一层节点。

1.3、解决问题

要解决这个问题也不难,只要把所有节点都放入 nums 中,并且保证它的单调性就OK了。

1.4、整体方案

因此,使用BFS + Set去重 + 单调栈的方式就能解出这道题,问题的关键点有以下3个:

  • 使用非递归的BFS方式进行遍历
  • 使用Set去除重复的数字
  • 使用单调栈保证数字升序排列

2、代码

var getKthMagicNumber = function (k) {
const nums = [1];
const set = new Set();

for (let i = 0; i < k; i++) {
if (i + 1 === k) return nums[i];
const arr = [3 * nums[i], 5 * nums[i], 7 * nums[i]];

arr.forEach(n => {
if (set.has(n)) return;
set.add(n);

if (n >= nums[nums.length - 1]) return nums.push(n);

// 以上代码是常规逻辑:BFS + Set去重
// 以下代码是为了保证 nums 的单调性
const arr = [];
while (n < nums[nums.length - 1]) {
arr.unshift(nums.pop());
}
nums.push(n, ...arr);
});
}
};