Conversation
| result.append(parens) | ||
| return | ||
| if open_count < n: | ||
| gen_parens_helper(parens + "(", open_count + 1, close_count) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
なので例えば以下のようになります。(以下は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 resultsThere was a problem hiding this comment.
あ、話として3つくらいあります。
-
たしかに文字列は再構築されるんですが、path + ["("] も再構築されています。なので、やるならば、[path, "("] という風にペアにして渡すといいでしょう。ペアはオブジェクトへのポインタの組なので全体の再構築がされません。
-
計算量が不利になるか怪しいと思います。
証明までの水準では考えられていません。カタラン数を生成する木の中間ノードの各層の数なんですが、ballot numbers などといわれるものの和で表せます。厳密な計算は大変そうですが、最後の層から少し戻ると幾何級数的に減っていきます。最終層から a 層戻ったときの減り方の極限は (a/2+1)^2 / 2^a (奇数の場合は、(a^2-1) / 4 / 2^a) なので、最後の数層のコピー以外はほとんど効かないんですね。
どちらにしても最後に全文字列を生成しているので、悪くなったとしてもせいぜい数倍でしょう。 -
文字列のコピーは速いです。100倍くらい。
実際に計測してみましょう。
There was a problem hiding this comment.
ありがとうございます。
- おっしゃる通りです、自分の方法も結局再構築してますね。
- 考えるのが難しく、諦めていました。なるほど、問題文も確かに 1<=n<=8 とのことですし、そこまで実時間で差が出るわけでもなさそうですね。
- なるほどですね。
ベンチマーク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
There was a problem hiding this comment.
あ、はい。なんとなく私の感覚こんなものな気がします。
generate_method2_recursive は、コピーせずに pop することでバックトラックする手がありますね。
There was a problem hiding this comment.
上で書いている「文字列のコピーは速い。100倍くらい。」というのは、Python インタープリターが通常100倍くらい遅いという制限がついているのが、文字列のコピーはネイティブなコードで動くということです。また、連続したメモリーブロックのコピーになり、ループアンローリングや SIMD など様々なテクニックが適用可能なので速いです。
There was a problem hiding this comment.
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
There was a problem hiding this comment.
@oda @docto-rin
レビューありがとうございます。
カタラン数を考えると最後の数層がノード数として支配的というのは新たな学びでした。
22. Generate Parentheses
次回予告: 31. Next Permutation