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
159 changes: 159 additions & 0 deletions problem52/memo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
## 取り組み方
- step1: 5分以内に空で書いてAcceptedされるまで解く + テストケースと関連する知識を連想してみる
- step2: コードを整える + 他の妥当な実装があれば実装してみる
- step3: 10分以内に1回もエラーを出さずに3回連続で解く

## step1
[1,2,3]でpermutationsを作ることを考えると、

- [1]
- [1,2]
- [1,2,3]
- [1,3]
- [1,3,2]
- [2]
- [2,1]
- [2,1,3]
- [2,3]
- [2,3,1]
- [3]
- [3,1]
- [3,1,2]
- [3,2]
- [3,2,1]

のようになれば良いので、
ある一つの作りかけのpermutation用の配列に、
1回、値を追加するのを、numsを全て見つつ使われていないものだけが追加されるように、
網羅的に分岐させていくイメージで処理が進めば良い。

リストがミュータブルなので、リストにリストを追加する部分には気をつける。

計算量は、作るpermutationがn!個あって、permutaionsに追加する際にコピーでnかかるので、O(n*n!)。

```py
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
def create_permutation(used: List[int]) -> List[int]:
if len(used) == len(nums):
permutations.append(used[:])
return
for num in nums:
if num in used:
continue
Copy link

Choose a reason for hiding this comment

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

num in used の時間計算量が線形のため、全体の計算量が O(n^2n!) になるように思いました。 used 以外に set を持たせると、時間計算量が O(nn!) に落ちると思いました。

Copy link
Owner Author

@Fuminiton Fuminiton Oct 25, 2025

Choose a reason for hiding this comment

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

レビューありがとうございます。
if num in usedの考慮が漏れていました。
おっしゃる通りO(n^2 * n!)が正しいですね。

used.append(num)
create_permutation(used)
used.pop()

permutations = []
create_permutation([])

return permutations
```

## step2
### 読んだ
- https://github.com/shining-ai/leetcode/pull/50/files
- https://github.com/tokuhirat/LeetCode/pull/50/files
- https://github.com/python/cpython/blob/main/Modules/itertoolsmodule.c#L2696
- https://github.com/Ryotaro25/leetcode_first60/pull/54/files

### 感想
- これまで使っているnumsの要素が入っているという意味でusedと命名していたが、candidate の方が実体に近いので変えるべき
- itertools.permutationsは読みづらかった。というか理解できていないので、時間をおいて読む

Choose a reason for hiding this comment

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

https://github.com/tokuhirat/LeetCode/pull/50/files#diff-f980f45b4d4f94c41556977f060a371872f422650416425386f1322e52deba58R88
itertools.permutations のコードにコメントをつけてみたので、ご参考になればと思います。

- とはいえ、再帰を使わない&インデックス操作なので元のデータがどんな型で重複があっても機能するという点で良いと思った
- が、どのみち n! で時間がかかるので、数値型を扱うならstep1のような形がシンプルで理解しやすいので良いかと思う

#### step1の洗練
使っていないnumsの要素を管理して、再帰の中のループをlen(nums)で回さなくて済むようにする。
ループ中にループの条件に使っている配列に値を追加したり、削除したりするとエラーになるらしいので、気をつける。
https://stackoverflow.com/questions/33679669/python-runtimeerror-dictionary-changed-size-during-iteration/33679686#33679686

ただ、not_usedの浅いコピーを作って回すことになるので、numsでループするのとトータルでどちらが速くなるのかはよくわからない。ので、測ってみる。

```py
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
def create_permutation(candidate: List[int]) -> List[int]:
if len(candidate) == len(nums):
permutations.append(candidate[:])
return

for num in list(not_used):
candidate.append(num)
not_used.remove(num)
create_permutation(candidate)
candidate.pop()
not_used.add(num)

permutations = []
not_used = set(nums)
create_permutation([])
```

##### step1のコード(numsでループ) vs step1の洗練(not_usedでループ)
nums = list(range(10)) で手元で計測したところ、not_used でループした方が少し早い。

###### step1のコード
1回目: 10.01 seconds
2回目: 9.89 seconds
3回目: 10.08 seconds

###### step1の洗練
1回目: 7.400 seconds
2回目: 7.232 seconds
3回目: 7.284 seconds

#### 再帰からループへの書き換え
1個目の候補を決める、2個目の候補を決める、・・・・と各ステップで何を受け渡して、次のステップに何を引き継げば良いかを考えて書き換える。

```py
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
permutations = []
candidates = []
for num in nums:
candidates.append([num])

while candidates:
candidate = candidates.pop()
if len(candidate) == len(nums):
permutations.append(candidate[:])
continue
for num in nums:
if num in candidate:
continue
candidate.append(num)
candidates.append(candidate[:])
candidate.pop()

return permutations
```

#### itertools風
- 後で読む

## step3
```py
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
permutations = []
candidates = []
for num in nums:
candidates.append([num])
Comment on lines +141 to +143

Choose a reason for hiding this comment

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

candidates = [[]] でも良いと思いました。

Copy link
Owner Author

@Fuminiton Fuminiton Oct 25, 2025

Choose a reason for hiding this comment

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

レビューありがとうございます。
candidateに何かを入れておかねばという意識が先行してnumを入れていましたが、おっしゃる通り[]を入れておくでも問題ないですね。


while candidates:
candidate = candidates.pop()
if len(candidate) == len(nums):
permutations.append(candidate)
continue
for num in nums:
if num in candidate:
continue
candidate.append(num)
candidates.append(candidate[:])
candidate.pop()

return permutations
```
Copy link

Choose a reason for hiding this comment

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

読みやすいです。
分かりやすさとのトレードオフはありそうですが、下のようにindexを引数にとる感じでヘルパー関数を定義してバックトラッキングを行うようなやり方もよく見ます。構成してきた順列と使っていない要素を一緒に一つの配列で管理できるようになり、if num in candidate:で弾く必要がなくなるので、多少効率的になるかもしれません。

def permuteHelper(i):
    if i == len(nums): # len(nums) - 1 でも動くかも
        permutations.append(nums[:])
        return
    for j in range(i, len(nums)):
        nums[i], nums[j] = nums[j], nums[i]
        permuteHelper(i + 1)
        nums[i], nums[j] = nums[j], nums[i]

Copy link
Owner Author

Choose a reason for hiding this comment

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

レビュー、代替案のご提示ありがとうございます。

  1. 0番目を固定して、0番目に入れ替えられるパターンを列挙する
  2. 0,1番目を固定して、1番目以降の要素で入れ替えられるパターンを列挙する
  3. 0,1,2番目を.....

という流れで組み合わせを列挙できる規則を利用するのですね。
気づけていなかったです。ありがとうございます!