Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 65 additions & 7 deletions src/bin/step1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,84 @@
// 正解したら終わり

/*
問題の理解
- 整数配列pricesが与えられる。prices[i]は株価を表している。株の売買を行って得られる最大利益を解として返す。
利益が発生しなければ0を返す。売買するかを決められるので損失がでるような売買は行わない。
制約
- 一度に保有できる株式は最大1株のみ
- 同じ日に複数回の売買は可能

何がわからなかったか
-
- 特になし

何を考えて解いていたか
-
- prices=[7, 1, 5, 3, 6, 4]の時
- day=1の時買い、day=2の時売る。 profit=5-1=4
- day=3の時買い、day=4の時売る。 profit=6-3=3
max_profit=4+3=7
- prices=[1, 2, 3, 4, 5]の時
- day=0の時買い、day=4の時売る。 profit=5-1=4
max_profit=4
- prices=[7, 6, 4, 3, 2, 1]の時
- どの時点で買っても利益が発生しないので、max_profit=0

- prices=[1,2,3,4,5]のときはいきなり5-1で答えを求めなくても、5-4-3-2-1と答えを求めていけば大丈夫そう。
今持っている株prices[i]よりも安い株を見つけたら、
- 売買候補株を更新する 売買はスキップ
今持っている株よりも高い株を見つけたら
- 利益を計算して通算利益を更新
- 今持っている株を購入した株(prices[i + 1])で更新する。

想定ユースケース
-
Leet Code採点システムは一発でAcceptedになった。
しかし、手元のテストケースを通らないコード(比較の方向を間違えた)を一度書いてしまったので、ホワイトボードでのコーディングだとアウトだなと思った。

正解してから気づいたこと
-
- 売買候補株を保持しておく変数名がもう少し適切なものがありそう。min_priceはpricesの中の最小を常に指すわけではないので少し忌避感がある。
- 前回の問題のレビューで提案のあったsplit_firstによる分割を行うように改善する。
- 再帰による実装も練習のためにしておくと良さそう。前回の問題(121. Best Time to Buy and Sell Stock)では入力のサイズが大きく、スタック深さがそのまま入力のサイズとなるので忌避感があった。
実装前に制約上の入力サイズでスタックサイズ上限が7MBと仮定した場合に1スタックフレームあたりに詰めるサイズを見積もってみる。
入力の制約 prices.length <= 3 * (10 ^ 4) = 30_000
スタックサイズ上限7MBとすると、7 * 1024 * 1024 / 30_000 = 約244Byte となり、1スタックフレームあたり244Byteまでならスタックオーバーフローとならない。
余裕そうなのでstep1a.rsで再帰による実装を練習する。
*/

pub struct Solution {}
impl Solution {}
impl Solution {
pub fn max_profit(prices: Vec<i32>) -> i32 {
let Some(mut candidate_stock) = prices.get(0) else {
return 0;
};
let mut max_profit = 0;

for price in &prices[1..] {
if price < candidate_stock {
candidate_stock = price;
continue;
}

max_profit += price - candidate_stock;
candidate_stock = price;
}

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]), 7);
assert_eq!(Solution::max_profit(vec![1, 2, 3, 4, 5]), 4);
assert_eq!(Solution::max_profit(vec![7, 6, 4, 3, 1]), 0);

assert_eq!(Solution::max_profit(vec![2, 2, 4, 8]), 6);
assert_eq!(Solution::max_profit(vec![2, 2, 4, 1]), 2);

assert_eq!(Solution::max_profit(vec![1, 5]), 4);
assert_eq!(Solution::max_profit(vec![1]), 0);
assert_eq!(Solution::max_profit(vec![]), 0);
}
}
83 changes: 83 additions & 0 deletions src/bin/step1a.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Step1a
// 目的: 練習のために再帰による実装を行う

/*
問題の理解
- 整数配列pricesが与えられる。prices[i]は株価を表している。株の売買を行って得られる最大利益を解として返す。
利益が発生しなければ0を返す。売買するかを決められるので損失がでるような売買は行わない。
制約
- 一度に保有できる株式は最大1株のみ
- 同じ日に複数回の売買は可能

何がわからなかったか
- 特になし

何を考えて解いていたか
- prices=[7, 1, 5, 3, 6, 4]の時
- day=1の時買い、day=2の時売る。 profit=5-1=4
- day=3の時買い、day=4の時売る。 profit=6-3=3
max_profit=4+3=7
- prices=[1, 2, 3, 4, 5]の時
- day=0の時買い、day=4の時売る。 profit=5-1=4
max_profit=4
- prices=[7, 6, 4, 3, 2, 1]の時
- どの時点で買っても利益が発生しないので、max_profit=0

再帰関数の設計
- 基本ケース
- 売買対象がなければ0を返す。
- 再帰ケース
- 持っている株よりも売買候補の株が安ければ、所持する株を安い株に更新して日付を一日進めて再帰に入る。
- 持っている株を売って利益を計算する。次の日付の売買を行うために再帰に入る。
- 計算した利益を全て合算して返す。

step1で見積もった空間計算量(スタックオーバーフローしないか)
入力の制約 prices.length <= 3 * (10 ^ 4) = 30_000
スタックサイズ上限7MBとすると、7 * 1024 * 1024 / 30_000 = 約244Byte となり、1スタックフレームあたり244Byteまでならスタックオーバーフローとならない。
余裕そうなのでstep1a.rsで再帰による実装を練習する。
*/

pub struct Solution {}
impl Solution {
pub fn max_profit(prices: Vec<i32>) -> i32 {
let Some((initial_holding_price, remaining_prices)) = prices.split_first() else {
return 0;
};

Self::make_max_profit(remaining_prices, 0, *initial_holding_price)
}

fn make_max_profit(prices: &[i32], day: usize, holding_price: i32) -> i32 {
let Some(price) = prices.get(day) else {
return 0;
};

if *price < holding_price {
return Self::make_max_profit(prices, day + 1, *price);
}

let current_profit = *price - holding_price;
let future_profits = Self::make_max_profit(prices, day + 1, *price);

current_profit + future_profits
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn step1a_test() {
assert_eq!(Solution::max_profit(vec![7, 1, 5, 3, 6, 4]), 7);
assert_eq!(Solution::max_profit(vec![1, 2, 3, 4, 5]), 4);
assert_eq!(Solution::max_profit(vec![7, 6, 4, 3, 1]), 0);

assert_eq!(Solution::max_profit(vec![2, 2, 4, 8]), 6);
assert_eq!(Solution::max_profit(vec![2, 2, 4, 1]), 2);

assert_eq!(Solution::max_profit(vec![1, 5]), 4);
assert_eq!(Solution::max_profit(vec![1]), 0);
assert_eq!(Solution::max_profit(vec![]), 0);
}
}
48 changes: 38 additions & 10 deletions src/bin/step2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,54 @@
// 改善する時に考えたこと

/*
講師陣はどのようなコメントを残すだろうか?
-

他の人のコードを読んで考えたこと
-

他の想定ユースケース
-
- 今回は特に思うところはなかった。

改善する時に考えたこと
-
- max_profitよりも通算利益っぽいtotal_profitの方が適切だと感じた
- 前回の問題のレビューで提案を受けたsplit_firstによる分割で書いてみた。
https://github.com/t9a-dev/LeetCode_arai60/pull/37#discussion_r2575562337
remaining(残りの)という語彙も増えたし、すっきり書けていて良いと思う。
- price - holding_priceはprofit変数に入れた方が丁寧な気がしたが、一度しか使わないのとtotal_profitに加算していることから過剰かなと思って止めた。
*/

pub struct Solution {}
impl Solution {}
impl Solution {
pub fn max_profit(prices: Vec<i32>) -> i32 {
let Some((mut holding_price, remaining_prices)) = prices.split_first() else {
return 0;
};
Comment on lines +29 to +31

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

自分がこの書き方するならあえて下のように分けたいですね。

Suggested change
let Some((mut holding_price, remaining_prices)) = prices.split_first() else {
return 0;
};
let Some((first_price, remaining_prices)) = prices.split_first() else {
return 0;
};
let mut holding_price = *first_price;

let mut total_profit = 0;

for price in remaining_prices {
if price < holding_price {
holding_price = price;
continue;
}

total_profit += price - holding_price;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

また、holding_priceという命名にも違和感があります。
holdする(つまり現金から株にする)かどうかは未来のpriceに依存して初めて決まるので、単にprevious_priceの方が自然だと思います。

holding_price = price;
Comment on lines +35 to +41

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

holding_priceの更新はif節の成立によらず行うのでわざわざ別々に書かれていることに違和感を持ちました。

Suggested change
if price < holding_price {
holding_price = price;
continue;
}
total_profit += price - holding_price;
holding_price = price;
if price < holding_price {
total_profit += price - holding_price;
}
holding_price = price;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

あと、ここのif分岐をなくして常にprice - holding_priceと0のうち大きい方をtotal_profitに加えると書く方法もあると思います。

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

holding_priceの更新はif節の成立によらず行うのでわざわざ別々に書かれていることに違和感を持ちました。

重複しているコードなので違和感を感じるのがソフトウェアエンジニアとしての普通の反応だと思いました。
提案頂いたコードはifの条件式を反転して動作することを確認しました。

            if holding_price < price {
                total_profit += price - holding_price;
            }

            holding_price = price;

あと、ここのif分岐をなくして常にprice - holding_priceと0のうち大きい方をtotal_profitに加えると書く方法もあると思います。

なるほど、こんな感じになると理解しました。
ありがとうございました。

        let min_profit = 0;

        for price in remaining_prices {
            total_profit += min_profit.max(price - holding_price);
            holding_price = price;
        }

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

すみません、不等号の向き逆でした.

ご提案で違和感ないです。(0はハードコードしちゃってもいいかもですが、そちらだとより意図が明確で良いですね。)

}

total_profit
}
}

#[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]), 7);
assert_eq!(Solution::max_profit(vec![1, 2, 3, 4, 5]), 4);
assert_eq!(Solution::max_profit(vec![7, 6, 4, 3, 1]), 0);

assert_eq!(Solution::max_profit(vec![2, 2, 4, 8]), 6);
assert_eq!(Solution::max_profit(vec![2, 2, 4, 1]), 2);

assert_eq!(Solution::max_profit(vec![1, 5]), 4);
assert_eq!(Solution::max_profit(vec![1]), 0);
assert_eq!(Solution::max_profit(vec![]), 0);
}
}
45 changes: 38 additions & 7 deletions src/bin/step3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,54 @@
// 作れないデータ構造があった場合は別途自作すること

/*
時間計算量:
空間計算量:
n = prices.len()
時間計算量: O(n)
空間計算量: O(1)
*/

/*
1回目: 分秒
2回目: 分秒
3回目: 分秒
1回目: 1分42秒
2回目: 1分46秒
3回目: 1分40秒
*/

pub struct Solution {}
impl Solution {}
impl Solution {
pub fn max_profit(prices: Vec<i32>) -> i32 {
let Some((mut holding_price, remaining_prices)) = prices.split_first() else {
return 0;
};
let mut total_profit = 0;

for price in remaining_prices {
if price < holding_price {
holding_price = price;
continue;
}

total_profit += price - holding_price;
holding_price = price;
}

total_profit
}
}

#[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]), 7);
assert_eq!(Solution::max_profit(vec![1, 2, 3, 4, 5]), 4);
assert_eq!(Solution::max_profit(vec![7, 6, 4, 3, 1]), 0);

assert_eq!(Solution::max_profit(vec![2, 2, 4, 8]), 6);
assert_eq!(Solution::max_profit(vec![2, 2, 4, 1]), 2);

assert_eq!(Solution::max_profit(vec![1, 5]), 4);
assert_eq!(Solution::max_profit(vec![1]), 0);
assert_eq!(Solution::max_profit(vec![]), 0);
}
}