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
86 changes: 86 additions & 0 deletions src/bin/step1.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Step1
// 目的: 方法を思いつく

// 方法
// 5分考えてわからなかったら答えをみる
// 答えを見て理解したと思ったら全部消して答えを隠して書く
// 5分筆が止まったらもう一回みて全部消す
// 正解したら終わり

/*
問題の理解
- 浮動小数点xと整数nが与えられるのでx ^ nを計算して返す。

何を考えて解いていたか
- 素直にループで書く方向で考える。
- pow *= xをn回行う。
- nが0未満のときは最後に除算する。(1/pow)

何がわからなかったか
- n==0のとき(xの0乗)をが1になることを忘れていた。
- x=0.00001 n=2147483647 のときにTime Limit Exceededになった。
- テストコードでassert_eq!による比較をすると浮動小数点の桁数全てで完全一致を要求するので、桁数を丸めて比較すべきだった。
数学のテクニック的な感じでナイーブな実装を行うのではなく効率的な計算方法がある気がするがわからないので解答を見る。

解法の理解
LeetCodeの解答例を見てもなぜそうしているのかがよく分からないのでGPT-5.2に聞いた。
TLE: Time Limit Exceeded
- ナイーブな実装だとn回計算を行うことになり時間計算量O(n)となる。入力の制約からnはi32::MIN ~ i32::MAXまで取り得るので最悪21億回のループになりTLEとなる。
- 数学的な性質で指数を半分にして計算できるので時間計算量O(log n)になる。
- 偶数
x ^ 10 = (x^5)^2
- 奇数
x ^ 11 = x * (x^5)^2

正解してから気づいたこと
- TLEすることを予想したうえでナイーブな実装をしたかった。TLEすること自体は理解できるので、余裕を持って周りを見渡しながら実装に取り組めるかという慣れの問題だと思った。
- i32::MIN ~ i32::MAXは (2 ^ -31) ~ (2 ^ 31) - 1 になるので、i32::MINを符号を反転させるとi32::MAXに収まりきらずオーバーフローすることに気づかなかった。
- 32bitのうち符号を表すために1ビット使うので-1しているということは知識としてはあったが扱う機会がないので気づかなかった。
- 指数の数学的な性質を知っているか、知っている場合コーディングで表現できるかという問題だと思った。あまり深堀りすることはなさそうだと思った。
ループでの解法も練習しておく。step1a.rs
*/

pub struct Solution {}
impl Solution {
pub fn my_pow(x: f64, n: i32) -> f64 {
if n == 0 {
return 1.0;
}

// x,nは参照ではないのでmutableにしても呼び出し元に影響を与えない。
let (mut x, mut n) = (x, n as i64);
if n < 0 {
x = 1.0 / x;
n = n.abs();
}

let half = Self::my_pow(x, (n / 2) as i32);
if n % 2 == 0 {
return half * half;
}

return x * half * half;
}
}

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

fn round_5(v: f64) -> f64 {
(v * 1e5).round() / 1e5
}

#[test]
fn step1_test() {
assert_eq!(round_5(Solution::my_pow(2.00000, 10)), 1024.00000);
assert_eq!(round_5(Solution::my_pow(2.10000, 3)), 9.26100);
assert_eq!(round_5(Solution::my_pow(2.00000, -2)), 0.25000);
}

#[test]
fn step1_not_overflow_test() {
assert!(Solution::my_pow(1.00000, i32::MAX).is_sign_positive());
assert!(Solution::my_pow(1.00000, i32::MIN).is_sign_positive());
}
}
58 changes: 58 additions & 0 deletions src/bin/step1a.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Step1a
// 目的: 別の実装方法を練習

/*
所感
- ループにすると直感的でないと感じる。
- 再帰処理の方では half = my_pow(x, n / 2)としているところが式に対応しているように見えるので分かりやすく感じる。
*/

pub struct Solution {}
impl Solution {
pub fn my_pow(x: f64, n: i32) -> f64 {
if n == 0 {
return 1.0;
}

let (mut x, mut n) = (x, n as i64);
if n < 0 {
x = 1.0 / x;
n = n.abs();
}

let mut result = 1.0;
while 0 < n {
// x^4 = (x^2)^2
// x^5 = (x^2)^2 * x
if n % 2 == 1 {
result *= x;
}
x *= x;
n = n / 2;
}

result
}
}

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

fn round_5(v: f64) -> f64 {
(v * 1e5).round() / 1e5
}

#[test]
fn step1a_test() {
assert_eq!(round_5(Solution::my_pow(2.00000, 10)), 1024.00000);
assert_eq!(round_5(Solution::my_pow(2.10000, 3)), 9.26100);
assert_eq!(round_5(Solution::my_pow(2.00000, -2)), 0.25000);
}

#[test]
fn step1a_not_overflow_test() {
assert!(Solution::my_pow(1.00000, i32::MAX).is_sign_positive());
assert!(Solution::my_pow(1.00000, i32::MIN).is_sign_positive());
}
}
114 changes: 114 additions & 0 deletions src/bin/step2.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// Step2
// 目的: 自然な書き方を考えて整理する

// 方法
// Step1のコードを読みやすくしてみる
// 他の人のコードを2つは読んでみること
// 正解したら終わり

// 以下をメモに残すこと
// 講師陣はどのようなコメントを残すだろうか?
// 他の人のコードを読んで考えたこと
// 改善する時に考えたこと

/*
コメント集、他の人のコードを読んで考えたこと
https://github.com/TORUS0818/leetcode/pull/47/changes#r2031685187
- 変数を破壊しない。呼び出し側に影響を与えないようにではなく、可読性をあげるという観点

https://github.com/hroc135/leetcode/pull/43#discussion_r2002294824
> この再帰をループに直すのは、自然に見えて(あまり違いがないように見えて)欲しいです。
- 自分もループに書き換えるのに少し手間取ったし、あまり自然に見えなかったので変数をimmutableにする方針で書いてみてどう感じるかを試す。

https://github.com/TORUS0818/leetcode/pull/47/changes#diff-91647df59bb2863e62120bcb064c143cab9cb1c4e78ed9feab30fc009844b5d0R153
https://github.com/Yoshiki-Iwasa/Arai60/pull/38/changes#diff-b99da78c4652531fb1dfdd24ee4608d9c18613a21307675674e3be8cddc3f746R30
- ビットシフトを利用している。実装の幅はあまり無いと思っていたが意外とあった。ビットシフトを利用した実装も折角なので書いておきたい。

https://github.com/hroc135/leetcode/pull/43#discussion_r2002298814
- IEEE-754の内部ビットの数について。何を言っているのかわからないので調べてみる。
Rustのf64型はIEEE 754-2008で定義されているbinary64を実装していると明記されている。
https://doc.rust-lang.org/std/primitive.f64.html
> A 64-bit floating-point type (specifically, the “binary64” type defined in IEEE 754-2008).
浮動小数点方式の仕様。ビット列として表現する時に、bitをどのように使って表現するかを規定している。
十進法を単精度で表現する例が分かりやすい。
https://ja.wikipedia.org/wiki/IEEE_754#:~:text=%E4%BE%8B,-%5B%E7%B7%A8%E9%9B%86
exponentビットがbinary32単精度のとき8bitとなり、binary64倍精度のとき11bitになる点を覚えておくと、符号は1ビットで表されることから残りのビットであるfractionが求められると理解した。
sign: 1bit
exponent: 8bit(binary32), 11bit(binary64)
fraction: 23bit(binary32), 52bit(binary64)

https://github.com/h1rosaka/arai60/pull/47#discussion_r2658664769
- 問題の制約に違反するような入力値のチェックを行う。

https://github.com/Yoshiki-Iwasa/Arai60/pull/38#issuecomment-2272528081
- RustにおけるPowの実装について。コンパイラ基盤であるLLVMの関数を使っているらしい。
https://ja.wikipedia.org/wiki/LLVM

改善する時に考えたこと
- 変数を変更しながら計算するのではなく、別の変数として扱う。
- 問題の制約ではxが0でないか、n>0であると定義されているので、この条件を満たさないケースをpanicさせる。
- 前提となる仕様を満たさない入力値を処理してしまうよりはpanic!した方が良いだろうという感覚。(実務ではResult型でErrを返すなどする。)

所感
- x *= x, n = n / 2 よりは変数名でどのような性質、変更を加えられたものなのかが分かるので可読性は向上したと感じた。
*/

pub struct Solution {}
impl Solution {
pub fn my_pow(x: f64, n: i32) -> f64 {
if x == 0.0 && n <= 0 {
panic!("x is not zero or n > 0")
}
if n == 0 {
return 1.0;
}
Comment on lines +59 to +64
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

自分が実装するなら、

if n == 0 { return 1.0; }
if x == 0.0 { return 0.0; }
...

とする気がします。
0^0 == 1.0, 0^非ゼロ == 0.0 と定義します。


let mut powered_x = x;
let mut upcasted_n = n as i64;
let mut result = 1.0;

if n < 0 {
powered_x = 1.0 / x;
upcasted_n = upcasted_n.abs();
Comment on lines +71 to +72
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ここをreturn 1.0 / Self::my_pow(x, -n);と書けそうですがn == i32::MINのときにオーバーフローしますね。

my_powのsignatureを維持するのであれば、return 1.0 / (x * Self::my_pow(x, -(n + 1)));と一律nを1だけ0方向に近づけるようにすることで無理やり回避することができますね。

}

while 0 < upcasted_n {
if upcasted_n % 2 == 1 {
result *= powered_x;
}

powered_x *= powered_x;
upcasted_n = upcasted_n / 2;
}

result
}
}

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

fn round_5(v: f64) -> f64 {
(v * 1e5).round() / 1e5
}

#[test]
fn step2_test() {
assert_eq!(round_5(Solution::my_pow(2.00000, 10)), 1024.00000);
assert_eq!(round_5(Solution::my_pow(2.10000, 3)), 9.26100);
assert_eq!(round_5(Solution::my_pow(2.00000, -2)), 0.25000);
}

#[test]
fn step2_not_overflow_test() {
assert!(Solution::my_pow(1.00000, i32::MAX).is_sign_positive());
assert!(Solution::my_pow(1.00000, i32::MIN).is_sign_positive());
}

#[test]
#[should_panic]
fn step2_invalid_params_panic_test() {
assert!(Solution::my_pow(0.0, -1).is_sign_positive());
}
}
94 changes: 94 additions & 0 deletions src/bin/step2a_bit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Step2a
// 目的: 別の実装方法を練習する

/*
https://github.com/Yoshiki-Iwasa/Arai60/pull/38/changes#diff-b99da78c4652531fb1dfdd24ee4608d9c18613a21307675674e3be8cddc3f746R30
- ビット演算の実装を写経しておく。

解法の理解
- 注目すべき点は以下
- if upcasted_n & 1 != 0
- 2進数において最下位ビット(LSB)が1かどうかで偶数、奇数を判定できるという性質を利用している。
- 0101 -> 5(10進)となり奇数なのが分かる。最下位ビットが1
- 0100 -> 4(10進)となり偶数なのが分かる。最下位ビットが0
- 1(2進)とのAND演算により最下位ビットが1かどうかを見ている。
- upcasted_n >>= 1
https://doc.rust-lang.org/std/ops/trait.Shr.html#required-methods
- bit列を右に1つシフトしている。
- 5(10進) -> 0000_0101(2進)
- 右に1つシフトすると、0000_0010(2進)となる。つまり、2(10進)になることが分かる。
所感
- 算術シフトとGPT-5.2と壁打ちしていて知ったが、単純に高速になりそうだから算術シフト(>>)するのは危険だということが分かった。(具体的にはbit_shift_test参照)
- 算術シフトの対象が負数の場合、signビットごとシフトするため。
- 今回の実装では、たまたまnが負数にならないように調整しているが、この点を意識せずに n / 2 を高速化するために n >> 1 と書き換えるような一般化をすると危険。
- 知識として算術シフトにより高速に計算できるという覚え方は良いが、かなり注意して使う必要があると理解した。
- それなりに読み書きされる部分で使うのはかなり危ういと感じた。低いレイヤーに押し込められないコードならあまり使いたくないなという感覚。
*/

pub struct Solution {}
impl Solution {
pub fn my_pow(x: f64, n: i32) -> f64 {
if x == 0.0 && n <= 0 {
panic!("x is not zero or n > 0")
}
if n == 0 {
return 1.0;
}

let mut result = 1.0;
let mut powered_x = x;
let mut upcasted_n = n as i64;

if n < 0 {
powered_x = 1.0 / x;
upcasted_n = upcasted_n.abs();
}

while 0 < upcasted_n {
if upcasted_n & 1 == 1 {
result *= powered_x;
}

powered_x *= powered_x;
upcasted_n >>= 1;
}

result
}
}

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

fn round_5(v: f64) -> f64 {
(v * 1e5).round() / 1e5
}

#[test]
fn bit_shift_test() {
let n = -5;
// 符号付きの値を算術シフトすると符号ビットも移動するので注意。
// nが負数の時、n >> 1 は n / 2 と等しくならない。
assert_ne!(n >> 1, n / 2);
}

#[test]
fn step2a_test() {
assert_eq!(round_5(Solution::my_pow(2.00000, 10)), 1024.00000);
assert_eq!(round_5(Solution::my_pow(2.10000, 3)), 9.26100);
assert_eq!(round_5(Solution::my_pow(2.00000, -2)), 0.25000);
}

#[test]
fn step2a_not_overflow_test() {
assert!(Solution::my_pow(1.00000, i32::MAX).is_sign_positive());
assert!(Solution::my_pow(1.00000, i32::MIN).is_sign_positive());
}

#[test]
#[should_panic]
fn step2a_invalid_params_panic_test() {
assert!(Solution::my_pow(0.0, -1).is_sign_positive());
}
}
Loading