diff --git a/src/bin/step1.rs b/src/bin/step1.rs new file mode 100644 index 0000000..f3292d4 --- /dev/null +++ b/src/bin/step1.rs @@ -0,0 +1,108 @@ +// Step1 +// 目的: 方法を思いつく + +// 方法 +// 5分考えてわからなかったら答えをみる +// 答えを見て理解したと思ったら全部消して答えを隠して書く +// 5分筆が止まったらもう一回みて全部消す +// 正解したら終わり + +/* + 問題の理解 + - ベルトコンベアに乗っている荷物の重さを表す自然数からなるweights配列と自然数のdaysが与えられる。 + daysで表される日数内で全ての荷物をベルトコンベアから船に積む時に必要な船の最小積載重量を求めて返す。 + 制約としてベルトコンベアの荷物の順番は変えることができない。つまり、与えられた時点のweights[i]は変更不可。 + + 何を考えて解いていたか + - 手作業でやることを考えてどこから始めるかを考える。 + - days内に全ての荷物を送るための一日あたりの荷物量は weights.len() / days で求められる。(最小積載量は無視した場合) + 手が止まったので答えを見る。 + + 何がわからなかったか + - 手作業でやる場合ですらどうやれば良いかが分からなかった。 + + 解法の理解 + https://leetcode.com/problems/capacity-to-ship-packages-within-d-days/solutions/3217242/javascript-rust-easy-to-understand-solut-y5lw/ + - 荷物の総重量をmax_capacityで表している。 + - 1日で全部船に積むとしたときの最小積載重量という考え方に見える。 + - 最も重い荷物をmin_capacityで表している。 + - 1日1個ずつ船に積むとしたときの最小積載重量という考え方に見える。 + - 上記の両極端なケースから区間の開始と終了を作って、区間を二分探索するという考え方に見える。 + - capacity = (min_capacity + max_capacity) / 2 があまり腑に落ちない。 + - 1日1個ずつ荷物を船に積む時の最小積載重量 ~ 1日で全ての荷物を船に積むときの最小積載重量 という区間の中間地点なのは分かる。 + - 荷物の数と日数が関連している。 + - 1日1個ずつ荷物を船に積むとき、荷物の数=日数になる。 + - 1日に全ての荷物を船に積むとき、日数=1になる。 + - 両極端なケースを考えてこの区間の間に答えが存在するように問題を再定義して二分探索をしているように見える。 + - 二分探索の条件(days < day)では何をしているか。 + - 指定された日数内で荷物を捌ききれないとき、一度に船に詰める載重量を+1して最小積載重量を増やしている。 + - 左の区間(より小さい積載重量)を捨てている。 + - 指定された日数内で荷物を捌き切れるとき、より小さい最小積載重量を探す。 + - 右の区間(より大きい積載重量)を捨てている。 + + 所感 + - 問題を二分探索で処理できる問題に再定義しているように見えた。 + - 問題を見て二分探索で解けることに気付けるレベルまで至っていないが、練習を続けていけば慣れるタイプのものだと思った。 + - 入力の制約としてはあり得ないが、引数のシグネチャとしてはi32の合計を求める部分はオーバーフローしうるので、i64で扱うようにした方が良いと思った。 +*/ + +pub struct Solution {} +impl Solution { + pub fn ship_within_days(weights: Vec, days: i32) -> i32 { + let mut max_capacity_of_day = 0; + let mut min_capacity_of_day = 0; + + for weight in &weights { + max_capacity_of_day += weight; + min_capacity_of_day = min_capacity_of_day.max(*weight); + } + + while min_capacity_of_day < max_capacity_of_day { + let capacity = (min_capacity_of_day + max_capacity_of_day) / 2; + let mut day = 1; + let mut load_of_day = 0; + + for weight in &weights { + load_of_day += weight; + + // 想定していた1日あたりの積載量を超えるので、次の日に持ち越し。 + if capacity < load_of_day { + day += 1; + load_of_day = *weight; + } + } + + if days < day { + min_capacity_of_day = capacity + 1; + } else { + max_capacity_of_day = capacity; + } + } + + min_capacity_of_day + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn step1_test() { + assert_eq!( + Solution::ship_within_days(vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 5), + 15 + ); + assert_eq!(Solution::ship_within_days(vec![3, 2, 2, 4, 1, 4], 3), 6); + assert_eq!(Solution::ship_within_days(vec![1, 2, 3, 1, 1], 4), 3); + } + + #[test] + #[should_panic] + fn step1_overflow_test() { + assert_eq!( + Solution::ship_within_days(vec![i32::MAX, i32::MAX, 5], 3), + i32::MAX + ); + } +} diff --git a/src/bin/step2.rs b/src/bin/step2.rs new file mode 100644 index 0000000..0a07aaf --- /dev/null +++ b/src/bin/step2.rs @@ -0,0 +1,127 @@ +// Step2 +// 目的: 自然な書き方を考えて整理する + +// 方法 +// Step1のコードを読みやすくしてみる +// 他の人のコードを2つは読んでみること +// 正解したら終わり + +// 以下をメモに残すこと +// 講師陣はどのようなコメントを残すだろうか? +// 他の人のコードを読んで考えたこと +// 改善する時に考えたこと + +/* + 他の人のコードを読んで考えたこと + + https://github.com/docto-rin/leetcode/pull/27/changes#diff-7621ca097a66d8850331853241ed62b900277d528a4f2ff8a3a9c56e63ea56a7R129 + - key関数を定義してbisect_leftで答えを求めている。key関数を定義する部分で抽象化が必要なので良い練習になると思った。 + Pythonのbisect_leftでrange(max_capacity)と引数に渡している部分は遅延評価なので空間計算量O(n)とはならずO(1)。 + Rustのbinary_search_byはスライスに対する操作(配列を実体化する必要がある)なので、空間計算量O(n)となるので注意が必要。 + Rustでは標準ライブラリでRangeに対する二分探索APIがない模様。 + bisectionという外部クレートを見つけたが、Rangeに対する二分探索は行えないようだった。 + IssuesやPull Requestを見ていたところ、二分探索アルゴリズムにおいて中間を計算するロジックがオーバーフローする可能性があるというIssueがあった。 + https://github.com/SteadBytes/bisection/issues/2 + コーディング練習会でもレビューで良く指摘があるのであるあるなんだなと思った。 + Issueの中でこの二分探索アルゴリズムに関するGoogle Researchの記事へのリンクが貼られており、過去にJDK用に実装された二分探索アルゴリズムでも同様のバグがあったのことで面白いと思った。 + https://research.google/blog/extra-extra-read-all-about-it-nearly-all-binary-searches-and-mergesorts-are-broken/ + ちなみに、bisectionクレートに依存しているライブラリにastral-sh/uv(Pythonパッケージ、プロジェクト管理ツール)があることに気付いた。 + uvは昨今のPythonパッケージ管理ツールとしてデファクトスタンダードになっている感じもあるので、依存しているクレートにバグが有ることを報告して実装を差し替えるなりのPull Requestチャンスかなと思って調べていたら、すでにPull Requestが行われていた。 + https://github.com/prefix-dev/async_http_range_reader/pull/18 + + https://github.com/nanae772/leetcode-arai60/pull/43/changes#diff-7003a5baa3f89ae7ceafbb688dab132a20dfe083744966edff31be3fc901637bR41 + - 引数のweights.max() == 0,days = 0のケースをどうするかについて。問題の制約としてありえないことが分かっているが、コーディング練習の一環として考えてみること自体が良いことだと思った。 + + https://github.com/h1rosaka/arai60/pull/46/changes + - Pull Requestでのやり取りを理解できるかという視点で読むと良さそう。ソフトウェアエンジニアとして職場の同僚の議論を聞いていて理解できる必要があるよねという感じ。 + + https://github.com/olsen-blue/Arai60/pull/44/changes#diff-4e146417f14c744a10f851601f26cd2cb17b420ff966720e568f6f5679aa475eR145 + - Pythonのbisect_leftの引数にrange(sum(weights) + 1)を渡しているが、なぜ+1しているのか少し考えた。 + rangeは引数が1つの場合にstopに対応していて、start <= i < stop な半開区間[start,stop)になるので+1することで調整していることが分かった。 + weights=[1, 2, 3]のときsum = 6となるが、range(6)とすると0 ~ 5になるので+1している。 + + 改善する時に考えたこと + - 入力の制約上あり得ないがオーバーフローを考慮した実装にする。 + - weights,daysの引数チェックも行う。LeetCodeのテンプレートのシグネチャがi32なのでpanic!()しているが、自分で定義するならResultやOptionで表現すると思った。 + - Rustだとstd::num::NonZeroというものが有り、0を許容しないことを表現できるのでこれを使うのがベストそう。 + https://doc.rust-lang.org/std/num/struct.NonZero.html + ```rust + use std::num::NonZero; + + pub fn ship_within_days(weights: Vec>, days: NonZero) -> Result> { + ... + } + ``` + - dayはdays_requiredの方がより分かりやすいと思った。 + + 所感 + - while loopの中のfor loopは関数に切り出したバージョンも書いてみる。step2a.rs +*/ + +pub struct Solution {} +impl Solution { + pub fn ship_within_days(weights: Vec, days: i32) -> i32 { + if weights.is_empty() { + return 0; + } + if weights.iter().any(|weight| *weight < 0) { + panic!("weights must contain only positive values"); + } + if days <= 0 { + panic!("days must be greater than 0"); + } + + let mut min_capacity_of_day = *weights.iter().max().unwrap() as i64; + let mut max_capacity_of_day = weights.iter().map(|x| *x as i64).sum::(); + + while min_capacity_of_day < max_capacity_of_day { + let capacity_of_day = + min_capacity_of_day + (max_capacity_of_day - min_capacity_of_day) / 2; + let mut days_required = 1; + let mut load_of_day = 0; + + for weight in &weights { + load_of_day += *weight as i64; + + if capacity_of_day < load_of_day { + days_required += 1; + load_of_day = *weight as i64; + } + } + + if days < days_required { + min_capacity_of_day = capacity_of_day + 1; + } else { + max_capacity_of_day = capacity_of_day; + } + } + + max_capacity_of_day.try_into().expect(&format!( + "max_capacity_of_day downcast failed. max_capacity_of_day: {}", + max_capacity_of_day + )) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn step2_test() { + assert_eq!( + Solution::ship_within_days(vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 5), + 15 + ); + assert_eq!(Solution::ship_within_days(vec![3, 2, 2, 4, 1, 4], 3), 6); + assert_eq!(Solution::ship_within_days(vec![1, 2, 3, 1, 1], 4), 3); + } + + #[test] + fn step2_no_overflow_test() { + assert_eq!( + Solution::ship_within_days(vec![i32::MAX, i32::MAX, 5], 3), + i32::MAX + ); + } +} diff --git a/src/bin/step2a.rs b/src/bin/step2a.rs new file mode 100644 index 0000000..0e57765 --- /dev/null +++ b/src/bin/step2a.rs @@ -0,0 +1,122 @@ +// Step2a +// 目的: 別の実装方法で練習する + +/* + 改善する時に考えたこと + - 全体的に変数名がしっくり来なかったのでGPT-5.2に相談しつつ変更した。 + - オーバーフロー対策でi64にアップキャストしている部分はGPT-5.2に提案されたコードの中身を精査して問題なさそうだったのでcopiedアダプタを用いた書き方にした。 + + 所感 + - copiedアダプタは見たことがあったものの使い所を知らなかったが、GPT-5.2の提案するコードに含まれていたので調べたところ新しいことが学べたので良かった。 +*/ + +pub struct Solution {} +impl Solution { + pub fn ship_within_days(weights: Vec, days: i32) -> i32 { + if weights.is_empty() { + return 0; + } + if weights.iter().any(|x| *x < 0) { + panic!("weights must contain only positive values"); + } + if days <= 0 { + panic!("days must be greater than 0") + } + + // iter.copied()はcopiedアダプタと呼ばれているもの。iterの要素が全てCopy型である場合に利用可能。 + // 意図として iter().map(|x| i64::from(*x)) を iter().copied().map(i64::from)とするためにcopiedアダプタを間に入れて参照ではなくコピーした値をmapに渡している。 + // weight.into_iter()とするとweightsの所有権がムーブし、以降weightsが使えなくなるので、参照 -> copiedアダプタ としている。 + // https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.copied + // 「プログラミングRust 第2版」 P.84, P.342 あたりを参照。 + let mut lower_capacity = weights.iter().copied().max().unwrap() as i64; + let mut upper_capacity = weights.iter().copied().map(i64::from).sum::(); + + while lower_capacity < upper_capacity { + let middle_capacity = lower_capacity + (upper_capacity - lower_capacity) / 2; + let required_days = Self::required_days_for_shipping(&weights, middle_capacity) + .expect("calculate required_days_for_shipping failed"); + + if days < required_days { + lower_capacity = middle_capacity + 1; + } else { + upper_capacity = middle_capacity; + } + } + + lower_capacity.try_into().expect(&format!( + "lower_capacity downcast failed. lower_capacity: {}", + lower_capacity + )) + } + + fn required_days_for_shipping(weights: &[i32], daily_capacity: i64) -> Result { + let mut days_required = 1; + let mut load_of_day = 0; + + for weight in weights { + let weight = i64::from(*weight); + if daily_capacity < weight { + return Err(format!("weight over daily_capacity. weight: {weight}, daily_capacity: {daily_capacity}")); + } + + // 積載量上限を超えるなら次の日に積む。 + if daily_capacity < load_of_day + weight { + days_required += 1; + load_of_day = 0; + } + load_of_day += weight; + } + + Ok(days_required) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn step2a_test() { + assert_eq!( + Solution::ship_within_days(vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 5), + 15 + ); + assert_eq!(Solution::ship_within_days(vec![3, 2, 2, 4, 1, 4], 3), 6); + assert_eq!(Solution::ship_within_days(vec![1, 2, 3, 1, 1], 4), 3); + } + + #[test] + fn step2a_no_overflow_test() { + assert_eq!( + Solution::ship_within_days(vec![i32::MAX, i32::MAX, 5], 3), + i32::MAX + ); + } + + #[test] + fn step2a_required_days_for_shipping_test() { + assert_eq!( + Solution::required_days_for_shipping(&vec![1, 2, 3], 4).unwrap(), + 2 + ); + assert_eq!( + Solution::required_days_for_shipping(&vec![1, 2, 3], 10).unwrap(), + 1 + ); + assert_eq!( + Solution::required_days_for_shipping(&vec![5], 5).unwrap(), + 1 + ); + assert_eq!( + Solution::required_days_for_shipping(&vec![3, 3, 3], 3).unwrap(), + 3 + ); + } + + #[test] + fn step2a_required_days_for_shipping_over_daily_capacity_test() { + // 1日あたりの積載重量上限を超える荷物があり運べないのでエラー。 + let daily_capacity = 1; + assert!(Solution::required_days_for_shipping(&vec![1, 2, 3], daily_capacity).is_err()); + } +} diff --git a/src/bin/step3.rs b/src/bin/step3.rs new file mode 100644 index 0000000..f8797ba --- /dev/null +++ b/src/bin/step3.rs @@ -0,0 +1,158 @@ +// Step3 +// 目的: 覚えられないのは、なんか素直じゃないはずなので、そこを探し、ゴールに到達する + +// 方法 +// 時間を測りながらもう一度解く +// 10分以内に一度もエラーを吐かず正解 +// これを3回連続でできたら終わり +// レビューを受ける +// 作れないデータ構造があった場合は別途自作すること + +/* + n は weights.sum() - weights.max() だが、Big-O記法において定数項は無視する。 + n = weights.sum() + m = weights.len() + 時間計算量: O(m log n) + 空間計算量: O(1) +*/ + +/* + 1回目: 8分28秒 + 2回目: 8分3秒 + 3回目: 分秒 例外処理を書いている時間が長いのでスキップ +*/ + +/* + 所感: + - step3を書いた時に変数名days_requiredを無意識にrequired_daysにしていた。GPT-5.2に聞いてみたところ、required_daysの方が自然だと言うことだったのでそのままにした。 + - lower_capacity,upper_capacityの初期化をbefore->afterに変更した。 + - 問題の文脈からして船の最小積載重量ロジックは頻繁に呼び出されると判断。 + - 頻繁に仕様変更が発生して読み書きされる部分では無いと判断。 + - 以上の理由から人によって頻繁に読み書きされず、頻繁に呼び出されそうなメソッドなので可読性を少し犠牲にして一度の走査で計算する方向にした。 + + ```rust + // before + let weights_iter = weights.iter().copied().map(i64::from); + let mut lower_capacity = weights_iter.clone().max().unwrap() as i64; + let mut upper_capacity = weights_iter.sum::(); + + //after + let (mut lower_capacity, mut upper_capacity) = weights + .iter() + .map(|x| i64::from(*x)) + .fold((i64::MIN, 0i64), |(lower_capacity, upper_capacity), x| { + (lower_capacity.max(x), upper_capacity.add(x)) + }); + ``` +*/ + +use std::ops::Add; + +pub struct Solution {} +impl Solution { + pub fn ship_within_days(weights: Vec, days: i32) -> i32 { + if weights.is_empty() { + return 0; + } + if weights.iter().any(|x| *x < 0) { + panic!("weights must contain only positive values"); + } + if days <= 0 { + panic!("days must be greater than 0"); + } + + let (mut lower_capacity, mut upper_capacity) = weights + .iter() + .map(|x| i64::from(*x)) + .fold((i64::MIN, 0i64), |(lower_capacity, upper_capacity), x| { + (lower_capacity.max(x), upper_capacity.add(x)) + }); + + while lower_capacity < upper_capacity { + let middle_capacity = lower_capacity + (upper_capacity - lower_capacity) / 2; + let required_days = + Self::required_days_for_shipping(&weights, middle_capacity).unwrap(); + + if days < required_days { + lower_capacity = middle_capacity + 1; + } else { + upper_capacity = middle_capacity; + } + } + + lower_capacity.try_into().expect(&format!( + "lower_capacity downcast failed. lower_capacity: {lower_capacity}" + )) + } + + fn required_days_for_shipping(weights: &[i32], daily_capacity: i64) -> Result { + let mut required_days = 1; + let mut load_of_day = 0; + + for weight in weights { + let weight = i64::from(*weight); + + if daily_capacity < weight { + return Err(format!("weight over daily_capacity. weight: {weight}, daily_capacity: {daily_capacity}")); + } + + if daily_capacity < load_of_day + weight { + required_days += 1; + load_of_day = 0; + } + load_of_day += weight; + } + + Ok(required_days) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn step3_test() { + assert_eq!( + Solution::ship_within_days(vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 5), + 15 + ); + assert_eq!(Solution::ship_within_days(vec![3, 2, 2, 4, 1, 4], 3), 6); + assert_eq!(Solution::ship_within_days(vec![1, 2, 3, 1, 1], 4), 3); + } + + #[test] + fn step3_no_overflow_test() { + assert_eq!( + Solution::ship_within_days(vec![i32::MAX, i32::MAX, 5], 3), + i32::MAX + ); + } + + #[test] + fn step3_required_days_for_shipping_test() { + assert_eq!( + Solution::required_days_for_shipping(&vec![1, 2, 3], 4).unwrap(), + 2 + ); + assert_eq!( + Solution::required_days_for_shipping(&vec![1, 2, 3], 10).unwrap(), + 1 + ); + assert_eq!( + Solution::required_days_for_shipping(&vec![5], 5).unwrap(), + 1 + ); + assert_eq!( + Solution::required_days_for_shipping(&vec![3, 3, 3], 3).unwrap(), + 3 + ); + } + + #[test] + fn step3_required_days_for_shipping_over_daily_capacity_test() { + // 1日あたりの積載重量上限を超える荷物があり運べないのでエラー。 + let daily_capacity = 1; + assert!(Solution::required_days_for_shipping(&vec![1, 2, 3], daily_capacity).is_err()); + } +}