diff --git a/src/bin/step1.rs b/src/bin/step1.rs index d640da2..9abf6c7 100644 --- a/src/bin/step1.rs +++ b/src/bin/step1.rs @@ -8,26 +8,79 @@ // 正解したら終わり /* + 問題の理解 + - 株価を表す配列pricesが与えられる。prices[i]はある日の株価である。 + - ある日に株を買った場合に、最大になる利益を値として返す。 + - 利益が発生しない場合は0を返す。 + e.g. + prices = [1, 2, 3, 4] out = (prices[3] - prices[0]) = 3 + prices = [4, 3, 2, 1] out = 0 // どの日に株を購入しても利益が発生しない + 何がわからなかったか - - + - 手作業でやることを考えたときに、 + - 最小株価を見つける。 + - 最大株価を見つける。 + - これらの株価の差を見る。ただし、最小株価の位置iは最大株価のiよりも小さい必要がある。 + この解法をより単純な考え方にする方法がわからなかった。 何を考えて解いていたか - - - - 想定ユースケース - - + - 2重ループで組み合わせを見れば解けると思う。ある時点の株価よりも高い株価の時の利益を都度更新していき、最終的に最大の利益を返す。 + 計算量はO(N ^ 2)となる。入力の制約からNは10 ^ 5なので、10 000 000 000回計算することになる。筋が悪そう。もう少し良さそうな方法を考えて思いつかなければこのナイーブな実装をする。 + - pricesの中で最小を探す。indexをdayとする。線形探索なのでO(N) + pricesの中で最大を探す。indexをdayとする。線形探索なのでO(N) + min_price_day < max_price_day であれば max_price - min_price を答えとして返す。 + 条件を満たさなければ0を返す。 + この方法だと利益自体は発生するケースを取りこぼす。 + 時間切れなのでナイーブな実装だけする。 + Time Limit Exceeded となる実装が出来上がった。 + 解答を見る。 - 正解してから気づいたこと - - + 解法の理解 + https://leetcode.com/problems/best-time-to-buy-and-sell-stock/solutions/4868897/most-optimized-kadanes-algorithm-java-c-2yt85/ + - pricesを先頭から全走査しながら、buyに見つけたprices[i]の最小値を入れている。 + - prices[i] - buy で利益を求めて、利益が今までに見つけた利益よりも大きければ利益を更新している。 + 解法は理解できので、step1a.rsで実装する。 */ pub struct Solution {} -impl Solution {} +impl Solution { + pub fn max_profit(prices: Vec) -> i32 { + /* + このコードはLeetCode採点システム上で Time Limit Exceeded となるコードです。 + */ + if prices.is_empty() { + return 0; + } + + let mut max_profit = 0; + for i in 0..prices.len() { + for j in i..prices.len() { + if prices[j] < prices[i] { + continue; + } + + max_profit = max_profit.max(prices[j] - prices[i]); + } + } + + max_profit + } +} #[cfg(test)] mod tests { use super::*; #[test] - fn step1_test() {} + fn step1_test() { + assert_eq!(Solution::max_profit(vec![7, 1, 5, 3, 6, 4]), 5); + assert_eq!(Solution::max_profit(vec![7, 2, 5, 3, 6, 1]), 4); + assert_eq!(Solution::max_profit(vec![1, 2, 3, 4]), 3); + assert_eq!(Solution::max_profit(vec![4, 3, 2, 1]), 0); + + assert_eq!(Solution::max_profit(vec![2, 1]), 0); + assert_eq!(Solution::max_profit(vec![1, 2]), 1); + assert_eq!(Solution::max_profit(vec![1]), 0); + assert_eq!(Solution::max_profit(vec![]), 0); // 株を購入できないので利益は0で意味が通ると思う + } } diff --git a/src/bin/step1a.rs b/src/bin/step1a.rs new file mode 100644 index 0000000..4206f44 --- /dev/null +++ b/src/bin/step1a.rs @@ -0,0 +1,69 @@ +// Step1a +// 目的: 解答の解法を理解したことを確認する +// https://leetcode.com/problems/best-time-to-buy-and-sell-stock/solutions/4868897/most-optimized-kadanes-algorithm-java-c-2yt85/ + +// 方法 +// 5分考えてわからなかったら答えをみる +// 答えを見て理解したと思ったら全部消して答えを隠して書く +// 5分筆が止まったらもう一回みて全部消す +// 正解したら終わり + +/* + 問題の理解 + - 株価を表す配列pricesが与えられる。prices[i]はある日の株価である。 + - ある日に株を買った場合に、最大になる利益を値として返す。 + - 利益が発生しない場合は0を返す。 + e.g. + prices = [1, 2, 3, 4] out = (prices[3] - prices[0]) = 3 + prices = [4, 3, 2, 1] out = 0 // どの日に株を購入しても利益が発生しない + + 何を考えて解いていたか + - 先頭から全走査しながら + - 最小株価を見つけるたびに最小株価を更新する + - 今見ている株価prices[i]と最小株価から利益を求めて最大利益を更新する + + 正解してから気づいたこと + - 解法を理解した状態からコードにするまでは距離を感じない。 + - 問題から解法にたどり着くまでに距離を感じる。 + - この解答は最適解だと思うので、前回の問題(House Robber)でやったように再帰処理+memoといった別パターン実装を練習した方が良いと思った。step1b.rsで実装する。 +*/ + +pub struct Solution {} +impl Solution { + pub fn max_profit(prices: Vec) -> i32 { + let mut min_price = match prices.len() { + 0 => return 0, + _ => prices[0], + }; + let mut max_price = 0; + + for price in prices.into_iter().skip(1) { + if price < min_price { + min_price = price; + continue; + } + + max_price = max_price.max(price - min_price); + } + + max_price + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn step1a_test() { + assert_eq!(Solution::max_profit(vec![7, 1, 5, 3, 6, 4]), 5); + assert_eq!(Solution::max_profit(vec![7, 2, 5, 3, 6, 1]), 4); + assert_eq!(Solution::max_profit(vec![1, 2, 3, 4]), 3); + assert_eq!(Solution::max_profit(vec![4, 3, 2, 1]), 0); + + assert_eq!(Solution::max_profit(vec![2, 1]), 0); + assert_eq!(Solution::max_profit(vec![1, 2]), 1); + assert_eq!(Solution::max_profit(vec![1]), 0); + assert_eq!(Solution::max_profit(vec![]), 0); // 株を購入できないので利益は0で意味が通ると思う + } +} diff --git a/src/bin/step1b.rs b/src/bin/step1b.rs new file mode 100644 index 0000000..1f29169 --- /dev/null +++ b/src/bin/step1b.rs @@ -0,0 +1,139 @@ +// Step1b +// 目的: 動的計画法の問題に慣れるための練習 + +// 方法 +// 5分考えてわからなかったら答えをみる +// 答えを見て理解したと思ったら全部消して答えを隠して書く +// 5分筆が止まったらもう一回みて全部消す +// 正解したら終わり + +/* + 問題の理解 + - 株価を表す配列pricesが与えられる。prices[i]はある日の株価である。 + - ある日に株を買った場合に、最大になる利益を値として返す。 + - 利益が発生しない場合は0を返す。 + e.g. + prices = [1, 2, 3, 4] out = (prices[3] - prices[0]) = 3 + prices = [4, 3, 2, 1] out = 0 // どの日に株を購入しても利益が発生しない + + 方針 + 具体例の内容はHouse Robberだが、動的計画法(Dynamic Programming)自体に広く適用して解法を思いつくまでの過程を練習する。 + https://leetcode.com/problems/house-robber/solutions/156523/from-good-to-great-how-to-approach-most-of-dp-problems/ + + 何を考えて解いていたか + - 再帰処理にする場合を考える + 解法を思いつかなかったのでChatGPT(GPT-5.1)の学習サポートモードで質問しながら理解を進めた。 + ある時点iで + a) 株を売って利益を求める(株価の最小値を知っている必要がある。) + b) 株を売らない + といった選択肢が取れる。 + - 基本ケース + - 株をそれ以上取引できない prices.len() <= i のとき0を返す。取引できないので利益は0 + - 再帰ケース + - 株を売る場合 + prices[i] - min_price により利益を求める + - 株を売らない場合 + 現在iをi+1でスキップして再帰に入る + 株価の最小値を更新していく必要がある。 + + - どの部分をメモ化するか + - 目的として再帰呼び出しを減らしたい + iは単調増加するのでprices.len()回までのスタック深さとなる。 + pricesの最大サイズは入力の制約から 10 ^ 5 となる。 + 1スタックフレームを50byteと見積もったとき、50byte * (10 ^ 5) = 5,000,000 となり、5,000,000 / (1024 * 1024) = 約5MBとなる + 手元の実行環境で limit コマンドを実行したところスタックサイズ7MBとなったので大分ギリギリな感じがする。LeetCodeの採点システムのスタックサイズはよくわからないがstack overflowするだろうという感覚。 + - iが単調増加するのでメモ化する部分がなく、入力のサイズから再帰による解法自体が筋が悪そう。 + 一応スタックオーバーフローするという予想でLeetCode採点システムで実行してみる。 + スタックオーバーフローしなかった。空間計算量の見積もりに間違いがありそうなのでChatGPT(GPT-5.1)に聞く。 + - スタック深さ(再帰呼び出しの回数)の見積もりはおk + - 1スタックフレームの見積もり50byteが悲観的過ぎる + - make_max_profitの引数のサイズ見積もり 合計28byte + - prices: &[i32] 16byte(fat pointerと呼ばれるものらしい。) https://stackoverflow.com/questions/57754901/what-is-a-fat-pointer + - i: usize プラットフォーム依存だが、8byteと見積もって良いと考える。64bitプラットフォームが一般的だという仮定。 + - min_price: i32 4byte + - make_max_profitメソッド内 合計12byte + - price: &i32 8byte + - max_profit: i32 4byte + コンパイラによる最適化などを考慮せず、そのまま見積もると1スタックフレームあたり40byteとなる + 40byte * (10 ^ 5) / (1024 * 1024) = 約3.8MBとなる + ここから更にコンパイラによる最適化で減る方向に値が動くことを考えるとスタックオーバーフローは大丈夫そうという見積もりになる。 + ただ、入力の制約が動いたらスタックオーバーフローでプログラムがクラッシュすることを考えると、スタック深さのサイズ見積もりから再帰処理による実装は筋が悪そうだと感じる感覚は間違っていないと思った。 + + 正解してから気づいたこと + - 入力の制約から最悪空間計算量で利用するスタックサイズが4MBあたりをうろつく時点で再帰処理による実装は忌避感を感じると思った。 + 入力として与えられる株価データは実務寄りで考えた場合、データの性質として増えていくものなので、再帰で実装するといずれスタックオーバーフローするだろうという感覚。 + + 所感 + - fat pointerという概念を知った。 + https://stackoverflow.com/questions/57754901/what-is-a-fat-pointer + - だいぶ脱線した気がするが、空間計算量見積もりの良い練習になった + - 動的計画法の実装練習で再帰から初めてメモ化して〜という用にメモ化による最適化ができる前提で進めていた。 + 当たり前だがメモ化による最適化が適用できないケースが存在するので、入力データに比例してスタックサイズが大きくなるケースでは再帰による実装は筋が悪いなと思った。 + + スライスについて + - スライスを雰囲気で利用していることに気付いた。 + https://doc.rust-jp.rs/rust-by-example-ja/primitives/array.html + + プログラミングRust 第2版(https://www.oreilly.co.jp/books/9784873119786/)より引用 + スライスは厳密には[T]型のデータを指すものであるが、スライスはほとんど常に参照として扱うので&[T]をスライスと呼ぶことがある。 + - プログラミングRust 第2版 P.63 より + スライス[T]は任意の長さであり得るので、直接変数に格納したり関数の引数として渡すことができない。常に参照として渡される。 + - プログラミングRust 第2版 P.61 より + + スライス: &[i32] 連続するデータの先頭データへのポインタとスライスの長さ(コンパイル時に決定されない)を持つ参照 所有権を持たない。 + スライスは2ワード(連続するデータ先頭データへのポインタ、スライスの長さ)からなる参照なので、fat pointer(太いポインタ)と呼ばれる + 通常のポインタ(thin(細い) pointer)がメモリアドレスだけを持つのに対して、データへのポインタとスライスの長さ2ワード分を持つ参照なのでfat pointer(太いポインタ)と呼ばれる + Vec: データ構造としてはヒープ領域に連続で配置したデータの先頭アドレスへのポインタ、データのサイズ、キャパシティをもつので、fat pointerか?と思ったが、Vec自体は参照ではなく構造体なのでそもそもポインタではなかった。 + &Vec: Vecへの参照なのでthin pointer +*/ + +pub struct Solution {} +impl Solution { + pub fn max_profit(prices: Vec) -> i32 { + if prices.is_empty() { + return 0; + } + + Self::make_max_profit(&prices, 0, prices[0]) + } + + fn make_max_profit(prices: &[i32], i: usize, min_price: i32) -> i32 { + let Some(price) = prices.get(i) else { return 0 }; + + if *price < min_price { + return Self::make_max_profit(prices, i + 1, *price); + } + + let max_profit = Self::make_max_profit(prices, i + 1, min_price).max(*price - min_price); + + max_profit + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[cfg(target_pointer_width = "64")] + fn playground() { + // https://stackoverflow.com/questions/57754901/what-is-a-fat-pointer + // 64bitプラットフォームで実行することを前提としている。仮に32bitプラットフォームで実行すると8byteになるはず。 + // pointerのサイズが64bitの場合にのみテストを実行するように構成できる。面白い。 + // https://doc.rust-lang.org/reference/conditional-compilation.html#r-cfg.target_pointer_width + assert_eq!(16, std::mem::size_of::<&[i32]>()); + } + + #[test] + fn step1b_test() { + assert_eq!(Solution::max_profit(vec![7, 1, 5, 3, 6, 4]), 5); + assert_eq!(Solution::max_profit(vec![7, 2, 5, 3, 6, 1]), 4); + assert_eq!(Solution::max_profit(vec![1, 2, 3, 4]), 3); + assert_eq!(Solution::max_profit(vec![4, 3, 2, 1]), 0); + + assert_eq!(Solution::max_profit(vec![2, 1]), 0); + assert_eq!(Solution::max_profit(vec![1, 2]), 1); + assert_eq!(Solution::max_profit(vec![1]), 0); + assert_eq!(Solution::max_profit(vec![]), 0); // 株を購入できないので利益は0で意味が通ると思う + } +} diff --git a/src/bin/step2.rs b/src/bin/step2.rs index e92520d..2d6b6a1 100644 --- a/src/bin/step2.rs +++ b/src/bin/step2.rs @@ -12,26 +12,58 @@ // 改善する時に考えたこと /* - 講師陣はどのようなコメントを残すだろうか? - - - 他の人のコードを読んで考えたこと - - + - 動的計画法の定義について。あまり深く考えたことはなかったが個人的にはそこまで重要では無いかなという感想。 + https://github.com/olsen-blue/Arai60/pull/37/files#r2030022958 + + - 自分も初期値でprices[0]を使うなら&prices[1..]からループを回した方が違和感を感じないと思った。 + ループ回数が減ってパフォーマンスが〜ということではなくて、直前でprices[0]を使っているのでこれを飛ばす方が自然だという感覚。 + https://github.com/docto-rin/leetcode/pull/42#discussion_r2471529109 - 他の想定ユースケース - - + - min_priceの更新を常に行うという方針。ifの条件を間違える可能性を排除できるので常にminでやるのもありだと思った。 + https://github.com/docto-rin/leetcode/pull/42#discussion_r2471449749 + + - 最初に思いつく動的計画法の解法として、一次元DPの方が自然な気がするので練習のために実装する。step2a.rs + https://github.com/olsen-blue/Arai60/pull/37/files#diff-0474f0ee7711182f0e97bb4047531dc4c65356748eafab139512400ac88c5c0bR10 改善する時に考えたこと - - + - prices.into_iter().skip(1)の部分はfor loopでは&prices[1..]の方が自然かも + prices.into_iter().skip(1).map(|x| ...) のようにするならメソッドチェーンで書けるのでskip()で良いかなという感覚。 + - min_priceの更新ifで判定せずに常に最小を更新していくほうがシンプルになる */ pub struct Solution {} -impl Solution {} +impl Solution { + pub fn max_profit(prices: Vec) -> i32 { + let mut min_price = match prices.len() { + 0 => return 0, + _ => prices[0], + }; + let mut max_price = 0; + + for price in &prices[1..] { + min_price = min_price.min(*price); + max_price = max_price.max(*price - min_price); + } + + max_price + } +} #[cfg(test)] mod tests { use super::*; #[test] - fn step2_test() {} + fn step2_test() { + assert_eq!(Solution::max_profit(vec![7, 1, 5, 3, 6, 4]), 5); + assert_eq!(Solution::max_profit(vec![7, 2, 5, 3, 6, 1]), 4); + assert_eq!(Solution::max_profit(vec![1, 2, 3, 4]), 3); + assert_eq!(Solution::max_profit(vec![4, 3, 2, 1]), 0); + + assert_eq!(Solution::max_profit(vec![2, 1]), 0); + assert_eq!(Solution::max_profit(vec![1, 2]), 1); + assert_eq!(Solution::max_profit(vec![1]), 0); + assert_eq!(Solution::max_profit(vec![]), 0); // 株を購入できないので利益は0で意味が通ると思う + } } diff --git a/src/bin/step2a.rs b/src/bin/step2a.rs new file mode 100644 index 0000000..766f314 --- /dev/null +++ b/src/bin/step2a.rs @@ -0,0 +1,61 @@ +// Step2a +// 目的: 一次元DPによる実装方法を練習しておく。 + +// 方法 +// Step1のコードを読みやすくしてみる +// 他の人のコードを2つは読んでみること +// 正解したら終わり + +// 以下をメモに残すこと +// 講師陣はどのようなコメントを残すだろうか? +// 他の人のコードを読んで考えたこと +// 改善する時に考えたこと + +/* + 解法の考え方 + - pricesを先頭から全走査しつつ、最小の価格を更新する + - 今見ている株価prices[i]と最小の価格差を計算してmax_profits配列に入れていく + - 最後にmax_profits配列の中から最大の値を返す + + n = prices.len() + 時間計算量: O(n) + 空間計算量: O(n) + + max_profitsを配列で管理せず、変数で管理すると空間計算量がO(1)となる。 +*/ + +pub struct Solution {} +impl Solution { + pub fn max_profit(prices: Vec) -> i32 { + let mut min_price = match prices.len() { + 0 => return 0, + _ => prices[0], + }; + let mut max_profits = vec![0]; + + for price in &prices[1..] { + min_price = min_price.min(*price); + max_profits.push(*price - min_price); + } + + max_profits.into_iter().max().unwrap_or(0) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn step2a_test() { + assert_eq!(Solution::max_profit(vec![7, 1, 5, 3, 6, 4]), 5); + assert_eq!(Solution::max_profit(vec![7, 2, 5, 3, 6, 1]), 4); + assert_eq!(Solution::max_profit(vec![1, 2, 3, 4]), 3); + assert_eq!(Solution::max_profit(vec![4, 3, 2, 1]), 0); + + assert_eq!(Solution::max_profit(vec![2, 1]), 0); + assert_eq!(Solution::max_profit(vec![1, 2]), 1); + assert_eq!(Solution::max_profit(vec![1]), 0); + assert_eq!(Solution::max_profit(vec![]), 0); // 株を購入できないので利益は0で意味が通ると思う + } +} diff --git a/src/bin/step3.rs b/src/bin/step3.rs index 0af0a4a..1dd3ef5 100644 --- a/src/bin/step3.rs +++ b/src/bin/step3.rs @@ -9,23 +9,49 @@ // 作れないデータ構造があった場合は別途自作すること /* - 時間計算量: - 空間計算量: + n = prices.len() + 時間計算量: O(n) + 空間計算量: O(1) */ /* - 1回目: 分秒 - 2回目: 分秒 - 3回目: 分秒 + 1回目: 1分24秒 + 2回目: 1分38秒 + 3回目: 1分14秒 */ pub struct Solution {} -impl Solution {} +impl Solution { + pub fn max_profit(prices: Vec) -> i32 { + let mut min_price = match prices.len() { + 0 => return 0, + _ => prices[0], + }; + let mut max_price = 0; + + for price in &prices[1..] { + min_price = min_price.min(*price); + max_price = max_price.max(*price - min_price); + } + + max_price + } +} #[cfg(test)] mod tests { use super::*; #[test] - fn step3_test() {} + fn step3_test() { + assert_eq!(Solution::max_profit(vec![7, 1, 5, 3, 6, 4]), 5); + assert_eq!(Solution::max_profit(vec![7, 2, 5, 3, 6, 1]), 4); + assert_eq!(Solution::max_profit(vec![1, 2, 3, 4]), 3); + assert_eq!(Solution::max_profit(vec![4, 3, 2, 1]), 0); + + assert_eq!(Solution::max_profit(vec![2, 1]), 0); + assert_eq!(Solution::max_profit(vec![1, 2]), 1); + assert_eq!(Solution::max_profit(vec![1]), 0); + assert_eq!(Solution::max_profit(vec![]), 0); // 株を購入できないので利益は0で意味が通ると思う + } }