Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ node_modules/
docs/.vuepress/dist/
docs/.vitepress/dist/
docs/.vitepress/cache/
src/.lcpr_data/
dist/
coverage/
*.log
pnpm-debug.log*
.idea/
/training-system/progress/
2 changes: 1 addition & 1 deletion docs/.vitepress/sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ const topicOrder: string[] = [
'dynamic-programming-state-machine',
'dynamic-programming-interval',
// 进阶数据结构篇
'monotonic-stack',
'monotonic-stack'
];

function getCategoryTitle(name: string): string {
Expand Down
70 changes: 47 additions & 23 deletions docs/docs/topic/slide-window.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

滑动窗口真正要训练的,不是记住几段代码,而是先把题目改写成:

1. 维护一个连续区间 `[left, right]` 或 `[left, right)`
1. 维护一个连续区间 `[left, right)`(左闭右开)
2. 用少量状态描述这个区间是否满足要求。
3. 每次只让一个元素进窗口、一个元素出窗口。
4. 在移动过程中更新答案。
Expand Down Expand Up @@ -47,18 +47,24 @@
```ts
function fixedWindow(nums: number[], k: number): void {
let left = 0;
let right = 0;
const state = initState();

for (let right = 0; right < nums.length; right++) {
add(state, nums[right]);
while (right < nums.length) {
// expand: add right element
const el = nums[right];
right++;

if (right - left + 1 > k) {
add(state, el);

// shrink: window exceeds k
if (right - left > k) {
remove(state, nums[left]);
left++;
}

if (right - left + 1 === k) {
// 变体插桩:更新答案
// update: window size equals k
if (right - left === k) {
updateAnswer(left, right, state);
}
}
Expand Down Expand Up @@ -88,19 +94,25 @@ function fixedWindow(nums: number[], k: number): void {
```ts
function longestWindow(nums: number[]): number {
let left = 0;
let right = 0;
let ans = 0;
const state = initState();

for (let right = 0; right < nums.length; right++) {
add(state, nums[right]);
while (right < nums.length) {
// expand: add right element
const el = nums[right];
right++;

add(state, el);

// shrink: window is invalid
while (needShrink(state)) {
remove(state, nums[left]);
left++;
}

// 此时窗口重新合法
ans = Math.max(ans, right - left + 1);
// update: window is valid again
ans = Math.max(ans, right - left);
}

return ans;
Expand Down Expand Up @@ -137,16 +149,22 @@ function longestWindow(nums: number[]): number {
```ts
function minWindowTemplate(s: string): string {
let left = 0;
let right = 0;
let bestLeft = 0;
let bestLen = Infinity;
const state = initState();

for (let right = 0; right < s.length; right++) {
add(state, s[right]);
while (right < s.length) {
// expand: add right element
const ch = s[right];
right++;

add(state, ch);

// shrink: window is valid, keep compressing
while (isValid(state)) {
if (right - left + 1 < bestLen) {
bestLen = right - left + 1;
if (right - left < bestLen) {
bestLen = right - left;
bestLeft = left;
}

Expand Down Expand Up @@ -192,27 +210,33 @@ function minWindowTemplate(s: string): string {
```ts
function countAtMost(nums: number[], k: number): number {
let left = 0;
let right = 0;
let ans = 0;
const state = initState();

for (let right = 0; right < nums.length; right++) {
add(state, nums[right]);
while (right < nums.length) {
// expand: add right element
const el = nums[right];
right++;

add(state, el);

// shrink: window is invalid
while (needShrink(state)) {
remove(state, nums[left]);
left++;
}

// 以 right 结尾的合法窗口共有 right - left + 1 个
ans += right - left + 1;
// count: all valid windows ending at right - 1
ans += right - left;
}

return ans;
}
```

为什么是 `right - left + 1`?
因为合法窗口为:`[left, right]`、`[left + 1, right]`、...、`[right, right]`
为什么是 `right - left`?
因为 `right` 已经自增,当前窗口为 `[left, right)`,合法窗口为:`[left, right)`、`[left + 1, right)`、...、`[right - 1, right)`,共 `right - left` 个

常见技巧:

Expand Down Expand Up @@ -242,7 +266,7 @@ exactly(k) = atMost(k) - atMost(k - 1)
| 题目 | 在模板上的插桩点 |
| ---- | ---------------- |
| `[1358] 包含所有三种字符的子字符串数目` | 一旦窗口包含 `a`、`b`、`c`,当前 `left` 之前的起点都可贡献答案。 |
| `[713] 乘积小于 K 的子数组` | 本质可看成至多型计数,合法后同样累加 `right - left + 1`。 |
| `[713] 乘积小于 K 的子数组` | 本质可看成"至多型计数",合法后同样累加 `right - left`。 |

## 五、滑动窗口真正要想清楚的 3 个问题

Expand All @@ -264,11 +288,11 @@ exactly(k) = atMost(k) - atMost(k - 1)

## 易错点清单

1. 区间定义不统一:`[left, right]` 和 `[left, right)` 混着写
1. 区间定义不统一:模板统一使用 `[left, right)` 左闭右开,`right` 先自增再操作,窗口大小为 `right - left`
2. 扩窗和缩窗时忘记同步更新 `state`。
3. 更新答案的时机不对,导致少算或多算。
4. 把“最长合法窗口”和“最短覆盖窗口”的缩窗逻辑写反。
5. 计数题不知道为什么能一次加上 `right - left + 1`。
5. 计数题不知道为什么能一次加上 `right - left`。
6. 用对象统计频次时,字符减到 `0` 后忘记处理,导致种类数判断错误。

## 总结:解题速查表
Expand Down
42 changes: 21 additions & 21 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,27 +37,27 @@ footer: false

以下专题覆盖了 LeetCode 最核心的算法知识体系,建议按顺序阅读:

| # | 专题 | 关键内容 |
| --- | --- | --- |
| 0 | [前言](/docs/topic/introduction) | 写给工程师的算法学习观 |
| 1 | [重新认识递归](/docs/topic/recursive) | 递归的本质、思维方式与代码模板 |
| 2 | [二叉树遍历算法](/docs/topic/tree) | 前/中/后序遍历框架(含 BST 中序应用) |
| 3 | [排序算法](/docs/topic/sort) | 经典排序算法对比与实现 |
| 4 | [双指针问题](/docs/topic/two-pointers) | 快慢指针、左右指针 |
| 5 | [二分搜索专题](/docs/topic/binary-search) | 统一二分搜索框架 |
| 6 | [滑动窗口算法](/docs/topic/slide-window) | 定长/不定长窗口的通用模板 |
| 7 | [前缀和算法](/docs/topic/partial-sum) | 区间求和与差分数组 |
| 8 | [回溯算法](/docs/topic/backtrack) | 排列/组合/子集的通用回溯框架 |
| 9 | [深度优先搜索](/docs/topic/depth-first-search) | 岛屿问题、连通分量的 DFS 通解 |
| 10 | [广度优先搜索](/docs/topic/breadth-first-search) | 最短路径、多源 BFS、状态空间搜索 |
| 11 | [图论算法](/docs/topic/graph) | 环检测、拓扑排序、二分图、Dijkstra |
| 12 | [贪心算法](/docs/topic/greedy) | 贪心选择性质、与 DP 的边界判断 |
| 13 | [动态规划:入门与线性/坐标模型](/docs/topic/dynamic-programming-normal) | 线性递推、网格路径、空间压缩 |
| 14 | [动态规划:背包模型](/docs/topic/dynamic-programming-backpack) | 0-1 背包、完全背包、多维背包 |
| 15 | [动态规划:序列与双序列模型](/docs/topic/dynamic-programming-subsequence) | LIS、LCS、编辑距离、匹配 DP |
| 16 | [动态规划:状态机模型](/docs/topic/dynamic-programming-state-machine) | 股票问题与阶段状态转移 |
| 17 | [动态规划:区间与划分模型](/docs/topic/dynamic-programming-interval) | 回文、切分、博弈类区间 DP |
| 18 | [单调栈算法](/docs/topic/monotonic-stack) | 下一个更大元素、柱状图的通解 |
| # | 专题 | 关键内容 |
| --- | ------------------------------------------------------------------------- | ------------------------------------- |
| 0 | [前言](/docs/topic/introduction) | 写给工程师的算法学习观 |
| 1 | [重新认识递归](/docs/topic/recursive) | 递归的本质、思维方式与代码模板 |
| 2 | [二叉树遍历算法](/docs/topic/tree) | 前/中/后序遍历框架(含 BST 中序应用) |
| 3 | [排序算法](/docs/topic/sort) | 经典排序算法对比与实现 |
| 4 | [双指针问题](/docs/topic/two-pointers) | 快慢指针、左右指针 |
| 5 | [二分搜索专题](/docs/topic/binary-search) | 统一二分搜索框架 |
| 6 | [滑动窗口算法](/docs/topic/slide-window) | 定长/不定长窗口的通用模板 |
| 7 | [前缀和算法](/docs/topic/partial-sum) | 区间求和与差分数组 |
| 8 | [回溯算法](/docs/topic/backtrack) | 排列/组合/子集的通用回溯框架 |
| 9 | [深度优先搜索](/docs/topic/depth-first-search) | 岛屿问题、连通分量的 DFS 通解 |
| 10 | [广度优先搜索](/docs/topic/breadth-first-search) | 最短路径、多源 BFS、状态空间搜索 |
| 11 | [图论算法](/docs/topic/graph) | 环检测、拓扑排序、二分图、Dijkstra |
| 12 | [贪心算法](/docs/topic/greedy) | 贪心选择性质、与 DP 的边界判断 |
| 13 | [动态规划:入门与线性/坐标模型](/docs/topic/dynamic-programming-normal) | 线性递推、网格路径、空间压缩 |
| 14 | [动态规划:背包模型](/docs/topic/dynamic-programming-backpack) | 0-1 背包、完全背包、多维背包 |
| 15 | [动态规划:序列与双序列模型](/docs/topic/dynamic-programming-subsequence) | LIS、LCS、编辑距离、匹配 DP |
| 16 | [动态规划:状态机模型](/docs/topic/dynamic-programming-state-machine) | 股票问题与阶段状态转移 |
| 17 | [动态规划:区间与划分模型](/docs/topic/dynamic-programming-interval) | 回文、切分、博弈类区间 DP |
| 18 | [单调栈算法](/docs/topic/monotonic-stack) | 下一个更大元素、柱状图的通解 |

## 📂 题解分类

Expand Down
42 changes: 17 additions & 25 deletions src/array/560.和为K的子数组.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
* https://leetcode.cn/problems/subarray-sum-equals-k/description/
*
* algorithms
* Medium (44.27%)
* Likes: 2702
* Medium (46.39%)
* Likes: 3130
* Dislikes: 0
* Total Accepted: 676.5K
* Total Submissions: 1.5M
* Total Accepted: 1.1M
* Total Submissions: 2.3M
* Testcase Example: '[1,1,1]\n2'
*
* 给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的子数组的个数 。
Expand Down Expand Up @@ -47,34 +47,26 @@

// @lc code=start
function subarraySum(nums: number[], k: number): number {
// 先算出前缀和
const preSum = Array(nums.length + 1).fill(0);
for (let i = 0; i < nums.length; i++) {
preSum[i + 1] = nums[i] + preSum[i];
}
let prefixSum = 0;
let res = 0;

// 因为前缀和中任意两个数 pre[i] 与 pre[j] 的差,即为数组 i~j 的和,代表一种子数组的情况
// 问题转换成,preSum 数组中有多少种情况使得其中两数之差为 k, 即 pre[i] - pre[j] = k
// 等价于 pre[j] = pre[i] - k,其中 i > j
const hash: Record<string, number> = {};
let result = 0;
for (let i = 0; i < preSum.length; i++) {
const preI = preSum[i];
const preJ = preI - k;
const hashMap: Map<number, number> = new Map();
// 前缀和 0 出现了 1 次
hashMap.set(0, 1);

// 如果找到符合要求的和 pre[j],result 中加上该种情况
result += hash[preJ] ? hash[preJ] : 0;
for (let i = 0; i < nums.length; i++) {
prefixSum += nums[i];

// 将当前的数字存入 hash 中方便后面以 O1 的复杂度取到
hash[preI] = hash[preI] ? hash[preI] + 1 : 1;
// 是否有 之前的前缀和 = prefixSum - k,有多少个就加多少个到 res 里
res += hashMap.get(prefixSum - k) ?? 0;
// 更新当前新的前缀和出现的次数
hashMap.set(prefixSum, (hashMap.get(prefixSum) ?? 0) + 1);
}

return result;
return res;
}
// @lc code=end

(() => {
const nums = [1, 1, -1, 1, -1],
k = 1;
console.log(subarraySum(nums, k));
LCT.func(subarraySum).auto();
})();
99 changes: 99 additions & 0 deletions src/backtracking/131.分割回文串.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* @lc app=leetcode.cn id=131 lang=typescript
*
* [131] 分割回文串
*
* https://leetcode.cn/problems/palindrome-partitioning/description/
*
* algorithms
* Medium (75.13%)
* Likes: 2203
* Dislikes: 0
* Total Accepted: 770.5K
* Total Submissions: 1M
* Testcase Example: '"aab"'
*
* 给你一个字符串 s,请你将 s 分割成一些 子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。
*
*
*
* 示例 1:
*
*
* 输入:s = "aab"
* 输出:[["a","a","b"],["aa","b"]]
*
*
* 示例 2:
*
*
* 输入:s = "a"
* 输出:[["a"]]
*
*
*
*
* 提示:
*
*
* 1 <= s.length <= 16
* s 仅由小写英文字母组成
*
*
*/

// @lc code=start
function partition(s: string): string[][] {
function isPalindrome(str: string): boolean {
let l = 0;
let r = str.length - 1;
while (l < r) {
if (str[l] !== str[r]) return false;
l++;
r--;
}
return true;
}

const res: string[][] = [];

function helper(start: number, path: string[]) {
if (start === s.length && path.length !== 0) {
res.push([...path]);
return;
}
for (let i = start + 1; i <= s.length; i++) {
const subStr = s.substring(start, i);
if (isPalindrome(subStr)) {
// 做选择
path.push(subStr);
helper(i, path);
// 取消选择
path.pop();
}
}
}

helper(0, []);
return res;
}
// @lc code=end

(() => {
LCT.func(partition).auto();

LCT.func(partition).cases([
{
input: ['abacaba'],
output: [
['a', 'b', 'a', 'c', 'a', 'b', 'a'],
['a', 'b', 'a', 'c', 'aba'],
['a', 'b', 'aca', 'b', 'a'],
['a', 'bacab', 'a'],
['aba', 'c', 'a', 'b', 'a'],
['aba', 'c', 'aba'],
['abacaba']
]
}
]);
})();
Loading
Loading