跳到主要内容

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

查看所有标签

· 阅读需 2 分钟

1、题干

正整数 n 代表生成括号的对数,请设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。

 

示例 1:

输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]

示例 2:

输入:n = 1
输出:["()"]

 

提示:

  • 1 <= n <= 8

 

注意:本题与主站 22 题相同: https://leetcode-cn.com/problems/generate-parentheses/

2、解题思路

总体思路是:从 n-1 推导出 n 的组合情况,只需要遍历 n-1 的所有组合,并在所有组合的每个位置填入一对括号 () 并去重即可。


3、举个例子

  • n=1时,组合情况仅一种:["()"]
  • n=2
    • 遍历 n=1 的组合情况 ["()"]
    • 对于情况 "()",在该字符串每个位置填入一对括号 () 后得到:["()()","(())","()()"]
    • 去重得到最终组合情况为:["()()","(())"]
  • n=3
    • 遍历 n=2 的组合情况 ["()()","(())"]
    • 对于情况 "()()",在每个位置填入一对括号 () 后得到:["()()()","(())()","()()()","()(())","()()()"]
    • 对于情况 "(())",在每个位置填入一对括号 () 后得到:["()(())","(()())","((()))","(()())","(())()"]
    • 去重得到最终组合情况为:["()()()","(())()","()(())","(()())","((()))"]

4、代码

var generateParenthesis = function (n) {
let set = new Set(['()']);
for (let c = 2; c <= n; c++) {
let nextSet = new Set();
for (const s of set) {
for (let j = 0; j <= s.length; j++) {
nextSet.add(s.slice(0, j) + '()' + s.slice(j));
}
}
set = nextSet;
}
return [...set];
};

5、执行结果

image.png

· 阅读需 2 分钟

1、题干

正整数 n 代表生成括号的对数,请设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。

 

示例 1:

输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]

示例 2:

输入:n = 1
输出:["()"]

 

提示:

  • 1 <= n <= 8

 

注意:本题与主站 22 题相同: https://leetcode-cn.com/problems/generate-parentheses/

2、解题思路

总体思路是:从 n-1 推导出 n 的组合情况,只需要遍历 n-1 的所有组合,并在所有组合的每个位置填入一对括号 () 并去重即可。


3、举个例子

  • n=1时,组合情况仅一种:["()"]
  • n=2
    • 遍历 n=1 的组合情况 ["()"]
    • 对于情况 "()",在该字符串每个位置填入一对括号 () 后得到:["()()","(())","()()"]
    • 去重得到最终组合情况为:["()()","(())"]
  • n=3
    • 遍历 n=2 的组合情况 ["()()","(())"]
    • 对于情况 "()()",在每个位置填入一对括号 () 后得到:["()()()","(())()","()()()","()(())","()()()"]
    • 对于情况 "(())",在每个位置填入一对括号 () 后得到:["()(())","(()())","((()))","(()())","(())()"]
    • 去重得到最终组合情况为:["()()()","(())()","()(())","(()())","((()))"]

4、代码

var generateParenthesis = function (n) {
let set = new Set(['()']);
for (let c = 2; c <= n; c++) {
let nextSet = new Set();
for (const s of set) {
for (let j = 0; j <= s.length; j++) {
nextSet.add(s.slice(0, j) + '()' + s.slice(j));
}
}
set = nextSet;
}
return [...set];
};

5、执行结果

image.png

· 阅读需 9 分钟

1、题干

给定三个字符串 s1s2s3,请你帮忙验证 s3 是否是由 s1 和 s2 交错 组成的。

两个字符串 st 交错 的定义与过程如下,其中每个字符串都会被分割成若干 非空 子字符串:

  • s = s1 + s2 + ... + sn
  • t = t1 + t2 + ... + tm
  • |n - m| <= 1
  • 交错s1 + t1 + s2 + t2 + s3 + t3 + ... 或者 t1 + s1 + t2 + s2 + t3 + s3 + ...

注意:a + b 意味着字符串 ab 连接。

 

示例 1:

输入:s1 = "aabcc", s2 = "dbbca", s3 = "aadbbcbcac"
输出:true

示例 2:

输入:s1 = "aabcc", s2 = "dbbca", s3 = "aadbbbaccc"
输出:false

示例 3:

输入:s1 = "", s2 = "", s3 = ""
输出:true

 

提示:

  • 0 <= s1.length, s2.length <= 100
  • 0 <= s3.length <= 200
  • s1s2、和 s3 都由小写英文字母组成

 

进阶:您能否仅使用 O(s2.length) 额外的内存空间来解决它?

要通过遍历解题需要用到3个指针,分别对应字符串 s1s2s3。官解说双指针不可行应该只考虑了 s1s2 的指针且没有考虑指针重置的情况,实际只要指针可以重置就能求出正解,详情见解法2。


2、解法1-回溯+记忆化

回溯过程可以理解为同时按字符串 s1s2s3 步进,若 s1 当前字符与 s3 当前字符相等则 s1s3 的下标同时加1,若 s2 当前字符与 s3 当前字符相等则 s2s3 的下标同时加1,若最终3个字符串能走到字符串末尾说明结果为 true


其中记忆化是将已访问过的路径使用哈希集合存储,若后续访问到已访问过的路径,则直接退出当前递归。


3、代码

var isInterleave = function (s1, s2, s3) {
if (s1.length + s2.length !== s3.length) return false;

const visited = new Set();
let res = false;
function backtrace(i1, i2, i3) {
if (i1 === s1.length && i2 === s2.length && i3 === s3.length) return res = true;
if (visited.has(`${i1}-${i2}-${i3}`)) return false;
visited.add(`${i1}-${i2}-${i3}`);

if (s1[i1] === s3[i3]) backtrace(i1 + 1, i2, i3 + 1);
if (s2[i2] === s3[i3]) backtrace(i1, i2 + 1, i3 + 1);
}

backtrace(0, 0, 0);

return res;
};

4、执行结果

image.png


5、解法2-三指针+模拟回溯+记忆化

本质上也是回溯+记忆化,只是使用三指针遍历模拟回溯过程,这是个非常规解法随便看看就好。


三指针遍历模拟回溯的大体方案是:遍历 s3,若遇到 s1s2 都可选的情况则选择 s1,并使用栈存储当前3个指针作为快照;若后续遍历无法继续下去且栈不为空则弹出栈顶快照,重置3个指针并选择 s2 继续遍历;若后续遍历无法继续且栈为空,则结果为 false


其中记忆化是将已访问过的路径使用哈希集合存储,若后续访问到已访问过的路径则看栈是否为空,栈为空则直接退出当前递归,栈不为空则弹出栈顶快照并重置指针。


6、代码

var isInterleave = function (s1, s2, s3) {
if (s1.length + s2.length !== s3.length) return false;

let i1 = -1, i2 = -1, useStack = false;
const stack = [], visited = new Set();

for (let i = 0; i < s3.length; i++) {
if (s1[i1 + 1] === s3[i] && s2[i2 + 1] === s3[i]) {
if (!useStack) stack.push([i, i1, i2]), i1 += 1;
else i2 += 1, useStack = false;
}
else if (s1[i1 + 1] === s3[i]) i1 += 1;
else if (s2[i2 + 1] === s3[i]) i2 += 1;
else {
if (!stack.length) return false;
[i, i1, i2] = stack.pop(), i -= 1, useStack = true;
continue;
}

if (visited.has(`${i}-${i1}-${i2}`)) {
if (!stack.length) return false;
[i, i1, i2] = stack.pop(), i -= 1, useStack = true;
}
else visited.add(`${i}-${i1}-${i2}`);
}

return true;
};

7、执行结果

  • 执行用时: 72 ms
  • 内存消耗: 45.4 MB

8、解法3-BFS+去重

这里使用非递归BFS解题,大体思路是:外层遍历字符串 s3,内层遍历当前可选择的所有 s1s2 的下标组合,并且把下一层可选择的所有 s1s2 的下标组合添加到下一层的队列中,如此遍历所有可行组合直至结束,若最终队列不为空则结果为 true


其中添加下标组合到队列中时一定要去重,否则会超时或内存溢出,去重可以使用双层 map 或者字符串哈希集合。


9、代码

// BFS+双层map去重
var isInterleave = function (s1, s2, s3) {
if (s1.length + s2.length !== s3.length) return false;

let choices = [[-1, -1]];
for (let i = 0; i < s3.length; i++) {
const nextChoices = [], map = {};
for (const [i1, i2] of choices) {
if (s3[i] === s1[i1 + 1] && (!map[i1 + 1] || !map[i1 + 1][i2])) {
nextChoices.push([i1 + 1, i2]);
map[i1 + 1] ? (map[i1 + 1][i2] = 1) : (map[i1 + 1] = { [i2]: 1 });
}
if (s3[i] === s2[i2 + 1] && (!map[i1] || !map[i1][i2 + 1])) {
nextChoices.push([i1, i2 + 1]);
map[i1] ? (map[i1][i2 + 1] = 1) : (map[i1] = { [i2 + 1]: 1 });
}
}
choices = nextChoices;
}

return choices.length > 0;
};

// BFS+字符串哈希集合去重
var isInterleave = function (s1, s2, s3) {
if (s1.length + s2.length !== s3.length) return false;

let choices = [[-1, -1]];
for (let i = 0; i < s3.length; i++) {
const nextChoices = [], set = new Set();
for (const [i1, i2] of choices) {
if (s3[i] === s1[i1 + 1] && !set.has(`${i1 + 1},${i2}`)) {
nextChoices.push([i1 + 1, i2]);
set.add(`${i1 + 1},${i2}`);
}
if (s3[i] === s2[i2 + 1] && !set.has(`${i1},${i2 + 1}`)) {
nextChoices.push([i1, i2 + 1]);
set.add(`${i1},${i2 + 1}`);
}
}
choices = nextChoices;
}

return choices.length > 0;
};

10、执行结果

  • 执行用时: 72 ms
  • 内存消耗: 48.3 MB

11、解法4-动态规划

官解和其他优秀题解对于动态规划解法已经说得很清楚了,这里就贴下代码和执行结果


12、代码

var isInterleave = function (s1, s2, s3) {
if (s1.length + s2.length !== s3.length) return false;

const dp = new Array(s1.length + 1).fill(0).map(() => new Array(s2.length + 1).fill(true));
for (let i = 1; i < dp.length; i++) dp[i][0] = dp[i - 1][0] && s3[i - 1] === s1[i - 1];
for (let j = 1; j < dp[0].length; j++) dp[0][j] = dp[0][j - 1] && s3[j - 1] === s2[j - 1];

for (let i = 1; i < dp.length; i++) {
for (let j = 1; j < dp[i].length; j++) {
dp[i][j] = (dp[i - 1][j] && s3[i + j - 1] === s1[i - 1]) || (dp[i][j - 1] && s3[i + j - 1] === s2[j - 1]);
}
}

return dp[s1.length][s2.length];
};

13、执行结果

  • 执行用时: 92 ms
  • 内存消耗: 47.7 MB

· 阅读需 8 分钟

1、题干

给定三个字符串 s1s2s3,请判断 s3 能不能由 s1 和 s2 交织(交错) 组成。

两个字符串 st 交织 的定义与过程如下,其中每个字符串都会被分割成若干 非空 子字符串:

  • s = s1 + s2 + ... + sn
  • t = t1 + t2 + ... + tm
  • |n - m| <= 1
  • 交织s1 + t1 + s2 + t2 + s3 + t3 + ... 或者 t1 + s1 + t2 + s2 + t3 + s3 + ...

提示:a + b 意味着字符串 ab 连接。

 

示例 1:

输入:s1 = "aabcc", s2 = "dbbca", s3 = "aadbbcbcac"
输出:true

示例 2:

输入:s1 = "aabcc", s2 = "dbbca", s3 = "aadbbbaccc"
输出:false

示例 3:

输入:s1 = "", s2 = "", s3 = ""
输出:true

 

提示:

  • 0 <= s1.length, s2.length <= 100
  • 0 <= s3.length <= 200
  • s1s2、和 s3 都由小写英文字母组成

 

注意:本题与主站 97 题相同: https://leetcode-cn.com/problems/interleaving-string/

遍历解题需要用到3个指针,分别对应字符串 s1s2s3。官解说双指针不可行应该只考虑了 s1s2 的指针且没有考虑指针重置的情况,实际可以借助指针重置进行解题,详情见解法2。


2、解法1-回溯+记忆化

回溯过程可以理解为同时按字符串 s1s2s3 步进,若 s1 当前字符与 s3 当前字符相等则 s1s3 的下标同时加1,若 s2 当前字符与 s3 当前字符相等则 s2s3 的下标同时加1,若最终3个字符串能走到字符串末尾说明结果为 true


其中记忆化是将已访问过的路径使用哈希集合存储,若后续访问到已访问过的路径,则直接退出当前递归。


3、代码

var isInterleave = function (s1, s2, s3) {
if (s1.length + s2.length !== s3.length) return false;

const visited = new Set();
let res = false;
function backtrace(i1, i2, i3) {
if (i1 === s1.length && i2 === s2.length && i3 === s3.length) return res = true;
if (visited.has(`${i1}-${i2}-${i3}`)) return false;
visited.add(`${i1}-${i2}-${i3}`);

if (s1[i1] === s3[i3]) backtrace(i1 + 1, i2, i3 + 1);
if (s2[i2] === s3[i3]) backtrace(i1, i2 + 1, i3 + 1);
}

backtrace(0, 0, 0);

return res;
};

4、执行结果

image.png


5、解法2-三指针+模拟回溯+记忆化

本质上也是回溯+记忆化,只是使用三指针遍历模拟回溯过程,这是个非常规解法随便看看就好。


三指针遍历模拟回溯的大体方案是:遍历 s3,若遇到 s1s2 都可选的情况则选择 s1,并使用栈存储当前3个指针作为快照;若后续遍历无法继续下去且栈不为空则弹出栈顶快照,重置3个指针并选择 s2 继续遍历;若后续遍历无法继续且栈为空,则结果为 false


其中记忆化是将已访问过的路径使用哈希集合存储,若后续访问到已访问过的路径则看栈是否为空,栈为空则直接退出当前递归,栈不为空则弹出栈顶快照并重置指针。


6、代码

var isInterleave = function (s1, s2, s3) {
if (s1.length + s2.length !== s3.length) return false;

let i1 = -1, i2 = -1, useStack = false;
const stack = [], visited = new Set();

for (let i = 0; i < s3.length; i++) {
if (s1[i1 + 1] === s3[i] && s2[i2 + 1] === s3[i]) {
if (!useStack) stack.push([i, i1, i2]), i1 += 1;
else i2 += 1, useStack = false;
}
else if (s1[i1 + 1] === s3[i]) i1 += 1;
else if (s2[i2 + 1] === s3[i]) i2 += 1;
else {
if (!stack.length) return false;
[i, i1, i2] = stack.pop(), i -= 1, useStack = true;
continue;
}

if (visited.has(`${i}-${i1}-${i2}`)) {
if (!stack.length) return false;
[i, i1, i2] = stack.pop(), i -= 1, useStack = true;
}
else visited.add(`${i}-${i1}-${i2}`);
}

return true;
};

7、执行结果

  • 执行用时: 72 ms
  • 内存消耗: 45.4 MB

8、解法3-BFS+去重

这里使用非递归BFS解题,大体思路是:外层遍历字符串 s3,内层遍历当前可选择的所有 s1s2 的下标组合,并且把下一层可选择的所有 s1s2 的下标组合添加到下一层的队列中,如此遍历所有可行组合直至结束,若最终队列不为空则结果为 true


其中添加下标组合到队列中时一定要去重,否则会超时或内存溢出,去重可以使用双层 map 或者字符串哈希集合。


9、代码

// BFS+双层map去重
var isInterleave = function (s1, s2, s3) {
if (s1.length + s2.length !== s3.length) return false;

let choices = [[-1, -1]];
for (let i = 0; i < s3.length; i++) {
const nextChoices = [], map = {};
for (const [i1, i2] of choices) {
if (s3[i] === s1[i1 + 1] && (!map[i1 + 1] || !map[i1 + 1][i2])) {
nextChoices.push([i1 + 1, i2]);
map[i1 + 1] ? (map[i1 + 1][i2] = 1) : (map[i1 + 1] = { [i2]: 1 });
}
if (s3[i] === s2[i2 + 1] && (!map[i1] || !map[i1][i2 + 1])) {
nextChoices.push([i1, i2 + 1]);
map[i1] ? (map[i1][i2 + 1] = 1) : (map[i1] = { [i2 + 1]: 1 });
}
}
choices = nextChoices;
}

return choices.length > 0;
};

// BFS+字符串哈希集合去重
var isInterleave = function (s1, s2, s3) {
if (s1.length + s2.length !== s3.length) return false;

let choices = [[-1, -1]];
for (let i = 0; i < s3.length; i++) {
const nextChoices = [], set = new Set();
for (const [i1, i2] of choices) {
if (s3[i] === s1[i1 + 1] && !set.has(`${i1 + 1},${i2}`)) {
nextChoices.push([i1 + 1, i2]);
set.add(`${i1 + 1},${i2}`);
}
if (s3[i] === s2[i2 + 1] && !set.has(`${i1},${i2 + 1}`)) {
nextChoices.push([i1, i2 + 1]);
set.add(`${i1},${i2 + 1}`);
}
}
choices = nextChoices;
}

return choices.length > 0;
};

10、执行结果

  • 执行用时: 72 ms
  • 内存消耗: 48.3 MB

11、解法4-动态规划

官解和其他优秀题解对于动态规划解法已经说得很清楚了,这里就贴下代码和执行结果


12、代码

var isInterleave = function (s1, s2, s3) {
if (s1.length + s2.length !== s3.length) return false;

const dp = new Array(s1.length + 1).fill(0).map(() => new Array(s2.length + 1).fill(true));
for (let i = 1; i < dp.length; i++) dp[i][0] = dp[i - 1][0] && s3[i - 1] === s1[i - 1];
for (let j = 1; j < dp[0].length; j++) dp[0][j] = dp[0][j - 1] && s3[j - 1] === s2[j - 1];

for (let i = 1; i < dp.length; i++) {
for (let j = 1; j < dp[i].length; j++) {
dp[i][j] = (dp[i - 1][j] && s3[i + j - 1] === s1[i - 1]) || (dp[i][j - 1] && s3[i + j - 1] === s2[j - 1]);
}
}

return dp[s1.length][s2.length];
};

13、执行结果

  • 执行用时: 92 ms
  • 内存消耗: 47.7 MB

· 阅读需 8 分钟

1、题干

给定三个字符串 s1s2s3,请判断 s3 能不能由 s1 和 s2 交织(交错) 组成。

两个字符串 st 交织 的定义与过程如下,其中每个字符串都会被分割成若干 非空 子字符串:

  • s = s1 + s2 + ... + sn
  • t = t1 + t2 + ... + tm
  • |n - m| <= 1
  • 交织s1 + t1 + s2 + t2 + s3 + t3 + ... 或者 t1 + s1 + t2 + s2 + t3 + s3 + ...

提示:a + b 意味着字符串 ab 连接。

 

示例 1:

输入:s1 = "aabcc", s2 = "dbbca", s3 = "aadbbcbcac"
输出:true

示例 2:

输入:s1 = "aabcc", s2 = "dbbca", s3 = "aadbbbaccc"
输出:false

示例 3:

输入:s1 = "", s2 = "", s3 = ""
输出:true

 

提示:

  • 0 <= s1.length, s2.length <= 100
  • 0 <= s3.length <= 200
  • s1s2、和 s3 都由小写英文字母组成

 

注意:本题与主站 97 题相同: https://leetcode-cn.com/problems/interleaving-string/

遍历解题需要用到3个指针,分别对应字符串 s1s2s3。官解说双指针不可行应该只考虑了 s1s2 的指针且没有考虑指针重置的情况,实际可以借助指针重置进行解题,详情见解法2。


2、解法1-回溯+记忆化

回溯过程可以理解为同时按字符串 s1s2s3 步进,若 s1 当前字符与 s3 当前字符相等则 s1s3 的下标同时加1,若 s2 当前字符与 s3 当前字符相等则 s2s3 的下标同时加1,若最终3个字符串能走到字符串末尾说明结果为 true


其中记忆化是将已访问过的路径使用哈希集合存储,若后续访问到已访问过的路径,则直接退出当前递归。


3、代码

var isInterleave = function (s1, s2, s3) {
if (s1.length + s2.length !== s3.length) return false;

const visited = new Set();
let res = false;
function backtrace(i1, i2, i3) {
if (i1 === s1.length && i2 === s2.length && i3 === s3.length) return res = true;
if (visited.has(`${i1}-${i2}-${i3}`)) return false;
visited.add(`${i1}-${i2}-${i3}`);

if (s1[i1] === s3[i3]) backtrace(i1 + 1, i2, i3 + 1);
if (s2[i2] === s3[i3]) backtrace(i1, i2 + 1, i3 + 1);
}

backtrace(0, 0, 0);

return res;
};

4、执行结果

image.png


5、解法2-三指针+模拟回溯+记忆化

本质上也是回溯+记忆化,只是使用三指针遍历模拟回溯过程,这是个非常规解法随便看看就好。


三指针遍历模拟回溯的大体方案是:遍历 s3,若遇到 s1s2 都可选的情况则选择 s1,并使用栈存储当前3个指针作为快照;若后续遍历无法继续下去且栈不为空则弹出栈顶快照,重置3个指针并选择 s2 继续遍历;若后续遍历无法继续且栈为空,则结果为 false


其中记忆化是将已访问过的路径使用哈希集合存储,若后续访问到已访问过的路径则看栈是否为空,栈为空则直接退出当前递归,栈不为空则弹出栈顶快照并重置指针。


6、代码

var isInterleave = function (s1, s2, s3) {
if (s1.length + s2.length !== s3.length) return false;

let i1 = -1, i2 = -1, useStack = false;
const stack = [], visited = new Set();

for (let i = 0; i < s3.length; i++) {
if (s1[i1 + 1] === s3[i] && s2[i2 + 1] === s3[i]) {
if (!useStack) stack.push([i, i1, i2]), i1 += 1;
else i2 += 1, useStack = false;
}
else if (s1[i1 + 1] === s3[i]) i1 += 1;
else if (s2[i2 + 1] === s3[i]) i2 += 1;
else {
if (!stack.length) return false;
[i, i1, i2] = stack.pop(), i -= 1, useStack = true;
continue;
}

if (visited.has(`${i}-${i1}-${i2}`)) {
if (!stack.length) return false;
[i, i1, i2] = stack.pop(), i -= 1, useStack = true;
}
else visited.add(`${i}-${i1}-${i2}`);
}

return true;
};

7、执行结果

  • 执行用时: 72 ms
  • 内存消耗: 45.4 MB

8、解法3-BFS+去重

这里使用非递归BFS解题,大体思路是:外层遍历字符串 s3,内层遍历当前可选择的所有 s1s2 的下标组合,并且把下一层可选择的所有 s1s2 的下标组合添加到下一层的队列中,如此遍历所有可行组合直至结束,若最终队列不为空则结果为 true


其中添加下标组合到队列中时一定要去重,否则会超时或内存溢出,去重可以使用双层 map 或者字符串哈希集合。


9、代码

// BFS+双层map去重
var isInterleave = function (s1, s2, s3) {
if (s1.length + s2.length !== s3.length) return false;

let choices = [[-1, -1]];
for (let i = 0; i < s3.length; i++) {
const nextChoices = [], map = {};
for (const [i1, i2] of choices) {
if (s3[i] === s1[i1 + 1] && (!map[i1 + 1] || !map[i1 + 1][i2])) {
nextChoices.push([i1 + 1, i2]);
map[i1 + 1] ? (map[i1 + 1][i2] = 1) : (map[i1 + 1] = { [i2]: 1 });
}
if (s3[i] === s2[i2 + 1] && (!map[i1] || !map[i1][i2 + 1])) {
nextChoices.push([i1, i2 + 1]);
map[i1] ? (map[i1][i2 + 1] = 1) : (map[i1] = { [i2 + 1]: 1 });
}
}
choices = nextChoices;
}

return choices.length > 0;
};

// BFS+字符串哈希集合去重
var isInterleave = function (s1, s2, s3) {
if (s1.length + s2.length !== s3.length) return false;

let choices = [[-1, -1]];
for (let i = 0; i < s3.length; i++) {
const nextChoices = [], set = new Set();
for (const [i1, i2] of choices) {
if (s3[i] === s1[i1 + 1] && !set.has(`${i1 + 1},${i2}`)) {
nextChoices.push([i1 + 1, i2]);
set.add(`${i1 + 1},${i2}`);
}
if (s3[i] === s2[i2 + 1] && !set.has(`${i1},${i2 + 1}`)) {
nextChoices.push([i1, i2 + 1]);
set.add(`${i1},${i2 + 1}`);
}
}
choices = nextChoices;
}

return choices.length > 0;
};

10、执行结果

  • 执行用时: 72 ms
  • 内存消耗: 48.3 MB

11、解法4-动态规划

官解和其他优秀题解对于动态规划解法已经说得很清楚了,这里就贴下代码和执行结果


12、代码

var isInterleave = function (s1, s2, s3) {
if (s1.length + s2.length !== s3.length) return false;

const dp = new Array(s1.length + 1).fill(0).map(() => new Array(s2.length + 1).fill(true));
for (let i = 1; i < dp.length; i++) dp[i][0] = dp[i - 1][0] && s3[i - 1] === s1[i - 1];
for (let j = 1; j < dp[0].length; j++) dp[0][j] = dp[0][j - 1] && s3[j - 1] === s2[j - 1];

for (let i = 1; i < dp.length; i++) {
for (let j = 1; j < dp[i].length; j++) {
dp[i][j] = (dp[i - 1][j] && s3[i + j - 1] === s1[i - 1]) || (dp[i][j - 1] && s3[i + j - 1] === s2[j - 1]);
}
}

return dp[s1.length][s2.length];
};

13、执行结果

  • 执行用时: 92 ms
  • 内存消耗: 47.7 MB

· 阅读需 2 分钟

1、题干

如果一个二进制字符串,是以一些 0(可能没有 0)后面跟着一些 1(也可能没有 1)的形式组成的,那么该字符串是 单调递增 的。

给你一个二进制字符串 s,你可以将任何 0 翻转为 1 或者将 1 翻转为 0

返回使 s 单调递增的最小翻转次数。

 

示例 1:

输入:s = "00110"
输出:1
解释:翻转最后一位得到 00111.

示例 2:

输入:s = "010110"
输出:2
解释:翻转得到 011111,或者是 000111。

示例 3:

输入:s = "00011000"
输出:2
解释:翻转得到 00000000。

 

提示:

  • 1 <= s.length <= 105
  • s[i]'0''1'

2、解题思路

字符串中的 10 打乱了递增顺序,要保证递增性只需要将其替换成 000111 中任意一种,至于替换成哪一种不重要,因为总有一个合适的,重要的是每出现1个 10 翻转次数需要加 1

因此要求最小翻转次数,只需要循环地把所有 10 全部剔除,并累加翻转次数即可


3、代码

var minFlipsMonoIncr = function (s) {
let c = 0;
while (s.indexOf('10') > -1) s = s.replace(/10/g, () => (c++, ''));
return c;
};

4、执行结果

image.png

· 阅读需 3 分钟

1、题干

如果一个由 '0''1' 组成的字符串,是以一些 '0'(可能没有 '0')后面跟着一些 '1'(也可能没有 '1')的形式组成的,那么该字符串是 单调递增 的。

我们给出一个由字符 '0''1' 组成的字符串s,我们可以将任何 '0' 翻转为 '1' 或者将 '1' 翻转为 '0'

返回使s 单调递增 的最小翻转次数。

 

示例 1:

输入:s = "00110"
输出:1
解释:我们翻转最后一位得到 00111.

示例 2:

输入:s = "010110"
输出:2
解释:我们翻转得到 011111,或者是 000111。

示例 3:

输入:s = "00011000"
输出:2
解释:我们翻转得到 00000000。

 

提示:

  • 1 <= s.length <= 20000
  • s 中只包含字符 '0' 和 '1'

 

注意:本题与主站 926 题相同: https://leetcode-cn.com/problems/flip-string-to-monotone-increasing/

2、解题思路

字符串中的 10 打乱了递增顺序,要保证递增性只需要将其替换成 000111 中任意一种,至于替换成哪一种不重要,因为总有一个合适的,重要的是每出现1个 10 翻转次数需要加 1

因此要求最小翻转次数,只需要循环地把所有 10 全部剔除,并累加翻转次数即可


3、代码

var minFlipsMonoIncr = function (s) {
let c = 0;
while (s.indexOf('10') > -1) s = s.replace(/10/g, () => (c++, ''));
return c;
};

4、执行结果

image.png

· 阅读需 3 分钟

1、题干

如果一个由 '0''1' 组成的字符串,是以一些 '0'(可能没有 '0')后面跟着一些 '1'(也可能没有 '1')的形式组成的,那么该字符串是 单调递增 的。

我们给出一个由字符 '0''1' 组成的字符串s,我们可以将任何 '0' 翻转为 '1' 或者将 '1' 翻转为 '0'

返回使s 单调递增 的最小翻转次数。

 

示例 1:

输入:s = "00110"
输出:1
解释:我们翻转最后一位得到 00111.

示例 2:

输入:s = "010110"
输出:2
解释:我们翻转得到 011111,或者是 000111。

示例 3:

输入:s = "00011000"
输出:2
解释:我们翻转得到 00000000。

 

提示:

  • 1 <= s.length <= 20000
  • s 中只包含字符 '0' 和 '1'

 

注意:本题与主站 926 题相同: https://leetcode-cn.com/problems/flip-string-to-monotone-increasing/

2、解题思路

字符串中的 10 打乱了递增顺序,要保证递增性只需要将其替换成 000111 中任意一种,至于替换成哪一种不重要,因为总有一个合适的,重要的是每出现1个 10 翻转次数需要加 1

因此要求最小翻转次数,只需要循环地把所有 10 全部剔除,并累加翻转次数即可


3、代码

var minFlipsMonoIncr = function (s) {
let c = 0;
while (s.indexOf('10') > -1) s = s.replace(/10/g, () => (c++, ''));
return c;
};

4、执行结果

image.png

· 阅读需 7 分钟

1、题干

给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

说明:一个机器人每次只能向下或者向右移动一步。

 

示例 1:

输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:因为路径 1→3→1→1→1 的总和最小。

示例 2:

输入:grid = [[1,2,3],[4,5,6]]
输出:12

 

提示:

  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 200
  • 0 <= grid[i][j] <= 100

 

注意:本题与主站 64 题相同: https://leetcode-cn.com/problems/minimum-path-sum/

2、解法1-暴力DFS

看完题目之后没多想误认为是走迷宫的题,于是写了一版暴力 DFS ,结果超时了。代码逻辑是从左上角到右下角进行深度遍历,并且累计路径和代入下一步计算。


超时的原因在于 :通常走迷宫的题不会重复经过某个节点,因此时间复杂度是 O(mn)O(mn),以 m,n<=200m,n <= 200 来算不会超时; 但这个题目的暴力 DFS 可以看做是遍历一棵高度为 n+m1n+m-1 的二叉树,时间复杂度接近指数级的 2n+m12^{n+m-1},超时也不足为奇。


从网格左上角到右下角的最短路径长度为 n+m1n+m-1,因此把它当成树的话高度为 n+m1n+m-1


暴力DFS超时代码

var minPathSum = function (grid) {
const m = grid.length, n = grid[0].length
let min = Infinity;

function dfs(i, j, sum) {
if (i >= m || j >= n) return;
sum += grid[i][j]

if (i === m - 1 && j === n - 1) min = Math.min(min, sum);

dfs(i + 1, j, sum);
dfs(i, j + 1, sum);
}

dfs(0, 0, 0);
return min;
};

3、暴力DFS-优化

通常暴力 DFS 可以通过剪枝、记忆化等方式进行优化,这里采用了剪枝优化,优化后AC了但耗时 3208 ms 依然很高。


这里的问题在于

  • 上述DFS代码属于暴力遍历所有情况并在终点时计算最小值,没有求局部最优解的倾向
  • 剪枝优化不稳定:这里剪枝的方式是记录节点前序路径和,当再次访问该节点时判断当前情况的前序路径和是否更小,以决定是否继续遍历以及是否更新该前序路径和,这种剪枝方式存在不稳定性,因为记录的前序路径和不是最小,后续遍历可能会有更小的前序路径和出现,所以这种剪枝没办法排除掉最小值以外的所有情况

4、代码

var minPathSum = function (grid) {
const m = grid.length, n = grid[0].length
let min = Infinity, minPreSum = new Map();

function dfs(i, j, sum) {
const key = i + '-' + j;
if (i >= m || j >= n || sum >= (minPreSum.get(key) || Infinity)) return;
minPreSum.set(key, sum);

sum += grid[i][j]

if (i === m - 1 && j === n - 1) min = Math.min(min, sum);

dfs(i + 1, j, sum);
dfs(i, j + 1, sum);
}

dfs(0, 0, 0);
return min;
};

5、执行结果

  • 执行用时: 3208 ms
  • 内存消耗: 53 MB

6、解法2-记忆化DFS

这个解法思路接近于动态规划, dfs 函数的意图是求局部最优解最终得到全局最优解,其中 dfs(i, j) 代表从终点 (m, n) 到点 (i, j) 的最小路径和,然后通过哈希表记录点 (i, j) 的最小路径和,后续再次遍历到该节点直接返回最小路径和,无需多余遍历与计算。


7、代码

var minPathSum = function (grid) {
const m = grid.length, n = grid[0].length
const minSum = new Map();

function dfs(i, j) {
if (i >= m || j >= n) return Infinity;
if (i === m - 1 && j === n - 1) return grid[i][j];

const key = i + '-' + j;
if (minSum.has(key)) return minSum.get(key);

const min = Math.min(dfs(i + 1, j), dfs(i, j + 1)) + grid[i][j];
return minSum.set(key, min), min;
}

return dfs(0, 0);
};

8、执行结果

  • 执行用时: 76 ms
  • 内存消耗: 49.2 MB

9、解法3-动态规划

解法2采用的是逆序累加和求解,这里为了方便理解采用顺序累加和进行求解。从题意可以看出对于任意节点 (i, j),其最小路径和取决于上方节点 (i-1, j) 和左方节点 (i, j-1),其值为左上两个节点中最小路径和的较小值加上自身的值。

  • dp 数组含义:dp 数组是一个 (m+1)*(n+1) 的数组,其中 dp[i][j] 表示从起点 (0, 0) 到点 (i, j) 的最小路径和,其中第一行和第一列只是为了方便计算,没有实际含义
  • 状态转移方程是:
    • i === 1 && j === 1 时:dp[i][j] = grid[i - 1][j - 1]
    • 其他情况:dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i - 1][j - 1]
  • dp[i][j] 对应的节点是 grid[i - 1][j - 1],因此遍历 dp 数组时 ij 都从下标 1 开始

10、代码

var minPathSum = function (grid) {
const m = grid.length, n = grid[0].length
const dp = new Array(m + 1).fill(0).map(() => new Array(n + 1).fill(Infinity));

for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (i === 1 && j === 1) dp[i][j] = grid[i - 1][j - 1];
else dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i - 1][j - 1];
}
}

return dp[m][n];
};

11、执行结果

  • 执行用时: 76 ms
  • 内存消耗: 44 MB

12、最后

题解区有更优秀的动态规划解法,有降维 DP(一维)、有把原始数组 grid 作为 DP 数组等,主要降低了空间复杂度,很值得学习。

· 阅读需 7 分钟

1、题干

给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

说明:一个机器人每次只能向下或者向右移动一步。

 

示例 1:

输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:因为路径 1→3→1→1→1 的总和最小。

示例 2:

输入:grid = [[1,2,3],[4,5,6]]
输出:12

 

提示:

  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 200
  • 0 <= grid[i][j] <= 100

 

注意:本题与主站 64 题相同: https://leetcode-cn.com/problems/minimum-path-sum/

2、解法1-暴力DFS

看完题目之后没多想误认为是走迷宫的题,于是写了一版暴力 DFS ,结果超时了。代码逻辑是从左上角到右下角进行深度遍历,并且累计路径和代入下一步计算。


超时的原因在于 :通常走迷宫的题不会重复经过某个节点,因此时间复杂度是 O(mn)O(mn),以 m,n<=200m,n <= 200 来算不会超时; 但这个题目的暴力 DFS 可以看做是遍历一棵高度为 n+m1n+m-1 的二叉树,时间复杂度接近指数级的 2n+m12^{n+m-1},超时也不足为奇。


从网格左上角到右下角的最短路径长度为 n+m1n+m-1,因此把它当成树的话高度为 n+m1n+m-1


暴力DFS超时代码

var minPathSum = function (grid) {
const m = grid.length, n = grid[0].length
let min = Infinity;

function dfs(i, j, sum) {
if (i >= m || j >= n) return;
sum += grid[i][j]

if (i === m - 1 && j === n - 1) min = Math.min(min, sum);

dfs(i + 1, j, sum);
dfs(i, j + 1, sum);
}

dfs(0, 0, 0);
return min;
};

3、暴力DFS-优化

通常暴力 DFS 可以通过剪枝、记忆化等方式进行优化,这里采用了剪枝优化,优化后AC了但耗时 3208 ms 依然很高。


这里的问题在于

  • 上述DFS代码属于暴力遍历所有情况并在终点时计算最小值,没有求局部最优解的倾向
  • 剪枝优化不稳定:这里剪枝的方式是记录节点前序路径和,当再次访问该节点时判断当前情况的前序路径和是否更小,以决定是否继续遍历以及是否更新该前序路径和,这种剪枝方式存在不稳定性,因为记录的前序路径和不是最小,后续遍历可能会有更小的前序路径和出现,所以这种剪枝没办法排除掉最小值以外的所有情况

4、代码

var minPathSum = function (grid) {
const m = grid.length, n = grid[0].length
let min = Infinity, minPreSum = new Map();

function dfs(i, j, sum) {
const key = i + '-' + j;
if (i >= m || j >= n || sum >= (minPreSum.get(key) || Infinity)) return;
minPreSum.set(key, sum);

sum += grid[i][j]

if (i === m - 1 && j === n - 1) min = Math.min(min, sum);

dfs(i + 1, j, sum);
dfs(i, j + 1, sum);
}

dfs(0, 0, 0);
return min;
};

5、执行结果

  • 执行用时: 3208 ms
  • 内存消耗: 53 MB

6、解法2-记忆化DFS

这个解法思路接近于动态规划, dfs 函数的意图是求局部最优解最终得到全局最优解,其中 dfs(i, j) 代表从终点 (m, n) 到点 (i, j) 的最小路径和,然后通过哈希表记录点 (i, j) 的最小路径和,后续再次遍历到该节点直接返回最小路径和,无需多余遍历与计算。


7、代码

var minPathSum = function (grid) {
const m = grid.length, n = grid[0].length
const minSum = new Map();

function dfs(i, j) {
if (i >= m || j >= n) return Infinity;
if (i === m - 1 && j === n - 1) return grid[i][j];

const key = i + '-' + j;
if (minSum.has(key)) return minSum.get(key);

const min = Math.min(dfs(i + 1, j), dfs(i, j + 1)) + grid[i][j];
return minSum.set(key, min), min;
}

return dfs(0, 0);
};

8、执行结果

  • 执行用时: 76 ms
  • 内存消耗: 49.2 MB

9、解法3-动态规划

解法2采用的是逆序累加和求解,这里为了方便理解采用顺序累加和进行求解。从题意可以看出对于任意节点 (i, j),其最小路径和取决于上方节点 (i-1, j) 和左方节点 (i, j-1),其值为左上两个节点中最小路径和的较小值加上自身的值。

  • dp 数组含义:dp 数组是一个 (m+1)*(n+1) 的数组,其中 dp[i][j] 表示从起点 (0, 0) 到点 (i, j) 的最小路径和,其中第一行和第一列只是为了方便计算,没有实际含义
  • 状态转移方程是:
    • i === 1 && j === 1 时:dp[i][j] = grid[i - 1][j - 1]
    • 其他情况:dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i - 1][j - 1]
  • dp[i][j] 对应的节点是 grid[i - 1][j - 1],因此遍历 dp 数组时 ij 都从下标 1 开始

10、代码

var minPathSum = function (grid) {
const m = grid.length, n = grid[0].length
const dp = new Array(m + 1).fill(0).map(() => new Array(n + 1).fill(Infinity));

for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (i === 1 && j === 1) dp[i][j] = grid[i - 1][j - 1];
else dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i - 1][j - 1];
}
}

return dp[m][n];
};

11、执行结果

  • 执行用时: 76 ms
  • 内存消耗: 44 MB

12、最后

题解区有更优秀的动态规划解法,有降维 DP(一维)、有把原始数组 grid 作为 DP 数组等,主要降低了空间复杂度,很值得学习。