diff --git a/src/bin/step1.rs b/src/bin/step1.rs new file mode 100644 index 0000000..31c268d --- /dev/null +++ b/src/bin/step1.rs @@ -0,0 +1,95 @@ +// Step1 +// 目的: 方法を思いつく + +// 方法 +// 5分考えてわからなかったら答えをみる +// 答えを見て理解したと思ったら全部消して答えを隠して書く +// 5分筆が止まったらもう一回みて全部消す +// 正解したら終わり + +/* + 問題の理解 + - 昇順にソートされた一意の整数を含む配列を回転させたものが入力として与えられる。配列中に含まれる最小の値を答えとして返す。 + - 時間計算量O(log n)のアルゴリズムで実装することが制約。 + - 時間計算量O(log n)を満たすことが必須なので、入力の長さなどから事前に時間計算量を見積もる必要はない。 + - 回転させた配列について + - 4回転のとき [0,1,2,4,5,6,7] -> [4,5,6,7,0,1,2] + - 7回転のとき [0,1,2,4,5,6,7] -> [0,1,2,4,5,6,7] + + 何を考えて解いていたか + - 時間計算量O(log n)を満たすために二分探索アルゴリズムで実装する必要がある。 + - しかし、入力の配列はn回回転したものが渡され、昇順ソートされている状態とは限らない。 + - 最小値を保持する変数を作っておき、探索を行いながら最小値を見つけるたびに更新する。探索終了後に最小値を返す。 + - ソート処理を実行するとO(n log n)となり、問題の制約を満たせない。 + 時間切れなので答えを見る + + 何がわからなかったか + - 何回回転したかが示されない状態で、入力をソートされた状態に戻す方法が分からなかった。 + + 解答の理解 + https://leetcode.com/problems/find-minimum-in-rotated-sorted-array/solutions/7178461/153-find-minimum-in-rotated-sorted-array-6m7g/?envType=problem-list-v2&envId=vnnqktms + - ソートされている配列を回転しても、ある区間(まとまり)はソートされている状態が保持されている。この点に注目している。 + - ある時点で見ている区間の左端の値 < 右端の値 が成立していれば、この区間の最小値が区間の左端であることが分かる。 + - 最初に配列全体がこの条件を満たしていれば、配列がソートされていることが分かるので、早期リターンnums[0]できる。 + - 右端の方が小さければ、最小値と比較して更新できるか試してから右端を飛ばして次の未探索領域を見る。 + + 正解してから気づいたこと + - 解き方を知っているかどうかという感じがした。 + - 回転という操作に惑わされず、区間(まとまり)に注目してソートされている状態が維持されているという点に気付くことができるかどうか。 +*/ + +pub struct Solution {} +impl Solution { + pub fn find_min(nums: Vec) -> i32 { + if nums.is_empty() { + panic!("nums must not be empty"); + } + + if nums.first().unwrap() <= nums.last().unwrap() { + return *nums.first().unwrap(); + } + + // [start,end) + // start <= i < end + let mut start = 0; + let mut end = nums.len(); + let mut min_value = i32::MAX; + + while start < end { + let middle = start + (end - start) / 2; + let middle_value = nums[middle]; + let start_value = nums[start]; + + if start_value <= middle_value { + min_value = min_value.min(start_value); + start = middle + 1; + } else { + min_value = min_value.min(middle_value); + end = middle; + } + } + + min_value + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn step1_test() { + assert_eq!(Solution::find_min(vec![3, 4, 5, 1, 2]), 1); + assert_eq!(Solution::find_min(vec![4, 5, 6, 7, 0, 1, 2]), 0); + assert_eq!(Solution::find_min(vec![11, 13, 15, 17]), 11); + + assert_eq!(Solution::find_min(vec![1]), 1); + } + + #[test] + #[should_panic] + fn step1_empty_nums_test() { + let empty_nums = Vec::new(); + Solution::find_min(empty_nums); + } +} diff --git a/src/bin/step2.rs b/src/bin/step2.rs new file mode 100644 index 0000000..a929034 --- /dev/null +++ b/src/bin/step2.rs @@ -0,0 +1,107 @@ +// Step2 +// 目的: 自然な書き方を考えて整理する + +// 方法 +// Step1のコードを読みやすくしてみる +// 他の人のコードを2つは読んでみること +// 正解したら終わり + +// 以下をメモに残すこと +// 講師陣はどのようなコメントを残すだろうか? +// 他の人のコードを読んで考えたこと +// 改善する時に考えたこと + +/* + コメント集と他の人のコードを読んで考えたこと + https://github.com/sakupan102/arai60-practice/pull/43/changes/BASE..36167fcd716b107e83476e6b2f73821e91d5554e#diff-4ccb56a4631a2bd95d503978df1e47c6cc3893b0b74adc1393bc155f0ce88bc2R2 + - nums[i] > nums[i+1] となる境界を探すという考え方は思いつかなかった。別の解法として二分探索の良い練習になりそう。 + + https://github.com/YukiMichishita/LeetCode/pull/7#discussion_r1561132164 + - 二分探索を実装するときの考え方や意識すると良いこと + + https://github.com/Ryotaro25/leetcode_first60/pull/46/changes/BASE..5cd497a61c1610dfb252de6f0dd2a0823e7b2bec#r1869993674 + - odaさんによるオプショナルな質問。自分でも考えてみる。 + Q.> 「2で割る処理がありますがこれは切り捨てでも切り上げでも構わないのでしょうか。」 + A. 良くない。切り捨てにしておかないとend側が動かなくなり無限ループになるので、切り上げは不可。切り捨てにしておくとmiddleは左に寄っていくのでend = middle としても未確定領域が縮小するという感覚。 + Q.> 「nums[middle] <= nums[right] とありますが、これは < でもいいですか。」 + A. 良い。境界を探しているわけではなく、最小値そのものを探しているため。 + Q.> 「nums[right] は、nums.back() でもいいですか。」 + A. 良くない。endポインタを更新して最後尾が移動すること(未確定領域の縮小)を期待しているのでnums.last()では不変条件が壊れて無限ループになるため。 + Q.> 「right の初期値は nums.size() でもいいですか。」 + A. 良くない。自分のコードで、end = nums.len() に書き換えただけでは動かなくなる。配列の添字を見ることを想定して実装している。配列中の最小値を探したいのに配列外を参照しようとすると不変条件が破綻する。 + + - 他の人のコードだと、最小値そのものを探すというよりは区間のまとまりの境界自体を探して答えを求めているコードが多いように見える。 + 自分がstep1で解答としてみたコードは最小値自体を探すものであったので、step2では境界を探すコードを書いたほうが二分探索の練習になりそう。 + + https://github.com/olsen-blue/Arai60/pull/42/changes#r1993813625 + > odaさんのコメントをいただいた上で、再考してみましたが、境界を求める二分探索という整理ができそうであれば、rightの初期値がlen(nums)なのもかなり良いと感じるようになりました。 + > 一方で、欲しいもの一つ見つける二分探索は、rightの初期値はlen(nums) - 1が良いかもという感覚です。 + > 使い分けたいかもしれません。 + - 自分も同じ感想になった。 + + odaさんのコメント + https://github.com/olsen-blue/Arai60/pull/42/changes#r1993204654 + > left を「左側、つまり、条件を満たさないことが判明している左側の最大の場所」として書くこともできます。 + > そうすると、初期値は -1 ですね。 + > この right も「条件を満たさないことが判明している右側の場所の最小」と思うと、right = middle が素直に見えるはずです。 + + 改善する時に考えたこと + - 境界を探そうと思って、[start,end)として扱う初期条件を考えてコードを書き始めたがうまく行かなかった。 + そもそも問題で求められている(探したい)のは、配列中に必ず存在する最小値であって、境界ではないことが原因だと思った。 + なので[start,end)のようなend側が配列の外側を指すような区間の設定をすると素直に書けないと思った。 + 配列中の最小値を探す時にendが配列の外側を指していると、そこに値は無いのでend側の端点も閉区間にしてしまった方が良いという感覚。 + 実装するときの考え方として、境界ではなく配列中の添字と対応する値を見ていると考える方が自然だと思った。 + + 所感 + - 何か特定の値を探す時は添字で直接配列中の値を見ていく考え方の方が自然だと感じた。 + lower_boundやupper_boundではtargetを境目として配列を分けるイメージなので境界を探すという感覚。 + - [start,end)な半開区間を扱うコードではうまく実装できず方針変更したが別で練習しておく。(step2a.rs) +*/ + +pub struct Solution {} +impl Solution { + pub fn find_min(nums: Vec) -> i32 { + if nums.is_empty() { + panic!("nums must not be empty"); + } + + // [start,end] + // start <= i <= end + let mut start = 0; + let mut end = nums.len() - 1; + + while start < end { + let middle = start + (end - start) / 2; + + if nums[middle] < nums[end] { + end = middle; + } else { + start = middle + 1; + } + } + + nums[end] + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn step2_test() { + assert_eq!(Solution::find_min(vec![3, 4, 5, 1, 2]), 1); + assert_eq!(Solution::find_min(vec![4, 5, 6, 7, 0, 1, 2]), 0); + assert_eq!(Solution::find_min(vec![11, 13, 15, 17]), 11); + assert_eq!(Solution::find_min(vec![3, 1, 2]), 1); + + assert_eq!(Solution::find_min(vec![1]), 1); + } + + #[test] + #[should_panic] + fn step1_empty_nums_test() { + let empty_nums = Vec::new(); + Solution::find_min(empty_nums); + } +} diff --git a/src/bin/step2a.rs b/src/bin/step2a.rs new file mode 100644 index 0000000..e10028c --- /dev/null +++ b/src/bin/step2a.rs @@ -0,0 +1,79 @@ +// Step2a +// 目的: 別の書き方で練習する。 + +/* + odaさんのコメント + https://github.com/olsen-blue/Arai60/pull/42/changes#r1993204654 + > left を「左側、つまり、条件を満たさないことが判明している左側の最大の場所」として書くこともできます。 + > そうすると、初期値は -1 ですね。 + > この right も「条件を満たさないことが判明している右側の場所の最小」と思うと、right = middle が素直に見えるはずです。 + + - step2.rsで書こうとしてうまく実装できなかった start = 0, end = nums.len() を初期値として実装する練習を行う。 + step2.rsの実装が自然に感じるが、あえて違和感のある方法で練習することで理解を深める狙い。 + https://github.com/t9a-dev/LeetCode_arai60/pull/41#discussion_r2607036540 + + 解法の理解 + - 常に配列の最後の要素より小さいかでendを更新している + - 配列の最後は動かないのになぜこれで良いのだろうか。 + - 配列の最後は配列中の値の最大値にも最小値にもなり得る。 + - 配列外(nums.len())をより大きい値の集合の始まりとして見ている? + - つまり右側とは何か値の集合があると仮定して、nums.last()より大きい値しか無いという番兵の考え方だと理解した。 + [ nums[i] <= nums.last() | nums.last() < ] + - 左側により小さい値を見つけたらそこまで探索範囲を縮小している。nums[middle] <= nums.last() then end = middle + - <= としているのはnums.last()が最小値の可能性があるので。 + - 右側(end)により小さい値があれば、左側を捨てる。 nums.last() < nums[middle] then start = middle + 1 // middleを探索範囲外にする。 + + 所感 + - nums.last()との比較でかなり面食らう感じがある。nums.last()は最大値、最小値、その他の値のどれでもあるのになぜ比較しているのか?と初見で思った。 + 自分には認知負荷が高い。 + (start,end]な半開区間も実装しようかと思ったが時間切れなのでスキップ。 +*/ + +pub struct Solution {} +impl Solution { + pub fn find_min(nums: Vec) -> i32 { + if nums.is_empty() { + panic!("nums must not be empty"); + } + + // [start,end) + // start <= i < end + let mut start = 0; + let mut end = nums.len(); + + while start < end { + let middle = start + (end - start) / 2; + + if nums[middle] <= *nums.last().unwrap() { + end = middle; + } else { + start = middle + 1; + } + } + + nums[start] + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn step2a_test() { + assert_eq!(Solution::find_min(vec![3, 4, 5, 1, 2]), 1); + assert_eq!(Solution::find_min(vec![4, 5, 6, 7, 0, 1, 2]), 0); + assert_eq!(Solution::find_min(vec![11, 13, 15, 17]), 11); + assert_eq!(Solution::find_min(vec![3, 1, 2]), 1); + assert_eq!(Solution::find_min(vec![2, 3, 1]), 1); + + assert_eq!(Solution::find_min(vec![1]), 1); + } + + #[test] + #[should_panic] + fn step1_empty_nums_test() { + let empty_nums = Vec::new(); + Solution::find_min(empty_nums); + } +} diff --git a/src/bin/step3.rs b/src/bin/step3.rs new file mode 100644 index 0000000..506c9a1 --- /dev/null +++ b/src/bin/step3.rs @@ -0,0 +1,68 @@ +// Step3 +// 目的: 覚えられないのは、なんか素直じゃないはずなので、そこを探し、ゴールに到達する + +// 方法 +// 時間を測りながらもう一度解く +// 10分以内に一度もエラーを吐かず正解 +// これを3回連続でできたら終わり +// レビューを受ける +// 作れないデータ構造があった場合は別途自作すること + +/* + n = nums.len() + 時間計算量: O(log n) + 空間計算量: O(1) +*/ + +/* + 1回目: 1分53秒 + 2回目: 1分36秒 + 3回目: 1分20秒 +*/ + +pub struct Solution {} +impl Solution { + pub fn find_min(nums: Vec) -> i32 { + if nums.is_empty() { + panic!("nums must not be empty"); + } + + let mut start = 0; + let mut end = nums.len() - 1; + + while start < end { + let middle = start + (end - start) / 2; + + if nums[middle] < nums[end] { + end = middle; + } else { + start = middle + 1; + } + } + + nums[end] + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn step3_test() { + assert_eq!(Solution::find_min(vec![3, 4, 5, 1, 2]), 1); + assert_eq!(Solution::find_min(vec![4, 5, 6, 7, 0, 1, 2]), 0); + assert_eq!(Solution::find_min(vec![11, 13, 15, 17]), 11); + assert_eq!(Solution::find_min(vec![3, 1, 2]), 1); + assert_eq!(Solution::find_min(vec![2, 3, 1]), 1); + + assert_eq!(Solution::find_min(vec![1]), 1); + } + + #[test] + #[should_panic] + fn step3_empty_nums_test() { + let empty_nums = Vec::new(); + Solution::find_min(empty_nums); + } +}