-
Notifications
You must be signed in to change notification settings - Fork 0
22.Generate Parentheses #50
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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() |
| 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) |
| 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)] | ||
| 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 | ||
| 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 = [] | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. parenthesisと見間違えそうです。parenthesisにも複数の括弧がありますし、意味もやや曖昧ですね。
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ご指摘の通りだと思います。 |
||
|
|
||
| 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) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sol1のようにparenthesis + "("としない理由はありますか?
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. バックトラックを意識して書いたためです There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. listで文字を貯めて、最後にjoinしたら良さそうですかね。現状は新しいstrオブジェクトを作り直しているだけの意図が不明な無駄な操作に見えます。
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. やっと意図が分かりました。strはimmutableでしたね。 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 文字列のコピーは割と速く、どうせ、このプログラムは n がカタラン数で増加するので20くらいがせいぜいでしょう。そう考えるとあまり大きな問題ではないかもしれません。どれくらい速くなっているかは計測してもいいでしょう。
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| 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 |
| 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. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. context-free grammarですね。
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 確かにそうですね。ただ、この問題では「一意に書ける」ことが重要なのではないかと思いました。 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. すみません、「ただ」以降のつながりがよく分かりませんでした。単にCFGですねというコメントでした。
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. なるほど、CFGであるか否かを意識するようにしようと思います。 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 列挙の際は抜け漏れなくすることが大事ですね。 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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]There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. あ、そうですね。unambiguous という条件をつければ抜け漏れないですね。
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 文字列操作の問題が出たときにCFGや正規表現、曖昧な言語かどうかは意識しようと思います。 このコードを見て気がつきましたが、この問題はカタラン数の漸化式が自然に連想されますね。 |
||
| 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)) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. f-stringが使えますね。
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. f-stringの方が可読性が良さそうですね。こちらに書き換えました。 |
||
| return result | ||
| 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
自分なら("", 0, 0) から始めるのですが、趣味の範囲だと思います。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
たしかにその書き方もできますね。考えていませんでした。