diff --git a/src/bin/step1.rs b/src/bin/step1.rs new file mode 100644 index 0000000..673cfdb --- /dev/null +++ b/src/bin/step1.rs @@ -0,0 +1,125 @@ +// Step1 +// 目的: 方法を思いつく + +// 方法 +// 5分考えてわからなかったら答えをみる +// 答えを見て理解したと思ったら全部消して答えを隠して書く +// 5分筆が止まったらもう一回みて全部消す +// 正解したら終わり + +/* + 問題の理解 + - 自然数からなる配列numsと自然数targetが与えられる。合計値がtarget以上となる部分配列の最小サイズを求めて返す。 + 合計値がtarget以上になるような部分配列が存在しない場合は0を返す。 + + 何を考えて解いていたか + - 累積和の規則とSliding windowの関係を考える。 + target=4 + [1,1,2,1] + [3,1,1] + [1,1,3] + - 部分配列の長さは2ポインタの差から求められる。 + - 部分配列の長さを保持する変数を用意しておき、累積和がtarget以上となった時に部分配列の最小サイズを更新できるか試行する。 + - 部分配列の最小長さを更新したら、 + - 累積和を0にリセット + - start = endにしてこれまで見た値を捨てて次のループへ + この解法で解けるなら時間計算量O(n),空間計算量O(1)になると考える。 + フォローアップで時間計算量がO(n log n)となるようなアルゴリズムも考えようとあるが、ソートしてtargetからピボットを探して配列の値を半分捨てるとかだろうか。 + 部分配列は配列内の連続した配列であるので、元の配列を並べ替えるのはダメそう。 + よく分からないのでまずは、思いついた解法を実装する。 + この解法ではWrong Answerとなったので解答を見る + + 何がわからなかったか + - target=11, nums=[1,2,3,4,5]のように列の後半にかけて解となる部分配列が出現するケースの対応方法。 + もう少し具体的にはstartポインタをどのような場合にどこまで動かすか。 + + 解法の理解 + https://leetcode.com/problems/minimum-size-subarray-sum/solutions/7266487/rust-efficient-two-pointer-approach-by-h-dg4q/ + - start,endによる閉区間の2ポインタで区間の端点を管理している。 + - sumで区間(部分配列)の合計値を管理している。 + - ポインタが不変条件(start <= end && end < nums.len())を満たす間以下を繰り返す。 + - 区間の合計値がtarget以上となったら、最小部分配列長の更新を行う。 + - この時点でstart側を縮小してさらに小さい部分配列が作れないかを試行している。 + - 区間の合計値がtarget未満のとき、条件を満たす部分配列を作るためにend側を伸ばして合計値を更新。 + + 正解してから気づいたこと + - 区間の始点、終点どちらかを固定するのではなく、不変条件(start <= end && end < nums.len())を満たす間、両側の端点を動かしている。 + + 所感 + - 問題の制約上、入力は自然数であることが分かっているのでメソッドのシグネチャはi32ではなく、u32で統一してほしいと思った。 +*/ + +pub struct Solution {} +impl Solution { + pub fn min_sub_array_len(target: i32, nums: Vec) -> i32 { + if nums.is_empty() { + return 0; + }; + + let n = nums.len() + 1; + let mut min_subarray_length = n; + let mut sum = nums[0]; + let (mut start, mut end) = (0, 0); + + while start <= end && end < nums.len() { + if target <= sum { + min_subarray_length = min_subarray_length.min((end - start) + 1); + sum -= nums[start]; + start += 1; + } else { + end += 1; + if end < nums.len() { + sum += nums[end]; + } + } + } + + if min_subarray_length == n { + return 0; + } + + min_subarray_length as i32 + } + + #[allow(dead_code)] + pub fn min_sub_array_len_wa(target: i32, nums: Vec) -> i32 { + /* + この実装はWrong Answerとなります。 + */ + let mut start = -1isize; + let mut accumulated_sum = 0; + let mut min_subarray_length = (nums.len() + 1) as isize; + + for end in 0..nums.len() { + if target <= accumulated_sum + nums[end] { + min_subarray_length = min_subarray_length.min(end as isize - start); + accumulated_sum = 0; + start = end as isize; + } + + accumulated_sum += nums[end]; + } + + if min_subarray_length == (nums.len() + 1) as isize { + return 0; + } + + min_subarray_length as i32 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn step1_test() { + assert_eq!(Solution::min_sub_array_len(7, vec![2, 3, 1, 2, 4, 3]), 2); + assert_eq!(Solution::min_sub_array_len(4, vec![1, 4, 4]), 1); + assert_eq!( + Solution::min_sub_array_len(11, vec![1, 1, 1, 1, 1, 1, 1, 1]), + 0 + ); + assert_eq!(Solution::min_sub_array_len(11, vec![1, 2, 3, 4, 5]), 3); + } +} diff --git a/src/bin/step2.rs b/src/bin/step2.rs new file mode 100644 index 0000000..395226e --- /dev/null +++ b/src/bin/step2.rs @@ -0,0 +1,124 @@ +// Step2 +// 目的: 自然な書き方を考えて整理する + +// 方法 +// Step1のコードを読みやすくしてみる +// 他の人のコードを2つは読んでみること +// 正解したら終わり + +// 以下をメモに残すこと +// 講師陣はどのようなコメントを残すだろうか? +// 他の人のコードを読んで考えたこと +// 改善する時に考えたこと + +/* + 他の人のコードを読んで考えたこと + https://github.com/hayashi-ay/leetcode/pull/51/changes + https://github.com/olsen-blue/Arai60/pull/50/changes + - 実装に幅があり参考になる。 + + https://github.com/Yoshiki-Iwasa/Arai60/pull/43/changes#r1709433297 + - 累積和を使った実装方法。また、累積和を計算する時にstd::iter::successorsを利用するときれいに書けるとある。 + successorsは知らないメソッドだったので勉強になった。 + std::iter::scanでも良いのでは?と思ったが、先頭に0を持つ累積和の配列を生成するためにnums.scan()としても、配列の先頭に0が含まれていないのでnums[0]に0を入れるような操作が必要になる。 + successorsでは初期値を起点として、Noneが返されるまで繰り返し操作を行うので、先頭に0を持つ累積和の配列を作れる。 + 本ファイル下部のplaygroundテストに確認用のコードを実装した。 + + https://github.com/naoto-iwase/leetcode/pull/50/changes#diff-d7328f247ea703c897516111c872b729bf96f2b2e54ab850566dd2ac46122d94R48 + > 前からの累積和は単調増加するので、二分探索(lower bound)で初めて和がtarget以上になる点がわかる。 + - 累積和と二分探索を組み合わせた解法について、わかりやすく一言でまとまっている。 + + 改善する時に考えたこと + - 最小部分配列長さの初期値を表す変数nは雑すぎるので、n -> sentinel_subarray_lengthに変更する。 + max_subarray_lengthにしようかと思ったが、nums.len() + 1は明らかにmax_subarray_lengthではないので、sentinelとした。 + - 変数名sumでも十分な気はするが、丁寧にするなら sum -> prefix_sumとする。減算もするので。 + - 条件を満たす部分配列が見つからなかった時の戻り値0を定数で宣言する。 + + 所感 + - 実装に幅があるので写経しておく + - 入力numsの累積和を計算してから利用する方法 + https://github.com/Yoshiki-Iwasa/Arai60/pull/43/changes#diff-705d13bf86ae090a0d8b47599b0a08e57431901d11cce1cdd7c2dea2f2568047R43 + - 二分探索も利用している + https://github.com/naoto-iwase/leetcode/pull/50/changes#diff-d7328f247ea703c897516111c872b729bf96f2b2e54ab850566dd2ac46122d94R62 + https://github.com/hayashi-ay/leetcode/pull/51/changes#diff-6d4eb2707ed57d8037bfa2e5985424b237a407741a7602ba92b31e090d1cb096R79 +*/ + +pub struct Solution {} +impl Solution { + pub fn min_sub_array_len(target: i32, nums: Vec) -> i32 { + const NOT_FOUND: i32 = 0; + + if nums.is_empty() { + return NOT_FOUND; + }; + + let sentinel_subarray_length: usize = nums.len() + 1; + let mut min_subarray_length = sentinel_subarray_length; + let mut prefix_sum = nums[0]; + let (mut start, mut end) = (0, 0); + + while start <= end && end < nums.len() { + if target <= prefix_sum { + min_subarray_length = min_subarray_length.min((end - start) + 1); + prefix_sum -= nums[start]; + start += 1; + } else { + end += 1; + if end < nums.len() { + prefix_sum += nums[end]; + } + } + } + + if min_subarray_length == sentinel_subarray_length { + return NOT_FOUND; + } + + min_subarray_length as i32 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn playground() { + let expect = vec![0, 1, 3, 6, 10, 15]; + let nums = vec![1, 2, 3, 4, 5]; + + // std::iter::scan + assert_eq!( + &[0].iter() + .chain(nums.iter()) + .scan(0, |state, x| { + *state += x; + Some(*state) + }) + .collect::>(), + &expect + ); + + // std::iter::successors + // MEMO: successorsのクロージャ内で nums.iter().map(|num| num + acc) として無限ループになった。 + // nums.iter()で毎回新たなイテレータを生成してしまい.next()が常にnums[0]を返すので無限ループになる。 + // 外側でイテレータを生成して、このイテレータを変更しながら行うべき。 + let mut nums_iter = nums.iter(); + assert_eq!( + std::iter::successors(Some(0), |acc| nums_iter.next().map(|num| num + acc)) + .collect::>(), + expect + ) + } + + #[test] + fn step2_test() { + assert_eq!(Solution::min_sub_array_len(7, vec![2, 3, 1, 2, 4, 3]), 2); + assert_eq!(Solution::min_sub_array_len(4, vec![1, 4, 4]), 1); + assert_eq!( + Solution::min_sub_array_len(11, vec![1, 1, 1, 1, 1, 1, 1, 1]), + 0 + ); + assert_eq!(Solution::min_sub_array_len(11, vec![1, 2, 3, 4, 5]), 3); + } +} diff --git a/src/bin/step2a.rs b/src/bin/step2a.rs new file mode 100644 index 0000000..d2453ea --- /dev/null +++ b/src/bin/step2a.rs @@ -0,0 +1,77 @@ +// Step2a +// 目的: 別の実装方法を練習 + +/* + 累積和と二分探索を組み合わせた実装を練習する + - 参考 + https://github.com/Yoshiki-Iwasa/Arai60/pull/43/changes#diff-705d13bf86ae090a0d8b47599b0a08e57431901d11cce1cdd7c2dea2f2568047R43 + - 二分探索も利用している + https://github.com/naoto-iwase/leetcode/pull/50/changes#diff-d7328f247ea703c897516111c872b729bf96f2b2e54ab850566dd2ac46122d94R62 + https://github.com/hayashi-ay/leetcode/pull/51/changes#diff-6d4eb2707ed57d8037bfa2e5985424b237a407741a7602ba92b31e090d1cb096R79 + + 解法の理解 + - 入力numsに対応する累積和の配列を生成する。累積和は先頭が0からスタートすることに注意。 + - target以上となる部分配列のうち、最小の部分配列を探してそのサイズを返したい。つまり、部分配列の和(累積和)がtarget以上になるものを探す。 + - ある区間の合計値は [start,end)な半開区間のとき、Sum(start,end) = prefix_sums[end] - prefix_sums[start] で求められる。 + nums = [1,2,3,4,5] + prefix_sums = [0,1,3,6,10,15] + Sum(1,5) = prefix_sums[5] - prefix_sums[1] = 15 - 1 = 14 + 区間(部分配列)の合計値がtarget以上になる最小の区間長さを探している。 + target <= prefix_sums[end] - prefix_sums[start] = target + prefix_sums[start] <= prefix_sums[end] + partition_pointメソッドでは、predicateがfalseとなる最初のインデックスを返す。 + prefix_sums[end] < target + prefix_sums[start] とするとtarget以上となる最初のインデックスが返ってくる。(targetと等しい値もfalseにしている。) + + 所感 + - lower_boundの条件で prefix_sums[end] < target + prefix_sums[start] としている箇所が難しく感じた。なぜこれが正しくなるのかすぐに理解できず、紙に書いてデバッグして正しいことが分かった感じ。 + ある区間の合計値は累積和を利用して求められるということが分かっている前提になっているからだと思った。(Sum(start,end) = prefix_sums[end] - prefix_sums[start]) +*/ + +pub struct Solution {} +impl Solution { + pub fn min_sub_array_len(target: i32, nums: Vec) -> i32 { + const NOT_FOUND: i32 = 0; + + if nums.is_empty() { + return NOT_FOUND; + } + + let mut nums_iter = nums.iter(); + let prefix_sums = std::iter::successors(Some(0), |x| nums_iter.next().map(|num| num + x)) + .collect::>(); + let sentinel_subarray_length = nums.len() + 1; + let mut min_subarray_length = sentinel_subarray_length; + + for start in 0..nums.len() { + let end = + prefix_sums.partition_point(|prefix_sum| *prefix_sum < target + prefix_sums[start]); + + if end == prefix_sums.len() { + break; + } + + min_subarray_length = min_subarray_length.min(end - start); + } + + if min_subarray_length == sentinel_subarray_length { + return NOT_FOUND; + } + + min_subarray_length as i32 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn step2a_test() { + assert_eq!(Solution::min_sub_array_len(7, vec![2, 3, 1, 2, 4, 3]), 2); + assert_eq!(Solution::min_sub_array_len(4, vec![1, 4, 4]), 1); + assert_eq!( + Solution::min_sub_array_len(11, vec![1, 1, 1, 1, 1, 1, 1, 1]), + 0 + ); + assert_eq!(Solution::min_sub_array_len(11, vec![1, 2, 3, 4, 5]), 3); + } +} diff --git a/src/bin/step3.rs b/src/bin/step3.rs new file mode 100644 index 0000000..6b0c29e --- /dev/null +++ b/src/bin/step3.rs @@ -0,0 +1,72 @@ +// Step3 +// 目的: 覚えられないのは、なんか素直じゃないはずなので、そこを探し、ゴールに到達する + +// 方法 +// 時間を測りながらもう一度解く +// 10分以内に一度もエラーを吐かず正解 +// これを3回連続でできたら終わり +// レビューを受ける +// 作れないデータ構造があった場合は別途自作すること + +/* + n = nums.len() + 時間計算量: O(n) + 空間計算量: O(1) +*/ + +/* + 1回目: 4分50秒 + 2回目: 4分01秒 + 3回目: 3分15秒 +*/ + +pub struct Solution {} +impl Solution { + pub fn min_sub_array_len(target: i32, nums: Vec) -> i32 { + const NOT_FOUND: i32 = 0; + + if nums.is_empty() { + return NOT_FOUND; + } + + let sentinel_subarray_length = nums.len() + 1; + let mut min_subarray_length = sentinel_subarray_length; + let mut prefix_sum = nums[0]; + let (mut start, mut end) = (0, 0); + + while start <= end && end < nums.len() { + if target <= prefix_sum { + min_subarray_length = min_subarray_length.min((end - start) + 1); + prefix_sum -= nums[start]; + start += 1; + } else { + end += 1; + if end < nums.len() { + prefix_sum += nums[end]; + } + } + } + + if min_subarray_length == sentinel_subarray_length { + return NOT_FOUND; + } + + min_subarray_length as i32 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn step3_test() { + assert_eq!(Solution::min_sub_array_len(7, vec![2, 3, 1, 2, 4, 3]), 2); + assert_eq!(Solution::min_sub_array_len(4, vec![1, 4, 4]), 1); + assert_eq!( + Solution::min_sub_array_len(11, vec![1, 1, 1, 1, 1, 1, 1, 1]), + 0 + ); + assert_eq!(Solution::min_sub_array_len(11, vec![1, 2, 3, 4, 5]), 3); + } +}