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
47 changes: 47 additions & 0 deletions 22.Generate-Parentheses/benchmark_sol2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import timeit
from pathlib import Path
from typing import List

DIR = Path(__file__).resolve().parent


def load_solution_class(path: Path) -> type:
src = path.read_text()
if "from typing" not in src.split("\n", 5)[:5]:
src = "from typing import List\n" + src
ns: dict = {"__name__": f"bench_{path.stem}", "List": List}
exec(compile(src, str(path), "exec"), ns)
return ns["Solution"]


def main() -> None:
Sol2 = load_solution_class(DIR / "sol2.py")
Sol2Revised = load_solution_class(DIR / "sol2_revised.py")

cases = [8, 10, 12, 13]
print("-" * 60)

for n in cases:
number = 5 if n <= 10 else 2 if n == 12 else 1
stmt1 = f"Solution().generateParenthesis({n})"
t1 = timeit.timeit(
stmt1,
number=number,
globals={"Solution": Sol2},
)
t2 = timeit.timeit(
stmt1,
number=number,
globals={"Solution": Sol2Revised},
)
ratio = t2 / t1 if t1 > 0 else float("inf")

print(
f"n={n:2d} number={number:2d} "
f"sol2: {t1 * 1000:8.2f} ms sol2_revised: {t2 * 1000:8.2f} ms "
f"(revised / sol2 = {ratio:.3f})"
)


if __name__ == "__main__":
main()
58 changes: 58 additions & 0 deletions 22.Generate-Parentheses/memo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# 22.Generate Parentheses

最初にDFSを思いついたので解く: sol1.py

バックトラック: sol2.py

### コメント集
> Python での計算時間の見積もり方。ネイティブコードで走るか否か。
joinはCPythonで書かれているんだった

> 計算量はあくまでも極限での振る舞いなので、計算量を使って計算時間を見積もるのが大事です。
> また、速度が速いかどうかは、普通コーディングにおいてそれほどプライオリティーが高くないです。
> Generator をキャッシュする方法についてのコメント。
Generatorをキャッシュするデコレータを作ろうとしたけど、kargsがキーの挿入順に依存してしまっている

https://github.com/olsen-blue/Arai60/pull/54#discussion_r2027288220

> これは解の空間をどう分けるか、分類するかの考え方で網羅的に分類できていればなんでもいいのです。
分類の網羅、は覚えておこう

https://github.com/hroc135/leetcode/pull/50#discussion_r2052246310


高階関数

https://github.com/fuga-98/arai60/pull/52#discussion_r2137961581
> 文字列を丸ごとコピーしないようにするやつを、Python にしてみました。
https://github.com/hroc135/leetcode/pull/50

strings.Builderに書き込む関数を再帰で組み合わせている
再帰は関数の合成、ということだろうか
文字列のコピーがないので高速
自分では書けそうにないが




https://github.com/olsen-blue/Arai60/pull/54#discussion_r2027288220

これは面白い解法だ。なるほど、最初の左かっこに対応する右かっこの位置で場合分けをしている: sol3.py
- メモ化再帰に直した

### 追記
sol2 と sol2_revisedを比較すると速さはそれほど変わらない、というかrevisedの方が少し遅かった(geminiに手伝ってもらいました)

答えの本数だけlistからjoinでstrに毎回変換し、文字列の更新よりもオーバヘッドが大きいためだと考えられる

-----------------------------------------------------------
n= 8 number= 5 sol2: 8.40 ms sol2_revised: 10.00 ms (revised / sol2 = 1.190)
n=10 number= 5 sol2: 103.18 ms sol2_revised: 116.89 ms (revised / sol2 = 1.133)
n=12 number= 2 sol2: 520.99 ms sol2_revised: 594.95 ms (revised / sol2 = 1.142)
n=13 number= 1 sol2: 933.25 ms sol2_revised: 1076.60 ms (revised / sol2 = 1.154)
18 changes: 18 additions & 0 deletions 22.Generate-Parentheses/sol1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
class Solution:
def generateParenthesis(self, n: int) -> List[str]:
if n == 0:
return [""]

parentheses = []
frontier = [("(", 1, 0)]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

自分なら("", 0, 0) から始めるのですが、趣味の範囲だと思います。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

たしかにその書き方もできますね。考えていませんでした。

while frontier:
parenthesis, left_used, right_used = frontier.pop()
if right_used == n:
parentheses.append(parenthesis)
continue
if left_used < n:
frontier.append((parenthesis + "(", left_used + 1, right_used))
if left_used > right_used:
frontier.append((parenthesis + ")", left_used, right_used + 1))

return parentheses
22 changes: 22 additions & 0 deletions 22.Generate-Parentheses/sol2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
class Solution:
def generateParenthesis(self, n: int) -> List[str]:
if n == 0:
return [""]

parentheses = []
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

parenthesisと見間違えそうです。parenthesisにも複数の括弧がありますし、意味もやや曖昧ですね。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

ご指摘の通りだと思います。
parenthesisはprefixに、parenthesesの方をresultsとしました。


def generate_parenthesis_from(parenthesis, left_used, right_used):
assert left_used >= right_used
if right_used == n:
parentheses.append(parenthesis)
return
if left_used < n:
parenthesis += "("
generate_parenthesis_from(parenthesis, left_used + 1, right_used)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

sol1のようにparenthesis + "("としない理由はありますか?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

バックトラックを意識して書いたためです

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

listで文字を貯めて、最後にjoinしたら良さそうですかね。現状は新しいstrオブジェクトを作り直しているだけの意図が不明な無駄な操作に見えます。

Copy link
Copy Markdown
Owner Author

@tom4649 tom4649 Apr 22, 2026

Choose a reason for hiding this comment

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

やっと意図が分かりました。strはimmutableでしたね。
listに直した方が良さそうですので修正しました。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

文字列のコピーは割と速く、どうせ、このプログラムは n がカタラン数で増加するので20くらいがせいぜいでしょう。そう考えるとあまり大きな問題ではないかもしれません。どれくらい速くなっているかは計測してもいいでしょう。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

ほぼ変わらない、というかrevisedが遅い場合の方が遅かったです。答えの数が大きいことによってjoinを毎回行うオーバーヘッドが大きくなっていそうです。意外な結果でした。

parenthesis = parenthesis[:-1]
if right_used < left_used:
parenthesis += ")"
generate_parenthesis_from(parenthesis, left_used, right_used + 1)

generate_parenthesis_from("", 0, 0)
return parentheses
25 changes: 25 additions & 0 deletions 22.Generate-Parentheses/sol2_revised.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
class Solution:
def generateParenthesis(self, n: int) -> List[str]:
if n == 0:
return [""]

results = []

def generate_parenthesis_from(
prefix: List[str], left_used: int, right_used: int
):
assert left_used >= right_used
if right_used == n:
results.append("".join(prefix))
return
if left_used < n:
prefix.append("(")
generate_parenthesis_from(prefix, left_used + 1, right_used)
prefix.pop()
if right_used < left_used:
prefix.append(")")
generate_parenthesis_from(prefix, left_used, right_used + 1)
prefix.pop()

generate_parenthesis_from([], 0, 0)
return results
15 changes: 15 additions & 0 deletions 22.Generate-Parentheses/sol3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import functools


class Solution:
@functools.cache
def generateParenthesis(self, n: int) -> List[str]:
# Valid parentheses must be written in the form of (A)B, where A and B are valid parentheses uniquely.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

context-free grammarですね。
S -> (S)S | ε

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

確かにそうですね。ただ、この問題では「一意に書ける」ことが重要なのではないかと思いました。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

すみません、「ただ」以降のつながりがよく分かりませんでした。単にCFGですねというコメントでした。

Copy link
Copy Markdown
Owner Author

@tom4649 tom4649 Apr 22, 2026

Choose a reason for hiding this comment

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

なるほど、CFGであるか否かを意識するようにしようと思います。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

列挙の際は抜け漏れなくすることが大事ですね。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

全ての有効な括弧列を生成でき、かつ、ambiguousではない文法なら、漏れなく重複なしで生成できますね。

class Solution:
    def generateParenthesis(self, n: int) -> List[str]:
        parens = [[] for _ in range(n + 1)]
        parens[0].append('')
        for size in range(1, n + 1):
            for inside_size in range(size):
                right_size = size - inside_size - 1
                for inside in parens[inside_size]:
                    for right in parens[right_size]:
                        parens[size].append(f'({inside}){right}')
        return parens[n]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

あ、そうですね。unambiguous という条件をつければ抜け漏れないですね。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

文字列操作の問題が出たときにCFGや正規表現、曖昧な言語かどうかは意識しようと思います。

このコードを見て気がつきましたが、この問題はカタラン数の漸化式が自然に連想されますね。
S -> (S)S | εの規則から答えの長さがカタラン数になることも分かりますね。

if n == 0:
return [""]
result = []
for i in range(n):
for A in self.generateParenthesis(i):
for B in self.generateParenthesis(n - 1 - i):
result.append("({}){}".format(A, B))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

f-stringが使えますね。

Copy link
Copy Markdown
Owner Author

@tom4649 tom4649 Apr 22, 2026

Choose a reason for hiding this comment

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

f-stringの方が可読性が良さそうですね。こちらに書き換えました。

return result
15 changes: 15 additions & 0 deletions 22.Generate-Parentheses/sol3_revised.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import functools


class Solution:
@functools.cache
def generateParenthesis(self, n: int) -> List[str]:
# Valid parentheses must be written in the form of (A)B, where A and B are valid parentheses uniquely.
if n == 0:
return [""]
results = []
for i in range(n):
for A in self.generateParenthesis(i):
for B in self.generateParenthesis(n - 1 - i):
results.append(f"(A)B")
return results