mirror of
https://github.com/krahets/hello-algo.git
synced 2026-08-31 04:17:14 +00:00
Add translated chapter exercise pages (#1941)
Add dedicated chapter exercise sections and generated exercise pages for the English, Japanese, and Russian editions. Add navigation for all published languages while keeping Taiwan Traditional Chinese exercise pages generated at build time.
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
<!-- utils/exercises/publish_exercises.py により自動生成されています。直接編集しないでください。 -->
|
||||
|
||||
# 演習
|
||||
|
||||
## 確認問題
|
||||
|
||||
### 配列と連結リストが要素を見つける方法
|
||||
|
||||
配列と単方向連結リストに、どちらも `[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)$ の時間がかかります。
|
||||
|
||||
ここでは位置を指定した読み取りだけを比較しており、連結リストがすべての操作で遅いという意味ではありません。
|
||||
|
||||
### 配列と連結リストに要素を挿入する方法
|
||||
|
||||
配列と単方向連結リストに、どちらも `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)$ の時間がかかる可能性があります。
|
||||
|
||||
### リストの容量が増える仕組み
|
||||
|
||||
配列を基に実装されたリストの現在の内容が `[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. 元の配列そのものが長くなったわけではありません。リストはより大きな新しい配列を作って既存の要素をコピーし、
|
||||
内部の保存先を新しい配列に切り替えます。そのため、利用者から見ると容量が増えたように見えます。
|
||||
|
||||
## プログラミング演習
|
||||
|
||||
### 配列で表した大きな整数に 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" }
|
||||
|
||||
### 単方向連結リストの反転
|
||||
|
||||
単方向連結リストの先頭ノード `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" }
|
||||
@@ -0,0 +1,89 @@
|
||||
<!-- utils/exercises/publish_exercises.py により自動生成されています。直接編集しないでください。 -->
|
||||
|
||||
# 演習
|
||||
|
||||
## 確認問題
|
||||
|
||||
### この順列アルゴリズムは結果を取りこぼすか
|
||||
|
||||
あるバックトラッキングアルゴリズムは、`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, 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)` は、すでに置かれた 2 つのクイーンのどちらとも、同じ列にも同じ対角線上にもありません。
|
||||
|
||||
3. 2 行目で試せる位置は `(2, 0)` だけです。
|
||||
|
||||
この段階では、現在の配置が条件を満たすことだけが分かります。この先でチェス盤を完成できなければ、戻って、それより前の別の選択を試す必要があります。
|
||||
|
||||
## プログラミング演習
|
||||
|
||||
### 重複要素のない全順列
|
||||
|
||||
整数配列 `nums` には少なくとも 1 つの要素があり、すべての要素が互いに異なります。
|
||||
各要素を 1 回ずつ使って作れるすべての並びを列挙し、それぞれの並びを配列として返してください。
|
||||
返す結果の中で、各順列を並べる順序は問いません。
|
||||
バックトラッキングを使い、各位置の要素が現在の順列にすでに選ばれているかをブール配列で記録してください。
|
||||
|
||||
??? tip "解法のヒント"
|
||||
|
||||
1. 再帰の深さは、順列の何番目の位置を埋めているかを表します
|
||||
2. 各層では、まだ使っていない要素だけを試します
|
||||
3. 経路の長さが `nums` の長さに達したら、そのコピーを答えへ追加します
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/permutations/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -0,0 +1,112 @@
|
||||
<!-- utils/exercises/publish_exercises.py により自動生成されています。直接編集しないでください。 -->
|
||||
|
||||
# 演習
|
||||
|
||||
## 確認問題
|
||||
|
||||
### 反復と再帰の時間・空間計算量
|
||||
|
||||
次の 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)$ です。
|
||||
|
||||
空間計算量を分析するときは、コード中の変数だけでなく、再帰呼び出しが使う空間も考える必要があります。
|
||||
|
||||
### 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$ となるため、二次の計算量です。
|
||||
|
||||
### どちらの反転が空間を節約できるか
|
||||
|
||||
配列 `nums` のすべての要素を逆順にする方法として、次の 2 つがあります。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 同じ長さの新しい配列 `res` を作り、逆順にコピーして返す。
|
||||
2. 2 つのインデックス `i` と `j` をそれぞれ先頭と末尾から中央へ動かし、`nums[i]` と `nums[j]` を順に交換する。
|
||||
|
||||
2 つの方法の空間計算量は、それぞれいくつですか?どちらが「インプレース」な操作ですか?
|
||||
|
||||
??? success "解答"
|
||||
|
||||
1. 入力と同じ長さの補助配列が必要なため、空間計算量は $O(n)$ です。
|
||||
|
||||
2. 使うのは 2 つのインデックス変数だけなので、
|
||||
空間計算量は $O(1)$ であり、インプレースな操作です。
|
||||
|
||||
ただし、インプレースな反転は入力配列を変更します。
|
||||
入力の変更が許される場合にのみ優先して使うべきです。元の配列を残す必要がある場合は、方法 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" }
|
||||
@@ -0,0 +1,88 @@
|
||||
<!-- utils/exercises/publish_exercises.py により自動生成されています。直接編集しないでください。 -->
|
||||
|
||||
# 演習
|
||||
|
||||
## 確認問題
|
||||
|
||||
### 身近な場面にあるデータの関係
|
||||
|
||||
データ同士の関係に基づいて、次の 3 つの場面に「線形構造、木構造、網状構造」のいずれかを選び、理由を説明してください。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 生徒が一列に並び、それぞれが自分の前と後ろにいる人だけに注目する。
|
||||
2. 学校が「学校 → 学年 → クラス」という階層で管理されている。
|
||||
3. 都市の道路が複数の交差点を結び、1 つの交差点から複数の別の交差点へ行けるほか、道路が環状につながることもある。
|
||||
|
||||
??? success "解答"
|
||||
|
||||
1. 線形構造です。列の先頭と最後にいる人を除き、それぞれの人は前後の 1 人ずつとだけ隣り合い、関係が一本の線に沿って並んでいます。
|
||||
|
||||
2. 木構造です。各クラスはいずれか 1 つの学年に属し、各学年はさらに学校に属しており、関係が上から下へ階層的に広がっています。
|
||||
|
||||
3. 網状構造です。1 つの交差点を複数の交差点と結ぶことができ、経路が環状につながる場合もあるため、1 つの順序や厳密な階層では表せません。
|
||||
|
||||
構造を判断するときは、メモリをどれだけ使うかを先に考えるのではなく、まず要素同士の関係を観察します。
|
||||
|
||||
### 論理的な順序をメモリに保存する方法
|
||||
|
||||
論理的な順序 `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` の順にアクセスできます。
|
||||
|
||||
このことからも、論理構造と物理構造は、同じデータを異なる視点から捉えたものだと分かります。
|
||||
|
||||
### 宿題の記録におけるデータ型と構造
|
||||
|
||||
ある学習グループが、座席順に 4 人の生徒が宿題を提出したかどうかを記録したところ、次の結果になりました。
|
||||
|
||||
`[true, false, true, true]`
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 各要素には、どの基本データ型が適していますか?
|
||||
2. この 4 つの要素は座席順に一列に並んでいます。どのような論理構造を使っていますか?
|
||||
3. 今後、各生徒の宿題の点数を `[90, 0, 85, 100]` と記録するように変えた場合、変わるのはデータの「内容の型」と「構成方法」のどちらですか?理由も説明してください。
|
||||
|
||||
??? success "解答"
|
||||
|
||||
1. 各要素は「はい」か「いいえ」だけを表すため、ブール型 `bool` が適しています。
|
||||
|
||||
2. これらの要素は座席順に並んで線形構造を作っており、配列で保存できます。
|
||||
|
||||
3. 変わるのは内容の型です。要素がブール値から整数に変わります。一方、構成方法は変わりません。
|
||||
データは引き続き座席順に一列に並び、配列という線形構造を使えます。
|
||||
|
||||
基本データ型は「何を保存するか」を、データ構造は「データをどのように構成するか」を表します。
|
||||
|
||||
## プログラミング演習
|
||||
|
||||
### 二進表現に含まれる 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" }
|
||||
@@ -0,0 +1,98 @@
|
||||
<!-- utils/exercises/publish_exercises.py により自動生成されています。直接編集しないでください。 -->
|
||||
|
||||
# 演習
|
||||
|
||||
## 確認問題
|
||||
|
||||
### 分割統治に適した処理
|
||||
|
||||
ある生徒は、「まず 2 つに分け、それぞれを解いてから結果をまとめる」という方法で、次の処理を行おうとしています。
|
||||
それぞれについて、「分割統治に適している」「分割統治はできるが、作業の総量は減らない」「2 つを独立して解けない」のいずれかを判断し、理由を説明してください。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. ソートされていない配列をソートする。
|
||||
2. 配列の最大値を求める。
|
||||
3. 一連の `push(x)`、`pop()` というスタック操作を順に実行し、各 `pop()` で得た要素を出力する。
|
||||
|
||||
??? success "解答"
|
||||
|
||||
1. 適しています。半分に分け、2 つを独立してソートし、$O(n)$ でマージします。これはマージソートそのものです。
|
||||
2. 分割統治はできますが、作業の総量は減りません。左右の 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. `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 回呼び出すと、大量の重複計算が発生します。
|
||||
|
||||
### 走査列から左右の部分木を分ける
|
||||
|
||||
重複するノードを持たない二分木の先行順走査と中間順走査は、それぞれ次のとおりです。
|
||||
|
||||
- 先行順走査:`[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` です。
|
||||
|
||||
## プログラミング演習
|
||||
|
||||
### 高速べき乗
|
||||
|
||||
実数 `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" }
|
||||
@@ -0,0 +1,113 @@
|
||||
<!-- utils/exercises/publish_exercises.py により自動生成されています。直接編集しないでください。 -->
|
||||
|
||||
# 演習
|
||||
|
||||
## 確認問題
|
||||
|
||||
### 動的計画法が適している場面
|
||||
|
||||
ある生徒は、「漸化式を書けるなら、必ず動的計画法を使うべきです」と言いました。
|
||||
次の 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` テーブルは不要です。「漸化式を書ける」ことと「動的計画法が必要である」ことは同じではありません。
|
||||
|
||||
### ナップサック表の 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 回の比較を表しています。
|
||||
|
||||
### ナップサック容量を更新する順序
|
||||
|
||||
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]` は、現在の品物を処理する前の値のままなので、
|
||||
同じ回の中で現在の品物を繰り返し使うことを防げます。
|
||||
|
||||
## プログラミング演習
|
||||
|
||||
### 階段を上る方法の数
|
||||
|
||||
`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" }
|
||||
|
||||
### 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. 同じ回の中で現在の品物を繰り返し選ばないように、容量は必ず大きいほうから小さいほうへ更新します
|
||||
@@ -0,0 +1,105 @@
|
||||
<!-- utils/exercises/publish_exercises.py により自動生成されています。直接編集しないでください。 -->
|
||||
|
||||
# 演習
|
||||
|
||||
## 確認問題
|
||||
|
||||
### 同じグラフを 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. 頂点が多く、辺が少ない場合、隣接リストは実際に存在する辺だけを記録します。そのため、頂点のすべての組に対して領域を用意する隣接行列よりも、一般に空間を節約できます。
|
||||
|
||||
### 幅優先走査と深さ優先走査の訪問順序
|
||||
|
||||
無向グラフの頂点は `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` のような閉路があります。訪問済みの頂点を記録しないと、
|
||||
閉路に沿って同じ頂点を何度も訪問し、走査が正常に終了しない可能性があります。
|
||||
|
||||
### 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 つの連結成分があります。
|
||||
|
||||
## プログラミング演習
|
||||
|
||||
### 無向グラフに経路が存在するかを判定する
|
||||
|
||||
$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" }
|
||||
@@ -0,0 +1,85 @@
|
||||
<!-- utils/exercises/publish_exercises.py により自動生成されています。直接編集しないでください。 -->
|
||||
|
||||
# 演習
|
||||
|
||||
## 確認問題
|
||||
|
||||
### 毎回最大の硬貨を選べば必ず最善か
|
||||
|
||||
硬貨の額面は `[1, 7, 10]`、目標金額は 14 です。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 「残りの金額を超えない最大の額面を毎回選ぶ」という方法を使い、選ぶ硬貨を書いてください。
|
||||
2. より少ない硬貨を使う方法はありますか?ある場合は 1 つ書き、ない場合は理由を説明してください。
|
||||
3. この結果から、この貪欲戦略がどのような硬貨の額面でも正しいといえますか?
|
||||
|
||||
??? success "解答"
|
||||
|
||||
1. 貪欲戦略では `10 + 1 + 1 + 1 + 1` の順に選び、硬貨は合計 5 枚です。
|
||||
|
||||
2. より少ない硬貨を使う方法があります。`7 + 7` なら、硬貨は 2 枚だけです。
|
||||
|
||||
3. 正しいとはいえません。この反例から、どのような硬貨の額面でも、現在選べる最大の額面を毎回選ぶ方法が最小の硬貨枚数を得るとは限らないと分かります。
|
||||
目の前で最大の選択をすると、その後のよりよい組合せを作れなくなる場合があります。
|
||||
|
||||
### ナップサックに先に入れる品物
|
||||
|
||||
容量 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 しか得られません。
|
||||
|
||||
### 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 枚の間の距離は必ず短くなり、容器の高さは動かしていない低い仕切り板によって引き続き制限されるため、
|
||||
高さは変わらないか低くなります。したがって、容量が移動前より大きくなることはありません。低い仕切り板を動かした場合にだけ、より高い仕切り板に出会える可能性があります。
|
||||
|
||||
## プログラミング演習
|
||||
|
||||
### 分数ナップサック
|
||||
|
||||
同じ長さの配列 `wgt` と `val` が与えられ、`wgt[i] > 0`、`val[i] >= 0`、ナップサックの容量は `cap >= 0` です。
|
||||
各種類の品物は 1 個ずつしかありませんが、その一部だけを入れることもできます。
|
||||
得られる価値は、入れた重さがその品物の総重量に占める割合に応じて決まります。貪欲法を使い、
|
||||
ナップサックで得られる最大の総価値を実数で返してください。
|
||||
|
||||
??? tip "解法のヒント"
|
||||
|
||||
1. まず、各品物の単位重量当たりの価値 val[i] / wgt[i] を計算します。除算結果の小数部分も残します
|
||||
2. 単位重量当たりの価値が高い品物からナップサックへ入れます
|
||||
3. 残り容量が現在の品物の重さより小さければ、ナップサックをちょうど満たす分だけ入れて終了します
|
||||
@@ -0,0 +1,103 @@
|
||||
<!-- utils/exercises/publish_exercises.py により自動生成されています。直接編集しないでください。 -->
|
||||
|
||||
# 演習
|
||||
|
||||
## 確認問題
|
||||
|
||||
### ハッシュ衝突後の探索方法
|
||||
|
||||
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 が互いに上書きされることはありません。
|
||||
|
||||
### ハッシュテーブルを拡張すると要素はどこへ移るか
|
||||
|
||||
連鎖アドレス法を使うハッシュテーブルには、もともと 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 番のバケットへ進むため、見つけられなくなります。
|
||||
|
||||
### 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 に戻ります)、後の挿入ではその位置を再利用できます。
|
||||
|
||||
## プログラミング演習
|
||||
|
||||
### 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" }
|
||||
@@ -0,0 +1,88 @@
|
||||
<!-- utils/exercises/publish_exercises.py により自動生成されています。直接編集しないでください。 -->
|
||||
|
||||
# 演習
|
||||
|
||||
## 確認問題
|
||||
|
||||
### 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 回です。
|
||||
|
||||
### 最小ヒープの親子関係を確認する
|
||||
|
||||
配列 `[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 つの数を保持する
|
||||
|
||||
データストリーム `[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 つの数です。
|
||||
|
||||
## プログラミング演習
|
||||
|
||||
### 配列内の 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" }
|
||||
@@ -0,0 +1,96 @@
|
||||
<!-- utils/exercises/publish_exercises.py により自動生成されています。直接編集しないでください。 -->
|
||||
|
||||
# 演習
|
||||
|
||||
## 確認問題
|
||||
|
||||
### 二分探索で探索区間を狭める方法
|
||||
|
||||
ソート済み配列 `[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 を返す |
|
||||
|
||||
配列はソート済みなので、中点の値が目的の値より小さければ、中点とその左側を除外できます。
|
||||
中点の値が目的の値より大きければ、中点とその右側を除外できます。
|
||||
|
||||
### 重複する要素の左右の境界
|
||||
|
||||
配列 `[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 つの場面に適した方法を選び、理由を説明してください。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. ソート済みで今後変更されない $10^7$ 個の整数を繰り返し探索する。ほかのデータ構造は追加で作らない。
|
||||
2. 挿入と削除が頻繁に行われるデータ集合で、あるキーが存在するかを繰り返し判定する。順序を保つ必要も、範囲探索を行う必要もない。
|
||||
3. ソートされていない配列から、ある値を 1 回だけ探索する。
|
||||
|
||||
??? success "解答"
|
||||
|
||||
1. 二分探索です。データがソート済みで変更されないため、追加の空間を使わず $O(\log n)$ で探索できます。
|
||||
2. ハッシュテーブルです。ハッシュ関数によってキーが各バケットへほぼ均等に分散される場合、挿入、削除、キーによる探索の平均時間計算量はいずれも $O(1)$ です。
|
||||
3. 先頭から末尾まで直接走査します。1 回しか探索しない場合、ソートやハッシュテーブルの作成でも、最初に配列全体を処理する必要があるため、
|
||||
この 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" }
|
||||
|
||||
### ソート済み配列への挿入位置
|
||||
|
||||
重複なく昇順に並んだ整数配列 `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" }
|
||||
@@ -0,0 +1,96 @@
|
||||
<!-- utils/exercises/publish_exercises.py により自動生成されています。直接編集しないでください。 -->
|
||||
|
||||
# 演習
|
||||
|
||||
## 確認問題
|
||||
|
||||
### 選択ソートとバブルソートの最初の数回
|
||||
|
||||
配列 `[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_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. この例では、選択ソートは等しい要素の元の順序を変えます。バブルソートは、左側の要素が右側の要素より大きい場合にだけ
|
||||
隣り合う要素を交換します。等しい要素同士は交換しないため、元の順序を保てます。
|
||||
|
||||
### 計数ソートと基数ソートの比較
|
||||
|
||||
学校で、8 桁に固定された多数の学籍番号をソートします。次の問いに答えてください。
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. 最下位桁から始める基数ソートでは、何回の処理が必要ですか?
|
||||
2. 学籍番号をそのまま整数として計数ソートを行うと、使われないカウント位置を大量に用意することになるのはなぜですか?
|
||||
3. 問い 1、2 の結果から、8 桁に固定された多数の学籍番号を計数ソートと基数ソートのどちらかで処理するなら、どちらを選びますか?その理由も説明してください。
|
||||
|
||||
??? success "解答"
|
||||
|
||||
1. 学籍番号には 8 個の桁があるため、最下位桁から最上位桁まで、合計 8 回処理します。各回では 0~9 だけを使って分類します。
|
||||
|
||||
2. そのまま数えるには、取り得るすべての 8 桁の値に対して位置を用意する必要があります。しかし、実際に生徒が使うのはそのうちのごく一部なので、
|
||||
ほとんどのカウント位置は常に 0 のままです。
|
||||
|
||||
3. 基数ソートを選びます。「桁数が固定され、各桁が 10 種類の値だけを取る」という特徴を利用し、安定した分類を 8 回繰り返すだけで済むためです。
|
||||
8 桁の学籍番号全体を整数のインデックスとして計数ソートを行うと、実際には現れない多数の値にもカウント位置を用意する必要があります。
|
||||
|
||||
## プログラミング演習
|
||||
|
||||
### マージソートで配列を並べる
|
||||
|
||||
整数配列 `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" }
|
||||
|
||||
### 計数ソートで整数配列を並べる
|
||||
|
||||
整数配列 `nums` と 0 以上の整数 $K$ が与えられ、配列の各要素は $0$ 以上 $K$ 以下です。
|
||||
|
||||
計数ソートを実装し、要素を非減少順に `nums` へ書き戻して、`nums` を返してください。
|
||||
要素同士の大小比較によって順序を決めたり、言語に組み込まれたソート関数を呼び出したりしてはいけません。
|
||||
|
||||
??? tip "解法のヒント"
|
||||
|
||||
1. 各要素は 0 以上 K 以下なので、その値をカウント配列のインデックスとして直接使えます
|
||||
2. 1 回目に nums を走査し、対応する位置のカウントに 1 を足します
|
||||
3. 次にカウント配列を 0 から K まで走査します。値 x が現れた回数だけ、nums に x を続けて書き込みます
|
||||
@@ -0,0 +1,101 @@
|
||||
<!-- utils/exercises/publish_exercises.py により自動生成されています。直接編集しないでください。 -->
|
||||
|
||||
# 演習
|
||||
|
||||
## 確認問題
|
||||
|
||||
### スタックとキューではどれが先に取り出されるか
|
||||
|
||||
空のスタック `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` の順に取り出されます。これは「先入れ先出し」を表しています。
|
||||
|
||||
### キューの末尾が配列の末尾を越えたら
|
||||
|
||||
長さ 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` を変更するだけです。環状配列では剰余によってインデックスを先頭に戻せるため、
|
||||
ほかの要素をまとめて前へ移動する必要はありません。
|
||||
|
||||
### 両端キューの両端での操作
|
||||
|
||||
ここでは、`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 つの操作をすべて実行できます。
|
||||
|
||||
## プログラミング演習
|
||||
|
||||
### 括弧列の検査
|
||||
|
||||
`()`、`[]`、`{}` の 3 種類の括弧だけを含む文字列 `s` が与えられます。スタックを使って、それが正しい括弧列かどうかを判定してください。
|
||||
|
||||
正しい括弧列は、次の条件を両方とも満たします。各閉じ括弧は、まだ対応付けられていない開き括弧のうち最も直近のものと種類が一致し、
|
||||
文字列を最後まで調べたときに、対応する閉じ括弧のない開き括弧が残っていません。判定結果をブール値で返してください。
|
||||
|
||||
??? tip "解法のヒント"
|
||||
|
||||
1. 「閉じ括弧から対応する開き括弧」への対応表を作れます
|
||||
2. 閉じ括弧に出会ったら、まずスタックが空かどうかを確認し、次にスタックトップと対応しているかを確認します
|
||||
3. 文字列を最後まで調べた後、スタックも空でなければなりません
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/valid-parentheses/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -0,0 +1,128 @@
|
||||
<!-- utils/exercises/publish_exercises.py により自動生成されています。直接編集しないでください。 -->
|
||||
|
||||
# 演習
|
||||
|
||||
## 確認問題
|
||||
|
||||
### 完全二分木・充満二分木・充足二分木
|
||||
|
||||
次の 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. どちらも充足二分木ではありません。どちらの木も最下層がすべて埋まっていないためです。
|
||||
|
||||
### 同じ木を走査する 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` は、右部分木の中間順走査の列です。
|
||||
|
||||
### 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 つのノードを順に通ります。木が高いほど、最悪の場合に経路上で比較するノードが多くなります。
|
||||
|
||||
## プログラミング演習
|
||||
|
||||
### 二分木の最大の深さ
|
||||
|
||||
二分木の根ノード `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" }
|
||||
|
||||
### 二分木のレベル順走査
|
||||
|
||||
二分木の根ノード `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" }
|
||||
|
||||
### 二分探索木で 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" }
|
||||
Reference in New Issue
Block a user