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
146 changes: 146 additions & 0 deletions 50_pow_x_n_medium/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# 問題へのリンク
[50. Pow(x, n)](https://leetcode.com/problems/powx-n/)

# 言語
Python

# 問題の概要
浮動小数点数 `x` と整数 `n` が与えられたとき、`x` の `n` 乗を計算する。

# 自分の解法

## step1
まず、繰り返し二乗法を再帰関数を用いて実装した。

```python
class Solution:
def myPow(self, x: float, n: int) -> float:
"""
Compute x to the power of n based on these four facts;
- x^(-n) = (1/x)^n (n>0)
- x^0 = 1
- x^1 = x
- if n = 2*q + r, then x^(n) = (x^q)^2 * (x)^r

"""
if n < 0:
return self.myPow(1 / x, -n)
if n == 0:
return 1.0
if n == 1:
return x
quotient, remainder = n // 2, n % 2

return self.myPow(x, quotient) ** 2 * self.myPow(x, remainder)
```

- `n`が偶数の場合は `x^n = (x^(n/2))^2`、奇数の場合は `x^n = x * (x^((n-1)/2))^2` となる性質を利用した再帰的な解法である。
- `n`を2で割っていくため、再帰の深さは`log(n)`となる。
- 時間計算量:`O(log(n))`。各`n`に対する計算は一度しか行われないためである。
- `remainder`は常に0または1であり、この計算はたかだか定数時間であるため、全体の計算量に大きな影響はない。(メモ化は不要)
- (個人的には)`n%2`が1か0かで場合分けするよりも、`remainder`をそのまま`myPow`に渡す方がわかりやすいと感じた。数学的に必ず`n=2*q+r`なら`x^n=(x^q)^2 * (x)^r`となるため
- 空間計算量:`O(log(n))`。再帰呼び出しのスタックに`log(n)`の空間が必要である。

### 発見
- べき乗演算子`**`の挙動について、`func(x) ** 2` のような式は `func(x)` を1回だけ評価することを確認した。

## step2
step1の再帰実装では空間計算量が`O(log(n))`であったため、これを`O(1)`に改善すべく、反復処理による実装に変更した。

```python
class Solution:
def myPow(self, x: float, n: int) -> float:
"""
Compute x to the power of n using doubling.
Example: x=2.0, n = 10
n = 2^3 + 2^1
x^n = x^(2^3) * x^(2^1)

Each x^(2^k) can be computed recursively;
x^(2^(k+1)) = x^(2*2^k) = x^(2^k) * x^(2^k)
"""
if n == 0:
return 1.0
if x == 0.0:
return 0.0
if n < 0:
# x^(-n) = (1/x)^n = 1/(x^n) (n>0)
return 1 / self.myPow(x, -n)
x_to_power_of_two = x # x^(2^0)
x_to_n = 1.0 # x^n
while n > 0:
if n % 2 == 1:
x_to_n *= x_to_power_of_two

# x^(2^(k+1)) = x^(2*2^k) = x^(2^k) * x^(2^k)
x_to_power_of_two = x_to_power_of_two * x_to_power_of_two
n //= 2
return x_to_n
```

- このアプローチは、`n`を2進数として捉えるものである。例えば `n=10` (2進数で`1010`) の場合、`x^10 = x^8 * x^2` となる。
- ループ内で`n`を右にシフト(`n //= 2`)しながら、最下位ビットが1かどうか(`n % 2 == 1`)を確認する。
- 「2で割る」操作を「右にビットシフトする」操作に、「2で割った余りを調べる」操作を「最下位ビットを調べる」操作にそれぞれ置き換えて考えることで、`n`の各ビットを順に処理できることが納得できる。
- ビットが1であれば、その位置に対応する`x`のべき乗(`x`, `x^2`, `x^4`, `x^8`, ...)を結果に乗算していく。
- `x`のべき乗は、ループごとに自身を2乗することで効率的に計算される。
- 時間計算量:`O(log(n))`。ループ回数が`n`のビット数に比例するためである。
- 空間計算量:`O(1)`。変数の使用量が`n`の大きさに依存しないためである。

## step3

### 反復処理による実装
```python
class Solution:
def myPow(self, x: float, n: int) -> float:
if n == 0:
return 1
if n < 0:
return self.myPow(1 / x, -n)
power_of_x = x
i = 0
two_to_i = 1
x_to_n = 1.0
while two_to_i <= n:
if n >> i & 1:
x_to_n *= power_of_x
i += 1
two_to_i *= 2
power_of_x = power_of_x * power_of_x
return x_to_n
```

### 再帰関数による実装
```python
class Solution:
def myPow(self, x: float, n: int) -> float:
if n == 0:
return 1.0
if x == 0.0:
return 0.0
if n == 1:
return x
if n < 0:
n = -n
x = 1 / x
return self.myPow(x, n)

half_n = n // 2
remainder = n % 2

return self.myPow(x, half_n) ** 2 * self.myPow(x, remainder)
```

## step4 (FB)
```python

```

# 別解・模範解答
```python

```

# 次に解く問題の予告
- [Coin Change - LeetCode](https://leetcode.com/problems/coin-change/)
- [Binary Tree Level Order Traversal - LeetCode](https://leetcode.com/problems/binary-tree-level-order-traversal/)
- [Unique Paths - LeetCode](https://leetcode.com/problems/unique-paths/description/)
31 changes: 31 additions & 0 deletions 50_pow_x_n_medium/step1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#
# @lc app=leetcode id=50 lang=python3
#
# [50] Pow(x, n)
#

# @lc code=start


class Solution:
def myPow(self, x: float, n: int) -> float:
"""
Compute x to the power of n based on these four facts;
- x^(-n) = (1/x)^n (n>0)
- x^0 = 1
- x^1 = x
- if n = 2*q + r, then x^(n) = (x^q)^2 * (x)^r

"""
if n < 0:
return self.myPow(1 / x, -n)
if n == 0:
return 1.0
if n == 1:
return x
quotient, remainder = n // 2, n % 2

return self.myPow(x, quotient) ** 2 * self.myPow(x, remainder)


# @lc code=end
46 changes: 46 additions & 0 deletions 50_pow_x_n_medium/step1_iterative.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#
# @lc app=leetcode id=50 lang=python3
#
# [50] Pow(x, n)
#

# @lc code=start


class Solution:
def myPow(self, x: float, n: int) -> float:
"""
Compute x to the power of n by using iterative method.
Example: x=2.0 and n = 10
n = 2^3 + 2^1, so
x^n = x^(2^3) * x^(2^1)
x^1, ..., x^(2^k) can be computed in O(k) by using this formula; x^(2^(k+1)) = x^(2*2^k) = x^(2^k) * x^(2^k)
Therefore, x^n can be also computed in O(log(n))

Time Complexity: O(log(n))
Space Complexity: O(1)
"""
if x == 0.0:
return 0.0
if n == 0:
return 1.0
if n < 0:
return 1 / self.myPow(x, -n)
if n == 1:
return x

x_to_n = 1.0 # x^n
bit_position = 0 # k
power_of_two = 1 # 2^k
x_to_power_of_two = x # x^(2^k)
while power_of_two <= n:
if (n >> bit_position) & 1:
x_to_n *= x_to_power_of_two
bit_position += 1
x_to_power_of_two = x_to_power_of_two * x_to_power_of_two
power_of_two = 2 * power_of_two

return x_to_n


# @lc code=end
38 changes: 38 additions & 0 deletions 50_pow_x_n_medium/step2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#
# @lc app=leetcode id=50 lang=python3
#
# [50] Pow(x, n)
#

# @lc code=start
class Solution:
def myPow(self, x: float, n: int) -> float:
"""
Compute x to the power of n using doubling.
Example: x=2.0, n = 10
n = 2^3 + 2^1
x^n = x^(2^3) * x^(2^1)

Each x^(2^k) can be computed recursively;
x^(2^(k+1)) = x^(2*2^k) = x^(2^k) * x^(2^k)
"""
if n == 0:
return 1.0
if x == 0.0:
return 0.0
if n < 0:
# x^(-n) = (1/x)^n = 1/(x^n) (n>0)
return 1 / self.myPow(x, -n)
x_to_power_of_two = x # x^(2^0)
x_to_n = 1.0 # x^n
while n > 0:
if n % 2 == 1:
x_to_n *= x_to_power_of_two

# x^(2^(k+1)) = x^(2*2^k) = x^(2^k) * x^(2^k)
x_to_power_of_two = x_to_power_of_two * x_to_power_of_two
n //= 2
return x_to_n


# @lc code=end
27 changes: 27 additions & 0 deletions 50_pow_x_n_medium/step3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#
# @lc app=leetcode id=50 lang=python3
#
# [50] Pow(x, n)
#

# @lc code=start
class Solution:
def myPow(self, x: float, n: int) -> float:
if n == 0:
return 1.0
if x == 0.0:
return 0.0
if n == 1:
return x
if n < 0:
n = -n
x = 1 / x
return self.myPow(x, n)

half_n = n // 2
remainder = n % 2

Choose a reason for hiding this comment

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

divmod を使うこともできますね。
https://docs.python.org/3.13/library/functions.html#divmod

Copy link
Owner Author

Choose a reason for hiding this comment

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

コメントありがとうございます。
「こんなのもあるよ」と教えてくださったのだとは思いますが、一応補足しますと、自分の以前のPR( #10 (comment) )のレビューにて、divmodが読みにくい、見慣れないというコメントがあったので、自分は//%とで書くように心がけています。

Choose a reason for hiding this comment

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

はい、どちらでも良いと思います!


return self.myPow(x, half_n) ** 2 * self.myPow(x, remainder)

Choose a reason for hiding this comment

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

remainder で場合分けした方が素直かなと個人的には思います。

Copy link
Owner Author

Choose a reason for hiding this comment

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

レビューありがとうございます。READMEで

(個人的には)n%2が1か0かで場合分けするよりも、remainderをそのままmyPowに渡す方がわかりやすいと感じた。数学的に必ずn=2*q+rならx^n=(x^q)^2 * (x)^rとなるため
と書いているように私はremainderを渡す方が直感的だと感じたのですが、場合分けの方が素直という意見もあるということですね。参考になります 🙏

Choose a reason for hiding this comment

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

数式上はそうですが、結局 r は 0, 1 のみで、1 の場合は追加で x を掛ける、0 の場合は追加で掛けない、というのが繰り返し二乗法のポイントで、この関数の仕事として場合分けした方が見やすいと思っています。



# @lc code=end
27 changes: 27 additions & 0 deletions 50_pow_x_n_medium/step3_iterative.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#
# @lc app=leetcode id=50 lang=python3
#
# [50] Pow(x, n)
#

# @lc code=start
class Solution:
def myPow(self, x: float, n: int) -> float:
if n == 0:
return 1
if n < 0:
return self.myPow(1 / x, -n)
power_of_x = x
i = 0

Choose a reason for hiding this comment

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

i を使わなくても書けるでしょうか?two_to_i が同様の情報を持っていそうです。

Copy link
Owner Author

Choose a reason for hiding this comment

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

確かに

if n >> i & 1:
    ...

の部分を

if n & two_to_i:
    ...

としても動くので、iを排除できますね。ただ可読性が下がるような印象です。

two_to_i = 1
x_to_n = 1.0
while two_to_i <= n:
if n >> i & 1:
x_to_n *= power_of_x
i += 1
two_to_i *= 2
power_of_x = power_of_x * power_of_x
return x_to_n

Choose a reason for hiding this comment

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

個人的に x_to_n が x^n を意味するように読めませんでした。英語的には x to the n だと思いますが、省略しても伝わるかわかりませんでした。
他の方のPRを読んでみてどのような変数名が使われているか見てみるのも良いと思います。

Copy link
Owner Author

Choose a reason for hiding this comment

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

レビューありがとうございます。

power, poweredあたりが多いような印象でした。2のべき乗とxのべき乗をどちらも使うコードなので、これらの変数は余り適切でないように感じました。一方でより適切な変数は思いつきませんでした...

Choose a reason for hiding this comment

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

2のべき乗は、n を二進数で表した時にある桁が 1 であるかを見ているので bit とすることもありだと思いました。
リンク先コードが正解というわけではありませんが、参考として共有いたします。
TORUS0818/leetcode#47 (comment)

Copy link
Owner Author

Choose a reason for hiding this comment

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

確かにビットシフトして計算しているのが伝わりやすい気がしました。ありがとうございます。



# @lc code=end