mirror of
https://github.com/krahets/hello-algo.git
synced 2026-07-22 19:36:06 +00:00
build
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- 此文件由 utils/exercises/publish_exercises.py 根据 exercises.yaml 自动生成,请勿直接修改。 -->
|
||||
|
||||
# 4.6 练习
|
||||
|
||||
## 4.6.1 知识巩固
|
||||
|
||||
### 1. 数组和链表怎样找到元素
|
||||
|
||||
数组和单向链表中都按顺序保存了 `[A, B, C, D, E]`。现在要读取第 4 个元素 `D`:
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 数组可以直接使用哪个索引?
|
||||
2. 单向链表从头节点 `A` 开始,要沿着 `next` 依次经过哪些节点?
|
||||
3. 当要读取的元素位置越来越靠后时,两种结构所需的步骤会怎样变化?哪一种更适合反复按位置读取?为什么?
|
||||
|
||||
??? success "参考答案"
|
||||
|
||||
1. 若索引从 0 开始,第 4 个元素的索引是 3,数组可直接读取 `arr[3]`。
|
||||
|
||||
2. 单向链表必须从头开始,访问路径为 `A → B → C → D`,需要沿 `next` 前进 3 次。
|
||||
|
||||
3. 数组可以根据首地址和索引直接定位元素,按位置访问的时间复杂度为 $O(1)$。
|
||||
单向链表访问第 $k$ 个节点时,必须从头节点开始,沿着 `next` 前进 $k-1$ 次,
|
||||
最坏需要 $O(n)$ 时间。
|
||||
|
||||
这里只比较按位置读取,不表示链表在所有操作上都更慢。
|
||||
|
||||
### 2. 数组和链表怎样插入元素
|
||||
|
||||
数组和单向链表中都保存了 `A、B、C、D`。现在要在 `B` 后面插入 `X`:
|
||||
|
||||
- 数组容量为 5,当前状态是 `[A, B, C, D, _]`;
|
||||
- 链表为 `A → B → C → D`,并且已经拿到了指向节点 `B` 的引用。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 数组需要移动哪些元素?写出插入后的数组。
|
||||
2. 链表应按什么顺序修改 `X.next` 和 `B.next`?写出插入后的链表。
|
||||
3. 为什么比较插入效率时,要特别说明“已经拿到了节点 B 的引用”?
|
||||
|
||||
??? success "参考答案"
|
||||
|
||||
1. 数组要先把 `D` 向右移动一格,再把 `C` 向右移动一格,最后把 `X` 放入索引 2,
|
||||
得到 `[A, B, X, C, D]`。
|
||||
|
||||
2. `B.next` 原本指向 `C`。应先令 `X.next = B.next`,让 `X` 指向 `C`;
|
||||
再令 `B.next = X`。结果为 `A → B → X → C → D`。
|
||||
如果先覆盖 `B.next` 又没有保存原来的连接,就可能找不到 `C`。
|
||||
|
||||
3. 已知 `B` 的位置后,链表插入只需修改两个连接,可以在 $O(1)$ 时间完成。
|
||||
如果还要从头查找 `B`,查找过程本身可能需要 $O(n)$ 时间。
|
||||
|
||||
### 3. 列表容量是怎样增长的
|
||||
|
||||
一个基于数组实现的列表当前内容为 `[A, B, C]`,长度 `size = 3`,容量 `capacity = 4`。
|
||||
规定容量不足时,新数组的容量扩大为原来的 2 倍。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 追加 `D` 后,列表的长度和容量分别是多少?是否需要扩容?
|
||||
2. 接着追加 `E` 时,容量会变为多少?需要复制几个原有元素?
|
||||
3. 底层数组的长度不可变,为什么列表的容量看起来却可以增长?
|
||||
|
||||
??? success "参考答案"
|
||||
|
||||
1. `D` 可以放入最后一个空位。此时内容为 `[A, B, C, D]`,
|
||||
`size = 4`、`capacity = 4`,不需要扩容。
|
||||
|
||||
2. 再追加 `E` 时已经没有空位,需要创建容量为 8 的新数组,
|
||||
将 `A、B、C、D` 共 4 个原有元素复制过去,再加入 `E`。
|
||||
此时 `size = 5`、`capacity = 8`。
|
||||
|
||||
3. 原来的数组本身没有变长。列表创建了一个更大的新数组,复制原有元素,
|
||||
再改用新数组作为底层存储,因此对使用者来说容量增长了。
|
||||
|
||||
## 4.6.2 编程练习
|
||||
|
||||
### 1. 数组表示的大整数加一
|
||||
|
||||
数组 `digits` 从左到右保存一个非负整数的各位数字,例如 `[3, 0, 8]` 表示 308。
|
||||
数字 0 用 `[0]` 表示;其他输入的第一位均不为 0。
|
||||
|
||||
请模拟一次十进制竖式加法,将这个整数增加 1,并把结果仍按相同的数组形式返回。
|
||||
可以直接修改 `digits`;若最前面产生新的进位,可以返回一个更长的数组。
|
||||
|
||||
??? tip "解题提示"
|
||||
|
||||
1. 像做竖式加法一样,从数组最后一位开始
|
||||
2. 当前位小于 9 时加一后即可立即返回
|
||||
3. 当前位等于 9 时把它改成 0;若所有位都是 9,需要在最前面补 1
|
||||
|
||||
[LeetCode](https://leetcode.cn/problems/plus-one/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
|
||||
### 2. 反转单向链表
|
||||
|
||||
给定一个单向链表的头节点 `head`。每个节点包含一个值和指向下一节点的 `next`。
|
||||
|
||||
请使用迭代方法反转所有节点之间的连接,并返回反转后的头节点。
|
||||
要求不创建新的链表节点。
|
||||
|
||||
??? tip "解题提示"
|
||||
|
||||
1. 先在纸上画出三个相连的节点和 prev、cur 两个指针
|
||||
2. 改写 cur.next 前,必须先用 nxt 保存原来的下一个节点
|
||||
3. 反转 cur.next 后,令 prev = cur,再令 cur = nxt,继续处理原链表中的下一个节点
|
||||
|
||||
[LeetCode](https://leetcode.cn/problems/reverse-linked-list/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } [题目解析](https://leetcode.cn/problems/reverse-linked-list/solutions/2361282/206-fan-zhuan-lian-biao-shuang-zhi-zhen-r1jel/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -20,3 +20,4 @@ icon: material/view-list-outline
|
||||
- [4.3 列表](list.md)
|
||||
- [4.4 内存与缓存 *](ram_and_cache.md)
|
||||
- [4.5 小结](summary.md)
|
||||
- [4.6 练习](exercises.md)
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- 此文件由 utils/exercises/publish_exercises.py 根据 exercises.yaml 自动生成,请勿直接修改。 -->
|
||||
|
||||
# 13.6 练习
|
||||
|
||||
## 13.6.1 知识巩固
|
||||
|
||||
### 1. 这段排列算法会漏掉结果吗
|
||||
|
||||
一个回溯算法按 `1、2、3` 的顺序尝试生成全部排列。每次选择数字 `x` 时,它会:
|
||||
|
||||
1. 把 `x` 加到当前路径末尾;
|
||||
2. 把 `x` 标记为“已使用”;
|
||||
3. 递归填写下一个位置。
|
||||
|
||||
递归返回后,同学只从路径末尾删除了 `x`,然后继续尝试下一个数字。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 算法首先得到哪个排列?它还能得到全部 6 个排列吗?
|
||||
2. 递归返回上一层前,只删除路径末尾的数字是否足够?如果不够,还需要做什么?说明理由。
|
||||
|
||||
??? success "参考答案"
|
||||
|
||||
1. 它首先得到 `[1, 2, 3]`,但无法得到全部排列。虽然返回时路径变短了,
|
||||
数字 1、2、3 的标记仍都是“已使用”,后续分支便没有可选数字。
|
||||
|
||||
2. 不够。删除路径末尾的 `x` 后,还必须把 `x` 重新标记为“未使用”。
|
||||
当前路径和已使用标记共同描述搜索状态;选择时修改了两处,回退时也必须把两处都恢复,
|
||||
其他分支才能再次选择 `x`。
|
||||
|
||||
### 2. 数字的选择顺序重要吗
|
||||
|
||||
给定排好序的数组 `[2, 3, 5]` 和目标值 5,每个数可以重复选择。
|
||||
算法规定每条搜索路径中的数字只能按从小到大的顺序出现。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 能得到哪些不同的组合?
|
||||
2. 为什么同一组数字不需要按不同顺序重复搜索?“从小到大”的限制起到了什么作用?
|
||||
3. 当前路径为 `[3]`、还差 2 时,下一个候选数是 3。为什么此时可以停止检查这一层后面的所有候选数?
|
||||
|
||||
??? success "参考答案"
|
||||
|
||||
1. 不同的组合为 `[2, 3]` 和 `[5]`。
|
||||
|
||||
2. 本题把 `[2, 3]` 和 `[3, 2]` 看作同一种组合,数字的选择顺序不计入答案。
|
||||
规定路径中的数字从小到大出现,就能在搜索时直接跳过 `[3, 2]` 这类重复组合。
|
||||
|
||||
3. 当前还差 2,而候选数 3 已经大于 2。因为数组已经排好序,
|
||||
3 后面的候选数只会更大,也都不可能加入当前组合,所以可以直接结束这一层的检查。
|
||||
|
||||
### 3. 下一枚皇后可以放在哪些位置
|
||||
|
||||
在一个 `4 × 4` 棋盘上按行放置皇后,行、列下标都从 0 开始。
|
||||
目前已经在 `(0, 1)` 和 `(1, 3)` 放置了皇后,现在要在第 2 行放置下一个皇后。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 哪些列会因为“同列”而被排除?
|
||||
2. 在剩余列中,哪些位置会因为“同一条对角线”而被排除?
|
||||
3. 第 2 行还剩哪些位置可以尝试?
|
||||
|
||||
??? success "参考答案"
|
||||
|
||||
1. 第 1 列和第 3 列已经有皇后,因此位置 `(2, 1)` 和 `(2, 3)` 被排除。
|
||||
|
||||
2. 在剩余位置中,`(2, 2)` 与 `(1, 3)` 位于同一条对角线上,因此也被排除。
|
||||
位置 `(2, 0)` 与已有两个皇后都不在同列或同一条对角线上。
|
||||
|
||||
3. 第 2 行可以尝试的位置只有 `(2, 0)`。
|
||||
|
||||
这一步只说明当前放置合法;之后若无法完成棋盘,仍需回退并尝试更早的其他选择。
|
||||
|
||||
## 13.6.2 编程练习
|
||||
|
||||
### 1. 无重复元素的全排列
|
||||
|
||||
整数数组 `nums` 至少包含一个元素,并且其中各元素互不相同。
|
||||
请列出把这些元素各使用一次所能形成的全部顺序,并将每一种顺序作为一个数组返回。
|
||||
结果中各排列的先后次序不作要求。
|
||||
请使用回溯,并用布尔数组记录每个位置的元素是否已经选入当前排列。
|
||||
|
||||
??? tip "解题提示"
|
||||
|
||||
1. 递归深度表示正在填写排列中的第几个位置
|
||||
2. 每一层只尝试尚未使用的元素
|
||||
3. 路径长度达到 `nums` 的长度时,把它的副本加入答案
|
||||
|
||||
[LeetCode](https://leetcode.cn/problems/permutations/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } [题目解析](https://leetcode.cn/problems/permutations/solutions/2363882/46-quan-pai-lie-hui-su-qing-xi-tu-jie-by-6o7h/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
|
||||
> **说明:** 链接题解通过交换数组元素,把已选元素依次放到数组前部;本练习使用布尔数组记录每个元素是否已选。两种方法都能避免同一元素被重复选择,但代码结构不同
|
||||
@@ -20,3 +20,4 @@ icon: material/map-marker-path
|
||||
- [13.3 子集和问题](subset_sum_problem.md)
|
||||
- [13.4 N 皇后问题](n_queens_problem.md)
|
||||
- [13.5 小结](summary.md)
|
||||
- [13.6 练习](exercises.md)
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- 此文件由 utils/exercises/publish_exercises.py 根据 exercises.yaml 自动生成,请勿直接修改。 -->
|
||||
|
||||
# 2.6 练习
|
||||
|
||||
## 2.6.1 知识巩固
|
||||
|
||||
### 1. 迭代与递归的时间和空间
|
||||
|
||||
下面两段代码都计算 $1 + 2 + \dots + n$(设 $n \ge 1$)。请把 `n` 设为 4,
|
||||
按照程序实际执行的顺序回答问题,然后比较两种写法的效率。
|
||||
|
||||
```python
|
||||
def sum_iter(n):
|
||||
s = 0
|
||||
for i in range(1, n + 1):
|
||||
s += i
|
||||
return s
|
||||
|
||||
def sum_recur(n):
|
||||
if n == 1:
|
||||
return 1
|
||||
return n + sum_recur(n - 1)
|
||||
```
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 执行 `sum_iter(4)` 时,每轮循环结束后,变量 `s` 的值分别是多少?
|
||||
2. 执行 `sum_recur(4)` 时,会依次调用哪些函数?从最深的一层开始返回时,结果怎样得到?
|
||||
3. 两种写法的时间复杂度和空间复杂度分别是多少?结合第 1、2 问的执行过程说明理由。
|
||||
|
||||
??? success "参考答案"
|
||||
|
||||
1. 循环变量 `i` 依次为 `1、2、3、4`,每轮结束后,`s` 依次变为
|
||||
`1、3、6、10`,所以 `sum_iter(4)` 返回 10。
|
||||
|
||||
2. 函数依次调用
|
||||
`sum_recur(4) → sum_recur(3) → sum_recur(2) → sum_recur(1)`。
|
||||
`sum_recur(1)` 返回 1,随后各层依次得到 `2 + 1 = 3`、`3 + 3 = 6`、`4 + 6 = 10`。
|
||||
在最深处,4 次函数调用都尚未结束。
|
||||
|
||||
3. 两段代码都进行与 $n$ 成正比的循环或调用,因此时间复杂度均为 $O(n)$ 。
|
||||
空间复杂度不同:迭代版只使用常数个变量,为 $O(1)$ ;
|
||||
递归版在到达终止条件前,前面的函数调用都要等待返回结果,因此调用栈中最多同时保存 $n$ 次调用,
|
||||
空间复杂度为 $O(n)$。
|
||||
|
||||
分析空间复杂度时,除代码中的变量外,还要考虑递归调用占用的空间。
|
||||
|
||||
### 2. 三段代码的时间复杂度
|
||||
|
||||
以下三个代码片段的输入均为正整数 $n$ 。请按时间复杂度从低到高排序,并写出各自的复杂度。
|
||||
|
||||
```python
|
||||
# 片段一
|
||||
s = 0
|
||||
for i in range(n):
|
||||
s += i
|
||||
|
||||
# 片段二
|
||||
s = 0
|
||||
for i in range(n):
|
||||
for j in range(i, n):
|
||||
s += j
|
||||
|
||||
# 片段三
|
||||
while n > 1:
|
||||
n = n // 2
|
||||
```
|
||||
|
||||
??? success "参考答案"
|
||||
|
||||
从低到高为:片段三 $O(\log n)$、片段一 $O(n)$、片段二 $O(n^2)$。
|
||||
片段三每轮把 $n$ 缩小为原来的一半,约循环 $\log_2 n$ 次。
|
||||
片段一的循环恰好执行 $n$ 次。片段二的内层循环次数依次为
|
||||
$n,n-1,\dots,1$,总次数为 $n(n+1)/2$,因此属于平方阶。
|
||||
|
||||
### 3. 哪种反转更节省空间
|
||||
|
||||
要将数组 `nums` 中的元素全部反转,有两种做法:
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 新建一个等长数组 `res`,倒序复制后返回;
|
||||
2. 用两个索引 `i` 和 `j` 分别从首、尾向中间移动,逐对交换 `nums[i]` 与 `nums[j]` 。
|
||||
|
||||
两种做法的空间复杂度各是多少?哪种属于“原地”操作?
|
||||
|
||||
??? success "参考答案"
|
||||
|
||||
1. 需要与输入等长的辅助数组,空间复杂度 $O(n)$。
|
||||
|
||||
2. 只使用两个索引变量,
|
||||
空间复杂度 $O(1)$ ,属于原地操作。
|
||||
|
||||
需要注意:原地反转会修改输入数组,
|
||||
仅在允许修改输入时才应优先选用;若需保留原数组,第 1 种做法的复制开销不可避免。
|
||||
|
||||
## 2.6.2 编程练习
|
||||
|
||||
### 1. 斐波那契数
|
||||
|
||||
斐波那契数列满足:$F(0)=0$、$F(1)=1$,并且当 $n\ge2$ 时,
|
||||
$F(n)=F(n-1)+F(n-2)$。
|
||||
|
||||
给定非负整数 `n`,请使用循环计算并返回 $F(n)$,不使用递归。
|
||||
|
||||
??? tip "解题提示"
|
||||
|
||||
1. 先单独处理 n 为 0 和 1 的情况
|
||||
2. 计算下一项时只需要前两项,无须保存整个数列
|
||||
3. 更新两个变量时,注意不要过早覆盖仍会用到的旧值
|
||||
|
||||
[LeetCode](https://leetcode.cn/problems/fibonacci-number/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } [题目解析](https://leetcode.cn/problems/fibonacci-number/solutions/2361746/509-fei-bo-na-qi-shu-dong-tai-gui-hua-qi-so8h/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -20,3 +20,4 @@ icon: material/timer-sand
|
||||
- [2.3 时间复杂度](time_complexity.md)
|
||||
- [2.4 空间复杂度](space_complexity.md)
|
||||
- [2.5 小结](summary.md)
|
||||
- [2.6 练习](exercises.md)
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- 此文件由 utils/exercises/publish_exercises.py 根据 exercises.yaml 自动生成,请勿直接修改。 -->
|
||||
|
||||
# 3.6 练习
|
||||
|
||||
## 3.6.1 知识巩固
|
||||
|
||||
### 1. 生活场景中的数据关系
|
||||
|
||||
请根据数据之间的关系,为下面三个场景选择“线性结构、树形结构、网状结构”中的一种,并说明理由:
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 同学们排成一列,每人只关心自己前面和后面的人;
|
||||
2. 学校按“学校 → 年级 → 班级”分层管理;
|
||||
3. 城市道路连接多个路口,一个路口可以通向多个其他路口,也可能形成环路。
|
||||
|
||||
??? success "参考答案"
|
||||
|
||||
1. 是线性结构。除排在最前和最后的人外,每个人都只与前面和后面各一人相邻,关系沿一条线展开。
|
||||
|
||||
2. 是树形结构。每个班级属于一个年级,每个年级又属于学校,关系从上到下分层展开。
|
||||
|
||||
3. 是网状结构。一个路口可以连接多个路口,路线之间还能形成环,不能排成单一顺序或严格层级。
|
||||
|
||||
判断结构时应先观察元素之间的关系,而不是先考虑它在内存中占多少空间。
|
||||
|
||||
### 2. 逻辑顺序怎样存进内存
|
||||
|
||||
为了保存逻辑顺序 `A → B → C`,现有两种简化的内存安排:
|
||||
|
||||
- 方案甲:`A、B、C` 分别放在编号为 `20、21、22` 的内存格中;
|
||||
- 方案乙:`A、B、C` 分别放在编号为 `20、7、31` 的内存格中,并由 `A` 记录 `B` 的位置、`B` 记录 `C` 的位置。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 哪个方案属于连续空间存储?哪个属于分散空间存储?
|
||||
2. 两个方案分别更接近数组还是链表?
|
||||
3. 方案乙的内存格编号没有按大小排列,为什么仍能表示 `A → B → C` 的逻辑顺序?
|
||||
|
||||
??? success "参考答案"
|
||||
|
||||
1. 方案甲使用连续的内存格,属于连续空间存储;方案乙的节点分散在不同位置,属于分散空间存储。
|
||||
|
||||
2. 方案甲更接近数组,方案乙更接近链表。
|
||||
|
||||
3. 逻辑顺序由节点之间记录的连接关系决定,而不是由内存格编号的大小决定。
|
||||
从 `A` 记录的位置可以找到 `B`,再从 `B` 记录的位置找到 `C`,所以仍能依次访问 `A、B、C`。
|
||||
|
||||
这也说明逻辑结构和物理结构是观察同一组数据的两个不同角度。
|
||||
|
||||
### 3. 作业记录中的数据类型与结构
|
||||
|
||||
某学习小组按座位顺序记录 4 名同学是否交了作业,得到:
|
||||
|
||||
`[true, false, true, true]`
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 每个元素适合使用哪种基本数据类型?
|
||||
2. 这 4 个元素按座位顺序排成一列,使用了什么逻辑结构?
|
||||
3. 如果以后改为记录每人的作业分数 `[90, 0, 85, 100]`,改变的是数据的“内容类型”还是“组织方式”?说明理由。
|
||||
|
||||
??? success "参考答案"
|
||||
|
||||
1. 每个元素只表示“是”或“否”,适合使用布尔类型 `bool`。
|
||||
|
||||
2. 这些元素按座位顺序排列,形成线性结构,可以用数组保存。
|
||||
|
||||
3. 改变的是内容类型:元素由布尔值变成了整数。组织方式没有改变,
|
||||
这些数据仍然按座位顺序排成一列,仍可使用数组这一线性结构。
|
||||
|
||||
基本数据类型描述“存的是什么”,数据结构描述“数据怎样组织”。
|
||||
|
||||
## 3.6.2 编程练习
|
||||
|
||||
### 1. 统计二进制表示中的 1
|
||||
|
||||
给定非负整数 `n`,请统计它的二进制表示中共有多少个 1。
|
||||
|
||||
要求使用位运算完成,不把二进制表示转换成字符串,也不使用直接统计 1 的内置函数。
|
||||
|
||||
??? tip "解题提示"
|
||||
|
||||
1. n & 1 可以取出 n 的最右边一位,用它判断这一位是否为 1
|
||||
2. 右移一位表示丢掉当前最右边的二进制位;多数语言使用运算符 >>
|
||||
3. 完成逐位检查并右移的方法后,再观察:n & (n - 1) 会把 n 中最右边的一个 1 变成 0
|
||||
|
||||
[LeetCode](https://leetcode.cn/problems/number-of-1-bits/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } [题目解析](https://leetcode.cn/problems/number-of-1-bits/solutions/2361978/191-wei-1-de-ge-shu-wei-yun-suan-qing-xi-40rw/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -20,3 +20,4 @@ icon: material/shape-outline
|
||||
- [3.3 数字编码 *](number_encoding.md)
|
||||
- [3.4 字符编码 *](character_encoding.md)
|
||||
- [3.5 小结](summary.md)
|
||||
- [3.6 练习](exercises.md)
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- 此文件由 utils/exercises/publish_exercises.py 根据 exercises.yaml 自动生成,请勿直接修改。 -->
|
||||
|
||||
# 12.6 练习
|
||||
|
||||
## 12.6.1 知识巩固
|
||||
|
||||
### 1. 哪些任务适合分治
|
||||
|
||||
一位同学想用“先分成两半,分别解决,再合并结果”的方法完成以下任务。
|
||||
请分别判断它们是“适合分治”“可以分治,但不会减少总工作量”还是“两半不能独立解决”,并说明理由。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 将一个无序数组排序;
|
||||
2. 求一个数组中的最大值;
|
||||
3. 按顺序执行一串 `push(x)`、`pop()` 栈操作,并输出每次 `pop()` 得到的元素。
|
||||
|
||||
??? success "参考答案"
|
||||
|
||||
1. 适合:对半分解、两半独立排序、$O(n)$ 合并——这正是归并排序。
|
||||
2. 可以分治,但不会减少总工作量:左右两半仍需合计检查全部 $n$ 个元素,
|
||||
和直接扫描一样都是 $O(n)$ 时间。
|
||||
3. 两半不能独立解决:后半段开始时的栈内容取决于前半段执行结果,
|
||||
两半不能在互不知道结果时独立完成。
|
||||
|
||||
### 2. 快速幂是怎样减少计算的
|
||||
|
||||
下面的递归函数用分治计算 $x^n$:
|
||||
|
||||
```python
|
||||
def fast_pow(x, n):
|
||||
if n == 0:
|
||||
return 1
|
||||
half = fast_pow(x, n // 2)
|
||||
if n % 2 == 0:
|
||||
return half * half
|
||||
return half * half * x
|
||||
```
|
||||
|
||||
用它计算 `fast_pow(3, 5)`:
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 递归调用时,参数 `n` 依次变成哪些值?
|
||||
2. 从最深层开始返回时,各层依次返回什么值?
|
||||
3. 为什么要先保存 `half`,而不是把 `fast_pow(x, n // 2)` 写两遍?
|
||||
|
||||
??? success "参考答案"
|
||||
|
||||
1. 参数依次为 `5 → 2 → 1 → 0`,每次都把指数减半,直到到达终止条件。
|
||||
|
||||
2. `n = 0` 时返回 1;`n = 1` 时返回 $1×1×3=3$;
|
||||
`n = 2` 时返回 $3×3=9$;`n = 5` 时返回 $9×9×3=243$。
|
||||
|
||||
3. 如果把 `fast_pow(x, n // 2)` 在乘法两边各写一次,两次递归会计算完全相同的子问题。
|
||||
先把结果保存为 `half`,每层就只递归一次,递归深度约为 $\log n$;
|
||||
调用两次会造成大量重复计算。
|
||||
|
||||
### 3. 从遍历序列拆分左右子树
|
||||
|
||||
一棵没有重复节点的二叉树,其前序遍历和中序遍历分别为:
|
||||
|
||||
- 前序遍历:`[A, B, D, E, C]`
|
||||
- 中序遍历:`[D, B, E, A, C]`
|
||||
|
||||
只完成根节点这一层的拆分,不必继续递归,也不必画出整棵树:
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 根节点是什么?
|
||||
2. 左、右子树在中序遍历中分别对应哪一段?
|
||||
3. 左、右子树在前序遍历中分别对应哪一段?根节点有哪些直接孩子?
|
||||
|
||||
??? success "参考答案"
|
||||
|
||||
1. 前序遍历的第一个节点是根节点,因此根节点为 `A`。
|
||||
|
||||
2. `A` 把中序遍历分成两部分:左子树为 `[D, B, E]`,右子树为 `[C]`。
|
||||
|
||||
3. 左子树有 3 个节点,因此根节点 `A` 后面的 3 个前序元素属于左子树,
|
||||
即 `[B, D, E]`;剩余的 `[C]` 属于右子树。
|
||||
所以根节点的左孩子是 `B`,右孩子是 `C`。
|
||||
|
||||
## 12.6.2 编程练习
|
||||
|
||||
### 1. 快速幂
|
||||
|
||||
给定实数 `x` 和整数 `n`,请计算 $x^n$,且不能调用语言内置的幂函数。
|
||||
要求使用递归分治:每次把指数缩小一半,并复用已经算出的子问题结果。
|
||||
本题规定 $x^0=1$,包括 `x = 0` 时;当 `n < 0` 时保证 `x != 0`,答案可转化为 $(1/x)^{-n}$。
|
||||
|
||||
??? tip "解题提示"
|
||||
|
||||
1. n 等于 0 时答案是 1
|
||||
2. 算出 x 的 n // 2 次方后保存为 half,不要递归调用第二次
|
||||
3. 当 n < 0 时,先把 x 改为 1 / x,再把 n 改为 -n;使用 C++ 或 Java 时,可先把 n 转为 64 位整数,避免最小的 32 位整数取相反数时溢出
|
||||
|
||||
[LeetCode](https://leetcode.cn/problems/powx-n/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } [题目解析](https://leetcode.cn/problems/powx-n/solutions/241471/50-powx-n-kuai-su-mi-qing-xi-tu-jie-by-jyd/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
|
||||
> **说明:** 链接中的题解使用迭代位运算,与本练习指定的递归分治方法不同;请根据本题的解题提示完成递归版本
|
||||
@@ -20,3 +20,4 @@ icon: material/set-split
|
||||
- [12.3 构建树问题](build_binary_tree_problem.md)
|
||||
- [12.4 汉诺塔问题](hanota_problem.md)
|
||||
- [12.5 小结](summary.md)
|
||||
- [12.6 练习](exercises.md)
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- 此文件由 utils/exercises/publish_exercises.py 根据 exercises.yaml 自动生成,请勿直接修改。 -->
|
||||
|
||||
# 14.8 练习
|
||||
|
||||
## 14.8.1 知识巩固
|
||||
|
||||
### 1. 什么时候适合使用动态规划
|
||||
|
||||
一位同学说:“只要能写出递推式,就应该使用动态规划。”
|
||||
请判断下面三个任务分别更适合使用动态规划、回溯,还是使用循环或数学公式而不建立 `dp` 表,并写出一个关键理由。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 使用面值 `[1, 3, 4]` 的硬币凑出金额 6,求最少硬币数,每种硬币可重复使用;
|
||||
2. 输出 `[1, 2, 3]` 的全部排列;
|
||||
3. 计算 $1 + 2 + \dots + n$。
|
||||
|
||||
对于你判断为适合动态规划的任务,再说明 `dp[i]` 表示什么。
|
||||
|
||||
??? success "参考答案"
|
||||
|
||||
1. 适合动态规划。令 `dp[i]` 表示凑出金额 `i` 所需的最少硬币数。
|
||||
对于每枚不超过 `i` 的硬币 `c`,都可以把 `dp[i-c] + 1` 作为候选答案,
|
||||
再从这些候选值中取最小值。不同选择会反复遇到同一金额,较大金额的最优解也能由较小金额的最优解构成。
|
||||
金额 6 的答案是 2,即 `3 + 3`。
|
||||
|
||||
2. 应使用回溯。题目要求逐个生成全部 6 个排列,回溯可以系统地尝试一个选择、继续搜索,
|
||||
再撤销选择并尝试其他分支。无论采用什么方法,实际输出这些排列时都不能跳过枚举。
|
||||
|
||||
3. 使用循环或等差数列公式即可。虽然能写出 `S(i) = S(i-1) + i`,但计算 `S(i)` 时只依赖一个更小的 `S(i-1)`,
|
||||
每个部分和只需计算一次,不存在重复子问题,因此无需建立 `dp` 表。“能写递推式”不等于“需要动态规划”。
|
||||
|
||||
### 2. 背包表中的一个格子怎么算
|
||||
|
||||
0-1 背包:物品重量 `wgt = [1, 2, 3]` ,价值 `val = [5, 11, 15]` ,背包容量 4 。
|
||||
`dp[i][c]` 表示只考虑前 $i$ 件物品、背包容量上限为 $c$ 时,能够取得的最大价值;
|
||||
不要求恰好装满背包。
|
||||
|
||||
现在只计算状态 `dp[3][4]`。已知 `dp[2][4] = 16`、`dp[2][1] = 5`:
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 不选第 3 件物品时,候选价值是多少?
|
||||
2. 选择第 3 件物品时,还剩多少容量,候选价值是多少?
|
||||
3. `dp[3][4]` 应取多少?对应选择了哪些物品?
|
||||
|
||||
??? success "参考答案"
|
||||
|
||||
1. 不选第 3 件物品时,沿用前两件物品的结果,候选价值为 `dp[2][4] = 16`。
|
||||
|
||||
2. 第 3 件物品重量为 3,放入后还剩容量 $4-3=1$;候选价值为
|
||||
`dp[2][1] + 15 = 5 + 15 = 20`。
|
||||
|
||||
3. 比较 16 和 20,得到 `dp[3][4] = 20`。它对应选择第 1、3 件物品,
|
||||
总重量为 $1+3=4$,总价值为 $5+15=20$。
|
||||
|
||||
这个状态的计算体现了 0-1 背包的一次“选或不选”比较。
|
||||
|
||||
### 3. 背包容量应该按什么顺序更新
|
||||
|
||||
一个 0-1 背包只有一件物品:重量为 2,价值为 5;背包容量为 4。
|
||||
每件物品最多选择一次,初始一维数组为 `dp = [0, 0, 0, 0, 0]`。
|
||||
|
||||
一位同学在处理这件物品时,按容量从 2 到 4 更新:
|
||||
|
||||
- 更新 `dp[2]` 后得到 5;
|
||||
- 更新 `dp[3]` 后也得到 5;
|
||||
- 更新 `dp[4]` 时又使用刚得到的 `dp[2]`,于是得到 `dp[4] = 10`。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. `dp[4] = 10` 这个结果是否正确?为什么?
|
||||
2. 根据“每件物品最多选择一次”的条件,`dp[4]` 应为多少?
|
||||
3. 处理每件物品时,容量应从大到小还是从小到大更新?这样做避免了什么问题?
|
||||
|
||||
??? success "参考答案"
|
||||
|
||||
1. 这个结果不正确。价值 10 相当于把这件价值为 5 的物品放入了两次,
|
||||
违反了“每件物品最多选择一次”的条件。
|
||||
|
||||
2. 背包中最多只能放入这一件物品,所以正确的 `dp[4]` 为 5。
|
||||
|
||||
3. 容量应从大到小更新,即依次处理 4、3、2。
|
||||
这样在计算 `dp[c]` 时,所读取的 `dp[c-2]` 仍是处理当前物品之前的结果,
|
||||
不会在同一轮中重复使用当前物品。
|
||||
|
||||
## 14.8.2 编程练习
|
||||
|
||||
### 1. 爬楼梯的方案数
|
||||
|
||||
一段楼梯共有 `n` 阶。你每次只能向上走 1 阶或 2 阶,且必须恰好到达第 `n` 阶。
|
||||
请计算共有多少种不同的走法。规定 `n >= 1`,走法只按每一步走 1 阶还是 2 阶来区分。
|
||||
请使用一维动态规划数组完成,暂不使用只保留两个状态的空间优化写法。
|
||||
|
||||
??? tip "解题提示"
|
||||
|
||||
1. 到达第 i 阶的最后一步,只可能跨 1 阶或 2 阶
|
||||
2. 因此有 dp[i] = dp[i-1] + dp[i-2]
|
||||
3. 先处理 n 为 1 和 2 的情况,再从第 3 阶开始填表
|
||||
|
||||
[LeetCode](https://leetcode.cn/problems/climbing-stairs/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } [题目解析](https://leetcode.cn/problems/climbing-stairs/solutions/2361764/70-pa-lou-ti-dong-tai-gui-hua-qing-xi-tu-ruwa/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
|
||||
> **说明:** 题解讲解了一维 dp 状态和转移,但给出的代码采用滚动变量压缩空间;本练习要求先实现一维 dp 表,滚动变量仅作为选做优化
|
||||
|
||||
### 2. 0-1 背包
|
||||
|
||||
给定等长数组 `wgt` 和 `val`,第 `i` 件物品的重量为正整数 `wgt[i]`、价值为非负整数 `val[i]`,
|
||||
背包容量 `cap` 为非负整数。每件物品最多选择一次,请在总重量不超过 `cap` 的条件下,
|
||||
求能够装入背包的最大总价值。请使用一维动态规划实现。
|
||||
|
||||
??? tip "解题提示"
|
||||
|
||||
1. 初始化长度为 cap + 1 的数组 dp,其中 dp[c] 表示容量上限为 c 时的最大价值
|
||||
2. 处理物品 i 时,比较“不选它”的 dp[c] 与“选它”的 dp[c-wgt[i]] + val[i]
|
||||
3. 容量必须从大到小更新,避免在同一轮中重复选择当前物品
|
||||
@@ -22,3 +22,4 @@ icon: material/table-pivot
|
||||
- [14.5 完全背包问题](unbounded_knapsack_problem.md)
|
||||
- [14.6 编辑距离问题](edit_distance_problem.md)
|
||||
- [14.7 小结](summary.md)
|
||||
- [14.8 练习](exercises.md)
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- 此文件由 utils/exercises/publish_exercises.py 根据 exercises.yaml 自动生成,请勿直接修改。 -->
|
||||
|
||||
# 9.5 练习
|
||||
|
||||
## 9.5.1 知识巩固
|
||||
|
||||
### 1. 用两种方式表示同一张图
|
||||
|
||||
一个无向图有 4 个顶点 `A、B、C、D`,边为
|
||||
`A-B、A-C、B-C、C-D`。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 写出它的邻接表;
|
||||
2. 填写只含 0 和 1 的邻接矩阵;
|
||||
3. 如果要判断 `A` 和 `D` 是否直接相连,哪种图的表示方法只需查看一个存储条目?
|
||||
4. 如果图的顶点很多但边很少,哪种表示通常更节省空间?
|
||||
|
||||
??? success "参考答案"
|
||||
|
||||
1. 邻接表为:
|
||||
|
||||
```text
|
||||
A: B, C
|
||||
B: A, C
|
||||
C: A, B, D
|
||||
D: C
|
||||
```
|
||||
|
||||
2. 邻接矩阵为:
|
||||
|
||||
| | A | B | C | D |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| A | 0 | 1 | 1 | 0 |
|
||||
| B | 1 | 0 | 1 | 0 |
|
||||
| C | 1 | 1 | 0 | 1 |
|
||||
| D | 0 | 0 | 1 | 0 |
|
||||
|
||||
3. 邻接矩阵可直接查看第 `A` 行、第 `D` 列,因此很适合判断任意两点是否直接相连。
|
||||
|
||||
4. 顶点很多但边很少时,邻接表只记录实际存在的边,通常比为每一对顶点都预留位置的邻接矩阵更节省空间。
|
||||
|
||||
### 2. 广度优先与深度优先访问顺序
|
||||
|
||||
一个无向图的顶点为 `A、B、C、D、E`,边为
|
||||
`A-B、A-C、B-D、C-D、D-E`。
|
||||
|
||||
从 A 出发,并规定遇到多个未访问邻接顶点时按字母顺序选择:
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 写出广度优先遍历(BFS)的访问顺序;
|
||||
2. 写出递归深度优先遍历(DFS)的访问顺序;
|
||||
3. 为什么两种遍历都需要记录已经访问过的顶点?
|
||||
|
||||
??? success "参考答案"
|
||||
|
||||
1. BFS 的访问顺序为 `A, B, C, D, E`。它先访问离 A 一条边的 B、C,
|
||||
再访问更远的 D、E。
|
||||
|
||||
2. DFS 的访问顺序为 `A, B, D, C, E`。它依次进入当前顶点的未访问邻接顶点,
|
||||
因此先走过 `A → B → D → C`;C 没有新的邻接顶点时回到 D,再访问 E。
|
||||
|
||||
3. 图中存在环,例如 `A-B-D-C-A`。如果不记录已访问顶点,
|
||||
遍历可能反复沿环访问同一批顶点,无法正常结束。
|
||||
|
||||
### 3. 一次 BFS 能访问整张图吗
|
||||
|
||||
一个无向图有顶点 `A、B、C、D、E、F`,边只有
|
||||
`A-B、B-C、D-E`。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 从 A 开始进行一次 BFS,可以访问哪些顶点?
|
||||
2. 根据第 1 问,这一次 BFS 是否已经访问图中的所有顶点?为什么?
|
||||
3. 若按字母顺序扫描所有顶点,每遇到一个未访问顶点就重新开始 BFS,
|
||||
各次 BFS 的起点是什么?这个图被分成几个互不连通的部分(连通分量)?
|
||||
|
||||
??? success "参考答案"
|
||||
|
||||
1. 从 A 出发只能访问 `A、B、C`。
|
||||
|
||||
2. 没有访问所有顶点。`D、E` 组成另一个互相连通的部分,F 是单独的顶点;
|
||||
它们与 A 之间都没有路径,因此从 A 出发无法到达。
|
||||
|
||||
3. 三次 BFS 的起点依次为 `A、D、F`,分别访问
|
||||
`{A, B, C}`、`{D, E}` 和 `{F}`。因此这个图有 3 个连通分量。
|
||||
|
||||
## 9.5.2 编程练习
|
||||
|
||||
### 1. 判断无向图中是否存在路径
|
||||
|
||||
给定一个含有 $n$ 个顶点的无向图,顶点编号为 $0$ 到 $n-1$。数组 `edges` 中的每一项 `[u, v]` 表示顶点 `u` 和 `v` 之间有一条无向边。
|
||||
|
||||
再给定起点 `source` 和终点 `destination`。请先根据 `edges` 建立邻接表,再使用 BFS 或 DFS
|
||||
判断是否存在一条从 `source` 到 `destination` 的路径:存在则返回 `true`,否则返回 `false`。
|
||||
图中可能有环,也可能不连通。
|
||||
|
||||
??? tip "解题提示"
|
||||
|
||||
1. 每条无向边需要同时加入两个方向
|
||||
2. 图中可能有环,必须记录已经访问过的节点
|
||||
3. 从 source 出发,遇到 destination 返回 true,遍历结束仍未遇到则返回 false
|
||||
|
||||
[LeetCode](https://leetcode.cn/problems/find-if-path-exists-in-graph/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -19,3 +19,4 @@ icon: material/graphql
|
||||
- [9.2 图基础操作](graph_operations.md)
|
||||
- [9.3 图的遍历](graph_traversal.md)
|
||||
- [9.4 小结](summary.md)
|
||||
- [9.5 练习](exercises.md)
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- 此文件由 utils/exercises/publish_exercises.py 根据 exercises.yaml 自动生成,请勿直接修改。 -->
|
||||
|
||||
# 15.6 练习
|
||||
|
||||
## 15.6.1 知识巩固
|
||||
|
||||
### 1. 每次选最大硬币一定最好吗
|
||||
|
||||
硬币面值为 `[1, 7, 10]`,目标金额为 14。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 按“每次取不超过剩余金额的最大面值”执行,写出所取硬币;
|
||||
2. 是否存在使用更少硬币的方案?如果存在,请写出一种;如果不存在,请说明理由;
|
||||
3. 这能否说明该贪心策略对任意硬币面值都正确?
|
||||
|
||||
??? success "参考答案"
|
||||
|
||||
1. 贪心依次选择 `10 + 1 + 1 + 1 + 1`,共 5 枚硬币。
|
||||
|
||||
2. 存在使用更少硬币的方案:`7 + 7`,只需 2 枚硬币。
|
||||
|
||||
3. 不能。这个反例说明,对任意硬币面值,每次选择当前能取的最大面值并不一定得到最少硬币数;
|
||||
眼前最大的选择可能破坏后续更好的组合。
|
||||
|
||||
### 2. 背包应该先装哪件物品
|
||||
|
||||
一个容量为 4 千克的背包中可以装入以下物品,物品允许只取一部分,
|
||||
所取价值与重量成正比:
|
||||
|
||||
- 物品 A:重量 4 千克,价值 20;
|
||||
- 物品 B:重量 3 千克,价值 18。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 两件物品每千克的价值分别是多少?应先装哪一件?
|
||||
2. 按分数背包的贪心策略装满背包,最终价值是多少?
|
||||
3. 在物品可以切分、背包限制总重量的情况下,选择物品时应该比较总价值还是每千克价值?为什么?
|
||||
|
||||
??? success "参考答案"
|
||||
|
||||
1. A 每千克价值为 `20 ÷ 4 = 5`,B 每千克价值为 `18 ÷ 3 = 6`,
|
||||
因此应先装单位价值更高的 B。
|
||||
|
||||
2. 先装入全部 B,占用 3 千克、获得价值 18;剩余 1 千克容量,
|
||||
再装入 1 千克 A,获得价值 5。最终价值为 `18 + 5 = 23`。
|
||||
|
||||
3. 背包限制的是总重量,而且物品可以切分,所以应比较单位重量的价值。
|
||||
A 的总价值虽然更高,但每千克价值低于 B;若先装满 A,只能得到价值 20。
|
||||
|
||||
### 3. 两个指针下一步怎样移动
|
||||
|
||||
隔板高度为 `[1, 8, 6, 2, 5]`,用首尾双指针寻找最大容量。
|
||||
容量等于“两块隔板中较短者的高度 × 两块隔板的下标距离”。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 初始时左指针在下标 0、右指针在下标 4,当前容量是多少?下一步应移动哪个指针?
|
||||
2. 按照第 1 问的选择移动一次后,两个指针分别位于哪个下标?此时容量是多少?下一步又应移动哪个指针?
|
||||
3. 在当前一对隔板中,可以移动较短隔板对应的指针,也可以移动较长隔板对应的指针。哪一种移动仍有可能得到更大的容量?为什么?
|
||||
|
||||
??? success "参考答案"
|
||||
|
||||
1. 当前容量为 `min(1, 5) × (4 - 0) = 4`。左侧隔板较短,因此移动左指针。
|
||||
|
||||
2. 移动左指针后,两个指针分别位于下标 1 和 4。当前容量为
|
||||
`min(8, 5) × (4 - 1) = 15`。右侧隔板较短,因此下一步移动右指针。
|
||||
|
||||
3. 移动较短隔板对应的指针,才仍有可能得到更大的容量。移动较长隔板后,两块隔板的距离一定缩短,而容器高度仍受未移动的短板限制,
|
||||
只可能不变或变小。因此容量不可能超过移动前;只有移动短板,才有机会遇到更高的隔板。
|
||||
|
||||
## 15.6.2 编程练习
|
||||
|
||||
### 1. 分数背包
|
||||
|
||||
给定等长数组 `wgt` 和 `val`,其中 `wgt[i] > 0`、`val[i] >= 0`,背包容量 `cap >= 0`。
|
||||
每种物品只有一件,但允许只装入其中一部分,
|
||||
获得的价值按所装重量占该物品总重量的比例计算。请使用贪心算法,
|
||||
并以实数返回背包能够获得的最大总价值。
|
||||
|
||||
??? tip "解题提示"
|
||||
|
||||
1. 先计算每件物品的单位重量价值 val[i] / wgt[i],除法结果要保留小数
|
||||
2. 单位价值越高的物品越先装入背包
|
||||
3. 若剩余容量小于当前物品重量,就装入恰好填满背包的部分并结束
|
||||
@@ -20,3 +20,4 @@ icon: material/head-heart-outline
|
||||
- [15.3 最大容量问题](max_capacity_problem.md)
|
||||
- [15.4 最大切分乘积问题](max_product_cutting_problem.md)
|
||||
- [15.5 小结](summary.md)
|
||||
- [15.6 练习](exercises.md)
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- 此文件由 utils/exercises/publish_exercises.py 根据 exercises.yaml 自动生成,请勿直接修改。 -->
|
||||
|
||||
# 6.5 练习
|
||||
|
||||
## 6.5.1 知识巩固
|
||||
|
||||
### 1. 发生哈希冲突后怎样查找
|
||||
|
||||
一个哈希表有 5 个桶,哈希函数为 $h(x)=x \bmod 5$,冲突时把元素依次放进该桶的列表中。
|
||||
依次插入 `[1, 6, 11, 7]`:
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 写出 0~4 号桶中的内容;
|
||||
2. 查找 6 时会先进入哪个桶,并依次检查哪些元素?
|
||||
3. 根据第 1 问写出的桶内容,后插入的元素是否覆盖了先插入的元素?结合这种冲突处理方式说明理由。
|
||||
|
||||
??? success "参考答案"
|
||||
|
||||
1. 因为 $1\bmod5=6\bmod5=11\bmod5=1$,而 $7\bmod5=2$,各桶为:
|
||||
|
||||
```text
|
||||
0: []
|
||||
1: [1, 6, 11]
|
||||
2: [7]
|
||||
3: []
|
||||
4: []
|
||||
```
|
||||
|
||||
2. 查找 6 时先进入 1 号桶,再依次比较 1、6,第二次比较时找到目标。
|
||||
|
||||
3. 哈希值相同只表示它们落入同一个桶,并不表示这些元素相等。链式地址把冲突元素都保存在桶内,
|
||||
查询时再逐个比较,因此 1、6、11 不会互相覆盖。
|
||||
|
||||
### 2. 哈希表扩容后元素去哪儿
|
||||
|
||||
一个使用链式地址的哈希表原有 5 个桶,哈希函数为 $h(x)=x\bmod5$。
|
||||
键 `[1, 6, 11]` 都在 1 号桶中。
|
||||
|
||||
现在把哈希表扩容为 7 个桶,哈希函数相应变为 $h(x)=x\bmod7$:
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 分别计算 1、6、11 的新桶号。
|
||||
2. 扩容后哪些桶中有元素?
|
||||
3. 扩容时能否把原来 1 号桶中的列表原样复制到新的 1 号桶?结合第 1、2 问的结果说明理由。
|
||||
|
||||
??? success "参考答案"
|
||||
|
||||
1. 新桶号分别为:
|
||||
|
||||
- $1\bmod7=1$;
|
||||
- $6\bmod7=6$;
|
||||
- $11\bmod7=4$。
|
||||
|
||||
2. 1 号桶保存 1,4 号桶保存 11,6 号桶保存 6。三个键不再挤在同一个桶中。
|
||||
|
||||
3. 不能原样复制。桶号由“键对桶数取余”得到。桶数从 5 变成 7 后,同一个键的新桶号可能改变,
|
||||
因此必须重新计算每个键的位置。若把旧的 1 号桶原样复制过去,之后按新公式查找 6 和 11 时,
|
||||
会分别前往 6 号桶和 4 号桶,从而找不到它们。
|
||||
|
||||
### 3. 删除 6 后还能找到 11 吗
|
||||
|
||||
一个哈希表有 5 个位置,索引为 `0~4`,哈希函数为 $h(x)=x\bmod5$。
|
||||
发生冲突时,从哈希函数算出的索引开始,向右寻找第一个空位。
|
||||
|
||||
依次插入 `[1, 6, 11]`:
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 三个数最终分别放在哪个索引?
|
||||
2. 查找 11 时,会依次检查哪些索引?
|
||||
3. 如果删除 6 时直接把它的位置改成“从未使用的空位”,而查找遇到空位就停止,
|
||||
再查找 11 时会发生什么?这个查找结果是否正确?如果有问题,应怎样避免?
|
||||
|
||||
??? success "参考答案"
|
||||
|
||||
1. 1 放在索引 1。6 也映射到索引 1,发生冲突后放在索引 2。
|
||||
11 同样从索引 1 开始,依次跳过已占用的索引 1、2,最终放在索引 3。
|
||||
|
||||
2. 查找 11 时依次检查索引 `1、2、3`,在索引 3 找到它。
|
||||
|
||||
3. 如果把索引 2 改成表示“从未使用”的空位,查找 11 时检查索引 1 后就会在索引 2 停止,
|
||||
从而错误地认为 11 不存在。删除时应留下“已删除”标记:
|
||||
查找遇到该标记时继续检查下一个索引(越过索引 4 后回到索引 0),而以后的插入仍可重新使用这个位置。
|
||||
|
||||
## 6.5.2 编程练习
|
||||
|
||||
### 1. 比较两个字符串的字符组成
|
||||
|
||||
给定两个只含小写英文字母的字符串 `s` 和 `t`。
|
||||
可以任意调整 `s` 中字符的位置,但不能添加、删除或替换字符。
|
||||
|
||||
请判断调整后能否得到 `t`;可以则返回 `true`,否则返回 `false`。
|
||||
请使用哈希表记录各字母的出现次数,不对字符串中的字符排序。
|
||||
|
||||
??? tip "解题提示"
|
||||
|
||||
1. 两个字符串长度不同,它们的字符组成一定不同
|
||||
2. 用哈希表记录每种字母的数量;扫描 s 时把对应计数加 1
|
||||
3. 扫描 t 时把对应计数减 1;所有计数最终都为 0,字符组成才相同
|
||||
|
||||
[LeetCode](https://leetcode.cn/problems/valid-anagram/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } [题目解析](https://leetcode.cn/problems/valid-anagram/solutions/2362065/242-you-xiao-de-zi-mu-yi-wei-ci-ha-xi-bi-cch7/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -19,3 +19,4 @@ icon: material/table-search
|
||||
- [6.2 哈希冲突](hash_collision.md)
|
||||
- [6.3 哈希算法](hash_algorithm.md)
|
||||
- [6.4 小结](summary.md)
|
||||
- [6.5 练习](exercises.md)
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- 此文件由 utils/exercises/publish_exercises.py 根据 exercises.yaml 自动生成,请勿直接修改。 -->
|
||||
|
||||
# 8.5 练习
|
||||
|
||||
## 8.5.1 知识巩固
|
||||
|
||||
### 1. 数字 10 入堆后怎样调整
|
||||
|
||||
数组 `[9, 7, 8, 3, 5]` 表示一个大顶堆。现在将数字 10 入堆。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 先把 10 添加到数组末尾,它的父节点值是多少?
|
||||
2. 从新节点开始向上堆化,写出每次交换后的数组。
|
||||
3. 最终的堆顶元素是什么?一共交换了几次?
|
||||
|
||||
??? success "参考答案"
|
||||
|
||||
1. 10 添加后的索引为 5,其父节点索引为
|
||||
$\lfloor(5-1)/2\rfloor=2$,父节点值为 8。
|
||||
|
||||
2. 10 大于 8,第一次交换后为 `[9, 7, 10, 3, 5, 8]`;
|
||||
10 又大于父节点 9,第二次交换后为 `[10, 7, 9, 3, 5, 8]`。
|
||||
此时 10 已到达根节点,堆化结束。
|
||||
|
||||
3. 最终堆顶为 10,共交换 2 次。
|
||||
|
||||
### 2. 检查小顶堆的父子关系
|
||||
|
||||
数组 `[1, 4, 3, 7, 6, 2]` 表示一棵完全二叉树。小顶堆要求每个父节点都不大于它的孩子。
|
||||
对索引 $i$,左、右孩子索引分别为 $2i+1$ 和 $2i+2$。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 索引 2 的孩子索引和值分别是什么?
|
||||
2. 索引 2 处的节点值为 3,它与孩子节点之间是否违反小顶堆规则?如果违反,应交换哪两个元素?
|
||||
3. 根据第 2 问的判断:若违反规则,写出交换后的数组;若不违反,说明无须交换。最后检查其余父子关系。
|
||||
|
||||
??? success "参考答案"
|
||||
|
||||
1. 索引 2 的左孩子索引是 5、值为 2;右孩子索引是 6,但数组长度为 6,因此右孩子不存在。
|
||||
|
||||
2. 父节点值 3 大于孩子值 2,违反小顶堆规则,应交换索引 2 和索引 5 的元素。
|
||||
|
||||
3. 交换后得到 `[1, 4, 2, 7, 6, 3]`。逐一检查:
|
||||
`1 ≤ 4`、`1 ≤ 2`;`4 ≤ 7`、`4 ≤ 6`;`2 ≤ 3`。
|
||||
所有父节点都不大于自己的孩子,因此现在满足小顶堆规则。
|
||||
|
||||
### 3. 用小顶堆保留最大的三个数
|
||||
|
||||
要从数据流 `[4, 1, 7, 3, 8]` 中保留最大的 3 个数,可以维护一个大小不超过 3 的小顶堆。
|
||||
|
||||
先把前 3 个数依次放入小顶堆。堆满后,每读入一个新数:
|
||||
如果它大于堆顶,就删除堆顶并放入新数;否则堆保持不变。
|
||||
|
||||
请写出每次读入数字后堆中保留的数字和堆顶。
|
||||
只需把保留的数字写成集合,不要求写出它们在堆数组中的排列。
|
||||
|
||||
??? success "参考答案"
|
||||
|
||||
每次读入数字后的结果如下:
|
||||
|
||||
| 读入数字 | 保留的数字 | 堆顶 |
|
||||
| --- | --- | --- |
|
||||
| 4 | `{4}` | 4 |
|
||||
| 1 | `{1, 4}` | 1 |
|
||||
| 7 | `{1, 4, 7}` | 1 |
|
||||
| 3 | `{3, 4, 7}` | 3 |
|
||||
| 8 | `{4, 7, 8}` | 4 |
|
||||
|
||||
堆满后,堆顶是当前保留数字中最小的一个。只有新数字大于堆顶时,
|
||||
才用新数字替换堆顶。最终保留的 `{4, 7, 8}` 正是最大的 3 个数。
|
||||
|
||||
## 8.5.2 编程练习
|
||||
|
||||
### 1. 寻找数组中的第 k 大元素
|
||||
|
||||
给定整数数组 `nums` 和整数 $k$,其中 $1 \le k \le n$,$n$ 是数组长度。把数组按从大到小排列后,请返回第 $k$ 个位置上的元素。
|
||||
|
||||
重复元素需要分别计数。例如,`[5, 5, 2]` 的第 2 大元素仍是 5。请使用一个大小不超过 $k$ 的小顶堆完成。
|
||||
|
||||
??? tip "解题提示"
|
||||
|
||||
1. 第 k 大元素是最大的 k 个数中最小的那个
|
||||
2. 每个数先入小顶堆,大小超过 k 时弹出最小值
|
||||
3. 遍历结束时堆中保留最大的 k 个数,堆顶就是答案
|
||||
|
||||
[LeetCode](https://leetcode.cn/problems/kth-largest-element-in-an-array/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } [题目解析](https://leetcode.cn/problems/kth-largest-element-in-an-array/solutions/2361969/215-shu-zu-zhong-de-di-k-ge-zui-da-yuan-d786p/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
|
||||
> **说明:** 链接中的题解讲解了快速排序和快速选择,没有使用堆;本练习请按照正文 8.3 节使用大小不超过 k 的小顶堆完成
|
||||
@@ -19,3 +19,4 @@ icon: material/family-tree
|
||||
- [8.2 建堆操作](build_heap.md)
|
||||
- [8.3 Top-k 问题](top_k.md)
|
||||
- [8.4 小结](summary.md)
|
||||
- [8.5 练习](exercises.md)
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- 此文件由 utils/exercises/publish_exercises.py 根据 exercises.yaml 自动生成,请勿直接修改。 -->
|
||||
|
||||
# 10.7 练习
|
||||
|
||||
## 10.7.1 知识巩固
|
||||
|
||||
### 1. 二分查找怎样缩小搜索区间
|
||||
|
||||
在有序数组 `[2, 5, 8, 12, 16, 23, 38]` 中查找 16。
|
||||
使用双闭区间 `[i, j]`,中点为
|
||||
$m=i+(j-i)/2$(向下取整)。
|
||||
|
||||
请写出每轮的 `(i, j, m)`、中点元素,以及下一步如何缩小区间,
|
||||
直到找到目标。
|
||||
|
||||
??? success "参考答案"
|
||||
|
||||
每轮查找过程如下:
|
||||
|
||||
| 轮次 | `(i, j, m)` | 中点元素 | 下一步 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | `(0, 6, 3)` | 12 | `12 < 16`,令 `i = 4` |
|
||||
| 2 | `(4, 6, 5)` | 23 | `23 > 16`,令 `j = 4` |
|
||||
| 3 | `(4, 4, 4)` | 16 | 找到目标,返回索引 4 |
|
||||
|
||||
数组有序,所以中点值小于目标时可以排除中点及其左侧;
|
||||
中点值大于目标时可以排除中点及其右侧。
|
||||
|
||||
### 2. 重复元素的左右边界
|
||||
|
||||
在数组 `[1, 2, 2, 2, 4, 6]` 中查找数字 2。
|
||||
一名同学用二分查找在索引 2 找到目标后立即返回,并说:
|
||||
“索引 2 就是数字 2 的左边界。”
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 这名同学的说法是否正确?数字 2 的左、右边界分别是多少?请说明理由。
|
||||
2. 查找左边界时,若中点元素等于目标,接下来应继续搜索哪一侧?
|
||||
3. 查找右边界时又应继续搜索哪一侧?只需说明方向,无须写出完整的查找过程。
|
||||
|
||||
??? success "参考答案"
|
||||
|
||||
1. 这名同学的说法不正确。找到一个 2 后立即返回,只能保证找到某一个 2,不能保证找到最左或最右的 2。
|
||||
本题的左边界为索引 1,右边界为索引 3。
|
||||
|
||||
2. 查找左边界时,即使中点元素等于 2,也要继续搜索左侧,
|
||||
例如在双闭区间写法中令 `j = m - 1`。
|
||||
|
||||
3. 查找右边界时,中点元素等于 2 后应继续搜索右侧,
|
||||
例如令 `i = m + 1`。
|
||||
|
||||
### 3. 不同数据该选哪种搜索方法
|
||||
|
||||
请在“顺序查找、二分查找、哈希表”中,为下面三个场景选择合适的方法,并说明理由:
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 在 $10^7$ 个已经有序且不再变动的整数中反复查找,不额外建立其他数据结构;
|
||||
2. 在频繁插入、删除的数据集中反复判断某个键是否存在,不要求保持有序,也不进行范围查找;
|
||||
3. 在无序数组中只查找一次某个值。
|
||||
|
||||
??? success "参考答案"
|
||||
|
||||
1. 二分查找:数据有序且静态,$O(\log n)$ 且无须额外空间。
|
||||
2. 哈希表:当哈希函数能把键较均匀地分散到各桶时,插入、删除和按键查找的平均时间复杂度都是 $O(1)$。
|
||||
3. 直接从头到尾遍历:只查找一次时,排序或建立哈希表都要先处理整个数组,
|
||||
并不会减少这一次任务的总工作量。
|
||||
|
||||
选择取决于数据是否有序、是否允许建立额外结构、查询次数以及需要执行哪些操作。
|
||||
|
||||
## 10.7.2 编程练习
|
||||
|
||||
### 1. 有序数组的二分查找
|
||||
|
||||
给定一个按严格递增顺序排列的整数数组 `nums` 和目标值 `target`。请使用二分查找找到 `target`:若存在,返回它的数组索引;若不存在,返回 -1。
|
||||
|
||||
??? tip "解题提示"
|
||||
|
||||
1. 初始区间是 left = 0、right = n - 1,非空条件是 left <= right
|
||||
2. 用 mid = left + (right - left) // 2 计算中点
|
||||
3. 若 `nums[mid]` 小于 `target`,就把左边界移到 `mid + 1`;若 `nums[mid]` 大于 `target`,就把右边界移到 `mid - 1`;相等时立即返回
|
||||
|
||||
[LeetCode](https://leetcode.cn/problems/binary-search/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } [题目解析](https://leetcode.cn/problems/binary-search/solutions/1692151/by-jyd-i7xr/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
|
||||
### 2. 有序数组的插入位置
|
||||
|
||||
给定一个按严格递增顺序排列的整数数组 `nums` 和目标值 `target`。
|
||||
|
||||
若 `target` 已在数组中,返回它的索引;否则,返回把 `target` 插入数组后仍能保持严格递增顺序的位置。答案可能是 0,也可能等于数组长度。请使用二分查找。
|
||||
|
||||
??? tip "解题提示"
|
||||
|
||||
1. 答案可能是 0,也可能是数组长度 n
|
||||
2. 使用双闭区间时,若 `nums[mid]` 大于等于 `target`,就令 `right = mid - 1`,继续检查更靠左的位置;否则令 `left = mid + 1`
|
||||
3. 循环结束时 left 就是插入位置
|
||||
|
||||
[LeetCode](https://leetcode.cn/problems/search-insert-position/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -21,3 +21,4 @@ icon: material/text-search
|
||||
- [10.4 哈希优化策略](replace_linear_by_hashing.md)
|
||||
- [10.5 重识搜索算法](searching_algorithm_revisited.md)
|
||||
- [10.6 小结](summary.md)
|
||||
- [10.7 练习](exercises.md)
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- 此文件由 utils/exercises/publish_exercises.py 根据 exercises.yaml 自动生成,请勿直接修改。 -->
|
||||
|
||||
# 11.12 练习
|
||||
|
||||
## 11.12.1 知识巩固
|
||||
|
||||
### 1. 选择排序和冒泡排序的前几轮
|
||||
|
||||
给定数组 `[4, 2, 5, 1, 3]`,以下都按从小到大排序。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 按选择排序模拟前两轮,写出每轮结束后的数组,并指出此时哪些位置已经确定。
|
||||
2. 按冒泡排序模拟第一轮,写出数组状态和交换次数,并指出哪个位置已经确定。
|
||||
|
||||
??? success "参考答案"
|
||||
|
||||
1. 前两轮为:
|
||||
|
||||
| 轮次 | 数组状态 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 1 | `[1, 2, 5, 4, 3]` | 最小元素 1 与首位交换 |
|
||||
| 2 | `[1, 2, 5, 4, 3]` | 数值 2 已位于索引 1,无须交换 |
|
||||
|
||||
此时前两个位置已经确定,后续只需在 `[5, 4, 3]` 中继续选择最小元素。
|
||||
|
||||
2. 依次比较相邻元素:4 与 2 交换、4 与 5 不换、5 与 1 交换、5 与 3 交换,
|
||||
得 `[2, 4, 1, 3, 5]` ,共 3 次交换;最大元素 5 已移动到数组末尾,因此最后一个位置已经确定。
|
||||
|
||||
### 2. 相同元素的先后次序会变吗
|
||||
|
||||
数组 $[2_a, 2_b, 1]$ 中,$2_a$ 和 $2_b$ 的数值相同,但用下标标出了原有次序。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 写出选择排序第一轮结束后的数组。$2_a$ 和 $2_b$ 的相对次序是否改变?
|
||||
2. 写出冒泡排序第一轮结束后的数组。$2_a$ 和 $2_b$ 的相对次序是否改变?
|
||||
3. 根据前两问,说明两种排序在保持相等元素原有次序方面有什么不同。
|
||||
|
||||
??? success "参考答案"
|
||||
|
||||
1. 选择排序第一轮选出最小元素 1,并与首位 $2_a$ 交换,得
|
||||
$[1, 2_b, 2_a]$ 。$2_a$ 被换到了 $2_b$ 之后,两者的相对次序已经改变。
|
||||
|
||||
2. 冒泡排序先比较 $2_a$ 和 $2_b$,因为两者相等,不交换;再比较 $2_b$ 和 1 并交换,
|
||||
第一轮结束后为 $[2_a, 1, 2_b]$ 。$2_a$ 仍在 $2_b$ 之前,相对次序没有改变。
|
||||
|
||||
3. 这个例子中,选择排序会改变相等元素的原有次序;冒泡排序只在左边元素大于右边元素时
|
||||
交换相邻元素。相等元素之间不交换,因此能保持它们原有的先后次序。
|
||||
|
||||
### 3. 比较计数排序和基数排序
|
||||
|
||||
学校要排序许多固定为 8 位的学号。请回答:
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 从最低位开始的基数排序需要处理几轮?
|
||||
2. 如果把学号直接当作整数使用计数排序,为什么会准备大量没有用到的计数位置?
|
||||
3. 根据前两问,如果在计数排序和基数排序之间选择一种处理大量固定为 8 位的学号,你会选择哪一种?为什么?
|
||||
|
||||
??? success "参考答案"
|
||||
|
||||
1. 学号有 8 个数位,因此从最低位到最高位共处理 8 轮;每轮只按 0~9 分组。
|
||||
|
||||
2. 直接计数需要为所有可能的 8 位取值预留位置,但实际学生只使用其中很少一部分,
|
||||
大多数计数位置会一直为 0。
|
||||
|
||||
3. 应选择基数排序。它利用了“位数固定、每位只有 10 种取值”的特点,只需重复 8 轮稳定分组。
|
||||
计数排序若把整个 8 位学号当作整数下标,则需要为大量实际不会出现的数值预留计数位置。
|
||||
|
||||
## 11.12.2 编程练习
|
||||
|
||||
### 1. 用归并排序排列数组
|
||||
|
||||
给定一个整数数组 `nums`,请自行实现归并排序,把其中的元素按非递减顺序排列并返回。不要调用语言自带的排序函数。
|
||||
|
||||
??? tip "解题提示"
|
||||
|
||||
1. 区间长度不超过 1 时,它已经有序
|
||||
2. 从中点把区间分成两半,并分别递归排序
|
||||
3. 用两个指针合并两个有序半区,再把结果写回原数组
|
||||
|
||||
[LeetCode](https://leetcode.cn/problems/sort-an-array/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
|
||||
### 2. 用计数排序排列整数数组
|
||||
|
||||
给定整数数组 `nums` 和非负整数 $K$,数组中的每个元素都在 $0$ 到 $K$ 之间。
|
||||
|
||||
请实现计数排序,把结果按非递减顺序写回 `nums` 并返回 `nums`。
|
||||
不要使用元素之间的大小比较来决定顺序,也不要调用语言自带的排序函数。
|
||||
|
||||
??? tip "解题提示"
|
||||
|
||||
1. 因为每个元素都在 0 到 K 之间,可以直接用元素值作为计数数组的索引
|
||||
2. 第一遍扫描 nums,令对应位置的计数加 1
|
||||
3. 再从 0 到 K 扫描计数数组;数值 x 出现多少次,就向 nums 中连续写入多少个 x
|
||||
@@ -26,3 +26,4 @@ icon: material/sort-ascending
|
||||
- [11.9 计数排序](counting_sort.md)
|
||||
- [11.10 基数排序](radix_sort.md)
|
||||
- [11.11 小结](summary.md)
|
||||
- [11.12 练习](exercises.md)
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- 此文件由 utils/exercises/publish_exercises.py 根据 exercises.yaml 自动生成,请勿直接修改。 -->
|
||||
|
||||
# 5.5 练习
|
||||
|
||||
## 5.5.1 知识巩固
|
||||
|
||||
### 1. 栈和队列会先取出谁
|
||||
|
||||
准备一个空栈 `S` 和一个空队列 `Q`,分别对它们执行下面同一组操作:
|
||||
|
||||
步骤 1:加入 `A`;
|
||||
步骤 2:加入 `B`;
|
||||
步骤 3:移除一个元素并记录;
|
||||
步骤 4:加入 `C`;
|
||||
步骤 5:不断移除并记录,直到容器为空。
|
||||
|
||||
请分别写出 `S` 和 `Q` 中元素被移除的顺序,并用“先入后出”或“先入先出”解释差异。
|
||||
|
||||
??? success "参考答案"
|
||||
|
||||
栈 `S` 的移除顺序是 `B、C、A`:加入 `A、B` 后先弹出最近加入的 `B`,再加入 `C`,
|
||||
此后依次弹出 `C、A`,体现“先入后出”。
|
||||
|
||||
队列 `Q` 的移除顺序是 `A、B、C`:加入 `A、B` 后先移除最早加入的 `A`,
|
||||
再加入 `C`,此后依次移除 `B、C`,体现“先入先出”。
|
||||
|
||||
### 2. 队尾越过数组末尾怎么办
|
||||
|
||||
用长度为 5 的环形数组实现队列,数组索引为 `0~4`。
|
||||
当前 `front = 3`、`size = 2`,队列中的 `A、B` 分别位于索引 3、4。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 执行“`C` 入队”时,`C` 应放在哪个索引?入队后 `size` 是多少?
|
||||
2. 接着执行一次出队,弹出哪个元素?新的 `front` 和 `size` 分别是多少?
|
||||
3. 此时从队首到队尾的逻辑顺序是什么?出队时是否需要移动数组中的其他元素?为什么?
|
||||
|
||||
??? success "参考答案"
|
||||
|
||||
1. 新元素的位置为
|
||||
`(front + size) % 5 = (3 + 2) % 5 = 0`,
|
||||
所以 `C` 放在索引 0。入队后 `size = 3`。
|
||||
|
||||
2. 出队弹出当前队首 `A`。新的队首索引为
|
||||
`(3 + 1) % 5 = 4`,因此 `front = 4`、`size = 2`。
|
||||
|
||||
3. 有效元素的逻辑顺序为 `B、C`,其中 `B` 位于索引 4,`C` 位于索引 0。
|
||||
出队时只需移动 `front` 并修改 `size`,环形数组用取余让索引回到开头,
|
||||
因此无须把其他元素整体向前移动。
|
||||
|
||||
### 3. 双向队列的两端操作
|
||||
|
||||
这里规定:`push_first` 表示从队首加入,`push_last` 表示从队尾加入,
|
||||
`pop_first` 表示从队首弹出,`pop_last` 表示从队尾弹出。
|
||||
|
||||
对一个空的双向队列 `deq` 依次执行:
|
||||
|
||||
1. `push_last(A)`
|
||||
2. `push_last(B)`
|
||||
3. `push_first(C)`
|
||||
4. `pop_last()`
|
||||
5. `push_last(D)`
|
||||
6. `pop_first()`
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 两次弹出的元素分别是什么?
|
||||
2. 全部操作完成后,从队首到队尾还剩哪些元素?
|
||||
3. 检查这 6 步操作:只允许从队尾加入、从队首删除的队列能否全部完成?如果不能,请指出无法完成的操作;再说明双向队列能否完成及其原因。
|
||||
|
||||
??? success "参考答案"
|
||||
|
||||
前三步后,双向队列从队首到队尾为 `[C, A, B]`。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. `pop_last()` 弹出 `B`;加入 `D` 后队列为 `[C, A, D]`,
|
||||
`pop_first()` 再弹出 `C`。
|
||||
|
||||
2. 最后剩下 `[A, D]`。
|
||||
|
||||
3. 只允许在队尾添加、在队首删除的队列不能完成全部操作:
|
||||
第 3 步 `push_first(C)` 要求从队首加入,第 4 步 `pop_last()` 要求从队尾删除,都超出了这种队列的操作范围。
|
||||
双向队列的两端都可以添加和删除,因此能够完成这 6 步操作。
|
||||
|
||||
## 5.5.2 编程练习
|
||||
|
||||
### 1. 检查括号序列
|
||||
|
||||
给定一个只包含 `()`、`[]`、`{}` 这三类括号的字符串 `s`,请使用栈判断它是否合法。
|
||||
|
||||
合法序列须同时满足:每个右括号都必须与最近一个尚未配对的左括号类型匹配,
|
||||
并且遍历结束后没有未配对的左括号。返回布尔值表示判断结果。
|
||||
|
||||
??? tip "解题提示"
|
||||
|
||||
1. 可以建立“右括号到对应左括号”的映射
|
||||
2. 遇到右括号时,先检查栈是否为空,再检查栈顶是否匹配
|
||||
3. 遍历结束后,栈也必须为空
|
||||
|
||||
[LeetCode](https://leetcode.cn/problems/valid-parentheses/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } [题目解析](https://leetcode.cn/problems/valid-parentheses/solutions/9185/valid-parentheses-fu-zhu-zhan-fa-by-jin407891080/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -19,3 +19,4 @@ icon: material/stack-overflow
|
||||
- [5.2 队列](queue.md)
|
||||
- [5.3 双向队列](deque.md)
|
||||
- [5.4 小结](summary.md)
|
||||
- [5.5 练习](exercises.md)
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- 此文件由 utils/exercises/publish_exercises.py 根据 exercises.yaml 自动生成,请勿直接修改。 -->
|
||||
|
||||
# 7.7 练习
|
||||
|
||||
## 7.7.1 知识巩固
|
||||
|
||||
### 1. 完全、完满与完美二叉树
|
||||
|
||||
下面两个数组按层序表示二叉树,`None` 表示空位:
|
||||
|
||||
- 树 A:`[1, 2, 3, 4, 5, 6]`
|
||||
- 树 B:`[1, 2, 3, None, None, 6, 7]`
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 哪一棵是完全二叉树?
|
||||
2. 哪一棵是完满二叉树,即每个非叶节点都有两个子节点?
|
||||
3. 两棵树中是否有完美二叉树?分别说明理由。
|
||||
|
||||
??? success "参考答案"
|
||||
|
||||
1. 树 A 是完全二叉树。它只有最底层未填满,并且节点从左到右连续排列。
|
||||
树 B 不是完全二叉树,因为最底层左侧已有空位,右侧却仍有节点。
|
||||
|
||||
2. 树 B 是完满二叉树:节点 1 和节点 3 各有两个子节点,其余节点都是叶节点。
|
||||
树 A 不是完满二叉树,因为节点 3 只有左子节点 6。
|
||||
|
||||
3. 两棵都不是完美二叉树,因为它们的最底层都没有完全填满。
|
||||
|
||||
### 2. 同一棵树的三种遍历顺序
|
||||
|
||||
将数组 `[1, 2, 3, 4, 5, 6, 7]` 按层序存入一棵完全二叉树。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 画出这棵树。
|
||||
2. 写出它的前序、中序、后序遍历序列。
|
||||
3. 在中序序列中,根节点 1 左侧和右侧的两段序列,分别对应树的哪一部分?
|
||||
|
||||
??? success "参考答案"
|
||||
|
||||
1. 这棵树为:
|
||||
|
||||
```text
|
||||
1
|
||||
/ \
|
||||
2 3
|
||||
/ \ / \
|
||||
4 5 6 7
|
||||
```
|
||||
|
||||
2. 前序遍历为 `1, 2, 4, 5, 3, 6, 7`;
|
||||
中序遍历为 `4, 2, 5, 1, 6, 3, 7`;
|
||||
后序遍历为 `4, 5, 2, 6, 7, 3, 1`。
|
||||
|
||||
3. 根节点 1 左侧的 `4, 2, 5` 是左子树的中序遍历序列;
|
||||
右侧的 `6, 3, 7` 是右子树的中序遍历序列。
|
||||
|
||||
### 3. 比较两棵二叉搜索树
|
||||
|
||||
将下面两组序列从左到右依次插入空的二叉搜索树:
|
||||
|
||||
- 序列 A:`[4, 2, 6, 1, 3, 5, 7]`
|
||||
- 序列 B:`[1, 2, 3, 4, 5, 6, 7]`
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 分别写出查找数字 7 时经过的节点。
|
||||
2. 若高度按根节点到最远叶节点经过的边数计算,两棵树的高度分别是多少?
|
||||
3. 根据前两问,你认为两棵树查找数字 7 时的效率是否相同?请结合两棵树的形状和查找路径说明理由。
|
||||
|
||||
??? success "参考答案"
|
||||
|
||||
1. 序列 A 建成的树中,查找路径为 `4 → 6 → 7`;
|
||||
序列 B 建成的树中,查找路径为 `1 → 2 → 3 → 4 → 5 → 6 → 7`。
|
||||
|
||||
2. 第一棵树的每一层都填满,高度为 2;第二棵树只有右孩子,高度为 6。
|
||||
|
||||
3. 两棵树查找 7 时的效率不同。插入顺序改变了二叉搜索树的形状和高度。查找 7 时,第一棵树只经过 3 个节点,
|
||||
第二棵树则要依次经过 7 个节点;树越高,最坏情况下需要沿路径比较的节点就越多。
|
||||
|
||||
## 7.7.2 编程练习
|
||||
|
||||
### 1. 二叉树的最大深度
|
||||
|
||||
给定一棵二叉树的根节点 `root`。每个节点都包含一个整数值,以及指向左、右子节点的引用。
|
||||
|
||||
把从根节点到最远叶节点经过的**节点数**称为二叉树的最大深度。请返回这棵树的最大深度;空树的最大深度为 0。
|
||||
请使用递归完成。
|
||||
|
||||
??? tip "解题提示"
|
||||
|
||||
1. 本题按节点数计算深度:只有一个根节点时,最大深度为 1
|
||||
2. 让递归函数返回以当前节点为根的子树的最大深度
|
||||
3. 空节点返回 0,非空节点返回 max(depth(left), depth(right)) + 1
|
||||
|
||||
[LeetCode](https://leetcode.cn/problems/maximum-depth-of-binary-tree/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } [题目解析](https://leetcode.cn/problems/maximum-depth-of-binary-tree/solutions/2361697/104-er-cha-shu-de-zui-da-shen-du-hou-xu-txzrx/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
|
||||
### 2. 按层遍历二叉树
|
||||
|
||||
给定一棵二叉树的根节点 `root`。请使用队列从上到下逐层访问所有节点,同一层内按照从左到右的顺序访问。
|
||||
|
||||
返回一个二维数组:第一个子数组保存根节点所在层的节点值,第二个子数组保存下一层的节点值,依此类推。
|
||||
若二叉树为空,返回空数组。
|
||||
|
||||
??? tip "解题提示"
|
||||
|
||||
1. 层序遍历需要先进入的节点先访问,因此使用队列
|
||||
2. 一轮开始时队列中的节点正好属于同一层
|
||||
3. 先记录队列长度,再弹出这么多个节点并加入它们的孩子
|
||||
|
||||
[LeetCode](https://leetcode.cn/problems/binary-tree-level-order-traversal/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } [题目解析](https://leetcode.cn/problems/binary-tree-level-order-traversal/solutions/2361604/102-er-cha-shu-de-ceng-xu-bian-li-yan-du-dyf7/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
|
||||
### 3. 二叉搜索树中的第 k 小元素
|
||||
|
||||
一棵二叉搜索树共有 `n` 个节点,各节点值互不相同。
|
||||
将所有节点值从小到大排列后,位置从 1 开始编号。
|
||||
|
||||
给定根节点 `root` 和满足 `1 <= k <= n` 的整数 `k`,请返回排在第 `k` 位的节点值。
|
||||
请在中序遍历过程中直接寻找答案,不先收集全部节点值。
|
||||
|
||||
??? tip "解题提示"
|
||||
|
||||
1. 二叉搜索树的中序遍历会按从小到大的顺序访问节点值
|
||||
2. 中序遍历依次处理左子树、当前节点和右子树;访问当前节点时把计数加 1
|
||||
3. 当计数第一次等于 k 时,当前节点值就是答案,此后无须继续遍历
|
||||
|
||||
[LeetCode](https://leetcode.cn/problems/kth-smallest-element-in-a-bst/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } [题目解析](https://leetcode.cn/problems/kth-smallest-element-in-a-bst/solutions/2361685/230-er-cha-sou-suo-shu-zhong-di-k-xiao-d-n3he/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -21,3 +21,4 @@ icon: material/graph-outline
|
||||
- [7.4 二叉搜索树](binary_search_tree.md)
|
||||
- [7.5 AVL 树 *](avl_tree.md)
|
||||
- [7.6 小结](summary.md)
|
||||
- [7.7 练习](exercises.md)
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- Generated by utils/exercises/publish_exercises.py; do not edit directly. -->
|
||||
|
||||
# 4.6 Exercises
|
||||
|
||||
## 4.6.1 Concept Review
|
||||
|
||||
### 1. How Arrays and Linked Lists Find an Element
|
||||
|
||||
An array and a singly linked list both store `[A, B, C, D, E]` in order. You now need to access the fourth element, `D`:
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Which index can the array use directly?
|
||||
2. Starting from the head node `A`, which nodes are visited in order as you follow `next`?
|
||||
3. As the requested element moves farther toward the end, how does the number of steps change for each structure? Which structure is better for repeated access by position, and why?
|
||||
|
||||
??? success "Answer"
|
||||
|
||||
1. With zero-based indexing, the fourth element has index 3, so the array can access `arr[3]` directly.
|
||||
|
||||
2. The singly linked list must start at the head. Its access path is `A → B → C → D`, requiring three moves through `next`.
|
||||
|
||||
3. An array can locate an element directly from its starting address and index, so access by position has a time complexity of $O(1)$.
|
||||
To access the $k$th node, a singly linked list must start at the head node and follow `next` $k-1$ times,
|
||||
which takes $O(n)$ time in the worst case.
|
||||
|
||||
This comparison concerns only access by position; it does not mean that linked lists are slower for every operation.
|
||||
|
||||
### 2. How Arrays and Linked Lists Insert an Element
|
||||
|
||||
An array and a singly linked list both store `A, B, C, D`. You now need to insert `X` after `B`:
|
||||
|
||||
- The array has capacity 5 and is currently `[A, B, C, D, _]`.
|
||||
- The linked list is `A → B → C → D`, and you already have a reference to node `B`.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Which elements must the array move? Write the array after insertion.
|
||||
2. In what order should the linked list update `X.next` and `B.next`? Write the linked list after insertion.
|
||||
3. When comparing insertion efficiency, why is it important to state that "you already have a reference to node B"?
|
||||
|
||||
??? success "Answer"
|
||||
|
||||
1. The array first moves `D` one position to the right, then moves `C` one position to the right, and finally places `X` at index 2,
|
||||
producing `[A, B, X, C, D]`.
|
||||
|
||||
2. `B.next` originally points to `C`. First set `X.next = B.next`, making `X` point to `C`;
|
||||
then set `B.next = X`. The result is `A → B → X → C → D`.
|
||||
If `B.next` is overwritten first without saving the original link, `C` may become unreachable.
|
||||
|
||||
3. Once the location of `B` is known, insertion into the linked list only changes two links and takes $O(1)$ time.
|
||||
If `B` must first be found from the head, that search alone may take $O(n)$ time.
|
||||
|
||||
### 3. How a List's Capacity Grows
|
||||
|
||||
An array-based list currently contains `[A, B, C]`, with length `size = 3` and capacity `capacity = 4`.
|
||||
When there is not enough capacity, a new array with twice the old capacity is created.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. After appending `D`, what are the list's length and capacity? Is capacity expansion needed?
|
||||
2. When `E` is appended next, what does the capacity become? How many existing elements must be copied?
|
||||
3. The length of the underlying array cannot change. Why does the list's capacity nevertheless appear able to grow?
|
||||
|
||||
??? success "Answer"
|
||||
|
||||
1. `D` fits in the last empty position. The contents are now `[A, B, C, D]`,
|
||||
with `size = 4` and `capacity = 4`, so no capacity expansion is needed.
|
||||
|
||||
2. There is no empty position when `E` is appended, so a new array with capacity 8 must be created.
|
||||
The four existing elements `A, B, C, D` are copied into it before `E` is added.
|
||||
The result is `size = 5` and `capacity = 8`.
|
||||
|
||||
3. The original array itself does not grow. The list creates a larger new array, copies the existing elements,
|
||||
and then uses the new array as its underlying storage. From the user's perspective, the capacity has grown.
|
||||
|
||||
## 4.6.2 Programming Exercises
|
||||
|
||||
### 1. Add One to a Large Integer Stored as an Array
|
||||
|
||||
The array `digits` stores the decimal digits of a non-negative integer from left to right. For example, `[3, 0, 8]` represents 308.
|
||||
The number 0 is represented by `[0]`; for every other input, the first digit is not 0.
|
||||
|
||||
Simulate decimal column addition to add 1 to this integer, and return the result in the same array format.
|
||||
You may modify `digits` directly. If a new carry appears at the front, you may return a longer array.
|
||||
|
||||
??? tip "Hints"
|
||||
|
||||
1. Start with the last digit of the array, just as in ordinary column addition
|
||||
2. If the current digit is less than 9, add one and return immediately
|
||||
3. If the current digit is 9, change it to 0; if every digit is 9, place a 1 at the front
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/plus-one/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
|
||||
### 2. Reverse a Singly Linked List
|
||||
|
||||
You are given the head node `head` of a singly linked list. Each node contains a value and a `next` reference to the following node.
|
||||
|
||||
Use an iterative approach to reverse all links between the nodes, and return the new head node.
|
||||
Do not create any new linked list nodes.
|
||||
|
||||
??? tip "Hints"
|
||||
|
||||
1. First draw three connected nodes and the two pointers prev and cur on paper
|
||||
2. Before changing cur.next, save the original next node in nxt
|
||||
3. After reversing cur.next, set prev = cur and then cur = nxt to continue with the next node in the original list
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/reverse-linked-list/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -20,3 +20,4 @@ icon: material/view-list-outline
|
||||
- [4.3 List](list.md)
|
||||
- [4.4 Random-Access Memory and Cache *](ram_and_cache.md)
|
||||
- [4.5 Summary](summary.md)
|
||||
- [4.6 Exercises](exercises.md)
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- Generated by utils/exercises/publish_exercises.py; do not edit directly. -->
|
||||
|
||||
# 13.6 Exercises
|
||||
|
||||
## 13.6.1 Concept Review
|
||||
|
||||
### 1. Will This Permutation Algorithm Miss Results?
|
||||
|
||||
A backtracking algorithm tries to generate all permutations using `1, 2, 3` in that order. Each time it chooses a number `x`, it:
|
||||
|
||||
1. Appends `x` to the current path.
|
||||
2. Marks `x` as "used."
|
||||
3. Recursively fills the next position.
|
||||
|
||||
When the recursive call returns, the student removes only `x` from the end of the path and then tries the next number.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Which permutation does the algorithm generate first? Can it still generate all 6 permutations?
|
||||
2. Before returning to the previous level, is removing only the last number from the path enough? If not, what else must be done? Explain why.
|
||||
|
||||
??? success "Answer"
|
||||
|
||||
1. It first generates `[1, 2, 3]`, but it cannot generate all the permutations. Although the path becomes shorter when calls return,
|
||||
the markers for 1, 2, and 3 all remain "used," leaving no available number for later branches.
|
||||
|
||||
2. It is not enough. After removing `x` from the end of the path, the algorithm must also mark `x` as "unused" again.
|
||||
The current path and the used markers together describe the search state. A choice changes both of them, so backtracking must restore both
|
||||
before another branch can choose `x` again.
|
||||
|
||||
### 2. Does the Order of Choosing Numbers Matter?
|
||||
|
||||
You are given the sorted array `[2, 3, 5]` and target value 5. Each number may be chosen repeatedly.
|
||||
The algorithm requires the numbers along each search path to appear only in nondecreasing order.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Which distinct combinations can be found?
|
||||
2. Why is there no need to search for the same group of numbers in different orders? What does the nondecreasing-order restriction accomplish?
|
||||
3. Suppose the current path is `[3]`, the remaining amount is 2, and the next candidate is 3. Why can the algorithm stop checking all later candidates at this level?
|
||||
|
||||
??? success "Answer"
|
||||
|
||||
1. The distinct combinations are `[2, 3]` and `[5]`.
|
||||
|
||||
2. This exercise treats `[2, 3]` and `[3, 2]` as the same combination; the order in which numbers are selected does not create a different answer.
|
||||
Requiring numbers in a path to appear in nondecreasing order lets the search skip duplicate arrangements such as `[3, 2]`.
|
||||
|
||||
3. The remaining amount is 2, while candidate 3 is already greater than 2. Because the array is sorted,
|
||||
all later candidates are even larger and cannot be added to the current combination, so the algorithm can stop checking this level immediately.
|
||||
|
||||
### 3. Where Can the Next Queen Be Placed?
|
||||
|
||||
Place queens row by row on a `4 × 4` chessboard, with both row and column indices starting at 0.
|
||||
Queens have already been placed at `(0, 1)` and `(1, 3)`. The next queen must be placed in row 2.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Which columns are ruled out because they already contain a queen?
|
||||
2. Among the remaining columns, which positions are ruled out because they share a diagonal with an existing queen?
|
||||
3. Which positions in row 2 remain available to try?
|
||||
|
||||
??? success "Answer"
|
||||
|
||||
1. Columns 1 and 3 already contain queens, so positions `(2, 1)` and `(2, 3)` are ruled out.
|
||||
|
||||
2. Among the remaining positions, `(2, 2)` lies on the same diagonal as `(1, 3)`, so it is also ruled out.
|
||||
Position `(2, 0)` shares neither a column nor a diagonal with either existing queen.
|
||||
|
||||
3. The only position to try in row 2 is `(2, 0)`.
|
||||
|
||||
This step shows only that the current placement is valid. If the board cannot be completed later, the algorithm must still backtrack and try an earlier alternative.
|
||||
|
||||
## 13.6.2 Programming Exercises
|
||||
|
||||
### 1. Permutations of Distinct Elements
|
||||
|
||||
The integer array `nums` contains at least one element, and all its elements are distinct.
|
||||
List every possible order formed by using each element exactly once, and return each order as an array.
|
||||
The permutations may appear in any order in the result.
|
||||
Use backtracking, with a Boolean array recording whether the element at each position has already been selected for the current permutation.
|
||||
|
||||
??? tip "Hints"
|
||||
|
||||
1. The recursion depth indicates which position of the permutation is currently being filled
|
||||
2. At each level, try only elements that have not yet been used
|
||||
3. When the path's length equals the length of `nums`, add a copy of the path to the answer
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/permutations/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -20,3 +20,4 @@ icon: material/map-marker-path
|
||||
- [13.3 Subset-Sum Problem](subset_sum_problem.md)
|
||||
- [13.4 N-Queens Problem](n_queens_problem.md)
|
||||
- [13.5 Summary](summary.md)
|
||||
- [13.6 Exercises](exercises.md)
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- Generated by utils/exercises/publish_exercises.py; do not edit directly. -->
|
||||
|
||||
# 2.6 Exercises
|
||||
|
||||
## 2.6.1 Concept Review
|
||||
|
||||
### 1. Time and Space Complexity of Iteration and Recursion
|
||||
|
||||
The two functions below both calculate $1 + 2 + \dots + n$ (assume $n \ge 1$). Set `n` to 4,
|
||||
answer the questions by following the program's actual execution order, and then compare the efficiency of the two approaches.
|
||||
|
||||
```python
|
||||
def sum_iter(n):
|
||||
s = 0
|
||||
for i in range(1, n + 1):
|
||||
s += i
|
||||
return s
|
||||
|
||||
def sum_recur(n):
|
||||
if n == 1:
|
||||
return 1
|
||||
return n + sum_recur(n - 1)
|
||||
```
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. When `sum_iter(4)` runs, what is the value of `s` after each loop iteration?
|
||||
2. When `sum_recur(4)` runs, which function calls occur in order? As the calls return from the deepest level, how is the result obtained?
|
||||
3. What are the time and space complexities of the two approaches? Explain your reasoning using the execution processes from Questions 1 and 2.
|
||||
|
||||
??? success "Answer"
|
||||
|
||||
1. The loop variable `i` takes the values `1, 2, 3, 4`. After each iteration, `s` becomes
|
||||
`1, 3, 6, 10`, respectively, so `sum_iter(4)` returns 10.
|
||||
|
||||
2. The function calls occur in this order:
|
||||
`sum_recur(4) → sum_recur(3) → sum_recur(2) → sum_recur(1)`.
|
||||
`sum_recur(1)` returns 1. The remaining calls then obtain `2 + 1 = 3`, `3 + 3 = 6`, and `4 + 6 = 10`, in that order.
|
||||
At the deepest point, all four function calls are still unfinished.
|
||||
|
||||
3. Both functions perform a number of loop iterations or calls proportional to $n$, so both have a time complexity of $O(n)$.
|
||||
Their space complexities differ. The iterative version uses only a constant number of variables, so its space complexity is $O(1)$.
|
||||
In the recursive version, earlier calls must wait for a result before returning, so the call stack holds up to $n$ calls at the same time.
|
||||
Its space complexity is $O(n)$.
|
||||
|
||||
When analyzing space complexity, remember to include the space used by recursive calls as well as the variables written in the code.
|
||||
|
||||
### 2. Time Complexity of Three Code Fragments
|
||||
|
||||
Each of the following code fragments takes a positive integer $n$ as input. Order them from lowest to highest time complexity, and give the complexity of each one.
|
||||
|
||||
```python
|
||||
# Fragment 1
|
||||
s = 0
|
||||
for i in range(n):
|
||||
s += i
|
||||
|
||||
# Fragment 2
|
||||
s = 0
|
||||
for i in range(n):
|
||||
for j in range(i, n):
|
||||
s += j
|
||||
|
||||
# Fragment 3
|
||||
while n > 1:
|
||||
n = n // 2
|
||||
```
|
||||
|
||||
??? success "Answer"
|
||||
|
||||
From lowest to highest, the order is Fragment 3 with $O(\log n)$, Fragment 1 with $O(n)$, and Fragment 2 with $O(n^2)$.
|
||||
Fragment 3 halves $n$ in each iteration, so it runs about $\log_2 n$ times.
|
||||
The loop in Fragment 1 runs exactly $n$ times. The inner loop in Fragment 2 runs
|
||||
$n,n-1,\dots,1$ times, for a total of $n(n+1)/2$, so its time complexity is quadratic.
|
||||
|
||||
### 3. Which Reversal Uses Less Space?
|
||||
|
||||
There are two ways to reverse all the elements in the array `nums`:
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Create a new array `res` of the same length, copy the elements into it in reverse order, and return it.
|
||||
2. Move two indices `i` and `j` inward from the beginning and end, swapping `nums[i]` and `nums[j]` at each step.
|
||||
|
||||
What is the space complexity of each approach? Which one is an "in-place" operation?
|
||||
|
||||
??? success "Answer"
|
||||
|
||||
1. This approach needs an auxiliary array with the same length as the input, so its space complexity is $O(n)$.
|
||||
|
||||
2. This approach uses only two index variables,
|
||||
so its space complexity is $O(1)$. It is an in-place operation.
|
||||
|
||||
Note that an in-place reversal changes the input array,
|
||||
so it should be preferred only when modifying the input is allowed. If the original array must be kept, the copying cost of the first approach is unavoidable.
|
||||
|
||||
## 2.6.2 Programming Exercises
|
||||
|
||||
### 1. Fibonacci Number
|
||||
|
||||
The Fibonacci sequence is defined by $F(0)=0$, $F(1)=1$, and, for $n\ge2$,
|
||||
$F(n)=F(n-1)+F(n-2)$.
|
||||
|
||||
Given a non-negative integer `n`, use a loop to calculate and return $F(n)$. Do not use recursion.
|
||||
|
||||
??? tip "Hints"
|
||||
|
||||
1. Handle the cases where n is 0 or 1 separately
|
||||
2. Only the previous two terms are needed to calculate the next term; there is no need to store the entire sequence
|
||||
3. When updating the two variables, take care not to overwrite an old value before it is used
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/fibonacci-number/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -20,3 +20,4 @@ icon: material/timer-sand
|
||||
- [2.3 Time Complexity](time_complexity.md)
|
||||
- [2.4 Space Complexity](space_complexity.md)
|
||||
- [2.5 Summary](summary.md)
|
||||
- [2.6 Exercises](exercises.md)
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- Generated by utils/exercises/publish_exercises.py; do not edit directly. -->
|
||||
|
||||
# 3.6 Exercises
|
||||
|
||||
## 3.6.1 Concept Review
|
||||
|
||||
### 1. Data Relationships in Everyday Situations
|
||||
|
||||
Based on the relationships among the data, choose one of "linear structure," "tree structure," and "network structure" for each situation below, and explain why:
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Students stand in a line; we consider only each student's immediate neighbors in front of and behind them.
|
||||
2. A school is organized in levels: "school → grade → class."
|
||||
3. City roads connect many intersections. One intersection may lead to several others, and the roads may form cycles.
|
||||
|
||||
??? success "Answer"
|
||||
|
||||
1. This is a linear structure. Except for the first and last students, each person is adjacent to exactly one person in front and one behind, so the relationships extend along a single line.
|
||||
|
||||
2. This is a tree structure. Each class belongs to one grade, and each grade belongs to the school, so the relationships form levels from top to bottom.
|
||||
|
||||
3. This is a network structure. An intersection can connect to several other intersections, and the routes can form cycles, so they cannot be arranged in a single order or a strict hierarchy.
|
||||
|
||||
To identify a structure, first examine the relationships among the elements rather than how much space it uses in memory.
|
||||
|
||||
### 2. Storing a Logical Order in Memory
|
||||
|
||||
Consider two simplified memory layouts for storing the logical order `A → B → C`:
|
||||
|
||||
- Layout A: `A, B, C` are stored in memory cells numbered `20, 21, 22`, respectively.
|
||||
- Layout B: `A, B, C` are stored in memory cells numbered `20, 7, 31`, respectively. `A` records the location of `B`, and `B` records the location of `C`.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Which layout uses contiguous-space storage, and which uses dispersed-space storage?
|
||||
2. Which layout is more like an array, and which is more like a linked list?
|
||||
3. The memory-cell numbers in Layout B are not in increasing order. Why can it still represent the logical order `A → B → C`?
|
||||
|
||||
??? success "Answer"
|
||||
|
||||
1. Layout A uses consecutive memory cells, so it uses contiguous-space storage. The nodes in Layout B are scattered across different locations, so it uses dispersed-space storage.
|
||||
|
||||
2. Layout A is more like an array, and Layout B is more like a linked list.
|
||||
|
||||
3. The logical order is determined by the links recorded between nodes, not by the numerical order of the memory-cell numbers.
|
||||
The location stored by `A` leads to `B`, and the location stored by `B` then leads to `C`, so `A, B, C` can still be visited in order.
|
||||
|
||||
This also shows that logical structure and physical structure are two different ways of viewing the same data.
|
||||
|
||||
### 3. Data Types and Structures in Homework Records
|
||||
|
||||
A study group records, in seating order, whether each of four students submitted their homework:
|
||||
|
||||
`[true, false, true, true]`
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Which basic data type is suitable for each element?
|
||||
2. The four elements are arranged in a line by seating order. What logical structure does this use?
|
||||
3. Suppose the group later records each student's homework score as `[90, 0, 85, 100]`. Does this change the data's "content type" or its "organization"? Explain why.
|
||||
|
||||
??? success "Answer"
|
||||
|
||||
1. Each element represents only "yes" or "no," so the Boolean type `bool` is suitable.
|
||||
|
||||
2. The elements are arranged in seating order, forming a linear structure that can be stored in an array.
|
||||
|
||||
3. The content type changes: the elements change from Boolean values to integers. The organization does not change.
|
||||
The data is still arranged in a line by seating order and can still be stored in an array, which is a linear structure.
|
||||
|
||||
A basic data type describes "what is stored," while a data structure describes "how the data is organized."
|
||||
|
||||
## 3.6.2 Programming Exercises
|
||||
|
||||
### 1. Count the 1s in a Binary Representation
|
||||
|
||||
Given a non-negative integer `n`, count the number of 1s in its binary representation.
|
||||
|
||||
Use bitwise operations. Do not convert the binary representation to a string or use a built-in function that directly counts 1s.
|
||||
|
||||
??? tip "Hints"
|
||||
|
||||
1. n & 1 extracts the rightmost bit of n, which tells you whether that bit is 1
|
||||
2. Shifting right by one discards the current rightmost bit; most languages use the operator >>
|
||||
3. After implementing the method that checks and shifts one bit at a time, observe that n & (n - 1) turns the rightmost 1 in n into 0
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/number-of-1-bits/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -20,3 +20,4 @@ icon: material/shape-outline
|
||||
- [3.3 Number Encoding *](number_encoding.md)
|
||||
- [3.4 Character Encoding *](character_encoding.md)
|
||||
- [3.5 Summary](summary.md)
|
||||
- [3.6 Exercises](exercises.md)
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- Generated by utils/exercises/publish_exercises.py; do not edit directly. -->
|
||||
|
||||
# 12.6 Exercises
|
||||
|
||||
## 12.6.1 Concept Review
|
||||
|
||||
### 1. Which Tasks Are Suitable for Divide and Conquer?
|
||||
|
||||
A student wants to solve each task below by "dividing it into two halves, solving each half separately, and then combining the results."
|
||||
Classify each task as "suitable for divide and conquer," "can use divide and conquer, but it will not reduce the total work," or "the two halves cannot be solved independently," and explain why.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Sort an unsorted array.
|
||||
2. Find the maximum value in an array.
|
||||
3. Execute a sequence of `push(x)` and `pop()` stack operations in order, and output the element returned by each `pop()`.
|
||||
|
||||
??? success "Answer"
|
||||
|
||||
1. Suitable: divide the array in half, sort each half independently, and merge them in $O(n)$ time. This is exactly merge sort.
|
||||
2. Divide and conquer can be used, but it does not reduce the total work. The two halves still require examining all $n$ elements in total,
|
||||
so the time complexity remains $O(n)$, just like a direct scan.
|
||||
3. The two halves cannot be solved independently. The stack's contents at the beginning of the second half depend on the result of executing the first half,
|
||||
so the two halves cannot be completed without knowing each other's results.
|
||||
|
||||
### 2. How Exponentiation by Squaring Reduces Computation
|
||||
|
||||
The recursive function below uses divide and conquer to calculate $x^n$:
|
||||
|
||||
```python
|
||||
def fast_pow(x, n):
|
||||
if n == 0:
|
||||
return 1
|
||||
half = fast_pow(x, n // 2)
|
||||
if n % 2 == 0:
|
||||
return half * half
|
||||
return half * half * x
|
||||
```
|
||||
|
||||
Use it to calculate `fast_pow(3, 5)`:
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. As the recursive calls proceed, which values does the argument `n` take in order?
|
||||
2. Starting from the deepest call, what value does each level return?
|
||||
3. Why should the result be stored in `half` instead of writing `fast_pow(x, n // 2)` twice?
|
||||
|
||||
??? success "Answer"
|
||||
|
||||
1. The argument takes the values `5 → 2 → 1 → 0`. The exponent is halved at each step until the base case is reached.
|
||||
|
||||
2. When `n = 0`, the function returns 1. When `n = 1`, it returns $1×1×3=3$.
|
||||
When `n = 2`, it returns $3×3=9$. When `n = 5`, it returns $9×9×3=243$.
|
||||
|
||||
3. If `fast_pow(x, n // 2)` were written once on each side of the multiplication, the two recursive calls would calculate exactly the same subproblem.
|
||||
Storing the result in `half` means that each level makes only one recursive call, so the recursion depth is about $\log n$.
|
||||
Making two calls would cause a great deal of repeated computation.
|
||||
|
||||
### 3. Split Traversal Sequences into Left and Right Subtrees
|
||||
|
||||
A binary tree has no duplicate nodes. Its preorder and inorder traversals are:
|
||||
|
||||
- Preorder: `[A, B, D, E, C]`
|
||||
- Inorder: `[D, B, E, A, C]`
|
||||
|
||||
Split the sequences only once, at the root. You do not need to continue recursively or draw the whole tree:
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Which node is the root?
|
||||
2. Which subsequences of the inorder traversal correspond to the left and right subtrees?
|
||||
3. Which subsequences of the preorder traversal correspond to the left and right subtrees? Which nodes are the root's direct children?
|
||||
|
||||
??? success "Answer"
|
||||
|
||||
1. The first node in a preorder traversal is the root, so the root is `A`.
|
||||
|
||||
2. `A` divides the inorder traversal into two parts: `[D, B, E]` for the left subtree and `[C]` for the right subtree.
|
||||
|
||||
3. The left subtree contains 3 nodes, so the 3 preorder elements after the root `A` belong to the left subtree,
|
||||
giving `[B, D, E]`. The remaining `[C]` belongs to the right subtree.
|
||||
Therefore, the root's left child is `B`, and its right child is `C`.
|
||||
|
||||
## 12.6.2 Programming Exercises
|
||||
|
||||
### 1. Exponentiation by Squaring
|
||||
|
||||
Given a real number `x` and an integer `n`, calculate $x^n$ without calling the language's built-in power function.
|
||||
Use recursive divide and conquer: halve the exponent at each step and reuse the result of the subproblem already calculated.
|
||||
This exercise defines $x^0=1$, including when `x = 0`. When `n < 0`, `x != 0` is guaranteed, and the answer can be transformed into $(1/x)^{-n}$.
|
||||
|
||||
??? tip "Hints"
|
||||
|
||||
1. When n is 0, the answer is 1
|
||||
2. After calculating x to the power n // 2, store the result in half rather than making the recursive call a second time
|
||||
3. When n < 0, first change x to 1 / x and then change n to -n; in C++ or Java, first convert n to a 64-bit integer to avoid overflow when negating the smallest 32-bit integer
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/powx-n/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -20,3 +20,4 @@ icon: material/set-split
|
||||
- [12.3 Building a Binary Tree Problem](build_binary_tree_problem.md)
|
||||
- [12.4 Hanota Problem](hanota_problem.md)
|
||||
- [12.5 Summary](summary.md)
|
||||
- [12.6 Exercises](exercises.md)
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- Generated by utils/exercises/publish_exercises.py; do not edit directly. -->
|
||||
|
||||
# 14.8 Exercises
|
||||
|
||||
## 14.8.1 Concept Review
|
||||
|
||||
### 1. When Is Dynamic Programming Appropriate?
|
||||
|
||||
A student says, "Whenever a recurrence can be written, dynamic programming should be used."
|
||||
For each task below, decide whether dynamic programming, backtracking, or a loop or mathematical formula without a `dp` table is more appropriate. Give one key reason.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Using coin denominations `[1, 3, 4]`, make an amount of 6 with the fewest coins. Each denomination may be used repeatedly.
|
||||
2. Output all permutations of `[1, 2, 3]`.
|
||||
3. Calculate $1 + 2 + \dots + n$.
|
||||
|
||||
For the task you consider suitable for dynamic programming, also state what `dp[i]` represents.
|
||||
|
||||
??? success "Answer"
|
||||
|
||||
1. Dynamic programming is suitable. Let `dp[i]` be the minimum number of coins needed to make amount `i`.
|
||||
For every coin `c` that does not exceed `i`, `dp[i-c] + 1` is a candidate answer,
|
||||
and the minimum of these candidates is chosen. Different choices repeatedly encounter the same amounts, and an optimal solution for a larger amount can be built from optimal solutions for smaller amounts.
|
||||
The answer for amount 6 is 2, using `3 + 3`.
|
||||
|
||||
2. Backtracking is suitable. The task requires generating all 6 permutations one by one. Backtracking can systematically make a choice, continue searching,
|
||||
undo the choice, and then try another branch. Regardless of the method, actually outputting every permutation requires enumerating them.
|
||||
|
||||
3. A loop or the arithmetic-series formula is sufficient. Although the recurrence `S(i) = S(i-1) + i` can be written, calculating `S(i)` depends on only one smaller value, `S(i-1)`.
|
||||
Each partial sum needs to be calculated only once, so there are no repeated subproblems and no need for a `dp` table. "A recurrence can be written" does not mean "dynamic programming is needed."
|
||||
|
||||
### 2. Calculating One Cell in a Knapsack Table
|
||||
|
||||
Consider this 0-1 knapsack problem: item weights `wgt = [1, 2, 3]`, values `val = [5, 11, 15]`, and knapsack capacity 4.
|
||||
`dp[i][c]` is the maximum value obtainable using only the first $i$ items with a knapsack capacity limit of $c$;
|
||||
the knapsack does not have to be filled exactly.
|
||||
|
||||
Calculate only the state `dp[3][4]`. You are given `dp[2][4] = 16` and `dp[2][1] = 5`:
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. If the third item is not selected, what is the candidate value?
|
||||
2. If the third item is selected, how much capacity remains, and what is the candidate value?
|
||||
3. What should `dp[3][4]` be? Which items does this value correspond to selecting?
|
||||
|
||||
??? success "Answer"
|
||||
|
||||
1. If the third item is not selected, keep the result from the first two items. The candidate value is `dp[2][4] = 16`.
|
||||
|
||||
2. The third item has weight 3, so capacity $4-3=1$ remains after it is placed in the knapsack. The candidate value is
|
||||
`dp[2][1] + 15 = 5 + 15 = 20`.
|
||||
|
||||
3. Comparing 16 and 20 gives `dp[3][4] = 20`. This corresponds to selecting the first and third items,
|
||||
whose total weight is $1+3=4$ and total value is $5+15=20$.
|
||||
|
||||
This state calculation demonstrates one "select or do not select" comparison in the 0-1 knapsack problem.
|
||||
|
||||
### 3. In Which Order Should Knapsack Capacities Be Updated?
|
||||
|
||||
A 0-1 knapsack problem has only one item, with weight 2 and value 5, and the knapsack has capacity 4.
|
||||
The item can be selected at most once. The initial one-dimensional array is `dp = [0, 0, 0, 0, 0]`.
|
||||
|
||||
A student processes the item by updating capacities from 2 to 4:
|
||||
|
||||
- After updating `dp[2]`, its value is 5.
|
||||
- After updating `dp[3]`, its value is also 5.
|
||||
- When updating `dp[4]`, the student uses the newly obtained `dp[2]`, producing `dp[4] = 10`.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Is `dp[4] = 10` correct? Why or why not?
|
||||
2. Given that each item may be selected at most once, what should `dp[4]` be?
|
||||
3. When processing each item, should capacities be updated from largest to smallest or from smallest to largest? What problem does this avoid?
|
||||
|
||||
??? success "Answer"
|
||||
|
||||
1. The result is incorrect. A value of 10 is equivalent to placing the item with value 5 into the knapsack twice,
|
||||
violating the condition that each item may be selected at most once.
|
||||
|
||||
2. The knapsack can contain at most this one item, so the correct value of `dp[4]` is 5.
|
||||
|
||||
3. Capacities should be updated from largest to smallest, in the order 4, 3, 2.
|
||||
Then, when calculating `dp[c]`, the value read from `dp[c-2]` still comes from before the current item was processed,
|
||||
preventing the current item from being reused during the same round.
|
||||
|
||||
## 14.8.2 Programming Exercises
|
||||
|
||||
### 1. Number of Ways to Climb Stairs
|
||||
|
||||
A staircase has `n` steps. Each move climbs either 1 or 2 steps, and you must land exactly on step `n`.
|
||||
Calculate the number of distinct ways to reach the top. Assume `n >= 1`; ways are distinguished only by their sequence of 1-step and 2-step moves.
|
||||
Use a one-dimensional dynamic programming array. For now, do not use the space optimization that keeps only two states.
|
||||
|
||||
??? tip "Hints"
|
||||
|
||||
1. The last move to step i can cover only 1 or 2 steps
|
||||
2. Therefore, dp[i] = dp[i-1] + dp[i-2]
|
||||
3. Handle the cases where n is 1 or 2 first, then fill the table starting from step 3
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/climbing-stairs/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
|
||||
### 2. 0-1 Knapsack
|
||||
|
||||
You are given equal-length arrays `wgt` and `val`. Item `i` has positive integer weight `wgt[i]` and non-negative integer value `val[i]`.
|
||||
The knapsack capacity `cap` is a non-negative integer. Each item may be selected at most once. Find the maximum total value that can be placed in the knapsack
|
||||
without exceeding `cap`. Use one-dimensional dynamic programming.
|
||||
|
||||
??? tip "Hints"
|
||||
|
||||
1. Initialize an array dp of length cap + 1, where dp[c] is the maximum value for a capacity limit of c
|
||||
2. When processing item i, compare dp[c], which does not select it, with dp[c-wgt[i]] + val[i], which does
|
||||
3. Update capacities from largest to smallest to avoid selecting the current item repeatedly in the same round
|
||||
@@ -22,3 +22,4 @@ icon: material/table-pivot
|
||||
- [14.5 Unbounded Knapsack Problem](unbounded_knapsack_problem.md)
|
||||
- [14.6 Edit Distance Problem](edit_distance_problem.md)
|
||||
- [14.7 Summary](summary.md)
|
||||
- [14.8 Exercises](exercises.md)
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- Generated by utils/exercises/publish_exercises.py; do not edit directly. -->
|
||||
|
||||
# 9.5 Exercises
|
||||
|
||||
## 9.5.1 Concept Review
|
||||
|
||||
### 1. Represent the Same Graph in Two Ways
|
||||
|
||||
An undirected graph has four vertices, `A, B, C, D`, and the edges
|
||||
`A-B, A-C, B-C, C-D`.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Write its adjacency list.
|
||||
2. Fill in its adjacency matrix using only 0s and 1s.
|
||||
3. To determine whether `A` and `D` are directly connected, which graph representation requires checking only one stored entry?
|
||||
4. If a graph has many vertices but few edges, which representation usually uses less space?
|
||||
|
||||
??? success "Answer"
|
||||
|
||||
1. The adjacency list is:
|
||||
|
||||
```text
|
||||
A: B, C
|
||||
B: A, C
|
||||
C: A, B, D
|
||||
D: C
|
||||
```
|
||||
|
||||
2. The adjacency matrix is:
|
||||
|
||||
| | A | B | C | D |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| A | 0 | 1 | 1 | 0 |
|
||||
| B | 1 | 0 | 1 | 0 |
|
||||
| C | 1 | 1 | 0 | 1 |
|
||||
| D | 0 | 0 | 1 | 0 |
|
||||
|
||||
3. In an adjacency matrix, you can directly check row `A`, column `D`, making it well suited to determining whether any two vertices are directly connected.
|
||||
|
||||
4. When a graph has many vertices but few edges, an adjacency list records only the edges that actually exist. It usually uses less space than an adjacency matrix, which reserves a position for every pair of vertices.
|
||||
|
||||
### 2. Breadth-First and Depth-First Traversal Orders
|
||||
|
||||
An undirected graph has vertices `A, B, C, D, E` and edges
|
||||
`A-B, A-C, B-D, C-D, D-E`.
|
||||
|
||||
Start at A. Whenever there are several unvisited adjacent vertices, choose them in alphabetical order:
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Write the visit order of breadth-first traversal (BFS).
|
||||
2. Write the visit order of recursive depth-first traversal (DFS).
|
||||
3. Why must both traversals record which vertices have already been visited?
|
||||
|
||||
??? success "Answer"
|
||||
|
||||
1. The BFS visit order is `A, B, C, D, E`. It first visits B and C, which are one edge away from A,
|
||||
and then visits the more distant D and E.
|
||||
|
||||
2. The DFS visit order is `A, B, D, C, E`. It repeatedly enters an unvisited adjacent vertex,
|
||||
first following `A → B → D → C`. When C has no new adjacent vertex, it returns to D and then visits E.
|
||||
|
||||
3. The graph contains a cycle, such as `A-B-D-C-A`. Without recording visited vertices,
|
||||
a traversal could repeatedly visit the same vertices around the cycle and fail to terminate normally.
|
||||
|
||||
### 3. Can One BFS Visit the Entire Graph?
|
||||
|
||||
An undirected graph has vertices `A, B, C, D, E, F` and only the edges
|
||||
`A-B, B-C, D-E`.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Which vertices can one BFS starting at A visit?
|
||||
2. Based on Question 1, has this BFS visited every vertex in the graph? Why or why not?
|
||||
3. Suppose you scan all vertices in alphabetical order and start a new BFS whenever you reach an unvisited vertex.
|
||||
What is the starting vertex of each BFS? Into how many mutually disconnected parts (connected components) is the graph divided?
|
||||
|
||||
??? success "Answer"
|
||||
|
||||
1. Starting from A, the traversal can visit only `A, B, C`.
|
||||
|
||||
2. It has not visited every vertex. `D, E` form another connected part, while F is an isolated vertex.
|
||||
None of them has a path to A, so they cannot be reached from A.
|
||||
|
||||
3. The three BFS traversals start at `A, D, F`, and visit
|
||||
`{A, B, C}`, `{D, E}`, and `{F}`, respectively. Therefore, the graph has 3 connected components.
|
||||
|
||||
## 9.5.2 Programming Exercises
|
||||
|
||||
### 1. Determine Whether a Path Exists in an Undirected Graph
|
||||
|
||||
You are given an undirected graph with $n$ vertices numbered from $0$ to $n-1$. Each entry `[u, v]` in the array `edges` represents an undirected edge between vertices `u` and `v`.
|
||||
|
||||
You are also given a starting vertex `source` and a destination vertex `destination`. First build an adjacency list from `edges`, then use BFS or DFS
|
||||
to determine whether a path exists from `source` to `destination`. Return `true` if one exists and `false` otherwise.
|
||||
The graph may contain cycles and may be disconnected.
|
||||
|
||||
??? tip "Hints"
|
||||
|
||||
1. Add every undirected edge in both directions
|
||||
2. The graph may contain cycles, so you must record which vertices have already been visited
|
||||
3. Starting from source, return true if you encounter destination; if the traversal ends without reaching it, return false
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/find-if-path-exists-in-graph/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -19,3 +19,4 @@ icon: material/graphql
|
||||
- [9.2 Basic Operations on Graphs](graph_operations.md)
|
||||
- [9.3 Graph Traversal](graph_traversal.md)
|
||||
- [9.4 Summary](summary.md)
|
||||
- [9.5 Exercises](exercises.md)
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- Generated by utils/exercises/publish_exercises.py; do not edit directly. -->
|
||||
|
||||
# 15.6 Exercises
|
||||
|
||||
## 15.6.1 Concept Review
|
||||
|
||||
### 1. Is Choosing the Largest Coin Always Best?
|
||||
|
||||
The coin denominations are `[1, 7, 10]`, and the target amount is 14.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Follow the rule "always choose the largest denomination that does not exceed the remaining amount," and write the coins selected.
|
||||
2. Is there a solution using fewer coins? If so, give one; otherwise, explain why not.
|
||||
3. Does this example show that the greedy strategy is correct for every set of coin denominations?
|
||||
|
||||
??? success "Answer"
|
||||
|
||||
1. The greedy strategy selects `10 + 1 + 1 + 1 + 1`, using 5 coins in total.
|
||||
|
||||
2. There is a solution using fewer coins: `7 + 7`, which uses only 2 coins.
|
||||
|
||||
3. No. This counterexample shows that, for arbitrary coin denominations, repeatedly choosing the largest currently available denomination does not necessarily minimize the number of coins.
|
||||
The largest immediate choice may prevent a better combination later.
|
||||
|
||||
### 2. Which Item Should Go into the Knapsack First?
|
||||
|
||||
A knapsack with a capacity of 4 kilograms can hold the following items. A fraction of an item may be taken,
|
||||
and the value obtained is proportional to its weight:
|
||||
|
||||
- Item A: weight 4 kilograms, value 20.
|
||||
- Item B: weight 3 kilograms, value 18.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. What is the value per kilogram of each item? Which item should be placed in the knapsack first?
|
||||
2. Fill the knapsack using the greedy strategy for the fractional knapsack problem. What is the final value?
|
||||
3. When items can be divided and the knapsack limits total weight, should items be compared by total value or by value per kilogram? Why?
|
||||
|
||||
??? success "Answer"
|
||||
|
||||
1. A has value `20 ÷ 4 = 5` per kilogram, while B has value `18 ÷ 3 = 6` per kilogram,
|
||||
so B, with the higher value per unit weight, should be placed in the knapsack first.
|
||||
|
||||
2. First take all of B, using 3 kilograms of capacity and gaining a value of 18. With 1 kilogram of capacity remaining,
|
||||
take 1 kilogram of A, gaining a value of 5. The final value is `18 + 5 = 23`.
|
||||
|
||||
3. The knapsack limits total weight, and items can be divided, so they should be compared by value per unit weight.
|
||||
Although A has a higher total value, its value per kilogram is lower than B's. Filling the knapsack with A first would yield only a value of 20.
|
||||
|
||||
### 3. Which Pointer Should Move Next?
|
||||
|
||||
The partition heights are `[1, 8, 6, 2, 5]`. Use two pointers, one at each end, to find the maximum capacity.
|
||||
The capacity equals "the height of the shorter partition × the distance between the partitions' indices."
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Initially, the left pointer is at index 0 and the right pointer is at index 4. What is the current capacity? Which pointer should move next?
|
||||
2. After making the move chosen in Question 1, at which indices are the two pointers? What is the capacity now? Which pointer should move next?
|
||||
3. For the current pair of partitions, you could move either the pointer at the shorter partition or the pointer at the taller partition. Which move could still produce a greater capacity, and why?
|
||||
|
||||
??? success "Answer"
|
||||
|
||||
1. The current capacity is `min(1, 5) × (4 - 0) = 4`. The left partition is shorter, so move the left pointer.
|
||||
|
||||
2. After the left pointer moves, the pointers are at indices 1 and 4. The current capacity is
|
||||
`min(8, 5) × (4 - 1) = 15`. The right partition is shorter, so move the right pointer next.
|
||||
|
||||
3. Moving the pointer at the shorter partition is the only move that could still produce a greater capacity. If the taller partition's pointer moves, the distance certainly decreases while the height remains limited by the unmoved shorter partition,
|
||||
so the capacity can only stay the same or decrease. Only by moving the shorter partition can a taller partition possibly be found.
|
||||
|
||||
## 15.6.2 Programming Exercises
|
||||
|
||||
### 1. Fractional Knapsack
|
||||
|
||||
You are given equal-length arrays `wgt` and `val`, where `wgt[i] > 0` and `val[i] >= 0`. The knapsack has capacity `cap >= 0`.
|
||||
There is only one of each item, but any fraction of an item may be placed in the knapsack.
|
||||
The value obtained is proportional to the fraction of the item's total weight that is included. Use a greedy algorithm
|
||||
and return the maximum total value the knapsack can hold as a real number.
|
||||
|
||||
??? tip "Hints"
|
||||
|
||||
1. First calculate each item's value per unit weight as val[i] / wgt[i], keeping the fractional part of the division
|
||||
2. Place items with higher value per unit weight into the knapsack first
|
||||
3. If the remaining capacity is less than the current item's weight, take exactly the fraction that fills the knapsack and stop
|
||||
@@ -20,3 +20,4 @@ icon: material/head-heart-outline
|
||||
- [15.3 Maximum Capacity Problem](max_capacity_problem.md)
|
||||
- [15.4 Maximum Product Cutting Problem](max_product_cutting_problem.md)
|
||||
- [15.5 Summary](summary.md)
|
||||
- [15.6 Exercises](exercises.md)
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- Generated by utils/exercises/publish_exercises.py; do not edit directly. -->
|
||||
|
||||
# 6.5 Exercises
|
||||
|
||||
## 6.5.1 Concept Review
|
||||
|
||||
### 1. Searching After a Hash Collision
|
||||
|
||||
A hash table has 5 buckets and uses the hash function $h(x)=x \bmod 5$. When a collision occurs, elements are placed one after another in a list within that bucket.
|
||||
Insert `[1, 6, 11, 7]` in order:
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Write the contents of buckets 0–4.
|
||||
2. When searching for 6, which bucket is checked first, and which elements are examined in order?
|
||||
3. Based on the bucket contents from Question 1, do later insertions overwrite earlier ones? Explain using this collision-resolution method.
|
||||
|
||||
??? success "Answer"
|
||||
|
||||
1. Because $1\bmod5=6\bmod5=11\bmod5=1$, while $7\bmod5=2$, the buckets are:
|
||||
|
||||
```text
|
||||
0: []
|
||||
1: [1, 6, 11]
|
||||
2: [7]
|
||||
3: []
|
||||
4: []
|
||||
```
|
||||
|
||||
2. A search for 6 first goes to bucket 1, then compares 1 and 6 in order. The target is found on the second comparison.
|
||||
|
||||
3. Equal hash values mean only that the elements go into the same bucket, not that the elements are equal. Separate chaining keeps all colliding elements in the bucket
|
||||
and compares them one at a time during a search, so 1, 6, and 11 do not overwrite one another.
|
||||
|
||||
### 2. Where Do Elements Go After a Hash Table Expands?
|
||||
|
||||
A hash table using separate chaining originally has 5 buckets and the hash function $h(x)=x\bmod5$.
|
||||
The keys `[1, 6, 11]` are all in bucket 1.
|
||||
|
||||
The table is now expanded to 7 buckets, and the hash function becomes $h(x)=x\bmod7$:
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Calculate the new bucket number for 1, 6, and 11.
|
||||
2. Which buckets contain elements after the expansion?
|
||||
3. During the expansion, can the list from the old bucket 1 simply be copied into the new bucket 1? Explain using the results from Questions 1 and 2.
|
||||
|
||||
??? success "Answer"
|
||||
|
||||
1. The new bucket numbers are:
|
||||
|
||||
- $1\bmod7=1$;
|
||||
- $6\bmod7=6$;
|
||||
- $11\bmod7=4$.
|
||||
|
||||
2. Bucket 1 stores 1, bucket 4 stores 11, and bucket 6 stores 6. The three keys are no longer crowded into the same bucket.
|
||||
|
||||
3. The list cannot be copied as is. A bucket number is calculated by taking the key modulo the number of buckets. When the bucket count changes from 5 to 7, a key's bucket number may change,
|
||||
so the position of every key must be recalculated. If the old bucket 1 were copied unchanged, later searches using the new formula would go to bucket 6 for 6 and bucket 4 for 11
|
||||
and would fail to find them.
|
||||
|
||||
### 3. Can 11 Still Be Found After Deleting 6?
|
||||
|
||||
A hash table has 5 positions with indices `0–4` and uses the hash function $h(x)=x\bmod5$.
|
||||
When a collision occurs, it searches to the right from the index produced by the hash function for the first empty position.
|
||||
|
||||
Insert `[1, 6, 11]` in order:
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. At which index is each number ultimately stored?
|
||||
2. When searching for 11, which indices are examined in order?
|
||||
3. Suppose deleting 6 changes its position directly to an "unused empty position," and a search stops whenever it reaches an empty position.
|
||||
What happens when searching for 11 afterward? Is this search result correct? If there is a problem, how can it be avoided?
|
||||
|
||||
??? success "Answer"
|
||||
|
||||
1. The number 1 is stored at index 1. The number 6 also maps to index 1, so after the collision it is stored at index 2.
|
||||
The number 11 also starts at index 1, skips the occupied indices 1 and 2, and is ultimately stored at index 3.
|
||||
|
||||
2. A search for 11 examines indices `1, 2, 3` in order and finds it at index 3.
|
||||
|
||||
3. If index 2 is changed to mean "never used," a search for 11 checks index 1 and then stops at index 2,
|
||||
incorrectly concluding that 11 is absent. Deletion should leave a "deleted" marker.
|
||||
A search that reaches this marker continues to the next index (wrapping from index 4 to index 0), while a later insertion may still reuse the position.
|
||||
|
||||
## 6.5.2 Programming Exercises
|
||||
|
||||
### 1. Compare the Character Counts of Two Strings
|
||||
|
||||
Given two strings `s` and `t` containing only lowercase English letters,
|
||||
you may rearrange the characters in `s` in any order, but you may not add, remove, or replace characters.
|
||||
|
||||
Determine whether the rearranged string can form `t`. Return `true` if it can and `false` otherwise.
|
||||
Use a hash table to record how many times each letter occurs. Do not sort the characters in the strings.
|
||||
|
||||
??? tip "Hints"
|
||||
|
||||
1. If the two strings have different lengths, they cannot contain each character the same number of times
|
||||
2. Use a hash table to record the count of each letter; increment the corresponding count while scanning s
|
||||
3. Decrement the corresponding count while scanning t; the strings contain each character the same number of times only if every count is 0 at the end
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/valid-anagram/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -19,3 +19,4 @@ icon: material/table-search
|
||||
- [6.2 Hash Collision](hash_collision.md)
|
||||
- [6.3 Hash Algorithm](hash_algorithm.md)
|
||||
- [6.4 Summary](summary.md)
|
||||
- [6.5 Exercises](exercises.md)
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- Generated by utils/exercises/publish_exercises.py; do not edit directly. -->
|
||||
|
||||
# 8.5 Exercises
|
||||
|
||||
## 8.5.1 Concept Review
|
||||
|
||||
### 1. How Does the Heap Change After Inserting 10?
|
||||
|
||||
The array `[9, 7, 8, 3, 5]` represents a max heap. Now insert the number 10.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. First append 10 to the end of the array. What is the value of its parent node?
|
||||
2. Starting from the new node, perform bottom-to-top heapify and write the array after each swap.
|
||||
3. What is the final top element of the heap? How many swaps occur in total?
|
||||
|
||||
??? success "Answer"
|
||||
|
||||
1. After 10 is appended, its index is 5, so its parent's index is
|
||||
$\lfloor(5-1)/2\rfloor=2$. The parent node's value is 8.
|
||||
|
||||
2. Since 10 is greater than 8, the array after the first swap is `[9, 7, 10, 3, 5, 8]`.
|
||||
Since 10 is also greater than its parent 9, the array after the second swap is `[10, 7, 9, 3, 5, 8]`.
|
||||
The value 10 has now reached the root node, so heapification is complete.
|
||||
|
||||
3. The final top element is 10, and 2 swaps occur in total.
|
||||
|
||||
### 2. Check Parent–Child Relationships in a Min Heap
|
||||
|
||||
The array `[1, 4, 3, 7, 6, 2]` represents a complete binary tree. In a min heap, every parent node must be no greater than its children.
|
||||
For index $i$, the left and right child indices are $2i+1$ and $2i+2$, respectively.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. What are the indices and values of the children of index 2?
|
||||
2. The node at index 2 has value 3. Does it violate the min-heap rule with its child? If so, which two elements should be swapped?
|
||||
3. Based on your answer to Question 2, write the array after the swap if the rule is violated; otherwise, explain why no swap is needed. Finally, check the remaining parent–child relationships.
|
||||
|
||||
??? success "Answer"
|
||||
|
||||
1. The left child of index 2 is at index 5 and has value 2. The right child's index is 6, but the array has length 6, so the right child does not exist.
|
||||
|
||||
2. The parent value 3 is greater than the child value 2, violating the min-heap rule. The elements at indices 2 and 5 should be swapped.
|
||||
|
||||
3. After the swap, the array is `[1, 4, 2, 7, 6, 3]`. Check each relationship:
|
||||
`1 ≤ 4`, `1 ≤ 2`; `4 ≤ 7`, `4 ≤ 6`; and `2 ≤ 3`.
|
||||
Every parent is now no greater than its children, so the min-heap rule is satisfied.
|
||||
|
||||
### 3. Keep the Three Largest Numbers with a Min Heap
|
||||
|
||||
To keep the 3 largest numbers from the data stream `[4, 1, 7, 3, 8]`, maintain a min heap containing no more than 3 elements.
|
||||
|
||||
First insert the first 3 numbers into the min heap in order. Once the heap is full, for each new number:
|
||||
if it is greater than the top element, remove the top and insert the new number; otherwise, leave the heap unchanged.
|
||||
|
||||
After each number is read, write the numbers kept in the heap and the top element.
|
||||
Write the kept numbers as a set; you do not need to give their order in the heap array.
|
||||
|
||||
??? success "Answer"
|
||||
|
||||
The result after each number is read is:
|
||||
|
||||
| Number read | Numbers kept | Top |
|
||||
| --- | --- | --- |
|
||||
| 4 | `{4}` | 4 |
|
||||
| 1 | `{1, 4}` | 1 |
|
||||
| 7 | `{1, 4, 7}` | 1 |
|
||||
| 3 | `{3, 4, 7}` | 3 |
|
||||
| 8 | `{4, 7, 8}` | 4 |
|
||||
|
||||
Once the heap is full, its top is the smallest of the numbers currently kept. A new number replaces the top only when it is greater than the top.
|
||||
The final set `{4, 7, 8}` contains exactly the 3 largest numbers.
|
||||
|
||||
## 8.5.2 Programming Exercises
|
||||
|
||||
### 1. Find the Kth Largest Element in an Array
|
||||
|
||||
Given an integer array `nums` and an integer $k$, where $1 \le k \le n$ and $n$ is the array's length, return the element that would appear at position $k$ if the array were arranged from largest to smallest.
|
||||
|
||||
Count duplicate elements separately. For example, the second-largest element of `[5, 5, 2]` is still 5. Use a min heap containing no more than $k$ elements.
|
||||
|
||||
??? tip "Hints"
|
||||
|
||||
1. The kth largest element is the smallest among the k largest numbers
|
||||
2. Insert each number into the min heap, and remove the smallest value whenever the heap's size exceeds k
|
||||
3. After the traversal, the heap contains the k largest numbers, and its top is the answer
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/kth-largest-element-in-an-array/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -19,3 +19,4 @@ icon: material/family-tree
|
||||
- [8.2 Heap Construction Operation](build_heap.md)
|
||||
- [8.3 Top-k Problem](top_k.md)
|
||||
- [8.4 Summary](summary.md)
|
||||
- [8.5 Exercises](exercises.md)
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- Generated by utils/exercises/publish_exercises.py; do not edit directly. -->
|
||||
|
||||
# 10.7 Exercises
|
||||
|
||||
## 10.7.1 Concept Review
|
||||
|
||||
### 1. How Binary Search Narrows the Search Interval
|
||||
|
||||
Search for 16 in the sorted array `[2, 5, 8, 12, 16, 23, 38]`.
|
||||
Use the closed interval `[i, j]` and calculate the midpoint as
|
||||
$m=i+(j-i)/2$, rounded down.
|
||||
|
||||
For each round, write `(i, j, m)`, the middle element, and how the interval is narrowed next,
|
||||
until the target is found.
|
||||
|
||||
??? success "Answer"
|
||||
|
||||
The search proceeds as follows:
|
||||
|
||||
| Round | `(i, j, m)` | Middle element | Next step |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | `(0, 6, 3)` | 12 | `12 < 16`, set `i = 4` |
|
||||
| 2 | `(4, 6, 5)` | 23 | `23 > 16`, set `j = 4` |
|
||||
| 3 | `(4, 4, 4)` | 16 | Target found; return index 4 |
|
||||
|
||||
Because the array is sorted, when the middle value is smaller than the target, the middle and everything to its left can be excluded.
|
||||
When the middle value is greater than the target, the middle and everything to its right can be excluded.
|
||||
|
||||
### 2. Left and Right Boundaries of Duplicate Elements
|
||||
|
||||
Search for the number 2 in the array `[1, 2, 2, 2, 4, 6]`.
|
||||
A student uses binary search, returns immediately after finding the target at index 2, and says,
|
||||
"Index 2 is the left boundary of the number 2."
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Is the student's statement correct? What are the left and right boundaries of 2? Explain why.
|
||||
2. When searching for the left boundary, if the middle element equals the target, which side should be searched next?
|
||||
3. When searching for the right boundary, which side should be searched next? State only the direction; you do not need to write the complete search process.
|
||||
|
||||
??? success "Answer"
|
||||
|
||||
1. The student's statement is incorrect. Returning as soon as a 2 is found guarantees only that some occurrence of 2 was found, not the leftmost or rightmost one.
|
||||
Here, the left boundary is index 1 and the right boundary is index 3.
|
||||
|
||||
2. When searching for the left boundary, continue searching on the left even if the middle element equals 2.
|
||||
For example, with a closed interval, set `j = m - 1`.
|
||||
|
||||
3. When searching for the right boundary, continue searching on the right after the middle element equals 2.
|
||||
For example, set `i = m + 1`.
|
||||
|
||||
### 3. Choosing a Search Method for Different Data
|
||||
|
||||
For each situation below, choose an appropriate method from "linear search," "binary search," and "hash table," and explain why:
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Repeatedly search among $10^7$ integers that are already sorted and never change, without building any additional data structure.
|
||||
2. Repeatedly test whether a key exists in a collection with frequent insertions and deletions. The collection need not remain sorted, and no range searches are needed.
|
||||
3. Search an unsorted array for a value only once.
|
||||
|
||||
??? success "Answer"
|
||||
|
||||
1. Binary search: the data is sorted and static, so searching takes $O(\log n)$ time without extra space.
|
||||
2. Hash table: when the hash function distributes keys fairly evenly among the buckets, insertion, deletion, and lookup by key all take $O(1)$ time on average.
|
||||
3. Scan directly from beginning to end. When searching only once, sorting the array or building a hash table still requires processing the entire array first,
|
||||
so neither reduces the total work for this single task.
|
||||
|
||||
The choice depends on whether the data is sorted, whether an additional structure is allowed, how many searches are needed, and which operations must be supported.
|
||||
|
||||
## 10.7.2 Programming Exercises
|
||||
|
||||
### 1. Binary Search in a Sorted Array
|
||||
|
||||
Given an integer array `nums` in strictly increasing order and a target value `target`, use binary search to find `target`. If it exists, return its array index; otherwise, return -1.
|
||||
|
||||
??? tip "Hints"
|
||||
|
||||
1. The initial interval is left = 0 and right = n - 1; it is nonempty while left <= right
|
||||
2. Calculate the midpoint with mid = left + (right - left) // 2
|
||||
3. If `nums[mid]` is less than `target`, move the left boundary to `mid + 1`; if `nums[mid]` is greater than `target`, move the right boundary to `mid - 1`; if they are equal, return immediately
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/binary-search/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
|
||||
### 2. Insertion Point in a Sorted Array
|
||||
|
||||
You are given an integer array `nums` in strictly increasing order and a target value `target`.
|
||||
|
||||
Return the index of `target` if it is already in the array. Otherwise, return the insertion point at which `target` can be inserted while keeping the array in strictly increasing order.
|
||||
The insertion point may be 0 or may equal the array's length. Use binary search.
|
||||
|
||||
??? tip "Hints"
|
||||
|
||||
1. The answer may be 0 or the array length n
|
||||
2. With a closed interval, if `nums[mid]` is greater than or equal to `target`, set `right = mid - 1` and continue checking farther left; otherwise, set `left = mid + 1`
|
||||
3. When the loop ends, left is the insertion point
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/search-insert-position/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -21,3 +21,4 @@ icon: material/text-search
|
||||
- [10.4 Hash Optimization Strategy](replace_linear_by_hashing.md)
|
||||
- [10.5 Searching Algorithms Revisited](searching_algorithm_revisited.md)
|
||||
- [10.6 Summary](summary.md)
|
||||
- [10.7 Exercises](exercises.md)
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- Generated by utils/exercises/publish_exercises.py; do not edit directly. -->
|
||||
|
||||
# 11.12 Exercises
|
||||
|
||||
## 11.12.1 Concept Review
|
||||
|
||||
### 1. The First Few Rounds of Selection Sort and Bubble Sort
|
||||
|
||||
Given the array `[4, 2, 5, 1, 3]`, sort it in ascending order in both parts below.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Simulate the first two rounds of selection sort. Write the array after each round and identify which positions are now fixed.
|
||||
2. Simulate the first round of bubble sort. Write the resulting array and the number of swaps, and identify which position is now fixed.
|
||||
|
||||
??? success "Answer"
|
||||
|
||||
1. The first two rounds are:
|
||||
|
||||
| Round | Array | Explanation |
|
||||
| --- | --- | --- |
|
||||
| 1 | `[1, 2, 5, 4, 3]` | The smallest element, 1, is swapped with the first element |
|
||||
| 2 | `[1, 2, 5, 4, 3]` | The value 2 is already at index 1, so no swap is needed |
|
||||
|
||||
The first two positions are now fixed. Later rounds need to find the smallest element only within `[5, 4, 3]`.
|
||||
|
||||
2. Compare adjacent elements in order: swap 4 and 2; do not swap 4 and 5; swap 5 and 1; then swap 5 and 3.
|
||||
The result is `[2, 4, 1, 3, 5]`, after 3 swaps. The largest element, 5, has moved to the end of the array, so the last position is now fixed.
|
||||
|
||||
### 2. Can Equal Elements Change Their Relative Order?
|
||||
|
||||
In the array $[2_a, 2_b, 1]$, $2_a$ and $2_b$ have equal values, but their subscripts mark their original order.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Write the array after the first round of selection sort. Has the relative order of $2_a$ and $2_b$ changed?
|
||||
2. Write the array after the first round of bubble sort. Has the relative order of $2_a$ and $2_b$ changed?
|
||||
3. Based on the first two questions, explain how the two sorting algorithms differ in preserving the original order of equal elements.
|
||||
|
||||
??? success "Answer"
|
||||
|
||||
1. In its first round, selection sort selects the smallest element, 1, and swaps it with the first element, $2_a$, producing
|
||||
$[1, 2_b, 2_a]$. The relative order has changed because $2_a$ has moved behind $2_b$.
|
||||
|
||||
2. Bubble sort first compares $2_a$ and $2_b$. Because they are equal, it does not swap them. It then compares $2_b$ and 1 and swaps them,
|
||||
producing $[2_a, 1, 2_b]$ after the first round. $2_a$ is still before $2_b$, so their relative order has not changed.
|
||||
|
||||
3. In this example, selection sort changes the original order of equal elements. Bubble sort swaps adjacent elements only when the left element is greater than the right one.
|
||||
Equal elements are not swapped, so their original relative order is preserved.
|
||||
|
||||
### 3. Compare Counting Sort and Radix Sort
|
||||
|
||||
A school needs to sort many student ID numbers, each exactly 8 digits long. Answer the following questions:
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. How many rounds does radix sort need when it starts with the least significant digit?
|
||||
2. If the student IDs are treated directly as integers for counting sort, why would the count array need many entries that are never used?
|
||||
3. Based on the first two questions, which would you choose for sorting many fixed-length 8-digit student IDs: counting sort or radix sort? Why?
|
||||
|
||||
??? success "Answer"
|
||||
|
||||
1. A student ID has 8 digits, so 8 rounds are needed from the least significant to the most significant digit. Each round groups values only by 0–9.
|
||||
|
||||
2. Direct counting would require an entry for every possible 8-digit value, but only a small fraction of those values are actually assigned to students.
|
||||
Most entries in the count array would remain 0.
|
||||
|
||||
3. Radix sort is the better choice. It uses the facts that the length is fixed and each digit has only 10 possible values, requiring only 8 rounds of stable grouping.
|
||||
Counting sort, if it used the entire 8-digit ID as an integer index, would require count-array entries for many values that never occur.
|
||||
|
||||
## 11.12.2 Programming Exercises
|
||||
|
||||
### 1. Sort an Array with Merge Sort
|
||||
|
||||
Given an integer array `nums`, implement merge sort yourself, arrange its elements in nondecreasing order, and return the result. Do not call the language's built-in sorting function.
|
||||
|
||||
??? tip "Hints"
|
||||
|
||||
1. An interval of length at most 1 is already sorted
|
||||
2. Divide the interval in half at its midpoint and recursively sort both halves
|
||||
3. Use two pointers to merge the two sorted halves, then write the result back into the original array
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/sort-an-array/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
|
||||
### 2. Sort an Integer Array with Counting Sort
|
||||
|
||||
You are given an integer array `nums` and a non-negative integer $K$. Every element of the array is between $0$ and $K$.
|
||||
|
||||
Implement counting sort, write the result back into `nums` in nondecreasing order, and return `nums`.
|
||||
Do not determine the order by comparing elements, and do not call the language's built-in sorting function.
|
||||
|
||||
??? tip "Hints"
|
||||
|
||||
1. Because every element is between 0 and K, use each element's value directly as an index in the counting array
|
||||
2. Scan nums once and increment the count at the corresponding position
|
||||
3. Then scan the counting array from 0 to K; if the value x occurs a certain number of times, write x into nums that many consecutive times
|
||||
@@ -26,3 +26,4 @@ icon: material/sort-ascending
|
||||
- [11.9 Counting Sort](counting_sort.md)
|
||||
- [11.10 Radix Sort](radix_sort.md)
|
||||
- [11.11 Summary](summary.md)
|
||||
- [11.12 Exercises](exercises.md)
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- Generated by utils/exercises/publish_exercises.py; do not edit directly. -->
|
||||
|
||||
# 5.5 Exercises
|
||||
|
||||
## 5.5.1 Concept Review
|
||||
|
||||
### 1. Which Element Leaves a Stack or Queue First?
|
||||
|
||||
Prepare an empty stack `S` and an empty queue `Q`. Perform the same sequence of operations on each one:
|
||||
|
||||
Step 1: Add `A`.
|
||||
Step 2: Add `B`.
|
||||
Step 3: Remove and record one element.
|
||||
Step 4: Add `C`.
|
||||
Step 5: Keep removing and recording elements until the container is empty.
|
||||
|
||||
Write the order in which elements are removed from `S` and from `Q`. Explain the difference using "last-in-first-out" or "first-in-first-out."
|
||||
|
||||
??? success "Answer"
|
||||
|
||||
The removal order for stack `S` is `B, C, A`. After the elements `A, B` are added, the most recently added element, `B`, is popped first. After `C` is added,
|
||||
`C, A` are popped in that order. This is "last-in-first-out."
|
||||
|
||||
The removal order for queue `Q` is `A, B, C`. After the elements `A, B` are added, the earliest added element, `A`, is removed first.
|
||||
After `C` is added, `B, C` are removed in that order. This is "first-in-first-out."
|
||||
|
||||
### 2. What Happens When the Rear Passes the End of the Array?
|
||||
|
||||
A queue is implemented with a circular array of length 5, whose indices are `0–4`.
|
||||
Currently, `front = 3` and `size = 2`; `A, B` are stored at indices 3 and 4, respectively.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. When `C` is enqueued, at which index should `C` be stored? What is `size` after the enqueue?
|
||||
2. Next, dequeue once. Which element is removed? What are the new values of `front` and `size`?
|
||||
3. What is the logical order from the front to the rear now? Does dequeuing require moving the other elements in the array? Why or why not?
|
||||
|
||||
??? success "Answer"
|
||||
|
||||
1. The new element's position is
|
||||
`(front + size) % 5 = (3 + 2) % 5 = 0`,
|
||||
so `C` is stored at index 0. After the enqueue, `size = 3`.
|
||||
|
||||
2. Dequeuing removes the current front element, `A`. The new front index is
|
||||
`(3 + 1) % 5 = 4`, so `front = 4` and `size = 2`.
|
||||
|
||||
3. The logical order of valid elements is `B, C`, with `B` at index 4 and `C` at index 0.
|
||||
Dequeuing requires only changing `front` and `size`. The circular array uses the remainder operation to wrap the index back to the beginning,
|
||||
so there is no need to shift all the other elements forward.
|
||||
|
||||
### 3. Operations at Both Ends of a Deque
|
||||
|
||||
Here, `push_first` adds an element at the front, `push_last` adds one at the rear,
|
||||
`pop_first` removes one from the front, and `pop_last` removes one from the rear.
|
||||
|
||||
Perform the following operations on an empty deque `deq`:
|
||||
|
||||
1. `push_last(A)`
|
||||
2. `push_last(B)`
|
||||
3. `push_first(C)`
|
||||
4. `pop_last()`
|
||||
5. `push_last(D)`
|
||||
6. `pop_first()`
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Which elements are returned by the two pop operations?
|
||||
2. After all operations are complete, which elements remain from front to rear?
|
||||
3. Examine the six operations. Can a queue that allows insertion only at the rear and removal only at the front perform all of them? If not, identify the operations it cannot perform. Then state whether a deque can perform them and explain why.
|
||||
|
||||
??? success "Answer"
|
||||
|
||||
After the first three steps, the deque from front to rear is `[C, A, B]`.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. `pop_last()` removes `B`. After `D` is added, the deque is `[C, A, D]`,
|
||||
and `pop_first()` then removes `C`.
|
||||
|
||||
2. `[A, D]` remains.
|
||||
|
||||
3. A queue that permits insertion only at the rear and removal only at the front cannot perform all the operations.
|
||||
Step 3, `push_first(C)`, requires insertion at the front, and Step 4, `pop_last()`, requires removal at the rear. Both are outside the operations supported by such a queue.
|
||||
A deque permits insertion and removal at both ends, so it can perform all six operations.
|
||||
|
||||
## 5.5.2 Programming Exercises
|
||||
|
||||
### 1. Check a Bracket Sequence
|
||||
|
||||
Given a string `s` containing only the three types of brackets `()`, `[]`, and `{}`, use a stack to determine whether the string is valid.
|
||||
|
||||
A valid sequence must satisfy both conditions: every closing bracket matches the type of the most recent unmatched opening bracket,
|
||||
and no unmatched opening bracket remains after the traversal. Return a Boolean value for the result.
|
||||
|
||||
??? tip "Hints"
|
||||
|
||||
1. You can create a mapping from each closing bracket to its matching opening bracket
|
||||
2. When you encounter a closing bracket, first check whether the stack is empty, and then check whether the top matches
|
||||
3. The stack must also be empty after the traversal
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/valid-parentheses/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -19,3 +19,4 @@ icon: material/stack-overflow
|
||||
- [5.2 Queue](queue.md)
|
||||
- [5.3 Deque](deque.md)
|
||||
- [5.4 Summary](summary.md)
|
||||
- [5.5 Exercises](exercises.md)
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- Generated by utils/exercises/publish_exercises.py; do not edit directly. -->
|
||||
|
||||
# 7.7 Exercises
|
||||
|
||||
## 7.7.1 Concept Review
|
||||
|
||||
### 1. Complete, Full, and Perfect Binary Trees
|
||||
|
||||
The following two arrays represent binary trees in level order, where `None` marks an empty position:
|
||||
|
||||
- Tree A: `[1, 2, 3, 4, 5, 6]`
|
||||
- Tree B: `[1, 2, 3, None, None, 6, 7]`
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Which tree is a complete binary tree?
|
||||
2. Which tree is a full binary tree, meaning every non-leaf node has two children?
|
||||
3. Is either tree a perfect binary tree? Explain the reason for each tree.
|
||||
|
||||
??? success "Answer"
|
||||
|
||||
1. Tree A is a complete binary tree. Only its lowest level is not full, and the nodes on that level occupy consecutive positions from left to right.
|
||||
Tree B is not complete because there are empty positions on the left of the lowest level while nodes still appear on the right.
|
||||
|
||||
2. Tree B is a full binary tree: nodes 1 and 3 each have two children, and all other nodes are leaves.
|
||||
Tree A is not full because node 3 has only one child, its left child 6.
|
||||
|
||||
3. Neither tree is perfect because the lowest level of each tree is not completely filled.
|
||||
|
||||
### 2. Three Traversal Orders for the Same Tree
|
||||
|
||||
Store the array `[1, 2, 3, 4, 5, 6, 7]` in level order in a complete binary tree.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Draw the tree.
|
||||
2. Write its preorder, inorder, and postorder traversal sequences.
|
||||
3. In the inorder sequence, which parts of the tree correspond to the subsequences to the left and right of root node 1?
|
||||
|
||||
??? success "Answer"
|
||||
|
||||
1. The tree is:
|
||||
|
||||
```text
|
||||
1
|
||||
/ \
|
||||
2 3
|
||||
/ \ / \
|
||||
4 5 6 7
|
||||
```
|
||||
|
||||
2. The preorder traversal is `1, 2, 4, 5, 3, 6, 7`;
|
||||
the inorder traversal is `4, 2, 5, 1, 6, 3, 7`;
|
||||
the postorder traversal is `4, 5, 2, 6, 7, 3, 1`.
|
||||
|
||||
3. The sequence `4, 2, 5` to the left of root node 1 is the inorder traversal of the left subtree;
|
||||
the sequence `6, 3, 7` to its right is the inorder traversal of the right subtree.
|
||||
|
||||
### 3. Compare Two Binary Search Trees
|
||||
|
||||
Insert each of the following sequences from left to right into an empty binary search tree:
|
||||
|
||||
- Sequence A: `[4, 2, 6, 1, 3, 5, 7]`
|
||||
- Sequence B: `[1, 2, 3, 4, 5, 6, 7]`
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. For each tree, write the nodes visited when searching for 7.
|
||||
2. If height is measured by the number of edges from the root node to the farthest leaf node, what is the height of each tree?
|
||||
3. Based on the first two questions, is searching for 7 equally efficient in the two trees? Explain using the trees' shapes and search paths.
|
||||
|
||||
??? success "Answer"
|
||||
|
||||
1. In the tree built from Sequence A, the search path is `4 → 6 → 7`.
|
||||
In the tree built from Sequence B, the search path is `1 → 2 → 3 → 4 → 5 → 6 → 7`.
|
||||
|
||||
2. Every level of the first tree is full, and its height is 2. The second tree has only right children, and its height is 6.
|
||||
|
||||
3. Searching for 7 is not equally efficient in the two trees. The insertion order changes the shape and height of a binary search tree. The search visits only 3 nodes in the first tree
|
||||
but all 7 nodes in the second. The taller the tree, the more nodes may need to be compared along a path in the worst case.
|
||||
|
||||
## 7.7.2 Programming Exercises
|
||||
|
||||
### 1. Maximum Depth of a Binary Tree
|
||||
|
||||
You are given the root node `root` of a binary tree. Each node contains an integer value and references to its left and right children.
|
||||
|
||||
The maximum depth is the **number of nodes** on the path from the root node to the farthest leaf node. Return the maximum depth of the tree; the maximum depth of an empty tree is 0.
|
||||
Use recursion.
|
||||
|
||||
??? tip "Hints"
|
||||
|
||||
1. Depth is measured by the number of nodes in this exercise, so a tree containing only a root node has a maximum depth of 1
|
||||
2. Let the recursive function return the maximum depth of the subtree rooted at the current node
|
||||
3. Return 0 for an empty node; for a nonempty node, return max(depth(left), depth(right)) + 1
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/maximum-depth-of-binary-tree/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
|
||||
### 2. Traverse a Binary Tree Level by Level
|
||||
|
||||
Given the root node `root` of a binary tree, use a queue to visit all nodes level by level from top to bottom and from left to right within each level.
|
||||
|
||||
Return a two-dimensional array: the first subarray stores the values at the root's level, the second stores the values at the next level, and so on.
|
||||
If the tree is empty, return an empty array.
|
||||
|
||||
??? tip "Hints"
|
||||
|
||||
1. Level-order traversal visits earlier enqueued nodes first, so use a queue
|
||||
2. At the beginning of each round, all nodes currently in the queue belong to the same level
|
||||
3. First record the queue's length, then remove exactly that many nodes and enqueue their children
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/binary-tree-level-order-traversal/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
|
||||
### 3. Kth Smallest Element in a Binary Search Tree
|
||||
|
||||
A binary search tree contains `n` nodes with distinct values.
|
||||
If all node values are arranged from smallest to largest, their positions are numbered starting from 1.
|
||||
|
||||
Given the root node `root` and an integer `k` satisfying `1 <= k <= n`, return the value at position `k`.
|
||||
Find the answer directly during an inorder traversal rather than collecting all node values first.
|
||||
|
||||
??? tip "Hints"
|
||||
|
||||
1. An inorder traversal of a binary search tree visits node values from smallest to largest
|
||||
2. Inorder traversal processes the left subtree, the current node, and then the right subtree; increment the count when visiting the current node
|
||||
3. When the count first equals k, the current node's value is the answer, so no further traversal is needed
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/kth-smallest-element-in-a-bst/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -21,3 +21,4 @@ icon: material/graph-outline
|
||||
- [7.4 Binary Search Tree](binary_search_tree.md)
|
||||
- [7.5 AVL Tree *](avl_tree.md)
|
||||
- [7.6 Summary](summary.md)
|
||||
- [7.7 Exercises](exercises.md)
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- utils/exercises/publish_exercises.py により自動生成されています。直接編集しないでください。 -->
|
||||
|
||||
# 4.6 演習
|
||||
|
||||
## 4.6.1 確認問題
|
||||
|
||||
### 1. 配列と連結リストが要素を見つける方法
|
||||
|
||||
配列と単方向連結リストに、どちらも `[A, B, C, D, E]` が順に保存されています。ここで 4 番目の要素 `D` を読み取ります。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 配列では、どのインデックスを直接使えますか?
|
||||
2. 単方向連結リストでは、先頭ノード `A` から始めて、`next` をたどりながらどのノードを通りますか?
|
||||
3. 読み取る位置が後ろになるほど、2 つの構造で必要な手順はどのように変わりますか?位置を指定した読み取りを繰り返すには、どちらが適していますか?その理由も説明してください。
|
||||
|
||||
??? success "解答"
|
||||
|
||||
1. インデックスが 0 から始まる場合、4 番目の要素のインデックスは 3 なので、配列では `arr[3]` を直接読み取れます。
|
||||
|
||||
2. 単方向連結リストでは、必ず先頭から始めます。アクセスする経路は `A → B → C → D` で、`next` を 3 回たどります。
|
||||
|
||||
3. 配列は先頭アドレスとインデックスから要素の位置を直接求められるため、位置を指定したアクセスの時間計算量は $O(1)$ です。
|
||||
単方向連結リストで $k$ 番目のノードにアクセスするには、先頭ノードから `next` を $k-1$ 回たどる必要があり、
|
||||
最悪の場合は $O(n)$ の時間がかかります。
|
||||
|
||||
ここでは位置を指定した読み取りだけを比較しており、連結リストがすべての操作で遅いという意味ではありません。
|
||||
|
||||
### 2. 配列と連結リストに要素を挿入する方法
|
||||
|
||||
配列と単方向連結リストに、どちらも `A、B、C、D` が保存されています。ここで `B` の後ろに `X` を挿入します。
|
||||
|
||||
- 配列の容量は 5 で、現在の状態は `[A, B, C, D, _]` です。
|
||||
- 連結リストは `A → B → C → D` で、ノード `B` を指す参照はすでに得られています。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 配列では、どの要素を移動する必要がありますか?挿入後の配列も書いてください。
|
||||
2. 連結リストでは、`X.next` と `B.next` をどの順序で変更すべきですか?挿入後の連結リストも書いてください。
|
||||
3. 挿入の効率を比較するときに、「ノード B を指す参照はすでに得られている」と明記する必要があるのはなぜですか?
|
||||
|
||||
??? success "解答"
|
||||
|
||||
1. 配列では、まず `D` を 1 つ右へ移動し、次に `C` を 1 つ右へ移動します。最後に `X` をインデックス 2 に置くと、
|
||||
`[A, B, X, C, D]` になります。
|
||||
|
||||
2. `B.next` はもともと `C` を指しています。まず `X.next = B.next` として `X` が `C` を指すようにし、
|
||||
次に `B.next = X` とします。その結果、`A → B → X → C → D` となります。
|
||||
元の接続を保存せずに先に `B.next` を上書きすると、`C` を見つけられなくなる可能性があります。
|
||||
|
||||
3. `B` の位置が分かっていれば、連結リストへの挿入は 2 つの接続を変更するだけなので、$O(1)$ の時間で行えます。
|
||||
先頭から `B` を探す必要もある場合、その探索自体に $O(n)$ の時間がかかる可能性があります。
|
||||
|
||||
### 3. リストの容量が増える仕組み
|
||||
|
||||
配列を基に実装されたリストの現在の内容が `[A, B, C]`、長さが `size = 3`、容量が `capacity = 4` であるとします。
|
||||
容量が足りないときは、新しい配列の容量を元の 2 倍にするものとします。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. `D` を追加した後、リストの長さと容量はそれぞれいくつですか?容量を増やす必要はありますか?
|
||||
2. 続けて `E` を追加すると、容量はいくつになりますか?既存の要素をいくつコピーする必要がありますか?
|
||||
3. 内部で使う配列の長さは変えられないのに、リストの容量が増えたように見えるのはなぜですか?
|
||||
|
||||
??? success "解答"
|
||||
|
||||
1. `D` は最後の空き位置に置けます。このとき内容は `[A, B, C, D]`、
|
||||
`size = 4`、`capacity = 4` となり、容量を増やす必要はありません。
|
||||
|
||||
2. さらに `E` を追加するときは空き位置がないため、容量 8 の新しい配列を作り、
|
||||
既存の要素 `A、B、C、D` の 4 つをコピーしてから `E` を追加します。
|
||||
このとき `size = 5`、`capacity = 8` です。
|
||||
|
||||
3. 元の配列そのものが長くなったわけではありません。リストはより大きな新しい配列を作って既存の要素をコピーし、
|
||||
内部の保存先を新しい配列に切り替えます。そのため、利用者から見ると容量が増えたように見えます。
|
||||
|
||||
## 4.6.2 プログラミング演習
|
||||
|
||||
### 1. 配列で表した大きな整数に 1 を足す
|
||||
|
||||
配列 `digits` には、0 以上の整数の各桁が左から右へ保存されています。たとえば、`[3, 0, 8]` は 308 を表します。
|
||||
数値 0 は `[0]` で表し、それ以外の入力の先頭の桁は 0 ではありません。
|
||||
|
||||
十進数の筆算と同じように、この整数に 1 を足し、結果を同じ配列形式で返してください。
|
||||
`digits` は直接変更してかまいません。先頭に新しい繰り上がりが生じた場合は、より長い配列を返せます。
|
||||
|
||||
??? tip "解法のヒント"
|
||||
|
||||
1. 筆算の加算と同じように、配列の末尾の桁から始めます
|
||||
2. 現在の桁が 9 より小さければ、1 を足してすぐに返せます
|
||||
3. 現在の桁が 9 なら 0 に変えます。すべての桁が 9 だった場合は、先頭に新しく 1 を置く必要があります
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/plus-one/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
|
||||
### 2. 単方向連結リストの反転
|
||||
|
||||
単方向連結リストの先頭ノード `head` が与えられます。各ノードには、値と次のノードを指す `next` が含まれています。
|
||||
|
||||
反復を使ってすべてのノード間の接続を反転し、反転後の先頭ノードを返してください。
|
||||
新しい連結リストノードを作ってはいけません。
|
||||
|
||||
??? tip "解法のヒント"
|
||||
|
||||
1. まず紙に 3 つの連続したノードと、2 つのポインタ prev、cur を描きます
|
||||
2. cur.next を書き換える前に、元の次のノードを nxt に保存する必要があります
|
||||
3. cur.next を反転した後、prev = cur、続いて cur = nxt とし、元の連結リストの次のノードを処理します
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/reverse-linked-list/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -20,3 +20,4 @@ icon: material/view-list-outline
|
||||
- [4.3 リスト](list.md)
|
||||
- [4.4 メモリとキャッシュ *](ram_and_cache.md)
|
||||
- [4.5 まとめ](summary.md)
|
||||
- [4.6 演習](exercises.md)
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- utils/exercises/publish_exercises.py により自動生成されています。直接編集しないでください。 -->
|
||||
|
||||
# 13.6 演習
|
||||
|
||||
## 13.6.1 確認問題
|
||||
|
||||
### 1. この順列アルゴリズムは結果を取りこぼすか
|
||||
|
||||
あるバックトラッキングアルゴリズムは、`1、2、3` の順で試しながら、すべての順列を生成します。数値 `x` を選ぶたびに、次の処理を行います。
|
||||
|
||||
1. `x` を現在の経路の末尾に追加する。
|
||||
2. `x` を「使用済み」として記録する。
|
||||
3. 次の位置を埋めるために再帰する。
|
||||
|
||||
再帰から戻った後、ある生徒は経路の末尾から `x` を削除するだけで、次の数値を試しました。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. このアルゴリズムが最初に得る順列は何ですか?6 通りすべての順列を得られますか?
|
||||
2. 1 つ上の層へ戻る前に、経路の末尾の数値を削除するだけで十分ですか?不十分な場合、ほかに何をする必要がありますか?理由も説明してください。
|
||||
|
||||
??? success "解答"
|
||||
|
||||
1. 最初に `[1, 2, 3]` を得ますが、すべての順列を得ることはできません。戻るときに経路は短くなっても、
|
||||
数値 1、2、3 の記録はすべて「使用済み」のままなので、それ以降の分岐では選べる数値がなくなります。
|
||||
|
||||
2. 十分ではありません。経路の末尾から `x` を削除した後、`x` を「未使用」に戻す必要もあります。
|
||||
現在の経路と使用済みの記録が一緒になって探索状態を表しています。選択時に 2 か所を変更したので、戻るときにも両方を元に戻さなければ、
|
||||
ほかの分岐で再び `x` を選べません。
|
||||
|
||||
### 2. 数値を選ぶ順序は重要か
|
||||
|
||||
ソート済み配列 `[2, 3, 5]` と目標値 5 が与えられ、各数値は何度でも選べます。
|
||||
アルゴリズムでは、各探索経路の数値を小さい順にだけ並べるものとします。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. どのような異なる組合せを得られますか?
|
||||
2. 同じ数値の組を異なる順序で繰り返し探索する必要がないのはなぜですか?「小さい順」という制限にはどのような役割がありますか?
|
||||
3. 現在の経路が `[3]` で、残りが 2 のとき、次の候補は 3 です。この時点で、同じ層の後ろにある候補をすべて調べずに終了できるのはなぜですか?
|
||||
|
||||
??? success "解答"
|
||||
|
||||
1. 異なる組合せは `[2, 3]` と `[5]` です。
|
||||
|
||||
2. この問題では `[2, 3]` と `[3, 2]` を同じ組合せとみなし、数値を選ぶ順序は答えに含めません。
|
||||
経路の数値を小さい順に並べると定めることで、探索時に `[3, 2]` のような重複する組合せを最初から除外できます。
|
||||
|
||||
3. 残りは 2 ですが、候補の 3 はすでに 2 より大きくなっています。配列はソート済みなので、
|
||||
3 より後ろの候補はさらに大きく、どれも現在の組合せへ追加できません。そのため、この層の確認をそのまま終了できます。
|
||||
|
||||
### 3. 次のクイーンを置ける位置
|
||||
|
||||
`4 × 4` のチェス盤に、行ごとにクイーンを置きます。行と列のインデックスはいずれも 0 から始まります。
|
||||
現在、`(0, 1)` と `(1, 3)` にクイーンがあり、2 行目に次のクイーンを置こうとしています。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 「同じ列」にあるため除外される列はどれですか?
|
||||
2. 残った列のうち、「同じ対角線」にあるため除外される位置はどれですか?
|
||||
3. 2 行目で試せる位置はどこですか?
|
||||
|
||||
??? success "解答"
|
||||
|
||||
1. 1 列目と 3 列目にはすでにクイーンがあるため、位置 `(2, 1)` と `(2, 3)` は除外されます。
|
||||
|
||||
2. 残った位置のうち、`(2, 2)` は `(1, 3)` と同じ対角線上にあるため、これも除外されます。
|
||||
位置 `(2, 0)` は、すでに置かれた 2 つのクイーンのどちらとも、同じ列にも同じ対角線上にもありません。
|
||||
|
||||
3. 2 行目で試せる位置は `(2, 0)` だけです。
|
||||
|
||||
この段階では、現在の配置が条件を満たすことだけが分かります。この先でチェス盤を完成できなければ、戻って、それより前の別の選択を試す必要があります。
|
||||
|
||||
## 13.6.2 プログラミング演習
|
||||
|
||||
### 1. 重複要素のない全順列
|
||||
|
||||
整数配列 `nums` には少なくとも 1 つの要素があり、すべての要素が互いに異なります。
|
||||
各要素を 1 回ずつ使って作れるすべての並びを列挙し、それぞれの並びを配列として返してください。
|
||||
返す結果の中で、各順列を並べる順序は問いません。
|
||||
バックトラッキングを使い、各位置の要素が現在の順列にすでに選ばれているかをブール配列で記録してください。
|
||||
|
||||
??? tip "解法のヒント"
|
||||
|
||||
1. 再帰の深さは、順列の何番目の位置を埋めているかを表します
|
||||
2. 各層では、まだ使っていない要素だけを試します
|
||||
3. 経路の長さが `nums` の長さに達したら、そのコピーを答えへ追加します
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/permutations/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -20,3 +20,4 @@ icon: material/map-marker-path
|
||||
- [13.3 部分和問題](subset_sum_problem.md)
|
||||
- [13.4 n クイーン問題](n_queens_problem.md)
|
||||
- [13.5 まとめ](summary.md)
|
||||
- [13.6 演習](exercises.md)
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- utils/exercises/publish_exercises.py により自動生成されています。直接編集しないでください。 -->
|
||||
|
||||
# 2.6 演習
|
||||
|
||||
## 2.6.1 確認問題
|
||||
|
||||
### 1. 反復と再帰の時間・空間計算量
|
||||
|
||||
次の 2 つのコードは、どちらも $1 + 2 + \dots + n$ を計算します($n \ge 1$ とします)。`n` を 4 として、
|
||||
プログラムが実際に実行される順序に沿って次の問いに答え、2 つの書き方の効率を比較してください。
|
||||
|
||||
```python
|
||||
def sum_iter(n):
|
||||
s = 0
|
||||
for i in range(1, n + 1):
|
||||
s += i
|
||||
return s
|
||||
|
||||
def sum_recur(n):
|
||||
if n == 1:
|
||||
return 1
|
||||
return n + sum_recur(n - 1)
|
||||
```
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. `sum_iter(4)` を実行すると、各ループの終了時に変数 `s` はそれぞれいくつになりますか?
|
||||
2. `sum_recur(4)` を実行すると、どの関数が順に呼び出されますか?最も深い呼び出しから戻るとき、結果はどのように求められますか?
|
||||
3. 2 つの書き方の時間計算量と空間計算量は、それぞれいくつですか?問い 1、2 の実行過程と結び付けて理由を説明してください。
|
||||
|
||||
??? success "解答"
|
||||
|
||||
1. ループ変数 `i` は `1、2、3、4` の順に変化し、各ループの終了時に `s` は
|
||||
`1、3、6、10` となります。したがって、`sum_iter(4)` は 10 を返します。
|
||||
|
||||
2. 関数は
|
||||
`sum_recur(4) → sum_recur(3) → sum_recur(2) → sum_recur(1)` の順に呼び出されます。
|
||||
`sum_recur(1)` が 1 を返した後、各呼び出しは順に `2 + 1 = 3`、`3 + 3 = 6`、`4 + 6 = 10` を得ます。
|
||||
最も深い呼び出しに到達した時点では、4 回の関数呼び出しはどれもまだ終了していません。
|
||||
|
||||
3. どちらのコードも、$n$ に比例する回数のループまたは呼び出しを行うため、時間計算量はともに $O(n)$ です。
|
||||
一方、空間計算量は異なります。反復版が使う変数は定数個だけなので $O(1)$ です。
|
||||
再帰版では、終了条件に到達するまで、それまでの関数呼び出しが結果を待つ必要があります。そのため、呼び出しスタックには最大で $n$ 回分の呼び出しが同時に保存され、
|
||||
空間計算量は $O(n)$ です。
|
||||
|
||||
空間計算量を分析するときは、コード中の変数だけでなく、再帰呼び出しが使う空間も考える必要があります。
|
||||
|
||||
### 2. 3 つのコードの時間計算量
|
||||
|
||||
次の 3 つのコード片はいずれも、正の整数 $n$ を入力とします。時間計算量が小さい順に並べ、それぞれの計算量を書いてください。
|
||||
|
||||
```python
|
||||
# コード片 1
|
||||
s = 0
|
||||
for i in range(n):
|
||||
s += i
|
||||
|
||||
# コード片 2
|
||||
s = 0
|
||||
for i in range(n):
|
||||
for j in range(i, n):
|
||||
s += j
|
||||
|
||||
# コード片 3
|
||||
while n > 1:
|
||||
n = n // 2
|
||||
```
|
||||
|
||||
??? success "解答"
|
||||
|
||||
小さい順に、コード片 3 は $O(\log n)$、コード片 1 は $O(n)$、コード片 2 は $O(n^2)$ です。
|
||||
コード片 3 では、ループのたびに $n$ が半分になるため、ループ回数は約 $\log_2 n$ 回です。
|
||||
コード片 1 のループはちょうど $n$ 回実行されます。コード片 2 の内側のループ回数は順に
|
||||
$n,n-1,\dots,1$ で、合計は $n(n+1)/2$ となるため、二次の計算量です。
|
||||
|
||||
### 3. どちらの反転が空間を節約できるか
|
||||
|
||||
配列 `nums` のすべての要素を逆順にする方法として、次の 2 つがあります。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 同じ長さの新しい配列 `res` を作り、逆順にコピーして返す。
|
||||
2. 2 つのインデックス `i` と `j` をそれぞれ先頭と末尾から中央へ動かし、`nums[i]` と `nums[j]` を順に交換する。
|
||||
|
||||
2 つの方法の空間計算量は、それぞれいくつですか?どちらが「インプレース」な操作ですか?
|
||||
|
||||
??? success "解答"
|
||||
|
||||
1. 入力と同じ長さの補助配列が必要なため、空間計算量は $O(n)$ です。
|
||||
|
||||
2. 使うのは 2 つのインデックス変数だけなので、
|
||||
空間計算量は $O(1)$ であり、インプレースな操作です。
|
||||
|
||||
ただし、インプレースな反転は入力配列を変更します。
|
||||
入力の変更が許される場合にのみ優先して使うべきです。元の配列を残す必要がある場合は、方法 1 のコピーにかかる空間を省くことはできません。
|
||||
|
||||
## 2.6.2 プログラミング演習
|
||||
|
||||
### 1. フィボナッチ数
|
||||
|
||||
フィボナッチ数列は、$F(0)=0$、$F(1)=1$ を満たし、$n\ge2$ のとき
|
||||
$F(n)=F(n-1)+F(n-2)$ となります。
|
||||
|
||||
0 以上の整数 `n` が与えられます。再帰を使わず、ループを使って $F(n)$ を計算し、返してください。
|
||||
|
||||
??? tip "解法のヒント"
|
||||
|
||||
1. まず、n が 0 と 1 の場合をそれぞれ処理します
|
||||
2. 次の項の計算に必要なのは直前の 2 項だけで、数列全体を保存する必要はありません
|
||||
3. 2 つの変数を更新するときは、後で使う古い値を先に上書きしないよう注意します
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/fibonacci-number/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -20,3 +20,4 @@ icon: material/timer-sand
|
||||
- [2.3 時間計算量](time_complexity.md)
|
||||
- [2.4 空間計算量](space_complexity.md)
|
||||
- [2.5 まとめ](summary.md)
|
||||
- [2.6 演習](exercises.md)
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- utils/exercises/publish_exercises.py により自動生成されています。直接編集しないでください。 -->
|
||||
|
||||
# 3.6 演習
|
||||
|
||||
## 3.6.1 確認問題
|
||||
|
||||
### 1. 身近な場面にあるデータの関係
|
||||
|
||||
データ同士の関係に基づいて、次の 3 つの場面に「線形構造、木構造、網状構造」のいずれかを選び、理由を説明してください。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 生徒が一列に並び、それぞれが自分の前と後ろにいる人だけに注目する。
|
||||
2. 学校が「学校 → 学年 → クラス」という階層で管理されている。
|
||||
3. 都市の道路が複数の交差点を結び、1 つの交差点から複数の別の交差点へ行けるほか、道路が環状につながることもある。
|
||||
|
||||
??? success "解答"
|
||||
|
||||
1. 線形構造です。列の先頭と最後にいる人を除き、それぞれの人は前後の 1 人ずつとだけ隣り合い、関係が一本の線に沿って並んでいます。
|
||||
|
||||
2. 木構造です。各クラスはいずれか 1 つの学年に属し、各学年はさらに学校に属しており、関係が上から下へ階層的に広がっています。
|
||||
|
||||
3. 網状構造です。1 つの交差点を複数の交差点と結ぶことができ、経路が環状につながる場合もあるため、1 つの順序や厳密な階層では表せません。
|
||||
|
||||
構造を判断するときは、メモリをどれだけ使うかを先に考えるのではなく、まず要素同士の関係を観察します。
|
||||
|
||||
### 2. 論理的な順序をメモリに保存する方法
|
||||
|
||||
論理的な順序 `A → B → C` を保存するために、次の 2 つの簡略化したメモリ配置を考えます。
|
||||
|
||||
- 方法 A:`A、B、C` を、それぞれ番号 `20、21、22` のメモリ領域に置く。
|
||||
- 方法 B:`A、B、C` を、それぞれ番号 `20、7、31` のメモリ領域に置き、`A` に `B` の位置、`B` に `C` の位置を記録する。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. どちらが連続した領域への保存で、どちらが分散した領域への保存ですか?
|
||||
2. 2 つの方法は、それぞれ配列と連結リストのどちらに近いですか?
|
||||
3. 方法 B のメモリ領域の番号は大きさの順に並んでいません。それでも `A → B → C` という論理的な順序を表せるのはなぜですか?
|
||||
|
||||
??? success "解答"
|
||||
|
||||
1. 方法 A は連続したメモリ領域を使っているため、連続した領域への保存です。方法 B のノードは別々の場所に分散しているため、分散した領域への保存です。
|
||||
|
||||
2. 方法 A は配列に、方法 B は連結リストに近いです。
|
||||
|
||||
3. 論理的な順序を決めるのはノード間に記録された接続関係であり、メモリ領域の番号の大小ではありません。
|
||||
`A` に記録された位置から `B` を見つけ、さらに `B` に記録された位置から `C` を見つけられるため、`A、B、C` の順にアクセスできます。
|
||||
|
||||
このことからも、論理構造と物理構造は、同じデータを異なる視点から捉えたものだと分かります。
|
||||
|
||||
### 3. 宿題の記録におけるデータ型と構造
|
||||
|
||||
ある学習グループが、座席順に 4 人の生徒が宿題を提出したかどうかを記録したところ、次の結果になりました。
|
||||
|
||||
`[true, false, true, true]`
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 各要素には、どの基本データ型が適していますか?
|
||||
2. この 4 つの要素は座席順に一列に並んでいます。どのような論理構造を使っていますか?
|
||||
3. 今後、各生徒の宿題の点数を `[90, 0, 85, 100]` と記録するように変えた場合、変わるのはデータの「内容の型」と「構成方法」のどちらですか?理由も説明してください。
|
||||
|
||||
??? success "解答"
|
||||
|
||||
1. 各要素は「はい」か「いいえ」だけを表すため、ブール型 `bool` が適しています。
|
||||
|
||||
2. これらの要素は座席順に並んで線形構造を作っており、配列で保存できます。
|
||||
|
||||
3. 変わるのは内容の型です。要素がブール値から整数に変わります。一方、構成方法は変わりません。
|
||||
データは引き続き座席順に一列に並び、配列という線形構造を使えます。
|
||||
|
||||
基本データ型は「何を保存するか」を、データ構造は「データをどのように構成するか」を表します。
|
||||
|
||||
## 3.6.2 プログラミング演習
|
||||
|
||||
### 1. 二進表現に含まれる 1 の個数
|
||||
|
||||
0 以上の整数 `n` が与えられます。その二進表現に 1 がいくつ含まれるかを数えてください。
|
||||
|
||||
ビット演算を使って求めてください。二進表現を文字列に変換したり、1 を直接数える組み込み関数を使ったりしてはいけません。
|
||||
|
||||
??? tip "解法のヒント"
|
||||
|
||||
1. n & 1 で n の最下位ビットを取り出し、そのビットが 1 かどうかを判定できます
|
||||
2. 1 ビット右へシフトすると、現在の最下位ビットが取り除かれます。多くの言語では演算子 >> を使います
|
||||
3. 各ビットを調べて右へシフトする方法を完成させたら、n & (n - 1) が n の最も右にある 1 を 0 に変えることも確認してみましょう
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/number-of-1-bits/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -20,3 +20,4 @@ icon: material/shape-outline
|
||||
- [3.3 数値エンコーディング *](number_encoding.md)
|
||||
- [3.4 文字エンコーディング *](character_encoding.md)
|
||||
- [3.5 まとめ](summary.md)
|
||||
- [3.6 演習](exercises.md)
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- utils/exercises/publish_exercises.py により自動生成されています。直接編集しないでください。 -->
|
||||
|
||||
# 12.6 演習
|
||||
|
||||
## 12.6.1 確認問題
|
||||
|
||||
### 1. 分割統治に適した処理
|
||||
|
||||
ある生徒は、「まず 2 つに分け、それぞれを解いてから結果をまとめる」という方法で、次の処理を行おうとしています。
|
||||
それぞれについて、「分割統治に適している」「分割統治はできるが、作業の総量は減らない」「2 つを独立して解けない」のいずれかを判断し、理由を説明してください。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. ソートされていない配列をソートする。
|
||||
2. 配列の最大値を求める。
|
||||
3. 一連の `push(x)`、`pop()` というスタック操作を順に実行し、各 `pop()` で得た要素を出力する。
|
||||
|
||||
??? success "解答"
|
||||
|
||||
1. 適しています。半分に分け、2 つを独立してソートし、$O(n)$ でマージします。これはマージソートそのものです。
|
||||
2. 分割統治はできますが、作業の総量は減りません。左右の 2 つを合わせると、結局は $n$ 個の要素をすべて確認する必要があり、
|
||||
直接走査する場合と同じく $O(n)$ の時間がかかります。
|
||||
3. 2 つを独立して解くことはできません。後半を開始するときのスタックの内容が前半の実行結果に依存するため、
|
||||
互いの結果を知らない状態では独立して処理できません。
|
||||
|
||||
### 2. 高速べき乗で計算を減らす仕組み
|
||||
|
||||
次の再帰関数は、分割統治を使って $x^n$ を計算します。
|
||||
|
||||
```python
|
||||
def fast_pow(x, n):
|
||||
if n == 0:
|
||||
return 1
|
||||
half = fast_pow(x, n // 2)
|
||||
if n % 2 == 0:
|
||||
return half * half
|
||||
return half * half * x
|
||||
```
|
||||
|
||||
この関数で `fast_pow(3, 5)` を計算します。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 再帰呼び出しでは、引数 `n` はどのような値に順に変わりますか?
|
||||
2. 最も深い呼び出しから戻るとき、各呼び出しはどの値を順に返しますか?
|
||||
3. `fast_pow(x, n // 2)` を 2 回書くのではなく、先に `half` へ保存するのはなぜですか?
|
||||
|
||||
??? success "解答"
|
||||
|
||||
1. 引数は `5 → 2 → 1 → 0` の順に変わります。毎回指数を半分にし、終了条件に達するまで続けます。
|
||||
|
||||
2. `n = 0` のとき 1 を返します。`n = 1` のとき $1×1×3=3$、
|
||||
`n = 2` のとき $3×3=9$、`n = 5` のとき $9×9×3=243$ を返します。
|
||||
|
||||
3. 乗算の両側に `fast_pow(x, n // 2)` を 1 回ずつ書くと、2 つの再帰呼び出しがまったく同じ部分問題を計算します。
|
||||
結果を先に `half` へ保存すれば、各層で再帰するのは 1 回だけとなり、再帰の深さは約 $\log n$ です。
|
||||
2 回呼び出すと、大量の重複計算が発生します。
|
||||
|
||||
### 3. 走査列から左右の部分木を分ける
|
||||
|
||||
重複するノードを持たない二分木の先行順走査と中間順走査は、それぞれ次のとおりです。
|
||||
|
||||
- 先行順走査:`[A, B, D, E, C]`
|
||||
- 中間順走査:`[D, B, E, A, C]`
|
||||
|
||||
根ノードの層だけを分けてください。それ以降の再帰を行ったり、木全体を描いたりする必要はありません。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 根ノードは何ですか?
|
||||
2. 左右の部分木は、中間順走査のどの部分にそれぞれ対応しますか?
|
||||
3. 左右の部分木は、先行順走査のどの部分にそれぞれ対応しますか?根ノードの直接の子ノードは何ですか?
|
||||
|
||||
??? success "解答"
|
||||
|
||||
1. 先行順走査の最初のノードが根ノードなので、根ノードは `A` です。
|
||||
|
||||
2. `A` によって中間順走査は 2 つに分かれます。左部分木は `[D, B, E]`、右部分木は `[C]` です。
|
||||
|
||||
3. 左部分木には 3 つのノードがあるため、根ノード `A` の後ろにある 3 つの先行順走査の要素が左部分木に属し、
|
||||
`[B, D, E]` となります。残りの `[C]` は右部分木に属します。
|
||||
したがって、根ノードの左子ノードは `B`、右子ノードは `C` です。
|
||||
|
||||
## 12.6.2 プログラミング演習
|
||||
|
||||
### 1. 高速べき乗
|
||||
|
||||
実数 `x` と整数 `n` が与えられます。言語に組み込まれたべき乗関数を呼び出さず、$x^n$ を計算してください。
|
||||
再帰による分割統治を使い、毎回指数を半分にして、計算済みの部分問題の結果を再利用してください。
|
||||
この問題では `x = 0` の場合も含めて $x^0=1$ とします。`n < 0` のときは `x != 0` が保証され、答えを $(1/x)^{-n}$ に変換できます。
|
||||
|
||||
??? tip "解法のヒント"
|
||||
|
||||
1. n が 0 のとき、答えは 1 です
|
||||
2. x の n // 2 乗を計算したら half に保存し、2 回目の再帰呼び出しを行わないようにします
|
||||
3. n < 0 のときは、まず x を 1 / x に変え、次に n を -n に変えます。C++ または Java を使う場合は、最小の 32 ビット整数の符号を反転したときのオーバーフローを避けるため、先に n を 64 ビット整数へ変換できます
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/powx-n/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -20,3 +20,4 @@ icon: material/set-split
|
||||
- [12.3 二分木の構築問題](build_binary_tree_problem.md)
|
||||
- [12.4 ハノイの塔の問題](hanota_problem.md)
|
||||
- [12.5 まとめ](summary.md)
|
||||
- [12.6 演習](exercises.md)
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- utils/exercises/publish_exercises.py により自動生成されています。直接編集しないでください。 -->
|
||||
|
||||
# 14.8 演習
|
||||
|
||||
## 14.8.1 確認問題
|
||||
|
||||
### 1. 動的計画法が適している場面
|
||||
|
||||
ある生徒は、「漸化式を書けるなら、必ず動的計画法を使うべきです」と言いました。
|
||||
次の 3 つの処理には、動的計画法、バックトラッキング、または `dp` テーブルを作らないループや数式のどれが適しているかを判断し、重要な理由を 1 つ書いてください。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 額面 `[1, 3, 4]` の硬貨を使って金額 6 を作るときの最小硬貨枚数を求める。各硬貨は何度でも使える。
|
||||
2. `[1, 2, 3]` のすべての順列を出力する。
|
||||
3. $1 + 2 + \dots + n$ を計算する。
|
||||
|
||||
動的計画法が適していると判断した処理については、`dp[i]` が何を表すかも説明してください。
|
||||
|
||||
??? success "解答"
|
||||
|
||||
1. 動的計画法が適しています。`dp[i]` を、金額 `i` を作るのに必要な最小硬貨枚数とします。
|
||||
`i` 以下の各硬貨 `c` について、`dp[i-c] + 1` を答えの候補とし、
|
||||
それらの候補の最小値を選べます。異なる選び方でも同じ金額を繰り返し扱い、より大きな金額の最適解をより小さな金額の最適解から作れるためです。
|
||||
金額 6 の答えは 2 で、`3 + 3` と選びます。
|
||||
|
||||
2. バックトラッキングを使うべきです。すべての 6 通りの順列を 1 つずつ生成する問題なので、1 つの選択を試して探索を続け、
|
||||
選択を取り消して別の分岐を試すバックトラッキングが使えます。どのような方法でも、これらの順列を実際に出力するには列挙を省けません。
|
||||
|
||||
3. ループまたは等差数列の公式で十分です。`S(i) = S(i-1) + i` という漸化式は書けますが、`S(i)` の計算が依存するのは 1 つ前の `S(i-1)` だけで、
|
||||
各部分和は 1 回だけ計算すればよく、重複部分問題がありません。そのため、`dp` テーブルは不要です。「漸化式を書ける」ことと「動的計画法が必要である」ことは同じではありません。
|
||||
|
||||
### 2. ナップサック表の 1 つの値を求める
|
||||
|
||||
0-1 ナップサック問題を考えます。品物の重さは `wgt = [1, 2, 3]`、価値は `val = [5, 11, 15]`、ナップサックの容量は 4 です。
|
||||
`dp[i][c]` は、先頭から $i$ 個の品物だけを考え、ナップサックの容量上限が $c$ のときに得られる最大価値を表します。
|
||||
ナップサックをちょうど満たす必要はありません。
|
||||
|
||||
ここでは状態 `dp[3][4]` だけを計算します。`dp[2][4] = 16`、`dp[2][1] = 5` であることが分かっています。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 3 番目の品物を選ばない場合、候補となる価値はいくつですか?
|
||||
2. 3 番目の品物を選ぶ場合、容量はいくつ残り、候補となる価値はいくつですか?
|
||||
3. `dp[3][4]` はいくつにすべきですか?どの品物を選んだことに対応しますか?
|
||||
|
||||
??? success "解答"
|
||||
|
||||
1. 3 番目の品物を選ばない場合は、最初の 2 つの品物による結果をそのまま使うため、候補となる価値は `dp[2][4] = 16` です。
|
||||
|
||||
2. 3 番目の品物の重さは 3 なので、入れた後の残り容量は $4-3=1$ です。候補となる価値は
|
||||
`dp[2][1] + 15 = 5 + 15 = 20` です。
|
||||
|
||||
3. 16 と 20 を比較して、`dp[3][4] = 20` とします。これは 1 番目と 3 番目の品物を選ぶことに対応し、
|
||||
総重量は $1+3=4$、総価値は $5+15=20$ です。
|
||||
|
||||
この状態の計算は、0-1 ナップサックにおける「選ぶか、選ばないか」という 1 回の比較を表しています。
|
||||
|
||||
### 3. ナップサック容量を更新する順序
|
||||
|
||||
0-1 ナップサックに、重さ 2、価値 5 の品物が 1 つだけあり、ナップサックの容量は 4 です。
|
||||
各品物は 1 回までしか選べません。最初の一次元配列は `dp = [0, 0, 0, 0, 0]` です。
|
||||
|
||||
ある生徒は、この品物を処理するとき、容量を 2 から 4 の順で更新しました。
|
||||
|
||||
- `dp[2]` を更新すると 5 になる。
|
||||
- `dp[3]` を更新しても 5 になる。
|
||||
- `dp[4]` を更新するとき、更新したばかりの `dp[2]` を使うため、`dp[4] = 10` になる。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. `dp[4] = 10` という結果は正しいですか?その理由も説明してください。
|
||||
2. 「各品物は 1 回までしか選べない」という条件に基づくと、`dp[4]` はいくつですか?
|
||||
3. 各品物を処理するとき、容量は大きいほうから小さいほうへ更新しますか、それとも小さいほうから大きいほうへ更新しますか?その順序によって、どのような問題を防げますか?
|
||||
|
||||
??? success "解答"
|
||||
|
||||
1. この結果は正しくありません。価値 10 は、この価値 5 の品物を 2 回入れたことに相当し、
|
||||
「各品物は 1 回までしか選べない」という条件に反します。
|
||||
|
||||
2. ナップサックへ入れられるのはこの品物 1 つだけなので、正しい `dp[4]` は 5 です。
|
||||
|
||||
3. 容量は大きいほうから小さいほうへ、つまり 4、3、2 の順に更新します。
|
||||
この順序なら、`dp[c]` の計算時に読み取る `dp[c-2]` は、現在の品物を処理する前の値のままなので、
|
||||
同じ回の中で現在の品物を繰り返し使うことを防げます。
|
||||
|
||||
## 14.8.2 プログラミング演習
|
||||
|
||||
### 1. 階段を上る方法の数
|
||||
|
||||
`n` 段の階段があります。1 回に 1 段または 2 段だけ上ることができ、ちょうど `n` 段目に着かなければなりません。
|
||||
異なる上り方が何通りあるかを求めてください。`n >= 1` とし、それぞれの上り方は、各回に 1 段進むか 2 段進むかだけで区別します。
|
||||
一次元の動的計画法の配列を使って求め、2 つの状態だけを残す空間最適化は、ここでは使わないでください。
|
||||
|
||||
??? tip "解法のヒント"
|
||||
|
||||
1. i 段目へ到達する最後の一歩では、1 段または 2 段だけ上れます
|
||||
2. そのため、dp[i] = dp[i-1] + dp[i-2] となります
|
||||
3. まず n が 1 と 2 の場合を処理し、3 段目から表を埋めます
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/climbing-stairs/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
|
||||
### 2. 0-1 ナップサック
|
||||
|
||||
同じ長さの配列 `wgt` と `val` が与えられます。`i` 番目の品物の重さは正の整数 `wgt[i]`、価値は 0 以上の整数 `val[i]` で、
|
||||
ナップサックの容量 `cap` は 0 以上の整数です。各品物は 1 回までしか選べません。総重量が `cap` 以下という条件で、
|
||||
ナップサックに入れられる最大の総価値を求めてください。一次元の動的計画法を使って実装してください。
|
||||
|
||||
??? tip "解法のヒント"
|
||||
|
||||
1. 長さ cap + 1 の配列 dp を初期化します。dp[c] は、容量上限が c のときの最大価値を表します
|
||||
2. 品物 i を処理するとき、「選ばない」場合の dp[c] と、「選ぶ」場合の dp[c-wgt[i]] + val[i] を比較します
|
||||
3. 同じ回の中で現在の品物を繰り返し選ばないように、容量は必ず大きいほうから小さいほうへ更新します
|
||||
@@ -22,3 +22,4 @@ icon: material/table-pivot
|
||||
- [14.5 完全ナップサック問題](unbounded_knapsack_problem.md)
|
||||
- [14.6 編集距離問題](edit_distance_problem.md)
|
||||
- [14.7 まとめ](summary.md)
|
||||
- [14.8 演習](exercises.md)
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- utils/exercises/publish_exercises.py により自動生成されています。直接編集しないでください。 -->
|
||||
|
||||
# 9.5 演習
|
||||
|
||||
## 9.5.1 確認問題
|
||||
|
||||
### 1. 同じグラフを 2 つの方法で表す
|
||||
|
||||
無向グラフに 4 つの頂点 `A、B、C、D` があり、辺は
|
||||
`A-B、A-C、B-C、C-D` です。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. このグラフの隣接リストを書いてください。
|
||||
2. 0 と 1 だけを使って隣接行列を埋めてください。
|
||||
3. `A` と `D` が直接つながっているかを調べるとき、1 つの記録を見るだけで済むのは、どちらの表現方法ですか?
|
||||
4. 頂点が多く、辺が少ないグラフでは、一般にどちらの表現方法が空間を節約できますか?
|
||||
|
||||
??? success "解答"
|
||||
|
||||
1. 隣接リストは次のとおりです。
|
||||
|
||||
```text
|
||||
A: B, C
|
||||
B: A, C
|
||||
C: A, B, D
|
||||
D: C
|
||||
```
|
||||
|
||||
2. 隣接行列は次のとおりです。
|
||||
|
||||
| | A | B | C | D |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| A | 0 | 1 | 1 | 0 |
|
||||
| B | 1 | 0 | 1 | 0 |
|
||||
| C | 1 | 1 | 0 | 1 |
|
||||
| D | 0 | 0 | 1 | 0 |
|
||||
|
||||
3. 隣接行列では `A` 行 `D` 列を直接見ればよいため、任意の 2 頂点が直接つながっているかを調べるのに適しています。
|
||||
|
||||
4. 頂点が多く、辺が少ない場合、隣接リストは実際に存在する辺だけを記録します。そのため、頂点のすべての組に対して領域を用意する隣接行列よりも、一般に空間を節約できます。
|
||||
|
||||
### 2. 幅優先走査と深さ優先走査の訪問順序
|
||||
|
||||
無向グラフの頂点は `A、B、C、D、E`、辺は
|
||||
`A-B、A-C、B-D、C-D、D-E` です。
|
||||
|
||||
A から開始し、未訪問の隣接頂点が複数ある場合はアルファベット順に選ぶものとします。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 幅優先走査(BFS)の訪問順序を書いてください。
|
||||
2. 再帰による深さ優先走査(DFS)の訪問順序を書いてください。
|
||||
3. どちらの走査でも、訪問済みの頂点を記録する必要があるのはなぜですか?
|
||||
|
||||
??? success "解答"
|
||||
|
||||
1. BFS の訪問順序は `A, B, C, D, E` です。まず A から辺 1 本で到達できる B、C を訪問し、
|
||||
次により遠い D、E を訪問します。
|
||||
|
||||
2. DFS の訪問順序は `A, B, D, C, E` です。現在の頂点から未訪問の隣接頂点へ順に進むため、
|
||||
まず `A → B → D → C` と進みます。C に新しい隣接頂点がなければ D に戻り、次に E を訪問します。
|
||||
|
||||
3. グラフには、たとえば `A-B-D-C-A` のような閉路があります。訪問済みの頂点を記録しないと、
|
||||
閉路に沿って同じ頂点を何度も訪問し、走査が正常に終了しない可能性があります。
|
||||
|
||||
### 3. 1 回の BFS でグラフ全体を訪問できるか
|
||||
|
||||
無向グラフに頂点 `A、B、C、D、E、F` があり、辺は
|
||||
`A-B、B-C、D-E` だけです。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. A から 1 回 BFS を行うと、どの頂点を訪問できますか?
|
||||
2. 問い 1 の結果から、この 1 回の BFS でグラフのすべての頂点を訪問できたといえますか?その理由も説明してください。
|
||||
3. すべての頂点をアルファベット順に調べ、未訪問の頂点に出会うたびに新しい BFS を開始します。
|
||||
各 BFS の開始頂点は何ですか?このグラフはいくつの互いにつながっていない部分(連結成分)に分かれますか?
|
||||
|
||||
??? success "解答"
|
||||
|
||||
1. A からは `A、B、C` だけを訪問できます。
|
||||
|
||||
2. すべての頂点を訪問できていません。`D、E` は別の互いにつながった部分を作り、F は単独の頂点です。
|
||||
これらと A の間には経路がないため、A から到達できません。
|
||||
|
||||
3. 3 回の BFS の開始頂点は順に `A、D、F` で、それぞれ
|
||||
`{A, B, C}`、`{D, E}`、`{F}` を訪問します。したがって、このグラフには 3 つの連結成分があります。
|
||||
|
||||
## 9.5.2 プログラミング演習
|
||||
|
||||
### 1. 無向グラフに経路が存在するかを判定する
|
||||
|
||||
$n$ 個の頂点を持つ無向グラフが与えられ、頂点には $0$ から $n-1$ までの番号が付いています。配列 `edges` の各要素 `[u, v]` は、頂点 `u` と `v` の間に無向辺があることを表します。
|
||||
|
||||
さらに、始点 `source` と終点 `destination` が与えられます。まず `edges` から隣接リストを作り、次に BFS または DFS を使って、
|
||||
`source` から `destination` への経路が存在するかを判定してください。存在する場合は `true`、存在しない場合は `false` を返します。
|
||||
グラフには閉路がある場合も、連結していない場合もあります。
|
||||
|
||||
??? tip "解法のヒント"
|
||||
|
||||
1. 各無向辺は、両方の向きで隣接リストへ追加する必要があります
|
||||
2. グラフには閉路がある可能性があるため、訪問済みのノードを必ず記録します
|
||||
3. source から開始し、destination に出会ったら true を返します。走査を終えても出会わなければ false を返します
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/find-if-path-exists-in-graph/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -19,3 +19,4 @@ icon: material/graphql
|
||||
- [9.2 グラフの基本操作](graph_operations.md)
|
||||
- [9.3 グラフの走査](graph_traversal.md)
|
||||
- [9.4 まとめ](summary.md)
|
||||
- [9.5 演習](exercises.md)
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- utils/exercises/publish_exercises.py により自動生成されています。直接編集しないでください。 -->
|
||||
|
||||
# 15.6 演習
|
||||
|
||||
## 15.6.1 確認問題
|
||||
|
||||
### 1. 毎回最大の硬貨を選べば必ず最善か
|
||||
|
||||
硬貨の額面は `[1, 7, 10]`、目標金額は 14 です。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 「残りの金額を超えない最大の額面を毎回選ぶ」という方法を使い、選ぶ硬貨を書いてください。
|
||||
2. より少ない硬貨を使う方法はありますか?ある場合は 1 つ書き、ない場合は理由を説明してください。
|
||||
3. この結果から、この貪欲戦略がどのような硬貨の額面でも正しいといえますか?
|
||||
|
||||
??? success "解答"
|
||||
|
||||
1. 貪欲戦略では `10 + 1 + 1 + 1 + 1` の順に選び、硬貨は合計 5 枚です。
|
||||
|
||||
2. より少ない硬貨を使う方法があります。`7 + 7` なら、硬貨は 2 枚だけです。
|
||||
|
||||
3. 正しいとはいえません。この反例から、どのような硬貨の額面でも、現在選べる最大の額面を毎回選ぶ方法が最小の硬貨枚数を得るとは限らないと分かります。
|
||||
目の前で最大の選択をすると、その後のよりよい組合せを作れなくなる場合があります。
|
||||
|
||||
### 2. ナップサックに先に入れる品物
|
||||
|
||||
容量 4 キログラムのナップサックに、次の品物を入れます。品物の一部だけを入れることもでき、
|
||||
得られる価値は入れた重さに比例します。
|
||||
|
||||
- 品物 A:重さ 4 キログラム、価値 20。
|
||||
- 品物 B:重さ 3 キログラム、価値 18。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 2 つの品物の 1 キログラム当たりの価値は、それぞれいくつですか?どちらを先に入れるべきですか?
|
||||
2. 分数ナップサックの貪欲戦略に従ってナップサックを満たすと、最終的な価値はいくつですか?
|
||||
3. 品物を分割でき、ナップサックに総重量の制限がある場合、品物を選ぶときに比較すべきなのは、総価値と 1 キログラム当たりの価値のどちらですか?その理由も説明してください。
|
||||
|
||||
??? success "解答"
|
||||
|
||||
1. A の 1 キログラム当たりの価値は `20 ÷ 4 = 5`、B は `18 ÷ 3 = 6` です。
|
||||
したがって、単位重量当たりの価値が高い B を先に入れます。
|
||||
|
||||
2. まず B をすべて入れ、3 キログラムを使って価値 18 を得ます。残りの容量は 1 キログラムなので、
|
||||
次に A を 1 キログラムだけ入れて価値 5 を得ます。最終的な価値は `18 + 5 = 23` です。
|
||||
|
||||
3. ナップサックが制限するのは総重量で、品物を分割できるため、単位重量当たりの価値を比較すべきです。
|
||||
A は総価値が高くても、1 キログラム当たりの価値は B より低くなっています。A を先に入れてナップサックを満たすと、価値 20 しか得られません。
|
||||
|
||||
### 3. 2 つのポインタを次にどう動かすか
|
||||
|
||||
仕切り板の高さが `[1, 8, 6, 2, 5]` であるとき、先頭と末尾に置いた 2 つのポインタを使って最大容量を求めます。
|
||||
容量は「2 枚の仕切り板のうち低いほうの高さ × 2 枚の仕切り板のインデックスの差」です。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 最初は左のポインタがインデックス 0、右のポインタがインデックス 4 にあります。現在の容量はいくつですか?次にどちらのポインタを動かしますか?
|
||||
2. 問い 1 で選んだポインタを 1 回動かした後、2 つのポインタはそれぞれどのインデックスにありますか?このときの容量はいくつですか?次はどちらのポインタを動かしますか?
|
||||
3. 現在の 2 枚の仕切り板について、低い仕切り板のポインタを動かすことも、高い仕切り板のポインタを動かすこともできます。より大きな容量を得られる可能性が残るのはどちらですか?その理由も説明してください。
|
||||
|
||||
??? success "解答"
|
||||
|
||||
1. 現在の容量は `min(1, 5) × (4 - 0) = 4` です。左側の仕切り板のほうが低いため、左のポインタを動かします。
|
||||
|
||||
2. 左のポインタを動かすと、2 つのポインタはそれぞれインデックス 1 と 4 にあります。現在の容量は
|
||||
`min(8, 5) × (4 - 1) = 15` です。右側の仕切り板のほうが低いため、次は右のポインタを動かします。
|
||||
|
||||
3. 低い仕切り板に対応するポインタを動かす場合に限り、より大きな容量を得られる可能性が残ります。高い仕切り板を動かすと、2 枚の間の距離は必ず短くなり、容器の高さは動かしていない低い仕切り板によって引き続き制限されるため、
|
||||
高さは変わらないか低くなります。したがって、容量が移動前より大きくなることはありません。低い仕切り板を動かした場合にだけ、より高い仕切り板に出会える可能性があります。
|
||||
|
||||
## 15.6.2 プログラミング演習
|
||||
|
||||
### 1. 分数ナップサック
|
||||
|
||||
同じ長さの配列 `wgt` と `val` が与えられ、`wgt[i] > 0`、`val[i] >= 0`、ナップサックの容量は `cap >= 0` です。
|
||||
各種類の品物は 1 個ずつしかありませんが、その一部だけを入れることもできます。
|
||||
得られる価値は、入れた重さがその品物の総重量に占める割合に応じて決まります。貪欲法を使い、
|
||||
ナップサックで得られる最大の総価値を実数で返してください。
|
||||
|
||||
??? tip "解法のヒント"
|
||||
|
||||
1. まず、各品物の単位重量当たりの価値 val[i] / wgt[i] を計算します。除算結果の小数部分も残します
|
||||
2. 単位重量当たりの価値が高い品物からナップサックへ入れます
|
||||
3. 残り容量が現在の品物の重さより小さければ、ナップサックをちょうど満たす分だけ入れて終了します
|
||||
@@ -20,3 +20,4 @@ icon: material/head-heart-outline
|
||||
- [15.3 最大容量問題](max_capacity_problem.md)
|
||||
- [15.4 最大積分割問題](max_product_cutting_problem.md)
|
||||
- [15.5 まとめ](summary.md)
|
||||
- [15.6 演習](exercises.md)
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- utils/exercises/publish_exercises.py により自動生成されています。直接編集しないでください。 -->
|
||||
|
||||
# 6.5 演習
|
||||
|
||||
## 6.5.1 確認問題
|
||||
|
||||
### 1. ハッシュ衝突後の探索方法
|
||||
|
||||
5 個のバケットを持つハッシュテーブルがあり、ハッシュ関数は $h(x)=x \bmod 5$ です。衝突が起きた場合は、要素をそのバケットのリストへ順に格納します。
|
||||
`[1, 6, 11, 7]` をこの順に挿入します。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 0~4 番の各バケットの内容を書いてください。
|
||||
2. 6 を探索するとき、最初にどのバケットへ行き、どの要素を順に調べますか?
|
||||
3. 問い 1 で書いたバケットの内容では、後から挿入した要素が先に挿入した要素を上書きしていますか?この衝突処理方法と結び付けて理由を説明してください。
|
||||
|
||||
??? success "解答"
|
||||
|
||||
1. $1\bmod5=6\bmod5=11\bmod5=1$、$7\bmod5=2$ なので、各バケットは次のようになります。
|
||||
|
||||
```text
|
||||
0: []
|
||||
1: [1, 6, 11]
|
||||
2: [7]
|
||||
3: []
|
||||
4: []
|
||||
```
|
||||
|
||||
2. 6 を探索するときは、まず 1 番のバケットへ行き、1、6 の順に比較します。2 回目の比較で目的の値が見つかります。
|
||||
|
||||
3. ハッシュ値が同じであることは、同じバケットに入ることを表すだけで、それらの要素が等しいという意味ではありません。連鎖アドレス法では、衝突した要素をすべてバケット内に保存し、
|
||||
探索時に 1 つずつ比較します。そのため、1、6、11 が互いに上書きされることはありません。
|
||||
|
||||
### 2. ハッシュテーブルを拡張すると要素はどこへ移るか
|
||||
|
||||
連鎖アドレス法を使うハッシュテーブルには、もともと 5 個のバケットがあり、ハッシュ関数は $h(x)=x\bmod5$ です。
|
||||
キー `[1, 6, 11]` はすべて 1 番のバケットに入っています。
|
||||
|
||||
ここでハッシュテーブルを 7 個のバケットに拡張し、ハッシュ関数も $h(x)=x\bmod7$ に変えます。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 1、6、11 の新しいバケット番号をそれぞれ計算してください。
|
||||
2. 拡張後、どのバケットに要素が入っていますか?
|
||||
3. 拡張するとき、もとの 1 番のバケットにあるリストを、そのまま新しい 1 番のバケットへコピーできますか?問い 1、2 の結果と結び付けて理由を説明してください。
|
||||
|
||||
??? success "解答"
|
||||
|
||||
1. 新しいバケット番号は、それぞれ次のとおりです。
|
||||
|
||||
- $1\bmod7=1$。
|
||||
- $6\bmod7=6$。
|
||||
- $11\bmod7=4$。
|
||||
|
||||
2. 1 番のバケットには 1、4 番のバケットには 11、6 番のバケットには 6 が入ります。3 つのキーは同じバケットに集中しなくなります。
|
||||
|
||||
3. そのままコピーすることはできません。バケット番号は「キーをバケット数で割った余り」で決まります。バケット数が 5 から 7 に変わると、同じキーでも新しいバケット番号が変わる場合があるため、
|
||||
各キーの位置を計算し直す必要があります。古い 1 番のバケットをそのままコピーすると、新しい式で 6 と 11 を探索したときに、
|
||||
それぞれ 6 番と 4 番のバケットへ進むため、見つけられなくなります。
|
||||
|
||||
### 3. 6 を削除した後も 11 を見つけられるか
|
||||
|
||||
5 個の位置を持つハッシュテーブルがあり、インデックスは `0~4`、ハッシュ関数は $h(x)=x\bmod5$ です。
|
||||
衝突が起きた場合は、ハッシュ関数で得たインデックスから右へ進み、最初の空き位置を探します。
|
||||
|
||||
`[1, 6, 11]` をこの順に挿入します。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 3 つの数は、最終的にそれぞれどのインデックスに入りますか?
|
||||
2. 11 を探索するとき、どのインデックスを順に調べますか?
|
||||
3. 6 を削除するとき、その位置を「一度も使われていない空き位置」に変え、探索は空き位置に出会うと終了するものとします。
|
||||
この状態で 11 を探索すると、どうなりますか?その探索結果は正しいですか?問題がある場合は、どのように防げますか?
|
||||
|
||||
??? success "解答"
|
||||
|
||||
1. 1 はインデックス 1 に入ります。6 もインデックス 1 に対応するため、衝突後にインデックス 2 へ入ります。
|
||||
11 もインデックス 1 から始め、使用中のインデックス 1、2 を順に飛ばして、最終的にインデックス 3 へ入ります。
|
||||
|
||||
2. 11 の探索では、インデックス `1、2、3` を順に調べ、インデックス 3 で見つけます。
|
||||
|
||||
3. インデックス 2 を「一度も使われていない」ことを表す空き位置に変えると、11 の探索ではインデックス 1 を調べた後、インデックス 2 で停止してしまい、
|
||||
11 が存在しないと誤って判断します。削除時には「削除済み」の印を残す必要があります。
|
||||
探索ではその印に出会っても次のインデックスを調べ続け(インデックス 4 を越えたらインデックス 0 に戻ります)、後の挿入ではその位置を再利用できます。
|
||||
|
||||
## 6.5.2 プログラミング演習
|
||||
|
||||
### 1. 2 つの文字列の文字構成を比較する
|
||||
|
||||
小文字の英字だけを含む 2 つの文字列 `s` と `t` が与えられます。
|
||||
`s` の文字は自由に並べ替えられますが、文字の追加、削除、置換はできません。
|
||||
|
||||
並べ替えることで `t` を作れる場合は `true`、作れない場合は `false` を返してください。
|
||||
文字列内の文字をソートせず、ハッシュテーブルを使って各文字の出現回数を記録してください。
|
||||
|
||||
??? tip "解法のヒント"
|
||||
|
||||
1. 2 つの文字列の長さが異なる場合、文字の構成も必ず異なります
|
||||
2. ハッシュテーブルに各文字の個数を記録します。s を走査するとき、対応するカウントに 1 を足します
|
||||
3. t を走査するとき、対応するカウントから 1 を引きます。最終的にすべてのカウントが 0 の場合に限り、文字の構成が同じです
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/valid-anagram/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -19,3 +19,4 @@ icon: material/table-search
|
||||
- [6.2 ハッシュ衝突](hash_collision.md)
|
||||
- [6.3 ハッシュアルゴリズム](hash_algorithm.md)
|
||||
- [6.4 まとめ](summary.md)
|
||||
- [6.5 演習](exercises.md)
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- utils/exercises/publish_exercises.py により自動生成されています。直接編集しないでください。 -->
|
||||
|
||||
# 8.5 演習
|
||||
|
||||
## 8.5.1 確認問題
|
||||
|
||||
### 1. 10 をヒープへ追加した後の調整
|
||||
|
||||
配列 `[9, 7, 8, 3, 5]` は最大ヒープを表しています。ここに 10 を追加します。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. まず 10 を配列の末尾に追加すると、その親ノードの値はいくつですか?
|
||||
2. 新しいノードから下から上へヒープ化し、交換するたびに配列を書いてください。
|
||||
3. 最終的なヒープ頂点の要素はいくつですか?交換は全部で何回行われますか?
|
||||
|
||||
??? success "解答"
|
||||
|
||||
1. 10 を追加した後のインデックスは 5 で、その親ノードのインデックスは
|
||||
$\lfloor(5-1)/2\rfloor=2$ です。親ノードの値は 8 です。
|
||||
|
||||
2. 10 は 8 より大きいため、1 回目の交換後は `[9, 7, 10, 3, 5, 8]` です。
|
||||
10 は親ノードの 9 よりも大きいため、2 回目の交換後は `[10, 7, 9, 3, 5, 8]` です。
|
||||
これで 10 が根ノードに到達したため、ヒープ化は終了します。
|
||||
|
||||
3. 最終的なヒープ頂点は 10 で、交換回数は 2 回です。
|
||||
|
||||
### 2. 最小ヒープの親子関係を確認する
|
||||
|
||||
配列 `[1, 4, 3, 7, 6, 2]` は完全二分木を表しています。最小ヒープでは、すべての親ノードがその子ノード以下でなければなりません。
|
||||
インデックス $i$ に対して、左右の子ノードのインデックスはそれぞれ $2i+1$ と $2i+2$ です。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. インデックス 2 の子ノードのインデックスと値は、それぞれいくつですか?
|
||||
2. インデックス 2 にあるノードの値は 3 です。子ノードとの間で最小ヒープの条件に反していますか?反している場合、どの 2 要素を交換しますか?
|
||||
3. 問い 2 の判断に基づき、条件に反している場合は交換後の配列を書き、反していない場合は交換が不要な理由を説明してください。最後に、ほかの親子関係も確認してください。
|
||||
|
||||
??? success "解答"
|
||||
|
||||
1. インデックス 2 の左子ノードはインデックス 5、値は 2 です。右子ノードのインデックスは 6 ですが、配列の長さは 6 なので、右子ノードは存在しません。
|
||||
|
||||
2. 親ノードの値 3 は子ノードの値 2 より大きく、最小ヒープの条件に反しています。そのため、インデックス 2 とインデックス 5 の要素を交換します。
|
||||
|
||||
3. 交換後の配列は `[1, 4, 2, 7, 6, 3]` です。順に確認すると、
|
||||
`1 ≤ 4`、`1 ≤ 2`、`4 ≤ 7`、`4 ≤ 6`、`2 ≤ 3` です。
|
||||
すべての親ノードがその子ノード以下なので、現在は最小ヒープの条件を満たしています。
|
||||
|
||||
### 3. 最小ヒープで最大の 3 つの数を保持する
|
||||
|
||||
データストリーム `[4, 1, 7, 3, 8]` から最大の 3 つの数を保持するために、大きさが 3 以下の最小ヒープを管理します。
|
||||
|
||||
まず、最初の 3 つの数を順に最小ヒープへ入れます。ヒープが満杯になった後は、新しい数を 1 つ読み込むたびに、
|
||||
その数がヒープ頂点より大きければヒープ頂点を削除して新しい数を入れ、そうでなければヒープを変更しません。
|
||||
|
||||
各数を読み込んだ後、ヒープに保持されている数とヒープ頂点を書いてください。
|
||||
保持されている数は集合として書けばよく、ヒープ配列内での並びを書く必要はありません。
|
||||
|
||||
??? success "解答"
|
||||
|
||||
各数を読み込んだ後の結果は、次のとおりです。
|
||||
|
||||
| 読み込んだ数 | 保持されている数 | ヒープ頂点 |
|
||||
| --- | --- | --- |
|
||||
| 4 | `{4}` | 4 |
|
||||
| 1 | `{1, 4}` | 1 |
|
||||
| 7 | `{1, 4, 7}` | 1 |
|
||||
| 3 | `{3, 4, 7}` | 3 |
|
||||
| 8 | `{4, 7, 8}` | 4 |
|
||||
|
||||
ヒープが満杯になった後、ヒープ頂点は、現在保持している数のうち最も小さい数です。新しい数がヒープ頂点より大きい場合に限り、
|
||||
ヒープ頂点を新しい数に置き換えます。最終的に保持される `{4, 7, 8}` は、まさに最大の 3 つの数です。
|
||||
|
||||
## 8.5.2 プログラミング演習
|
||||
|
||||
### 1. 配列内の k 番目に大きい要素
|
||||
|
||||
整数配列 `nums` と整数 $k$ が与えられます。ここで $1 \le k \le n$ で、$n$ は配列の長さです。配列を大きい順に並べたとき、$k$ 番目にある要素を返してください。
|
||||
|
||||
重複する要素は別々に数えます。たとえば、`[5, 5, 2]` で 2 番目に大きい要素も 5 です。大きさが $k$ 以下の最小ヒープを使って求めてください。
|
||||
|
||||
??? tip "解法のヒント"
|
||||
|
||||
1. k 番目に大きい要素は、最大の k 個の数のうち最も小さい数です
|
||||
2. 各数を最小ヒープへ入れ、大きさが k を超えたら最小値を取り出します
|
||||
3. 走査を終えると、ヒープには最大の k 個の数が残り、ヒープ頂点が答えになります
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/kth-largest-element-in-an-array/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -19,3 +19,4 @@ icon: material/family-tree
|
||||
- [8.2 ヒープ構築](build_heap.md)
|
||||
- [8.3 Top-k 問題](top_k.md)
|
||||
- [8.4 まとめ](summary.md)
|
||||
- [8.5 演習](exercises.md)
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- utils/exercises/publish_exercises.py により自動生成されています。直接編集しないでください。 -->
|
||||
|
||||
# 10.7 演習
|
||||
|
||||
## 10.7.1 確認問題
|
||||
|
||||
### 1. 二分探索で探索区間を狭める方法
|
||||
|
||||
ソート済み配列 `[2, 5, 8, 12, 16, 23, 38]` から 16 を探索します。
|
||||
両閉区間 `[i, j]` を使い、中点を
|
||||
$m=i+(j-i)/2$(小数点以下切り捨て)とします。
|
||||
|
||||
目的の値が見つかるまで、各回の `(i, j, m)`、中点の要素、次に区間をどのように狭めるかを書いてください。
|
||||
|
||||
??? success "解答"
|
||||
|
||||
各回の探索過程は次のとおりです。
|
||||
|
||||
| 回 | `(i, j, m)` | 中点の要素 | 次の操作 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | `(0, 6, 3)` | 12 | `12 < 16` なので `i = 4` とする |
|
||||
| 2 | `(4, 6, 5)` | 23 | `23 > 16` なので `j = 4` とする |
|
||||
| 3 | `(4, 4, 4)` | 16 | 目的の値を発見し、インデックス 4 を返す |
|
||||
|
||||
配列はソート済みなので、中点の値が目的の値より小さければ、中点とその左側を除外できます。
|
||||
中点の値が目的の値より大きければ、中点とその右側を除外できます。
|
||||
|
||||
### 2. 重複する要素の左右の境界
|
||||
|
||||
配列 `[1, 2, 2, 2, 4, 6]` から数値 2 を探索します。
|
||||
ある生徒は、二分探索でインデックス 2 に目的の値を見つけるとすぐに返し、次のように言いました。
|
||||
「インデックス 2 が数値 2 の左端境界です。」
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. この生徒の説明は正しいですか?数値 2 の左端境界と右端境界は、それぞれどこですか?理由も説明してください。
|
||||
2. 左端境界を探索するとき、中点の要素が目的の値と等しければ、次にどちら側を探索すべきですか?
|
||||
3. 右端境界を探索するときは、どちら側を探索すべきですか?方向だけを説明し、探索過程をすべて書く必要はありません。
|
||||
|
||||
??? success "解答"
|
||||
|
||||
1. この生徒の説明は正しくありません。2 を 1 つ見つけてすぐに返す方法では、いずれかの 2 を見つけたことしか保証できず、最も左または最も右の 2 だとは限りません。
|
||||
この問題の左端境界はインデックス 1、右端境界はインデックス 3 です。
|
||||
|
||||
2. 左端境界を探索するときは、中点の要素が 2 と等しくても、引き続き左側を探索します。
|
||||
たとえば、両閉区間を使う場合は `j = m - 1` とします。
|
||||
|
||||
3. 右端境界を探索するときは、中点の要素が 2 と等しければ、引き続き右側を探索します。
|
||||
たとえば、`i = m + 1` とします。
|
||||
|
||||
### 3. データに応じた探索方法の選び方
|
||||
|
||||
「線形探索、二分探索、ハッシュテーブル」から、次の 3 つの場面に適した方法を選び、理由を説明してください。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. ソート済みで今後変更されない $10^7$ 個の整数を繰り返し探索する。ほかのデータ構造は追加で作らない。
|
||||
2. 挿入と削除が頻繁に行われるデータ集合で、あるキーが存在するかを繰り返し判定する。順序を保つ必要も、範囲探索を行う必要もない。
|
||||
3. ソートされていない配列から、ある値を 1 回だけ探索する。
|
||||
|
||||
??? success "解答"
|
||||
|
||||
1. 二分探索です。データがソート済みで変更されないため、追加の空間を使わず $O(\log n)$ で探索できます。
|
||||
2. ハッシュテーブルです。ハッシュ関数によってキーが各バケットへほぼ均等に分散される場合、挿入、削除、キーによる探索の平均時間計算量はいずれも $O(1)$ です。
|
||||
3. 先頭から末尾まで直接走査します。1 回しか探索しない場合、ソートやハッシュテーブルの作成でも、最初に配列全体を処理する必要があるため、
|
||||
この 1 回の処理に必要な作業の総量は減りません。
|
||||
|
||||
どの方法を選ぶかは、データがソート済みか、追加の構造を作れるか、探索回数、必要な操作の種類によって決まります。
|
||||
|
||||
## 10.7.2 プログラミング演習
|
||||
|
||||
### 1. ソート済み配列の二分探索
|
||||
|
||||
重複なく昇順に並んだ整数配列 `nums` と目的の値 `target` が与えられます。二分探索を使って `target` を見つけてください。存在する場合はその配列インデックスを返し、存在しない場合は -1 を返します。
|
||||
|
||||
??? tip "解法のヒント"
|
||||
|
||||
1. 最初の区間は left = 0、right = n - 1 で、区間が空でない条件は left <= right です
|
||||
2. mid = left + (right - left) // 2 で中点を計算します
|
||||
3. もし `nums[mid]` が `target` より小さければ左端を `mid + 1` へ移し、`nums[mid]` が `target` より大きければ右端を `mid - 1` へ移します。等しければすぐに返します
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/binary-search/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
|
||||
### 2. ソート済み配列への挿入位置
|
||||
|
||||
重複なく昇順に並んだ整数配列 `nums` と目的の値 `target` が与えられます。
|
||||
|
||||
`target` がすでに配列にあれば、そのインデックスを返します。なければ、`target` を挿入した後も重複のない昇順を保てる位置を返します。答えは 0 の場合も、配列の長さと等しい場合もあります。二分探索を使って求めてください。
|
||||
|
||||
??? tip "解法のヒント"
|
||||
|
||||
1. 答えは 0 の場合も、配列の長さ n の場合もあります
|
||||
2. 両閉区間を使う場合、`nums[mid]` が `target` 以上なら `right = mid - 1` とし、さらに左の位置を調べます。そうでなければ `left = mid + 1` とします
|
||||
3. ループが終了した時点で、left が挿入位置です
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/search-insert-position/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -21,3 +21,4 @@ icon: material/text-search
|
||||
- [10.4 ハッシュによる最適化戦略](replace_linear_by_hashing.md)
|
||||
- [10.5 探索アルゴリズム再考](searching_algorithm_revisited.md)
|
||||
- [10.6 まとめ](summary.md)
|
||||
- [10.7 演習](exercises.md)
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- utils/exercises/publish_exercises.py により自動生成されています。直接編集しないでください。 -->
|
||||
|
||||
# 11.12 演習
|
||||
|
||||
## 11.12.1 確認問題
|
||||
|
||||
### 1. 選択ソートとバブルソートの最初の数回
|
||||
|
||||
配列 `[4, 2, 5, 1, 3]` が与えられます。次の操作では、いずれも小さい順にソートします。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 選択ソートの最初の 2 回を行い、各回の終了時の配列を書いてください。また、この時点で位置が確定した要素を示してください。
|
||||
2. バブルソートの最初の 1 回を行い、配列の状態と交換回数を書いてください。また、どの位置が確定したかを示してください。
|
||||
|
||||
??? success "解答"
|
||||
|
||||
1. 最初の 2 回は次のとおりです。
|
||||
|
||||
| 回 | 配列の状態 | 説明 |
|
||||
| --- | --- | --- |
|
||||
| 1 | `[1, 2, 5, 4, 3]` | 最小要素 1 を先頭の要素と交換する |
|
||||
| 2 | `[1, 2, 5, 4, 3]` | 値 2 はすでにインデックス 1 にあるため、交換は不要 |
|
||||
|
||||
この時点で、先頭の 2 つの位置が確定しています。以後は `[5, 4, 3]` から最小要素を選び続ければよいです。
|
||||
|
||||
2. 隣り合う要素を順に比較します。4 と 2 は交換し、4 と 5 は交換せず、5 と 1、5 と 3 はそれぞれ交換します。
|
||||
その結果は `[2, 4, 1, 3, 5]` で、交換回数は 3 回です。最大要素 5 が配列の末尾へ移動したため、最後の位置が確定しています。
|
||||
|
||||
### 2. 等しい要素の順序は変わるか
|
||||
|
||||
配列 $[2_a, 2_b, 1]$ で、$2_a$ と $2_b$ の値は等しいものの、添字によって元の順序を区別しています。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 選択ソートの 1 回目が終わった後の配列を書いてください。$2_a$ と $2_b$ の相対的な順序は変わりますか?
|
||||
2. バブルソートの 1 回目が終わった後の配列を書いてください。$2_a$ と $2_b$ の相対的な順序は変わりますか?
|
||||
3. 問い 1、2 の結果から、等しい要素の元の順序を保つという点で、2 つのソートにはどのような違いがあるかを説明してください。
|
||||
|
||||
??? success "解答"
|
||||
|
||||
1. 選択ソートの 1 回目では最小要素 1 を選び、先頭の $2_a$ と交換するため、
|
||||
$[1, 2_b, 2_a]$ となります。$2_a$ は $2_b$ の後ろへ移動し、相対的な順序が変わっています。
|
||||
|
||||
2. バブルソートでは、まず $2_a$ と $2_b$ を比較します。両者は等しいので交換しません。次に $2_b$ と 1 を比較して交換すると、
|
||||
1 回目の終了時は $[2_a, 1, 2_b]$ となります。$2_a$ は引き続き $2_b$ より前にあり、相対的な順序は変わっていません。
|
||||
|
||||
3. この例では、選択ソートは等しい要素の元の順序を変えます。バブルソートは、左側の要素が右側の要素より大きい場合にだけ
|
||||
隣り合う要素を交換します。等しい要素同士は交換しないため、元の順序を保てます。
|
||||
|
||||
### 3. 計数ソートと基数ソートの比較
|
||||
|
||||
学校で、8 桁に固定された多数の学籍番号をソートします。次の問いに答えてください。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 最下位桁から始める基数ソートでは、何回の処理が必要ですか?
|
||||
2. 学籍番号をそのまま整数として計数ソートを行うと、使われないカウント位置を大量に用意することになるのはなぜですか?
|
||||
3. 問い 1、2 の結果から、8 桁に固定された多数の学籍番号を計数ソートと基数ソートのどちらかで処理するなら、どちらを選びますか?その理由も説明してください。
|
||||
|
||||
??? success "解答"
|
||||
|
||||
1. 学籍番号には 8 個の桁があるため、最下位桁から最上位桁まで、合計 8 回処理します。各回では 0~9 だけを使って分類します。
|
||||
|
||||
2. そのまま数えるには、取り得るすべての 8 桁の値に対して位置を用意する必要があります。しかし、実際に生徒が使うのはそのうちのごく一部なので、
|
||||
ほとんどのカウント位置は常に 0 のままです。
|
||||
|
||||
3. 基数ソートを選びます。「桁数が固定され、各桁が 10 種類の値だけを取る」という特徴を利用し、安定した分類を 8 回繰り返すだけで済むためです。
|
||||
8 桁の学籍番号全体を整数のインデックスとして計数ソートを行うと、実際には現れない多数の値にもカウント位置を用意する必要があります。
|
||||
|
||||
## 11.12.2 プログラミング演習
|
||||
|
||||
### 1. マージソートで配列を並べる
|
||||
|
||||
整数配列 `nums` が与えられます。マージソートを自分で実装し、要素を非減少順に並べて返してください。言語に組み込まれたソート関数を呼び出してはいけません。
|
||||
|
||||
??? tip "解法のヒント"
|
||||
|
||||
1. 区間の長さが 1 以下なら、すでにソートされています
|
||||
2. 中点で区間を 2 つに分け、それぞれを再帰的にソートします
|
||||
3. 2 つのポインタでソート済みの左右の区間をマージし、結果を元の配列へ書き戻します
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/sort-an-array/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
|
||||
### 2. 計数ソートで整数配列を並べる
|
||||
|
||||
整数配列 `nums` と 0 以上の整数 $K$ が与えられ、配列の各要素は $0$ 以上 $K$ 以下です。
|
||||
|
||||
計数ソートを実装し、要素を非減少順に `nums` へ書き戻して、`nums` を返してください。
|
||||
要素同士の大小比較によって順序を決めたり、言語に組み込まれたソート関数を呼び出したりしてはいけません。
|
||||
|
||||
??? tip "解法のヒント"
|
||||
|
||||
1. 各要素は 0 以上 K 以下なので、その値をカウント配列のインデックスとして直接使えます
|
||||
2. 1 回目に nums を走査し、対応する位置のカウントに 1 を足します
|
||||
3. 次にカウント配列を 0 から K まで走査します。値 x が現れた回数だけ、nums に x を続けて書き込みます
|
||||
@@ -26,3 +26,4 @@ icon: material/sort-ascending
|
||||
- [11.9 計数ソート](counting_sort.md)
|
||||
- [11.10 基数ソート](radix_sort.md)
|
||||
- [11.11 まとめ](summary.md)
|
||||
- [11.12 演習](exercises.md)
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- utils/exercises/publish_exercises.py により自動生成されています。直接編集しないでください。 -->
|
||||
|
||||
# 5.5 演習
|
||||
|
||||
## 5.5.1 確認問題
|
||||
|
||||
### 1. スタックとキューではどれが先に取り出されるか
|
||||
|
||||
空のスタック `S` と空のキュー `Q` を用意し、それぞれに同じ次の操作を行います。
|
||||
|
||||
手順 1:`A` を追加する。
|
||||
手順 2:`B` を追加する。
|
||||
手順 3:要素を 1 つ取り出して記録する。
|
||||
手順 4:`C` を追加する。
|
||||
手順 5:中身が空になるまで要素を取り出し、その順序を記録する。
|
||||
|
||||
`S` と `Q` から要素が取り出される順序をそれぞれ書き、「後入れ先出し」または「先入れ先出し」を使って違いを説明してください。
|
||||
|
||||
??? success "解答"
|
||||
|
||||
スタック `S` から取り出される順序は `B、C、A` です。`A、B` を追加した後、最後に追加した `B` を先に取り出します。次に `C` を追加し、
|
||||
その後は `C、A` の順に取り出されます。これは「後入れ先出し」を表しています。
|
||||
|
||||
キュー `Q` から取り出される順序は `A、B、C` です。`A、B` を追加した後、最初に追加した `A` を先に取り出します。
|
||||
次に `C` を追加し、その後は `B、C` の順に取り出されます。これは「先入れ先出し」を表しています。
|
||||
|
||||
### 2. キューの末尾が配列の末尾を越えたら
|
||||
|
||||
長さ 5 の環状配列を使ってキューを実装します。配列のインデックスは `0~4` です。
|
||||
現在、`front = 3`、`size = 2` で、キュー内の `A、B` はそれぞれインデックス 3、4 にあります。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 「`C` をエンキューする」とき、`C` はどのインデックスに置きますか?エンキュー後の `size` はいくつですか?
|
||||
2. 続けて 1 回デキューすると、どの要素が取り出されますか?新しい `front` と `size` はそれぞれいくつですか?
|
||||
3. この時点で、キュー先頭から末尾までの論理的な順序はどうなっていますか?デキューするときに、配列内のほかの要素を移動する必要はありますか?その理由も説明してください。
|
||||
|
||||
??? success "解答"
|
||||
|
||||
1. 新しい要素の位置は
|
||||
`(front + size) % 5 = (3 + 2) % 5 = 0` なので、
|
||||
`C` はインデックス 0 に置きます。エンキュー後は `size = 3` です。
|
||||
|
||||
2. デキューでは、現在のキュー先頭 `A` が取り出されます。新しい先頭のインデックスは
|
||||
`(3 + 1) % 5 = 4` なので、`front = 4`、`size = 2` です。
|
||||
|
||||
3. 有効な要素の論理的な順序は `B、C` で、`B` はインデックス 4、`C` はインデックス 0 にあります。
|
||||
デキューするときは `front` を動かして `size` を変更するだけです。環状配列では剰余によってインデックスを先頭に戻せるため、
|
||||
ほかの要素をまとめて前へ移動する必要はありません。
|
||||
|
||||
### 3. 両端キューの両端での操作
|
||||
|
||||
ここでは、`push_first` はキュー先頭への追加、`push_last` はキュー末尾への追加、
|
||||
`pop_first` はキュー先頭からの取り出し、`pop_last` はキュー末尾からの取り出しを表すものとします。
|
||||
|
||||
空の両端キュー `deq` に、次の操作を順に行います。
|
||||
|
||||
1. `push_last(A)`
|
||||
2. `push_last(B)`
|
||||
3. `push_first(C)`
|
||||
4. `pop_last()`
|
||||
5. `push_last(D)`
|
||||
6. `pop_first()`
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 2 回の取り出しで、それぞれどの要素が得られますか?
|
||||
2. すべての操作を終えた後、キュー先頭から末尾までにどの要素が残っていますか?
|
||||
3. この 6 つの操作を確認してください。キュー末尾への追加とキュー先頭からの削除だけが許される通常のキューで、すべてを実行できますか?できない場合は実行できない操作を挙げ、両端キューなら実行できるか、その理由とともに説明してください。
|
||||
|
||||
??? success "解答"
|
||||
|
||||
最初の 3 つの操作を終えた時点で、両端キューは先頭から末尾へ `[C, A, B]` となっています。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. `pop_last()` は `B` を取り出します。`D` を追加するとキューは `[C, A, D]` となり、
|
||||
続く `pop_first()` は `C` を取り出します。
|
||||
|
||||
2. 最後に `[A, D]` が残ります。
|
||||
|
||||
3. キュー末尾への追加とキュー先頭からの削除だけが許される通常のキューでは、すべての操作を実行できません。
|
||||
手順 3 の `push_first(C)` はキュー先頭への追加を、手順 4 の `pop_last()` はキュー末尾からの削除を求めており、どちらも通常のキューで許された操作ではありません。
|
||||
両端キューは両端で追加と削除ができるため、この 6 つの操作をすべて実行できます。
|
||||
|
||||
## 5.5.2 プログラミング演習
|
||||
|
||||
### 1. 括弧列の検査
|
||||
|
||||
`()`、`[]`、`{}` の 3 種類の括弧だけを含む文字列 `s` が与えられます。スタックを使って、それが正しい括弧列かどうかを判定してください。
|
||||
|
||||
正しい括弧列は、次の条件を両方とも満たします。各閉じ括弧は、まだ対応付けられていない開き括弧のうち最も直近のものと種類が一致し、
|
||||
文字列を最後まで調べたときに、対応する閉じ括弧のない開き括弧が残っていません。判定結果をブール値で返してください。
|
||||
|
||||
??? tip "解法のヒント"
|
||||
|
||||
1. 「閉じ括弧から対応する開き括弧」への対応表を作れます
|
||||
2. 閉じ括弧に出会ったら、まずスタックが空かどうかを確認し、次にスタックトップと対応しているかを確認します
|
||||
3. 文字列を最後まで調べた後、スタックも空でなければなりません
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/valid-parentheses/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -19,3 +19,4 @@ icon: material/stack-overflow
|
||||
- [5.2 キュー](queue.md)
|
||||
- [5.3 両端キュー](deque.md)
|
||||
- [5.4 まとめ](summary.md)
|
||||
- [5.5 演習](exercises.md)
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- utils/exercises/publish_exercises.py により自動生成されています。直接編集しないでください。 -->
|
||||
|
||||
# 7.7 演習
|
||||
|
||||
## 7.7.1 確認問題
|
||||
|
||||
### 1. 完全二分木・充満二分木・充足二分木
|
||||
|
||||
次の 2 つの配列は二分木をレベル順に表しており、`None` は空き位置を表します。
|
||||
|
||||
- 木 A:`[1, 2, 3, 4, 5, 6]`
|
||||
- 木 B:`[1, 2, 3, None, None, 6, 7]`
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. どちらが完全二分木ですか?
|
||||
2. どちらが充満二分木、つまりすべての非葉ノードが 2 つの子ノードを持つ二分木ですか?
|
||||
3. この 2 つに充足二分木はありますか?それぞれ理由を説明してください。
|
||||
|
||||
??? success "解答"
|
||||
|
||||
1. 木 A は完全二分木です。最下層だけがすべて埋まっておらず、ノードが左から右へ隙間なく並んでいます。
|
||||
木 B は完全二分木ではありません。最下層の左側に空き位置がある一方で、その右側にはノードがあるためです。
|
||||
|
||||
2. 木 B は充満二分木です。ノード 1 とノード 3 はそれぞれ 2 つの子ノードを持ち、それ以外のノードはすべて葉ノードです。
|
||||
木 A は充満二分木ではありません。ノード 3 が左子ノード 6 だけを持つためです。
|
||||
|
||||
3. どちらも充足二分木ではありません。どちらの木も最下層がすべて埋まっていないためです。
|
||||
|
||||
### 2. 同じ木を走査する 3 つの順序
|
||||
|
||||
配列 `[1, 2, 3, 4, 5, 6, 7]` をレベル順に完全二分木へ格納します。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. この木を描いてください。
|
||||
2. 先行順・中間順・後行順走査の列をそれぞれ書いてください。
|
||||
3. 中間順走査の列で、根ノード 1 の左側と右側にある 2 つの列は、木のどの部分にそれぞれ対応しますか?
|
||||
|
||||
??? success "解答"
|
||||
|
||||
1. この木は次のとおりです。
|
||||
|
||||
```text
|
||||
1
|
||||
/ \
|
||||
2 3
|
||||
/ \ / \
|
||||
4 5 6 7
|
||||
```
|
||||
|
||||
2. 先行順走査は `1, 2, 4, 5, 3, 6, 7`、
|
||||
中間順走査は `4, 2, 5, 1, 6, 3, 7`、
|
||||
後行順走査は `4, 5, 2, 6, 7, 3, 1` です。
|
||||
|
||||
3. 根ノード 1 の左側にある `4, 2, 5` は、左部分木の中間順走査の列です。
|
||||
右側にある `6, 3, 7` は、右部分木の中間順走査の列です。
|
||||
|
||||
### 3. 2 つの二分探索木を比較する
|
||||
|
||||
次の 2 つの列を、左から右へ順に空の二分探索木へ挿入します。
|
||||
|
||||
- 列 A:`[4, 2, 6, 1, 3, 5, 7]`
|
||||
- 列 B:`[1, 2, 3, 4, 5, 6, 7]`
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. それぞれの木で 7 を探索するときに通るノードを書いてください。
|
||||
2. 高さを根ノードから最も遠い葉ノードまでに通る辺の数とすると、2 つの木の高さはそれぞれいくつですか?
|
||||
3. 問い 1、2 の結果から、2 つの木で 7 を探索する効率は同じだと考えられますか?木の形と探索経路をもとに理由を説明してください。
|
||||
|
||||
??? success "解答"
|
||||
|
||||
1. 列 A から作った木では、探索経路は `4 → 6 → 7` です。
|
||||
列 B から作った木では、探索経路は `1 → 2 → 3 → 4 → 5 → 6 → 7` です。
|
||||
|
||||
2. 1 つ目の木はすべての層が埋まっているため、高さは 2 です。2 つ目の木では各ノードが右の子だけを持ち、高さは 6 です。
|
||||
|
||||
3. 2 つの木で 7 を探索する効率は異なります。挿入順序によって二分探索木の形と高さが変わります。1 つ目の木で 7 を探索するときに通るノードは 3 つだけですが、
|
||||
2 つ目の木では 7 つのノードを順に通ります。木が高いほど、最悪の場合に経路上で比較するノードが多くなります。
|
||||
|
||||
## 7.7.2 プログラミング演習
|
||||
|
||||
### 1. 二分木の最大の深さ
|
||||
|
||||
二分木の根ノード `root` が与えられます。各ノードには、整数値と左右の子ノードを指す参照が含まれています。
|
||||
|
||||
根ノードから最も遠い葉ノードまでに通る**ノード数**を、二分木の最大の深さとします。この木の最大の深さを返してください。空の木の最大の深さは 0 です。
|
||||
再帰を使って求めてください。
|
||||
|
||||
??? tip "解法のヒント"
|
||||
|
||||
1. この問題ではノード数で深さを数えます。根ノードだけがある場合、最大の深さは 1 です
|
||||
2. 再帰関数が、現在のノードを根とする部分木の最大の深さを返すようにします
|
||||
3. 空のノードは 0 を返し、空でないノードは max(depth(left), depth(right)) + 1 を返します
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/maximum-depth-of-binary-tree/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
|
||||
### 2. 二分木のレベル順走査
|
||||
|
||||
二分木の根ノード `root` が与えられます。キューを使い、上から下へ層ごとにすべてのノードを訪問してください。同じ層では、左から右の順に訪問します。
|
||||
|
||||
二次元配列を返してください。1 つ目の部分配列には根ノードがある層のノード値、2 つ目の部分配列には次の層のノード値を保存し、以降も同様に続けます。
|
||||
二分木が空の場合は、空の配列を返してください。
|
||||
|
||||
??? tip "解法のヒント"
|
||||
|
||||
1. レベル順走査では先に入ったノードを先に訪問するため、キューを使います
|
||||
2. 各回の開始時にキューに入っているノードは、ちょうど同じ層に属しています
|
||||
3. 先にキューの長さを記録し、その個数のノードを取り出して、それぞれの子ノードを追加します
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/binary-tree-level-order-traversal/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
|
||||
### 3. 二分探索木で k 番目に小さい要素
|
||||
|
||||
二分探索木には `n` 個のノードがあり、すべてのノード値は互いに異なります。
|
||||
すべてのノード値を小さい順に並べたときの位置には、1 から番号を付けます。
|
||||
|
||||
根ノード `root` と、`1 <= k <= n` を満たす整数 `k` が与えられます。`k` 番目のノード値を返してください。
|
||||
すべてのノード値を先に集めるのではなく、中間順走査の途中で答えを直接探してください。
|
||||
|
||||
??? tip "解法のヒント"
|
||||
|
||||
1. 二分探索木を中間順走査すると、ノード値を小さい順に訪問できます
|
||||
2. 中間順走査では、左部分木、現在のノード、右部分木の順に処理します。現在のノードを訪問するとき、カウントに 1 を足します
|
||||
3. カウントが初めて k になったとき、現在のノード値が答えです。それ以降は走査を続ける必要がありません
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/kth-smallest-element-in-a-bst/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -21,3 +21,4 @@ icon: material/graph-outline
|
||||
- [7.4 二分探索木](binary_search_tree.md)
|
||||
- [7.5 AVL 木 *](avl_tree.md)
|
||||
- [7.6 まとめ](summary.md)
|
||||
- [7.7 演習](exercises.md)
|
||||
|
||||
@@ -232,12 +232,12 @@ html:has(body[data-md-color-scheme="default"]) {
|
||||
filter: brightness(0.85) invert(0.05);
|
||||
}
|
||||
|
||||
.md-typeset a:not(.md-button) {
|
||||
.md-typeset a:not(.md-button):not(.rounded-button) {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.md-typeset a:not(.md-button):hover,
|
||||
.md-typeset a:not(.md-button):focus-visible {
|
||||
.md-typeset a:not(.md-button):not(.rounded-button):hover,
|
||||
.md-typeset a:not(.md-button):not(.rounded-button):focus-visible {
|
||||
color: var(--md-typeset-a-color);
|
||||
text-decoration: underline;
|
||||
}
|
||||
@@ -519,6 +519,44 @@ html:has(body[data-md-color-scheme="default"]) {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Exercise actions reuse the landing-page CTA with a light-content treatment. */
|
||||
.rounded-button.exercise-button {
|
||||
border-color: rgb(52 152 144 / 0.3);
|
||||
background-color: rgb(52 152 144 / 0.08);
|
||||
color: #277d76 !important;
|
||||
font-size: 0.85em;
|
||||
box-shadow: none;
|
||||
text-decoration: none;
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
}
|
||||
|
||||
.rounded-button.exercise-button:hover,
|
||||
.rounded-button.exercise-button:focus-visible {
|
||||
border-color: rgb(52 152 144 / 0.45);
|
||||
background-color: rgb(52 152 144 / 0.14);
|
||||
color: #216c66 !important;
|
||||
box-shadow: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .rounded-button.exercise-button {
|
||||
border-color: rgb(86 116 122);
|
||||
background-color: rgb(36 52 57);
|
||||
color: rgb(233 245 243) !important;
|
||||
box-shadow: none;
|
||||
backdrop-filter: saturate(115%) blur(0.18rem);
|
||||
-webkit-backdrop-filter: saturate(115%) blur(0.18rem);
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .rounded-button.exercise-button:hover,
|
||||
[data-md-color-scheme="slate"] .rounded-button.exercise-button:focus-visible {
|
||||
border-color: rgb(92 111 117);
|
||||
background-color: rgb(47 80 83);
|
||||
color: rgb(242 249 248) !important;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.heading-div .rounded-button:first-of-type {
|
||||
border-color: rgb(160 223 217 / 0.42);
|
||||
background-color: rgb(42 104 99 / 0.2);
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- Автоматически создано utils/exercises/publish_exercises.py; не редактируйте напрямую. -->
|
||||
|
||||
# 4.6 Упражнения
|
||||
|
||||
## 4.6.1 Вопросы для самопроверки
|
||||
|
||||
### 1. Как массив и связный список находят элемент
|
||||
|
||||
В массиве и односвязном списке по порядку хранятся `[A, B, C, D, E]`. Требуется прочитать 4-й элемент `D`.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Какой индекс можно напрямую использовать в массиве?
|
||||
2. Через какие узлы нужно последовательно пройти по `next` в односвязном списке, начиная с головного узла `A`?
|
||||
3. Как меняется число шагов в каждой структуре по мере удаления нужного элемента от начала? Какая структура лучше подходит для многократного доступа по позиции и почему?
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Если индексация начинается с 0, индекс 4-го элемента равен 3, поэтому в массиве можно напрямую обратиться к `arr[3]`.
|
||||
|
||||
2. В односвязном списке нужно начинать с головы. Путь имеет вид `A → B → C → D`, то есть по `next` нужно перейти 3 раза.
|
||||
|
||||
3. Массив позволяет напрямую определить положение элемента по адресу начала и индексу, поэтому временная сложность доступа по позиции равна $O(1)$.
|
||||
Чтобы обратиться к $k$-му узлу односвязного списка, нужно начать с головного узла и перейти по `next` $k-1$ раз,
|
||||
что в худшем случае требует $O(n)$ времени.
|
||||
|
||||
Здесь сравнивается только доступ по позиции; это не означает, что связный список медленнее при всех операциях.
|
||||
|
||||
### 2. Как вставить элемент в массив и связный список
|
||||
|
||||
В массиве и односвязном списке хранятся `A, B, C, D`. Требуется вставить `X` после `B`.
|
||||
|
||||
- емкость массива равна 5, его текущее состояние — `[A, B, C, D, _]`;
|
||||
- связный список имеет вид `A → B → C → D`, и ссылка на узел `B` уже получена.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Какие элементы массива нужно переместить? Запишите массив после вставки.
|
||||
2. В каком порядке следует изменить `X.next` и `B.next`? Запишите связный список после вставки.
|
||||
3. Почему при сравнении эффективности вставки важно уточнить, что «ссылка на узел B уже получена»?
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. В массиве сначала нужно сдвинуть `D` на одну позицию вправо, затем так же сдвинуть `C` и, наконец, поместить `X` по индексу 2.
|
||||
Получится `[A, B, X, C, D]`.
|
||||
|
||||
2. Изначально `B.next` указывает на `C`. Сначала следует выполнить `X.next = B.next`, чтобы `X` указывал на `C`,
|
||||
а затем `B.next = X`. Получится `A → B → X → C → D`.
|
||||
Если сначала перезаписать `B.next`, не сохранив прежнюю связь, узел `C` можно потерять.
|
||||
|
||||
3. Если положение `B` известно, для вставки в связный список достаточно изменить две связи, что занимает $O(1)$ времени.
|
||||
Если же `B` еще нужно искать от головы списка, сам поиск может потребовать $O(n)$ времени.
|
||||
|
||||
### 3. Как растет емкость списка
|
||||
|
||||
Список на основе массива сейчас содержит `[A, B, C]`, его длина `size = 3`, а емкость `capacity = 4`.
|
||||
При нехватке места емкость нового массива увеличивается в 2 раза.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Чему будут равны длина и емкость списка после добавления `D`? Потребуется ли увеличить емкость?
|
||||
2. Затем добавляют `E`. Какой станет емкость и сколько прежних элементов потребуется скопировать?
|
||||
3. Длина базового массива неизменна. Почему тогда кажется, что емкость списка может расти?
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. `D` можно поместить в последнюю свободную ячейку. Теперь содержимое равно `[A, B, C, D]`,
|
||||
`size = 4`, `capacity = 4`, и увеличивать емкость не требуется.
|
||||
|
||||
2. Для `E` свободного места уже нет, поэтому нужно создать новый массив емкостью 8,
|
||||
скопировать в него 4 прежних элемента `A, B, C, D`, а затем добавить `E`.
|
||||
После этого `size = 5`, `capacity = 8`.
|
||||
|
||||
3. Исходный массив сам по себе не удлиняется. Список создает новый массив большего размера, копирует прежние элементы
|
||||
и затем использует новый массив как базовое хранилище. Поэтому для пользователя емкость выглядит увеличившейся.
|
||||
|
||||
## 4.6.2 Задачи по программированию
|
||||
|
||||
### 1. Прибавление единицы к большому числу в массиве
|
||||
|
||||
Массив `digits` хранит слева направо цифры неотрицательного целого числа. Например, `[3, 0, 8]` обозначает 308.
|
||||
Число 0 представлено как `[0]`; у всех остальных входных данных первая цифра не равна 0.
|
||||
|
||||
Смоделируйте сложение столбиком в десятичной системе: увеличьте это число на 1 и верните результат в том же формате массива.
|
||||
Массив `digits` можно изменять непосредственно. Если перед первой цифрой появляется новый перенос, можно вернуть более длинный массив.
|
||||
|
||||
??? tip "Подсказки"
|
||||
|
||||
1. Как при сложении столбиком, начните с последней цифры массива
|
||||
2. Если текущая цифра меньше 9, увеличьте ее на один и сразу верните результат
|
||||
3. Если текущая цифра равна 9, замените ее на 0; если все цифры равны 9, добавьте 1 в начало
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/plus-one/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
|
||||
### 2. Разворот односвязного списка
|
||||
|
||||
Дан головной узел `head` односвязного списка. Каждый узел содержит значение и поле `next`, указывающее на следующий узел.
|
||||
|
||||
Итеративно разверните все связи между узлами и верните новый головной узел.
|
||||
Не создавайте новые узлы связного списка.
|
||||
|
||||
??? tip "Подсказки"
|
||||
|
||||
1. Нарисуйте на бумаге три связанных узла и два указателя prev и cur
|
||||
2. Прежде чем изменить cur.next, сохраните исходный следующий узел в nxt
|
||||
3. Развернув cur.next, выполните prev = cur, затем cur = nxt и продолжайте с очередным узлом исходного списка
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/reverse-linked-list/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -20,3 +20,4 @@ icon: material/view-list-outline
|
||||
- [4.3 Список](list.md)
|
||||
- [4.4 Оперативная память и кэш *](ram_and_cache.md)
|
||||
- [4.5 Резюме](summary.md)
|
||||
- [4.6 Упражнения](exercises.md)
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- Автоматически создано utils/exercises/publish_exercises.py; не редактируйте напрямую. -->
|
||||
|
||||
# 13.6 Упражнения
|
||||
|
||||
## 13.6.1 Вопросы для самопроверки
|
||||
|
||||
### 1. Пропустит ли этот алгоритм некоторые перестановки
|
||||
|
||||
Алгоритм поиска с возвратом пытается построить все перестановки, перебирая числа в порядке `1, 2, 3`. Выбрав число `x`, он:
|
||||
|
||||
1. добавляет `x` в конец текущего пути;
|
||||
2. помечает `x` как «использованное»;
|
||||
3. рекурсивно заполняет следующую позицию.
|
||||
|
||||
После возврата из рекурсии ученик удалил `x` только из конца пути и перешел к следующему числу.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Какую перестановку алгоритм получит первой? Сможет ли он получить все 6 перестановок?
|
||||
2. Достаточно ли перед возвратом на предыдущий уровень удалить число из конца пути? Если нет, что еще нужно сделать и почему?
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Первой получится перестановка `[1, 2, 3]`, но алгоритм не сможет получить все перестановки. Хотя после возврата путь становится короче,
|
||||
числа 1, 2 и 3 по-прежнему помечены как «использованные», поэтому в последующих ветвях не останется доступных чисел.
|
||||
|
||||
2. Нет. После удаления `x` из конца пути нужно также снова пометить `x` как «неиспользованное».
|
||||
Текущий путь и отметки «использовано» вместе описывают состояние поиска. При выборе изменяются обе части состояния, поэтому при возврате нужно восстановить обе,
|
||||
чтобы другие ветви могли снова выбрать `x`.
|
||||
|
||||
### 2. Важен ли порядок выбора чисел
|
||||
|
||||
Дан упорядоченный массив `[2, 3, 5]` и целевое значение 5. Каждое число разрешено выбирать повторно.
|
||||
Алгоритм требует, чтобы числа в каждом пути поиска располагались только в неубывающем порядке.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Какие различные комбинации можно получить?
|
||||
2. Почему один и тот же набор чисел не нужно повторно перебирать в разном порядке? Какую роль играет ограничение «по неубыванию»?
|
||||
3. Текущий путь равен `[3]`, до цели не хватает 2, а следующий возможный элемент равен 3. Почему теперь можно прекратить проверку всех следующих элементов на этом уровне?
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Различные комбинации: `[2, 3]` и `[5]`.
|
||||
|
||||
2. В этой задаче `[2, 3]` и `[3, 2]` считаются одной комбинацией: порядок выбора чисел не влияет на ответ.
|
||||
Требование располагать числа в пути по неубыванию позволяет сразу исключить из поиска повторные комбинации наподобие `[3, 2]`.
|
||||
|
||||
3. До цели осталось 2, а возможный элемент 3 уже больше 2. Поскольку массив упорядочен,
|
||||
все последующие элементы будут еще больше и также не смогут войти в текущую комбинацию. Поэтому проверку этого уровня можно сразу завершить.
|
||||
|
||||
### 3. Куда можно поставить следующего ферзя
|
||||
|
||||
Ферзи расставляются по строкам на доске `4 × 4`; индексы строк и столбцов начинаются с 0.
|
||||
Ферзи уже стоят на позициях `(0, 1)` и `(1, 3)`. Следующего ферзя нужно поставить в строке 2.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Какие столбцы исключаются из-за того, что в них уже есть ферзь?
|
||||
2. Какие из оставшихся позиций исключаются из-за расположения на одной диагонали с другим ферзем?
|
||||
3. Какие позиции в строке 2 еще можно попробовать?
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. В столбцах 1 и 3 уже стоят ферзи, поэтому позиции `(2, 1)` и `(2, 3)` исключаются.
|
||||
|
||||
2. Среди оставшихся позиций `(2, 2)` находится на одной диагонали с `(1, 3)`, поэтому ее также нужно исключить.
|
||||
Позиция `(2, 0)` не совпадает по столбцу ни с одним из уже поставленных ферзей и не лежит с ними на одной диагонали.
|
||||
|
||||
3. В строке 2 можно попробовать только позицию `(2, 0)`.
|
||||
|
||||
Этот шаг показывает лишь, что текущее расположение допустимо. Если завершить доску затем не удастся, потребуется вернуться и попробовать другие, более ранние варианты.
|
||||
|
||||
## 13.6.2 Задачи по программированию
|
||||
|
||||
### 1. Все перестановки различных элементов
|
||||
|
||||
Массив целых чисел `nums` содержит хотя бы один элемент, и все его элементы попарно различны.
|
||||
Перечислите все возможные порядки, в которых каждый элемент используется ровно один раз, и верните каждый порядок как отдельный массив.
|
||||
Порядок самих перестановок в результате не имеет значения.
|
||||
Используйте поиск с возвратом и булев массив, отмечающий, выбран ли элемент из каждой позиции входного массива в текущую перестановку.
|
||||
|
||||
??? tip "Подсказки"
|
||||
|
||||
1. Глубина рекурсии показывает, какую по счету позицию перестановки нужно заполнить
|
||||
2. На каждом уровне пробуйте только еще не использованные элементы
|
||||
3. Когда длина пути станет равна длине `nums`, добавьте в ответ копию пути
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/permutations/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -20,3 +20,4 @@ icon: material/map-marker-path
|
||||
- [13.3 Задача о сумме подмножеств](subset_sum_problem.md)
|
||||
- [13.4 Задача о n ферзях](n_queens_problem.md)
|
||||
- [13.5 Резюме](summary.md)
|
||||
- [13.6 Упражнения](exercises.md)
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- Автоматически создано utils/exercises/publish_exercises.py; не редактируйте напрямую. -->
|
||||
|
||||
# 2.6 Упражнения
|
||||
|
||||
## 2.6.1 Вопросы для самопроверки
|
||||
|
||||
### 1. Временная и пространственная сложность итерации и рекурсии
|
||||
|
||||
Обе функции ниже вычисляют $1 + 2 + \dots + n$ (считайте, что $n \ge 1$). Присвойте `n` значение 4,
|
||||
ответьте на вопросы в порядке фактического выполнения программы, а затем сравните эффективность двух вариантов.
|
||||
|
||||
```python
|
||||
def sum_iter(n):
|
||||
s = 0
|
||||
for i in range(1, n + 1):
|
||||
s += i
|
||||
return s
|
||||
|
||||
def sum_recur(n):
|
||||
if n == 1:
|
||||
return 1
|
||||
return n + sum_recur(n - 1)
|
||||
```
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Какие значения принимает переменная `s` после каждой итерации при выполнении `sum_iter(4)`?
|
||||
2. Какие функции поочередно вызываются при выполнении `sum_recur(4)`? Как формируется результат при возврате, начиная с самого глубокого вызова?
|
||||
3. Каковы временная и пространственная сложности каждого варианта? Обоснуйте ответ, опираясь на процессы выполнения из вопросов 1 и 2.
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Переменная цикла `i` поочередно принимает значения `1, 2, 3, 4`, а `s` после каждой итерации становится равной
|
||||
`1, 3, 6, 10`. Поэтому `sum_iter(4)` возвращает 10.
|
||||
|
||||
2. Функции вызываются в следующем порядке:
|
||||
`sum_recur(4) → sum_recur(3) → sum_recur(2) → sum_recur(1)`.
|
||||
`sum_recur(1)` возвращает 1, после чего следующие уровни поочередно получают `2 + 1 = 3`, `3 + 3 = 6` и `4 + 6 = 10`.
|
||||
В самой глубокой точке все 4 вызова функции еще не завершены.
|
||||
|
||||
3. В обеих функциях число итераций или вызовов пропорционально $n$, поэтому их временная сложность равна $O(n)$.
|
||||
Пространственная сложность различается: итеративный вариант использует постоянное число переменных, поэтому она равна $O(1)$;
|
||||
в рекурсивном варианте предыдущие вызовы ожидают возврата результата до достижения условия завершения, поэтому в стеке вызовов одновременно хранится до $n$ вызовов.
|
||||
Его пространственная сложность равна $O(n)$.
|
||||
|
||||
При анализе пространственной сложности нужно учитывать не только переменные в коде, но и память, занимаемую рекурсивными вызовами.
|
||||
|
||||
### 2. Временная сложность трех фрагментов кода
|
||||
|
||||
Во всех трех фрагментах кода входные данные — положительное целое число $n$. Расположите фрагменты в порядке возрастания временной сложности и укажите сложность каждого из них.
|
||||
|
||||
```python
|
||||
# Фрагмент 1
|
||||
s = 0
|
||||
for i in range(n):
|
||||
s += i
|
||||
|
||||
# Фрагмент 2
|
||||
s = 0
|
||||
for i in range(n):
|
||||
for j in range(i, n):
|
||||
s += j
|
||||
|
||||
# Фрагмент 3
|
||||
while n > 1:
|
||||
n = n // 2
|
||||
```
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
Порядок от меньшей сложности к большей: фрагмент 3 — $O(\log n)$, фрагмент 1 — $O(n)$, фрагмент 2 — $O(n^2)$.
|
||||
В фрагменте 3 на каждой итерации $n$ уменьшается вдвое, поэтому цикл выполняется примерно $\log_2 n$ раз.
|
||||
Цикл во фрагменте 1 выполняется ровно $n$ раз. Число итераций внутреннего цикла во фрагменте 2 последовательно равно
|
||||
$n,n-1,\dots,1$, а их сумма — $n(n+1)/2$, поэтому сложность квадратичная.
|
||||
|
||||
### 3. Какой способ разворота экономнее по памяти
|
||||
|
||||
Все элементы массива `nums` можно расположить в обратном порядке двумя способами:
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. создать новый массив `res` той же длины, скопировать в него элементы в обратном порядке и вернуть его;
|
||||
2. задать два индекса `i` и `j`, перемещать их от начала и конца массива к середине и попарно менять местами `nums[i]` и `nums[j]`.
|
||||
|
||||
Какова пространственная сложность каждого способа? Какой из них выполняет операцию «на месте»?
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Требуется вспомогательный массив той же длины, что и входной, поэтому пространственная сложность равна $O(n)$.
|
||||
|
||||
2. Используются только две индексные переменные,
|
||||
поэтому пространственная сложность равна $O(1)$, а операция выполняется на месте.
|
||||
|
||||
Обратите внимание: разворот на месте изменяет входной массив,
|
||||
поэтому его следует предпочитать только тогда, когда менять входные данные разрешено. Если исходный массив нужно сохранить, затрат на копирование в способе 1 не избежать.
|
||||
|
||||
## 2.6.2 Задачи по программированию
|
||||
|
||||
### 1. Число Фибоначчи
|
||||
|
||||
Последовательность Фибоначчи задается так: $F(0)=0$, $F(1)=1$, а при $n\ge2$
|
||||
выполняется $F(n)=F(n-1)+F(n-2)$.
|
||||
|
||||
Дано неотрицательное целое число `n`. Вычислите и верните $F(n)$ с помощью цикла, не используя рекурсию.
|
||||
|
||||
??? tip "Подсказки"
|
||||
|
||||
1. Сначала отдельно обработайте случаи, когда n равно 0 или 1
|
||||
2. Для вычисления следующего члена нужны только два предыдущих, поэтому хранить всю последовательность не требуется
|
||||
3. Обновляя две переменные, не перезапишите слишком рано старое значение, которое еще понадобится
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/fibonacci-number/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -20,3 +20,4 @@ icon: material/timer-sand
|
||||
- [2.3 Временная сложность](time_complexity.md)
|
||||
- [2.4 Пространственная сложность](space_complexity.md)
|
||||
- [2.5 Резюме](summary.md)
|
||||
- [2.6 Упражнения](exercises.md)
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- Автоматически создано utils/exercises/publish_exercises.py; не редактируйте напрямую. -->
|
||||
|
||||
# 3.6 Упражнения
|
||||
|
||||
## 3.6.1 Вопросы для самопроверки
|
||||
|
||||
### 1. Связи между данными в повседневных ситуациях
|
||||
|
||||
Для каждого из трех примеров выберите по характеру связей между данными один вариант: «линейная структура», «древовидная структура» или «сетевая структура». Объясните свой выбор.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Ученики стоят в ряд, и каждого интересуют только стоящие непосредственно перед ним и за ним;
|
||||
2. школа организована по уровням «школа → параллель → класс»;
|
||||
3. городские дороги соединяют множество перекрестков: из одного перекрестка можно попасть в несколько других, а дороги могут образовывать циклы.
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Это линейная структура. Кроме первого и последнего в ряду, каждый ученик соседствует ровно с одним человеком впереди и одним сзади, а связи вытянуты в одну линию.
|
||||
|
||||
2. Это древовидная структура. Каждый класс относится к одной параллели, а каждая параллель — к школе; связи образуют уровни сверху вниз.
|
||||
|
||||
3. Это сетевая структура. Один перекресток может быть связан с несколькими другими, а дороги могут образовывать циклы, поэтому связи нельзя выстроить в единую последовательность или строгую иерархию.
|
||||
|
||||
Определяя структуру, сначала рассматривайте связи между элементами, а не объем памяти, который они занимают.
|
||||
|
||||
### 2. Как сохранить логический порядок в памяти
|
||||
|
||||
Логический порядок `A → B → C` можно сохранить в памяти двумя упрощенными способами:
|
||||
|
||||
- вариант 1: `A, B, C` размещены в ячейках памяти с номерами `20, 21, 22` соответственно;
|
||||
- вариант 2: `A, B, C` размещены в ячейках с номерами `20, 7, 31` соответственно, причем `A` хранит расположение `B`, а `B` — расположение `C`.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Какой вариант использует хранение в непрерывном, а какой — в разрозненном пространстве памяти?
|
||||
2. Какой вариант ближе к массиву, а какой — к связному списку?
|
||||
3. Почему вариант 2 по-прежнему задает логический порядок `A → B → C`, хотя номера ячеек памяти не упорядочены по величине?
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Вариант 1 использует соседние ячейки памяти, поэтому хранение происходит в непрерывном пространстве. Узлы в варианте 2 находятся в разных местах, поэтому они хранятся в разрозненном пространстве.
|
||||
|
||||
2. Вариант 1 ближе к массиву, а вариант 2 — к связному списку.
|
||||
|
||||
3. Логический порядок задается связями, записанными между узлами, а не величиной номеров ячеек памяти.
|
||||
По адресу, который хранит `A`, можно найти `B`, а затем по адресу из `B` найти `C`, поэтому элементы по-прежнему доступны в порядке `A, B, C`.
|
||||
|
||||
Это также показывает, что логическая и физическая структуры — два разных взгляда на одни и те же данные.
|
||||
|
||||
### 3. Типы данных и структура записей о домашних заданиях
|
||||
|
||||
Учебная группа записала в порядке рассадки, сдали ли домашнее задание 4 ученика:
|
||||
|
||||
`[true, false, true, true]`
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Какой базовый тип данных подходит для каждого элемента?
|
||||
2. Какую логическую структуру образуют эти 4 элемента, расположенные в ряд по порядку рассадки?
|
||||
3. Если затем записывать оценки за задание каждого ученика как `[90, 0, 85, 100]`, изменится «тип содержимого» или «способ организации» данных? Объясните ответ.
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Каждый элемент выражает только «да» или «нет», поэтому подходит логический тип `bool`.
|
||||
|
||||
2. Элементы расположены по порядку рассадки и образуют линейную структуру, которую можно хранить в массиве.
|
||||
|
||||
3. Изменится тип содержимого: логические значения будут заменены целыми числами. Способ организации не изменится:
|
||||
данные по-прежнему расположены в ряд по порядку рассадки и могут храниться в массиве как линейная структура.
|
||||
|
||||
Базовый тип данных описывает, «что хранится», а структура данных — «как организованы данные».
|
||||
|
||||
## 3.6.2 Задачи по программированию
|
||||
|
||||
### 1. Число единиц в двоичной записи
|
||||
|
||||
Дано неотрицательное целое число `n`. Подсчитайте количество единиц в его двоичной записи.
|
||||
|
||||
Используйте побитовые операции. Не преобразовывайте двоичную запись в строку и не применяйте встроенную функцию, которая напрямую подсчитывает единицы.
|
||||
|
||||
??? tip "Подсказки"
|
||||
|
||||
1. Выражение n & 1 извлекает крайний правый бит n и позволяет проверить, равен ли он 1
|
||||
2. Сдвиг вправо на один разряд отбрасывает текущий крайний правый бит; в большинстве языков для этого используется оператор >>
|
||||
3. Реализовав проверку каждого бита со сдвигом вправо, обратите внимание: n & (n - 1) превращает в 0 крайнюю правую единицу в n
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/number-of-1-bits/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -20,3 +20,4 @@ icon: material/shape-outline
|
||||
- [3.3 Кодирование чисел *](number_encoding.md)
|
||||
- [3.4 Кодирование символов *](character_encoding.md)
|
||||
- [3.5 Резюме](summary.md)
|
||||
- [3.6 Упражнения](exercises.md)
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- Автоматически создано utils/exercises/publish_exercises.py; не редактируйте напрямую. -->
|
||||
|
||||
# 12.6 Упражнения
|
||||
|
||||
## 12.6.1 Вопросы для самопроверки
|
||||
|
||||
### 1. Какие задачи подходят для метода «разделяй и властвуй»
|
||||
|
||||
Ученик хочет решить следующие задачи способом «разделить на две половины, решить каждую отдельно, затем объединить результаты».
|
||||
Для каждой задачи выберите одно из утверждений: «подходит для метода “разделяй и властвуй”», «разделение возможно, но не уменьшает общий объем работы» или «половины нельзя решить независимо». Объясните свой выбор.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Отсортировать неупорядоченный массив;
|
||||
2. найти максимальный элемент массива;
|
||||
3. последовательно выполнить набор операций стека `push(x)` и `pop()`, выводя элемент, полученный при каждой операции `pop()`.
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Подходит: массив делится пополам, обе половины независимо сортируются, а затем сливаются за $O(n)$ — именно так работает сортировка слиянием.
|
||||
2. Разделение возможно, но оно не уменьшит общий объем работы: в обеих половинах в сумме все равно нужно проверить все $n$ элементов,
|
||||
поэтому, как и при прямом обходе, потребуется $O(n)$ времени.
|
||||
3. Половины нельзя решить независимо: содержимое стека в начале второй половины операций зависит от результата первой,
|
||||
поэтому решить их независимо, не зная результатов друг друга, невозможно.
|
||||
|
||||
### 2. Как быстрое возведение в степень уменьшает число вычислений
|
||||
|
||||
Рекурсивная функция ниже вычисляет $x^n$ методом «разделяй и властвуй»:
|
||||
|
||||
```python
|
||||
def fast_pow(x, n):
|
||||
if n == 0:
|
||||
return 1
|
||||
half = fast_pow(x, n // 2)
|
||||
if n % 2 == 0:
|
||||
return half * half
|
||||
return half * half * x
|
||||
```
|
||||
|
||||
Функция вызывается как `fast_pow(3, 5)`.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Какие значения последовательно принимает параметр `n` при рекурсивных вызовах?
|
||||
2. Какие значения поочередно возвращаются на каждом уровне, начиная с самого глубокого?
|
||||
3. Почему результат нужно сначала сохранить в `half`, а не дважды записывать `fast_pow(x, n // 2)`?
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Параметр последовательно принимает значения `5 → 2 → 1 → 0`: на каждом шаге показатель степени уменьшается вдвое до достижения условия завершения.
|
||||
|
||||
2. При `n = 0` возвращается 1; при `n = 1` возвращается $1×1×3=3$;
|
||||
при `n = 2` — $3×3=9$; при `n = 5` — $9×9×3=243$.
|
||||
|
||||
3. Если записать `fast_pow(x, n // 2)` по обе стороны умножения, два рекурсивных вызова будут решать одну и ту же подзадачу.
|
||||
Когда результат сохраняется в `half`, на каждом уровне выполняется только один рекурсивный вызов, а глубина рекурсии составляет примерно $\log n$;
|
||||
два вызова привели бы к множеству повторных вычислений.
|
||||
|
||||
### 3. Разделение левого и правого поддеревьев по последовательностям обхода
|
||||
|
||||
Двоичное дерево без повторяющихся узлов имеет следующие последовательности прямого и симметричного обходов:
|
||||
|
||||
- прямой обход: `[A, B, D, E, C]`
|
||||
- симметричный обход: `[D, B, E, A, C]`
|
||||
|
||||
Выполните только разделение на уровне корневого узла: продолжать рекурсию и рисовать все дерево не требуется.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Какой узел является корневым?
|
||||
2. Какие участки симметричного обхода соответствуют левому и правому поддеревьям?
|
||||
3. Какие участки прямого обхода соответствуют левому и правому поддеревьям? Какие непосредственные дочерние узлы имеет корень?
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Первый узел прямого обхода является корневым, поэтому корень — `A`.
|
||||
|
||||
2. Узел `A` делит симметричный обход на две части: левому поддереву соответствует `[D, B, E]`, правому — `[C]`.
|
||||
|
||||
3. Левое поддерево содержит 3 узла, поэтому следующие после корня `A` 3 элемента прямого обхода относятся к левому поддереву:
|
||||
`[B, D, E]`. Оставшийся участок `[C]` относится к правому поддереву.
|
||||
Следовательно, левый дочерний узел корня — `B`, а правый — `C`.
|
||||
|
||||
## 12.6.2 Задачи по программированию
|
||||
|
||||
### 1. Быстрое возведение в степень
|
||||
|
||||
Даны вещественное число `x` и целое число `n`. Вычислите $x^n$, не используя встроенную функцию возведения в степень.
|
||||
Примените рекурсивный метод «разделяй и властвуй»: каждый раз уменьшайте показатель степени вдвое и повторно используйте уже вычисленный результат подзадачи.
|
||||
В этой задаче $x^0=1$, в том числе при `x = 0`; если `n < 0`, гарантируется, что `x != 0`, а ответ можно преобразовать к $(1/x)^{-n}$.
|
||||
|
||||
??? tip "Подсказки"
|
||||
|
||||
1. При n, равном 0, ответ равен 1
|
||||
2. Вычислив x в степени n // 2, сохраните результат в half и не выполняйте рекурсивный вызов второй раз
|
||||
3. Если n < 0, сначала замените x на 1 / x, а n — на -n; в C++ или Java предварительно преобразуйте n в 64-битное целое число, чтобы избежать переполнения при смене знака у минимального 32-битного целого
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/powx-n/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -20,3 +20,4 @@ icon: material/set-split
|
||||
- [12.3 Задача построения двоичного дерева](build_binary_tree_problem.md)
|
||||
- [12.4 Задача о Ханойской башне](hanota_problem.md)
|
||||
- [12.5 Резюме](summary.md)
|
||||
- [12.6 Упражнения](exercises.md)
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- Автоматически создано utils/exercises/publish_exercises.py; не редактируйте напрямую. -->
|
||||
|
||||
# 14.8 Упражнения
|
||||
|
||||
## 14.8.1 Вопросы для самопроверки
|
||||
|
||||
### 1. Когда подходит динамическое программирование
|
||||
|
||||
Ученик утверждает: «Если можно записать рекуррентное соотношение, нужно использовать динамическое программирование».
|
||||
Для каждой из трех задач выберите наиболее подходящий из трех вариантов: динамическое программирование; поиск с возвратом; цикл или математическая формула без таблицы `dp`. Укажите одну основную причину.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Набрать сумму 6 с помощью монет номиналов `[1, 3, 4]`, используя каждый номинал любое число раз, и найти минимальное число монет;
|
||||
2. вывести все перестановки `[1, 2, 3]`;
|
||||
3. вычислить $1 + 2 + \dots + n$.
|
||||
|
||||
Для задачи, которой подходит динамическое программирование, также объясните значение `dp[i]`.
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Подходит динамическое программирование. Пусть `dp[i]` — минимальное число монет, необходимое для получения суммы `i`.
|
||||
Для каждой монеты `c`, не превосходящей `i`, значение `dp[i-c] + 1` можно рассматривать как возможный ответ,
|
||||
а затем выбрать минимум среди всех вариантов. Разные последовательности выбора многократно приводят к одной и той же сумме, а оптимальное решение для большей суммы можно составить из оптимальных решений для меньших сумм.
|
||||
Для суммы 6 ответ равен 2: `3 + 3`.
|
||||
|
||||
2. Следует использовать поиск с возвратом. Задача требует поочередно получить все 6 перестановок; поиск с возвратом позволяет систематически сделать выбор, продолжить поиск,
|
||||
затем отменить выбор и перейти к другой ветви. Каким бы ни был метод, при фактическом выводе всех перестановок пропустить их перебор нельзя.
|
||||
|
||||
3. Достаточно цикла или формулы суммы арифметической прогрессии. Хотя можно записать `S(i) = S(i-1) + i`, при вычислении `S(i)` требуется только одно меньшее значение `S(i-1)`,
|
||||
а каждая частичная сумма вычисляется один раз. Повторяющихся подзадач нет, поэтому таблица `dp` не нужна. Возможность записать рекуррентное соотношение не означает необходимости динамического программирования.
|
||||
|
||||
### 2. Как вычислить одну ячейку таблицы рюкзака
|
||||
|
||||
Задача о рюкзаке 0-1: веса предметов `wgt = [1, 2, 3]`, их стоимости `val = [5, 11, 15]`, вместимость рюкзака равна 4.
|
||||
`dp[i][c]` — максимальная стоимость, которую можно получить, рассматривая только первые $i$ предметов при предельной вместимости рюкзака $c$;
|
||||
заполнять рюкзак точно до предела не требуется.
|
||||
|
||||
Вычислите только состояние `dp[3][4]`. Известно, что `dp[2][4] = 16` и `dp[2][1] = 5`.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Какова возможная стоимость, если не брать 3-й предмет?
|
||||
2. Какая вместимость останется, если взять 3-й предмет, и чему равна возможная стоимость?
|
||||
3. Какое значение нужно присвоить `dp[3][4]`? Какие предметы ему соответствуют?
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Если не брать 3-й предмет, сохраняется результат для первых двух предметов, поэтому возможная стоимость равна `dp[2][4] = 16`.
|
||||
|
||||
2. Вес 3-го предмета равен 3, после его добавления остается вместимость $4-3=1$. Возможная стоимость равна
|
||||
`dp[2][1] + 15 = 5 + 15 = 20`.
|
||||
|
||||
3. Сравнив 16 и 20, получаем `dp[3][4] = 20`. Это соответствует выбору 1-го и 3-го предметов:
|
||||
их общий вес равен $1+3=4$, а общая стоимость — $5+15=20$.
|
||||
|
||||
Вычисление этого состояния показывает один выбор в задаче о рюкзаке 0-1: «взять или не взять».
|
||||
|
||||
### 3. В каком порядке обновлять вместимость рюкзака
|
||||
|
||||
В задаче о рюкзаке 0-1 есть только один предмет весом 2 и стоимостью 5; вместимость рюкзака равна 4.
|
||||
Каждый предмет разрешено выбрать не более одного раза, а исходный одномерный массив имеет вид `dp = [0, 0, 0, 0, 0]`.
|
||||
|
||||
Обрабатывая предмет, ученик обновляет вместимость от 2 до 4:
|
||||
|
||||
- после обновления `dp[2]` получается 5;
|
||||
- после обновления `dp[3]` также получается 5;
|
||||
- при обновлении `dp[4]` снова используется только что полученное `dp[2]`, поэтому выходит `dp[4] = 10`.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Правилен ли результат `dp[4] = 10`? Почему?
|
||||
2. Чему должно быть равно `dp[4]`, если каждый предмет можно выбрать не более одного раза?
|
||||
3. При обработке каждого предмета вместимость нужно перебирать от большей к меньшей или от меньшей к большей? Какую проблему предотвращает такой порядок?
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Результат неверен. Стоимость 10 означает, что предмет стоимостью 5 положили в рюкзак дважды,
|
||||
нарушив условие «каждый предмет можно выбрать не более одного раза».
|
||||
|
||||
2. В рюкзак можно положить не более одного такого предмета, поэтому правильное значение `dp[4]` равно 5.
|
||||
|
||||
3. Вместимость нужно обновлять от большей к меньшей, то есть в порядке 4, 3, 2.
|
||||
Тогда при вычислении `dp[c]` читаемое значение `dp[c-2]` все еще относится к состоянию до обработки текущего предмета,
|
||||
и этот предмет нельзя повторно использовать на той же итерации.
|
||||
|
||||
## 14.8.2 Задачи по программированию
|
||||
|
||||
### 1. Число способов подняться по лестнице
|
||||
|
||||
Лестница состоит из `n` ступеней. За один шаг можно подняться только на 1 или 2 ступени, причем нужно попасть точно на ступень `n`.
|
||||
Вычислите число различных способов подъема. Считайте, что `n >= 1`; способы различаются только последовательностью шагов на 1 и 2 ступени.
|
||||
Используйте одномерный массив динамического программирования; пока не применяйте оптимизацию памяти, сохраняющую лишь два состояния.
|
||||
|
||||
??? tip "Подсказки"
|
||||
|
||||
1. Последний шаг на ступень i мог быть только на 1 или на 2 ступени
|
||||
2. Поэтому dp[i] = dp[i-1] + dp[i-2]
|
||||
3. Сначала обработайте случаи n, равного 1 и 2, а затем заполняйте таблицу начиная с 3-й ступени
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/climbing-stairs/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
|
||||
### 2. Рюкзак 0-1
|
||||
|
||||
Даны массивы одинаковой длины `wgt` и `val`: вес предмета `i` — положительное целое число `wgt[i]`, а его стоимость — неотрицательное целое число `val[i]`.
|
||||
Вместимость рюкзака `cap` — неотрицательное целое число. Каждый предмет можно выбрать не более одного раза. Найдите максимальную суммарную стоимость предметов,
|
||||
общий вес которых не превышает `cap`. Используйте одномерное динамическое программирование.
|
||||
|
||||
??? tip "Подсказки"
|
||||
|
||||
1. Создайте массив dp длины cap + 1, где dp[c] — максимальная стоимость при предельной вместимости c
|
||||
2. Обрабатывая предмет i, сравните dp[c] для варианта «не брать предмет» и dp[c-wgt[i]] + val[i] для варианта «взять предмет»
|
||||
3. Вместимость нужно обновлять от большей к меньшей, чтобы не выбрать текущий предмет повторно на одной итерации
|
||||
@@ -22,3 +22,4 @@ icon: material/table-pivot
|
||||
- [14.5 Задача о полном рюкзаке](unbounded_knapsack_problem.md)
|
||||
- [14.6 Задача о расстоянии редактирования](edit_distance_problem.md)
|
||||
- [14.7 Резюме](summary.md)
|
||||
- [14.8 Упражнения](exercises.md)
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- Автоматически создано utils/exercises/publish_exercises.py; не редактируйте напрямую. -->
|
||||
|
||||
# 9.5 Упражнения
|
||||
|
||||
## 9.5.1 Вопросы для самопроверки
|
||||
|
||||
### 1. Два представления одного графа
|
||||
|
||||
Неориентированный граф содержит 4 вершины `A, B, C, D` и ребра
|
||||
`A-B, A-C, B-C, C-D`.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Запишите его список смежности.
|
||||
2. Заполните матрицу смежности, содержащую только 0 и 1.
|
||||
3. Какое представление графа позволяет определить, соединены ли напрямую `A` и `D`, просмотрев лишь один элемент хранилища?
|
||||
4. Какое представление обычно экономнее по памяти, если в графе много вершин, но мало ребер?
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Список смежности:
|
||||
|
||||
```text
|
||||
A: B, C
|
||||
B: A, C
|
||||
C: A, B, D
|
||||
D: C
|
||||
```
|
||||
|
||||
2. Матрица смежности:
|
||||
|
||||
| | A | B | C | D |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| A | 0 | 1 | 1 | 0 |
|
||||
| B | 1 | 0 | 1 | 0 |
|
||||
| C | 1 | 1 | 0 | 1 |
|
||||
| D | 0 | 0 | 1 | 0 |
|
||||
|
||||
3. В матрице смежности достаточно посмотреть ячейку на пересечении строки `A` и столбца `D`, поэтому она удобна для проверки прямой связи любой пары вершин.
|
||||
|
||||
4. Если вершин много, а ребер мало, список смежности хранит только существующие ребра и обычно занимает меньше памяти, чем матрица, в которой место отводится каждой паре вершин.
|
||||
|
||||
### 2. Порядок обхода в ширину и в глубину
|
||||
|
||||
Неориентированный граф содержит вершины `A, B, C, D, E` и ребра
|
||||
`A-B, A-C, B-D, C-D, D-E`.
|
||||
|
||||
Начните с A и при наличии нескольких непосещенных смежных вершин выбирайте их в алфавитном порядке.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Запишите порядок обхода в ширину (BFS).
|
||||
2. Запишите порядок рекурсивного обхода в глубину (DFS).
|
||||
3. Почему при обоих обходах необходимо отмечать уже посещенные вершины?
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Порядок BFS: `A, B, C, D, E`. Сначала посещаются B и C, отстоящие от A на одно ребро,
|
||||
а затем более далекие D и E.
|
||||
|
||||
2. Порядок DFS: `A, B, D, C, E`. Обход последовательно переходит в непосещенную смежную вершину текущей вершины,
|
||||
поэтому сначала проходит путь `A → B → D → C`. Когда у C не остается новых смежных вершин, обход возвращается к D и посещает E.
|
||||
|
||||
3. В графе есть цикл, например `A-B-D-C-A`. Если не отмечать посещенные вершины,
|
||||
обход может снова и снова проходить по циклу через одни и те же вершины и не завершиться.
|
||||
|
||||
### 3. Можно ли посетить весь граф за один BFS
|
||||
|
||||
Неориентированный граф содержит вершины `A, B, C, D, E, F` и только ребра
|
||||
`A-B, B-C, D-E`.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Какие вершины можно посетить за один BFS, начав с A?
|
||||
2. Посещены ли после этого все вершины графа? Почему?
|
||||
3. Если перебирать все вершины в алфавитном порядке и запускать новый BFS при каждой непосещенной вершине,
|
||||
какие вершины станут начальными для отдельных запусков? На сколько несвязанных частей (компонент связности) разделен граф?
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Из A можно посетить только `A, B, C`.
|
||||
|
||||
2. Не все вершины посещены. `D, E` образуют другую связную часть, а F — отдельная вершина;
|
||||
путей от A до них нет, поэтому из A они недостижимы.
|
||||
|
||||
3. Начальными вершинами трех запусков BFS будут последовательно `A, D, F`; они посетят
|
||||
`{A, B, C}`, `{D, E}` и `{F}` соответственно. Следовательно, граф содержит 3 компоненты связности.
|
||||
|
||||
## 9.5.2 Задачи по программированию
|
||||
|
||||
### 1. Проверка существования пути в неориентированном графе
|
||||
|
||||
Дан неориентированный граф из $n$ вершин, пронумерованных от $0$ до $n-1$. Каждый элемент `[u, v]` массива `edges` обозначает неориентированное ребро между вершинами `u` и `v`.
|
||||
|
||||
Также даны начальная вершина `source` и конечная вершина `destination`. Сначала постройте по `edges` список смежности, а затем с помощью BFS или DFS
|
||||
определите, существует ли путь от `source` до `destination`: если существует, верните `true`, иначе — `false`.
|
||||
Граф может содержать циклы и быть несвязным.
|
||||
|
||||
??? tip "Подсказки"
|
||||
|
||||
1. Каждое неориентированное ребро нужно добавить в обоих направлениях
|
||||
2. Граф может содержать циклы, поэтому обязательно отмечайте уже посещенные вершины
|
||||
3. Начните с source: встретив destination, верните true; если обход завершился без нее, верните false
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/find-if-path-exists-in-graph/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -19,3 +19,4 @@ icon: material/graphql
|
||||
- [9.2 Базовые операции графа](graph_operations.md)
|
||||
- [9.3 Обход графа](graph_traversal.md)
|
||||
- [9.4 Краткие итоги](summary.md)
|
||||
- [9.5 Упражнения](exercises.md)
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- Автоматически создано utils/exercises/publish_exercises.py; не редактируйте напрямую. -->
|
||||
|
||||
# 15.6 Упражнения
|
||||
|
||||
## 15.6.1 Вопросы для самопроверки
|
||||
|
||||
### 1. Всегда ли лучше брать самую крупную монету
|
||||
|
||||
Даны монеты номиналов `[1, 7, 10]` и целевая сумма 14.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Запишите монеты, выбранные по правилу «каждый раз брать наибольший номинал, не превышающий оставшуюся сумму».
|
||||
2. Существует ли способ использовать меньше монет? Если да, приведите один пример; если нет, объясните почему.
|
||||
3. Доказывает ли результат, что эта жадная стратегия верна для любых номиналов монет?
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Жадный алгоритм последовательно выбирает `10 + 1 + 1 + 1 + 1`, всего 5 монет.
|
||||
|
||||
2. Способ с меньшим числом монет существует: `7 + 7`, всего 2 монеты.
|
||||
|
||||
3. Нет. Этот контрпример показывает, что для произвольных номиналов выбор наибольшей допустимой в данный момент монеты не всегда дает минимальное число монет;
|
||||
лучший на текущем шаге выбор может помешать получить более выгодную комбинацию в дальнейшем.
|
||||
|
||||
### 2. Какой предмет первым положить в рюкзак
|
||||
|
||||
В рюкзак вместимостью 4 килограмма можно положить следующие предметы. Разрешено брать только часть предмета,
|
||||
а полученная ценность пропорциональна взятому весу.
|
||||
|
||||
- предмет A: вес 4 килограмма, ценность 20;
|
||||
- предмет B: вес 3 килограмма, ценность 18.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Какова ценность одного килограмма каждого предмета? Какой предмет нужно брать первым?
|
||||
2. Заполните рюкзак по жадной стратегии для дробного рюкзака. Какова итоговая ценность?
|
||||
3. Если предметы можно делить, а рюкзак ограничен по общему весу, что следует сравнивать при выборе: общую ценность или ценность килограмма? Почему?
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Ценность килограмма A равна `20 ÷ 4 = 5`, а килограмма B — `18 ÷ 3 = 6`,
|
||||
поэтому первым нужно взять B с большей удельной ценностью.
|
||||
|
||||
2. Сначала берется весь предмет B: он занимает 3 килограмма и дает ценность 18. В рюкзаке остается место для 1 килограмма,
|
||||
поэтому затем берется 1 килограмм предмета A ценностью 5. Итоговая ценность равна `18 + 5 = 23`.
|
||||
|
||||
3. Рюкзак ограничен по общему весу, а предметы можно делить, поэтому нужно сравнивать ценность единицы веса.
|
||||
Хотя общая ценность A выше, ценность его килограмма ниже, чем у B. Если сначала полностью положить A, получится только ценность 20.
|
||||
|
||||
### 3. Как переместить два указателя на следующем шаге
|
||||
|
||||
Высоты вертикальных перегородок заданы массивом `[1, 8, 6, 2, 5]`. Для поиска наибольшей вместимости используются два указателя на концах массива.
|
||||
Вместимость равна произведению высоты более короткой из двух перегородок на расстояние между их индексами.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Сначала левый указатель находится по индексу 0, а правый — по индексу 4. Чему равна текущая вместимость? Какой указатель нужно переместить следующим?
|
||||
2. После одного перемещения, выбранного в вопросе 1, по каким индексам находятся указатели? Чему теперь равна вместимость? Какой указатель нужно переместить следующим?
|
||||
3. Для текущей пары перегородок можно переместить указатель как более короткой, так и более высокой перегородки. Какое перемещение еще может привести к большей вместимости и почему?
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Текущая вместимость равна `min(1, 5) × (4 - 0) = 4`. Левая перегородка короче, поэтому нужно переместить левый указатель.
|
||||
|
||||
2. После перемещения левый и правый указатели находятся по индексам 1 и 4. Текущая вместимость равна
|
||||
`min(8, 5) × (4 - 1) = 15`. Теперь короче правая перегородка, поэтому следующим нужно переместить правый указатель.
|
||||
|
||||
3. Большую вместимость еще может дать перемещение указателя более короткой перегородки. При перемещении более высокой перегородки расстояние между ними обязательно уменьшается,
|
||||
а высота контейнера по-прежнему ограничена оставшейся короткой перегородкой, поэтому вместимость может только остаться прежней или уменьшиться.
|
||||
Только перемещение короткой перегородки дает возможность встретить более высокую.
|
||||
|
||||
## 15.6.2 Задачи по программированию
|
||||
|
||||
### 1. Дробный рюкзак
|
||||
|
||||
Даны массивы одинаковой длины `wgt` и `val`, где `wgt[i] > 0`, `val[i] >= 0`, а вместимость рюкзака `cap >= 0`.
|
||||
Каждый предмет имеется в одном экземпляре, но разрешено положить в рюкзак только его часть;
|
||||
полученная ценность пропорциональна доле веса предмета, которую положили в рюкзак. Используйте жадный алгоритм
|
||||
и верните максимальную суммарную ценность как вещественное число.
|
||||
|
||||
??? tip "Подсказки"
|
||||
|
||||
1. Сначала вычислите ценность единицы веса каждого предмета как val[i] / wgt[i]; результат деления должен сохранять дробную часть
|
||||
2. Чем выше ценность единицы веса предмета, тем раньше его нужно класть в рюкзак
|
||||
3. Если оставшаяся вместимость меньше веса текущего предмета, возьмите такую его часть, которая точно заполнит рюкзак, и завершите работу
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user