Skip to content

Conversation

@skypenguins
Copy link
Owner

22. Generate Parentheses

次回予告: 31. Next Permutation

@skypenguins skypenguins self-assigned this Nov 16, 2025
result.append(parens)
return
if open_count < n:
gen_parens_helper(parens + "(", open_count + 1, close_count)

Choose a reason for hiding this comment

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

parents + "("で逐次文字列の再構築が走るので時間計算量で不利になると思います。

forループを書き下すと '' + 's[0]' + 's[1]' + ... + 's[-1]' のようなイミュータブルなシーケンスの結合になっており、パフォーマンスの観点から避けるべきだと思います。

イミュータブルなシーケンスの結合は、常に新しいオブジェクトを返します。これは、結合の繰り返しでシーケンスを構築する実行時間コストがシーケンスの長さの合計の二次式になることを意味します。実行時間コストを線形にするには、代わりに以下のいずれかにしてください:

  • str オブジェクトを結合するには、リストを構築して最後に str.join() を使うか、 io.StringIO インスタンスに書き込んで完成してから値を取得してください
    ...

https://docs.python.org/ja/3.13/library/stdtypes.html#string-methods

https://discord.com/channels/1084280443945353267/1248276309738651669/1429794088366112909

Choose a reason for hiding this comment

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

なので例えば以下のようになります。(以下はloop + stackになってますが、再帰関数でも同じです。)

class Solution:
    def generateParenthesis(self, n: int) -> list[str]:
        results = []
        stack = [([], n, n)]
        while stack:
            path, open_remain, close_remain = stack.pop()
            
            if open_remain == 0 and close_remain == 0:
                results.append("".join(path))
                continue
            if open_remain > 0:
                stack.append((path + ["("], open_remain - 1, close_remain))
            if close_remain > open_remain:
                stack.append((path + [")"], open_remain, close_remain - 1))

        return results

Copy link

Choose a reason for hiding this comment

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

あ、話として3つくらいあります。

  1. たしかに文字列は再構築されるんですが、path + ["("] も再構築されています。なので、やるならば、[path, "("] という風にペアにして渡すといいでしょう。ペアはオブジェクトへのポインタの組なので全体の再構築がされません。

  2. 計算量が不利になるか怪しいと思います。
    証明までの水準では考えられていません。カタラン数を生成する木の中間ノードの各層の数なんですが、ballot numbers などといわれるものの和で表せます。厳密な計算は大変そうですが、最後の層から少し戻ると幾何級数的に減っていきます。最終層から a 層戻ったときの減り方の極限は (a/2+1)^2 / 2^a (奇数の場合は、(a^2-1) / 4 / 2^a) なので、最後の数層のコピー以外はほとんど効かないんですね。
    どちらにしても最後に全文字列を生成しているので、悪くなったとしてもせいぜい数倍でしょう。

  3. 文字列のコピーは速いです。100倍くらい。

実際に計測してみましょう。

Copy link

@docto-rin docto-rin Nov 17, 2025

Choose a reason for hiding this comment

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

ありがとうございます。

  1. おっしゃる通りです、自分の方法も結局再構築してますね。
  2. 考えるのが難しく、諦めていました。なるほど、問題文も確かに 1<=n<=8 とのことですし、そこまで実時間で差が出るわけでもなさそうですね。
  3. なるほどですね。

ベンチマーク3通りの方法で試してみました。

n str+str list+list pair
1 0.003ms 0.001ms 0.002ms
2 0.002ms 0.002ms 0.003ms
3 0.003ms 0.005ms 0.008ms
4 0.010ms 0.014ms 0.020ms
5 0.036ms 0.054ms 0.066ms
6 0.098ms 0.141ms 0.241ms
7 0.350ms 0.466ms 0.777ms
8 1.170ms 1.594ms 2.679ms
9 3.776ms 5.598ms 9.969ms
10 13.568ms 21.905ms 35.230ms

のようで、str + strが素朴ながら最速でした。

ベンチマーク実装:
https://share.solve.it.com/d/114a4176d6e52cfcdd18923ac655d598

Copy link

Choose a reason for hiding this comment

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

あ、はい。なんとなく私の感覚こんなものな気がします。
generate_method2_recursive は、コピーせずに pop することでバックトラックする手がありますね。

Copy link

Choose a reason for hiding this comment

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

上で書いている「文字列のコピーは速い。100倍くらい。」というのは、Python インタープリターが通常100倍くらい遅いという制限がついているのが、文字列のコピーはネイティブなコードで動くということです。また、連続したメモリーブロックのコピーになり、ループアンローリングや SIMD など様々なテクニックが適用可能なので速いです。

Choose a reason for hiding this comment

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

generate_method2_recursive は、コピーせずに pop することでバックトラックする手がありますね。

こちら、バックトラックも試したところ、実行時間がstr + strの方法より0.8~1.18倍で、わずかに速そうでした。

実行時間の比較 (10回実行の平均)
M1=method1(str+str), M2=method2(list+list), M3=method3(pair), BT=backtrack

 n    M1-再帰 M1-stack  M2-再帰 M2-stack  M2-BT    M3-再帰 M3-stack
 1  0.001ms  0.001ms  0.001ms  0.001ms  0.001ms  0.002ms  0.001ms
 2  0.002ms  0.002ms  0.002ms  0.002ms  0.002ms  0.003ms  0.003ms
 3  0.003ms  0.004ms  0.005ms  0.006ms  0.004ms  0.008ms  0.008ms
 4  0.011ms  0.012ms  0.024ms  0.016ms  0.009ms  0.021ms  0.023ms
 5  0.030ms  0.038ms  0.044ms  0.051ms  0.031ms  0.066ms  0.086ms
 6  0.103ms  0.121ms  0.139ms  0.177ms  0.090ms  0.228ms  0.276ms
 7  0.343ms  0.403ms  0.472ms  0.548ms  0.308ms  0.780ms  0.873ms
 8  1.225ms  1.322ms  1.588ms  1.835ms  0.987ms  2.684ms  2.853ms
 9  3.724ms  4.459ms  5.592ms  6.307ms  3.409ms  9.924ms 10.484ms
10 13.304ms 15.498ms 20.156ms 22.385ms 12.327ms 36.442ms 37.757ms

https://share.solve.it.com/d/114a4176d6e52cfcdd18923ac655d598

Copy link
Owner Author

Choose a reason for hiding this comment

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

@oda @docto-rin
レビューありがとうございます。
カタラン数を考えると最後の数層がノード数として支配的というのは新たな学びでした。

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants