Skip to content

Conversation

@Kaichi-Irie
Copy link
Owner

問題へのリンク

50. Pow(x, n)

言語

Python

問題の概要

浮動小数点数 x と整数 n が与えられたとき、xn 乗を計算する。

自分の解法

step1

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

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)に改善すべく、反復処理による実装に変更した。

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)を確認する。
  • ビットが1であれば、その位置に対応するxのべき乗(x, x^2, x^4, x^8, ...)を結果に乗算していく。
  • xのべき乗は、ループごとに自身を2乗することで効率的に計算される。
  • 時間計算量:O(log(n))。ループ回数がnのビット数に比例するためである。
  • 空間計算量:O(1)。変数の使用量がnの大きさに依存しないためである。

step3

反復処理による実装

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

再帰関数による実装

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)

別解・模範解答

次に解く問題の予告

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を排除できますね。ただ可読性が下がるような印象です。

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.

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

half_n = n // 2
remainder = n % 2

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 の場合は追加で掛けない、というのが繰り返し二乗法のポイントで、この関数の仕事として場合分けした方が見やすいと思っています。

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.

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants