-
Notifications
You must be signed in to change notification settings - Fork 0
solve: 1011.Capacity To Ship Packages Within D Days #44
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<i32>, 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 | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<NonZero<u32>>, days: NonZero<u32>) -> Result<NonZero<u32>> { | ||
| ... | ||
| } | ||
| ``` | ||
| - dayはdays_requiredの方がより分かりやすいと思った。 | ||
|
|
||
| 所感 | ||
| - while loopの中のfor loopは関数に切り出したバージョンも書いてみる。step2a.rs | ||
| */ | ||
|
|
||
| pub struct Solution {} | ||
| impl Solution { | ||
| pub fn ship_within_days(weights: Vec<i32>, days: i32) -> i32 { | ||
| if weights.is_empty() { | ||
| return 0; | ||
| } | ||
| if weights.iter().any(|weight| *weight < 0) { | ||
| panic!("weights must contain only positive values"); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 条件とメッセージを整合させたいと思いました。 |
||
| } | ||
| 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::<i64>(); | ||
|
|
||
| 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 | ||
| )) | ||
|
Comment on lines
+99
to
+102
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 趣味の範囲ですが、Rustのexpectは、エラーが発生していなくても引数が評価されるそうです。したがって、この書き方だと成功時でも毎回format!マクロが実行され、文字列のアロケーション(メモリ確保)が発生してしまいます。 代案としてはunwrap_or_else(|_| panic!(...))が使えそうです。 |
||
| } | ||
| } | ||
|
|
||
| #[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 | ||
| ); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<i32>, 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::<i64>(); | ||
|
|
||
| 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<i32, String> { | ||
| 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()); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
このあたり非常に面白く読ませていただきました!
使っている道具の仕様(言語仕様やライブラリの特性)を深掘りしたくなる気持ちや、ライブラリ実装のバグの可能性を察知して直そうとする姿勢、いずれもSWEのマインドセットそのもので、とてもいいなと思いました。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Range は範囲を意味していて、iterate することは想定されているもののランダムアクセスができる想定はないのでしょう。Rust の slice は view なので作ってもコピーされません。こちらが想定される二分探索の対象でしょう。