Skip to content
Open
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
128 changes: 128 additions & 0 deletions 22. Generate Parentheses/memo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@

## step1:
nのParenthesesをn-1のParenthesesから生成するとなったとき、n-1での各文字列に対して左から右へ()を挿入していく場所を探していくと、
"("に当たった時はその"("を"()"で囲むか、"()("のように左に添える2パターンがあり、")"に当たった時は"())"のように置くパターンがあり、""に当たった時はそこに"()"をおけば良い。それを素直にコードにすると以下のようになる。
### code
```python
class Solution:
# when facing (, put () or surround the ( with ()
# when facing ), put () before )
# when facing "", put ()
def generateParenthesis(self, n: int) -> List[str]:
answer = []
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
Owner Author

Choose a reason for hiding this comment

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

この変数使ってないですね。削除しておくべきですがもし使うとしたらそのままparenthesisと名付けるのが自然ですかね

def backtrack(parenthesis, m):
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

一般論として、関数名はアルゴリズム名よりは、実態に即した別のもののほうが良いかなと思います。
また、backtrackと書かれていると、読み手は返却値がDFSで解いたときの並びを期待する人が一定数居ると思いますが、next_parenthesisがsetなので、一般的なDFSの並びにはならないです。そのような意味で、(setでなければDFSになるが)setを使っているので結果の並びがDFSではない、みたいなときにbacktrackということにも違和感があります。

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.

なるほど、以前もdfsと名付けたのをtraverseと命名する感じのことを指摘されましたがこの場合でも気をつけるようにします。
ChatGPTに命名を尋ねたら単にgenerateやexpandを提案されましたが引数mがぱっと見でなんの役割か分かりづらいのが悩みですね。
おっしゃる通りdfsでないのでbacktrackと命名すると読み手に迷いを生じさせそうなので適した名前にするようにします。

if m == 0:
return parenthesis
next_parenthesis = set()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

この問題では順番は求められていませんが、一般にsetを使った結果をそのままlist()して返却すると、順番はぐちゃぐちゃになります。

for parenthe in parenthesis:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ブラ/ケットに近い命名だとは思いますが、parentheは普通の単語ではないので、ちょっと違和感を持つ人が多いかなと思います。特に、parentheだと"("みたいなのを想像するかなと思います。
parenthesis自体が単数形なので、next_parenthesisともども複数形のparenthesesを使い、parenthe -> parenthesis がよいかなと思います。
英文でコードを書くときは、多くのエディタではタイポを指摘されると思っていて、parentheだとタイポ指摘が存在する状態になる、というのもあります。
以下、指摘の例(これはブラウザですが)↓

Image

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が単数形であることは後々知りました。最初はparenthesesを単数形にするとなんだろうとなって結局parentheとぶつ切りにする感じにしてしまいました...
実務だときちんと調べて正確な単数形にすると思うのですが面接中などは思いつきの単語で回すか面接官に聞くしかなさそうですね...

for i in range(len(parenthe)+1):
if i < len(parenthe) and parenthe[i] == "(":
next_parenthesis.add(parenthe[:i] + "()" + parenthe[i:])
index_to_insert_closing = i + find_index_of_corresponding_symbol(parenthe[i:])
next_parenthesis.add(parenthe[:i] + "(" + parenthe[i:index_to_insert_closing] + ")" + parenthe[index_to_insert_closing:])
else:
next_parenthesis.add(parenthe[:i] + "()" + parenthe[i:])
return backtrack(next_parenthesis, m - 1)

def find_index_of_corresponding_symbol(part):
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

この関数名で突然symbolが出てきているので、parenthesisで良いかなと思います。ただ、関数名が長いので、find_index_of_closerぐらいが良いかなと思いました。

が、よくよく考えると、partを受け取って最初の(が閉じる位置を返却する、なので、(未定義ですが)実態としてはfind_first_edge(_index)とかfind_first_break(_index)とかですかね。

opening = 0
closing = 0
for i in range(len(part)):
if part[i] == "(":
opening += 1
else:
closing += 1
if opening == closing:
return i
return -1
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

この関数単体で見たときに-1はよくあるfindの動作だと思いますが、このプログラムが正しく動作している限りにおいてはここには来ないので、そのコメントがあると親切かなと思いました。


return list(backtrack([""], n))
```
これでacceptされたが、geminiに読ませてみると以下の問題を言われた。
## 非効率な全探索(Setによる管理)
今のコードは、各ステップで「既存の文字列の全箇所に () を入れる」という操作を繰り返しています。これは計算量が非常に多くなり、n が大きくなると set への追加と文字列操作でパフォーマンスが劇的に悪化します。

## 「バックトラック」の一般的な解法との違い
通常、この問題(LeetCode 22. Generate Parentheses)をバックトラックで解く場合、**「空の状態から1文字ずつ ( か ) を足していく」**という手法をとります。

なのでこれをヒントに一回1から書いてみる。まず作りたいparenthesesは2*nの長さあり、それぞれに対してnこの(とnこの)をそれぞれ挿入していく。ここでwell-formedにするにはすでに(を配置した数分より大きい)を途中で置いてはいけない。この(と)の配置を全探索していくと答えにつながる。backtrackでも解けるがちょっと捻ってstackを使ったdfsで解いてみる。

```python
class Solution:
def generateParenthesis(self, n: int) -> List[str]:
answer = []
parentheses_and_open_and_close = []
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

これは「(parentheses: str, open: int, close: int)のtupleと同じ中身のlist」のlistだと思いますが、名前がかなりわかりにくいです。
pythonであっても型を明示するようにしたほうが、練習としては効果的ではないかと思います。
(parentheses: str, open: int, close: int)の部分はまずtupleでよく(listにするとlist[str | int]になってしまうし長さも固定されない)、

parentheses_and_open_and_close: list[tuple[str, int, int]] = []

とかでよいと思います。
関数名は、冗長ですがparentheses_with_counts_stackとかですかね。
単にstackとかstatesみたいなものもAIには提案されました。

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.

AIの提案通りにstackやstatesにすると個人的には何が入ってるか迷ってしまいそうなので
parentheses_and_open_and_close: list[tuple[str, int, int]] = []
これが最も読みやすいと感じますね。parentheses_with_counts_stackもコメントなどでparentheses, open, closeがどう返ってくるかを添えておけば読みやすいと思います。

parentheses_and_open_and_close.append(["", n, n])
while parentheses_and_open_and_close:
parentheses, open_num, close_num = parentheses_and_open_and_close.pop()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

open_num, close_numはremaining_open, remaining_closeみたいな、残りのopen数、残りのclose数、みたいなことがわかる変数名のほうが適切かなと思いました。
もしopen_num, close_numを維持するなら、処理を["", 0, 0]で始めて、+-を逆転させて、close_num == n、open_num < n、close_num < open_num、でそれぞれ判定するのがよいかなと思います。

if open_num == close_num == 0:
answer.append(parentheses)
continue
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

このcontinueは後ろのifの評価を避ける程度の意味しかないので、なくてよいかなと思いました。好みの問題かもしれません。

if open_num > 0:
parentheses_and_open_and_close.append([parentheses + "(", open_num - 1, close_num])
if close_num > 0 and close_num > open_num:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

close_num > 0は、open_numが負にならないので不要ですかね。
そもそも、この処理の中でopen_num <= close_numは保証されているので、最初のopen_num == close_num == 0もclose_num == 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.

close_num > 0は、open_numが負にならないので不要ですかね。

これはそうですね。

この処理の中でopen_num <= close_numは保証されているので、最初のopen_num == close_num == 0もclose_num == 0で十分と思いました。

これも正しいのですが、open_num, close_numを残りの"(", ")"を置ける数と定義している以上最後はopen_num == close_num == 0と置いた方が読み手に終了条件が伝わりやすくていいかなと個人的に思いました。

parentheses_and_open_and_close.append([parentheses + ")", open_num, close_num - 1])
return answer
```
これだと(と)に対してそれぞれの文字位置に対して置く、置かないを羅派しているのでset()で重複を省く必要がない。

## step2:
### code
```python
class Solution:
def generateParenthesis(self, n: int) -> List[str]:
answer = []
parentheses_and_open_and_close = []
parentheses_and_open_and_close.append(["", n, n])
while parentheses_and_open_and_close:
parentheses, open_num, close_num = parentheses_and_open_and_close.pop()
if open_num == close_num == 0:
answer.append(parentheses)
continue
if open_num > 0:
parentheses_and_open_and_close.append([parentheses + "(", open_num - 1, close_num])
if close_num > 0 and close_num > open_num:
parentheses_and_open_and_close.append([parentheses + ")", open_num, close_num - 1])
return answer
```

## step3:
どうせなのでbacktrack, bfsで解いてみる。
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

step3は、最終的に自分が妥当だと思うコードをすぐに書けるようにする定着の目的もあります。意図的に自分が最も妥当だと思うコードを選んで練習するのがよいと思います。
また、他の人のPR・コードを読んで、それについての感想などを書くと、それも自分が書くコードの品質を良くすると思います。

### code
```python
class Solution:
def generateParenthesis(self, n: int) -> List[str]:
answer = []

def backtrack(parentheses, open_num, close_num):
if open_num == close_num == 0:
answer.append(parentheses)
if open_num > 0:
backtrack(parentheses + "(", open_num - 1, close_num)
if close_num > 0 and close_num > open_num:
backtrack(parentheses + ")", open_num, close_num - 1)

backtrack("", n, n)
return answer
```

```python
from collections import deque
class Solution:
def generateParenthesis(self, n: int) -> List[str]:
answer = []
parentheses_open_close = deque()
parentheses_open_close.append(["", n, n])

while parentheses_open_close:
parentheses, open_num, close_num = parentheses_open_close.popleft()
if open_num == close_num == 0:
answer.append(parentheses)
continue
if open_num > 0:
parentheses_open_close.append([parentheses + "(", open_num - 1, close_num])
if close_num > 0 and open_num < close_num:
parentheses_open_close.append([parentheses + ")", open_num, close_num - 1])

return answer
```