Skip to content
Closed
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/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export default defineConfig({
nav: [
{ text: '首页', link: '/' },
{ text: '📖 题解', link: '/docs/list/array/1.two-sum' },
{ text: '📖 专题', link: '/docs/topic/0.introduction' }
{ text: '📖 专题', link: '/docs/topic/introduction' }
],

sidebar,
Expand Down
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
125 changes: 125 additions & 0 deletions src/Unknown/2196.根据描述创建二叉树.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* @lc app=leetcode.cn id=2196 lang=typescript
*
* [2196] 根据描述创建二叉树
*
* https://leetcode.cn/problems/create-binary-tree-from-descriptions/description/
*
* algorithms
* Medium (73.68%)
* Likes: 68
* Dislikes: 0
* Total Accepted: 16.1K
* Total Submissions: 21.2K
* Testcase Example: '[[20,15,1],[20,17,0],[50,20,1],[50,80,0],[80,19,1]]'
*
* 给你一个二维整数数组 descriptions ,其中 descriptions[i] = [parenti, childi, isLefti] 表示
* parenti 是 childi 在 二叉树 中的 父节点,二叉树中各节点的值 互不相同 。此外:
*
*
* 如果 isLefti == 1 ,那么 childi 就是 parenti 的左子节点。
* 如果 isLefti == 0 ,那么 childi 就是 parenti 的右子节点。
*
*
* 请你根据 descriptions 的描述来构造二叉树并返回其 根节点 。
*
* 测试用例会保证可以构造出 有效 的二叉树。
*
*
*
* 示例 1:
*
*
*
*
* 输入:descriptions = [[20,15,1],[20,17,0],[50,20,1],[50,80,0],[80,19,1]]
* 输出:[50,20,80,15,17,19]
* 解释:根节点是值为 50 的节点,因为它没有父节点。
* 结果二叉树如上图所示。
*
*
* 示例 2:
*
*
*
*
* 输入:descriptions = [[1,2,1],[2,3,0],[3,4,1]]
* 输出:[1,2,null,null,3,4]
* 解释:根节点是值为 1 的节点,因为它没有父节点。
* 结果二叉树如上图所示。
*
*
*
* 提示:
*
*
* 1 <= descriptions.length <= 10^4
* descriptions[i].length == 3
* 1 <= parenti, childi <= 10^5
* 0 <= isLefti <= 1
* descriptions 所描述的二叉树是一棵有效二叉树
*
*
*/

// @lc code=start
/**
* Definition for a binary tree node.
* class TreeNode {
* val: number
* left: TreeNode | null
* right: TreeNode | null
* constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
* }
*/

function createBinaryTree(descriptions: number[][]): TreeNode | null {
const nodeMap: Map<number, TreeNode> = new Map();
const parents = new Set<number>();
const childs = new Set<number>();

for (let i = 0; i < descriptions.length; i++) {
const [parent, child, isLeft] = descriptions[i];

parents.add(parent);
childs.add(child);

if (!nodeMap.has(parent)) {
nodeMap.set(parent, new TreeNode(parent));
}
if (!nodeMap.has(child)) {
nodeMap.set(child, new TreeNode(child));
}
const parentNode = nodeMap.get(parent);
const childNode = nodeMap.get(child);
if (isLeft) {
parentNode.left = childNode;
} else {
parentNode.right = childNode;
}
}

for (const p of parents) {
if (!childs.has(p)) {
return nodeMap.get(p)!;
}
}
return null;
}

function normalizeTreeOutput(root: TreeNode | null): Array<number | null> {
return JSON.parse(Tree.serialize(root)).map((value: number | string | null) =>
value === 'null' || value === null ? null : value
);
}
// @lc code=end

(() => {
LCT.func(createBinaryTree).auto({
output: normalizeTreeOutput
});
})();
Loading