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
51 changes: 51 additions & 0 deletions 49_group_anagrams_medium/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# 問題へのリンク

[Group Anagrams - LeetCode](https://leetcode.com/problems/group-anagrams/)

# 言語
Python


# 自分の解法
`strs`の各文字列を順番に走査し、各文字列をソートした結果をキーとして、ハッシュマップ`anagram_groups: dict[str, list[str]]`に格納する。
アナグラム同士は、同じソート結果を持つため、同じキーに格納される。

`strs`の配列の長さを`n`、各文字列の長さの最大値を`m`とする。
本問では、`0<= n <= 10^4`、`0 <= m <= 100`である。
- 時間計算量:`O(n * m log(m))`
- 空間計算量:`O(n * m)`



## step2
- ハッシュマップを`anagram_groups`と命名。
- キーを生成する処理を`generate_anagram_key`関数に切り出す。
- ハッシュマップのキーを`canonical_key`と命名。(アナグラムの正規形を表すキー、の意)

# 別解・模範解答(`char_count_key.py`)
もし、`strs`の各文字列の長さが長い場合、ソートにかかる時間が大きくなるため、`key`を文字列のカウントのタプル( `(cnt_1, cnt_2, ..., cnt_k): tuple[int]` の形式)にする方法も考えられる。
ここで、ハッシュマップのキーに使えるのは、イミュータブルなオブジェクトである必要があるため、リストではなくタプルを使う。


`strs`の配列の長さを`n`、各文字列の長さの最大値を`m`、各文字列の含む文字の種類数を`k`とすると
本問では、`k= 26`(英小文字のみ)である。
- 時間計算量:`O(n * m)`
- 空間計量:`O(n * (m + k))`

説明:

時間計算量について
`word`の文字数を`m`、文字の種類数を`k`とします。(小文字英字のみを考えると、`k = 26`です。)
- `word`に対する`key`の生成一回あたりの時間計算量は、`word`の各文字を見ていくので`O(m)`です。
- よって、全体の時間計算量は`O(n * m)`となります。
Copy link

Choose a reason for hiding this comment

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

計算量を見積もるのもいいんですが、計算量は計算時間を見積もる手段なので、計算時間まで見積もってください。

Copy link

Choose a reason for hiding this comment

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

Copy link
Owner Author

Choose a reason for hiding this comment

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

アドバイスありがとうございます。いわゆる「計算量」は漸近的な挙動しかとらえていないというのは常に忘れないようにします。一方で、leetcodeの問題を解く際に意識できる計算時間というのは、せいぜい単純な演算の回数のことでしょうか。

Copy link

Choose a reason for hiding this comment

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

分岐予測にどれくらい失敗しそうだとか、キャッシュミスをどれくらいしそうかとか、通信時間とか、そういったものも考えています。
たとえば、ここでリアロケーションの時間をナノ秒単位で考えています。
https://discord.com/channels/1084280443945353267/1237649827240742942/1359177255158419638

Python だとインタープリターのために見積もりはより難しいですが、ネイティブコードにすると20倍くらい変わったりします。
https://discord.com/channels/1084280443945353267/1367399154200088626/1372840818397810701

Copy link
Owner Author

Choose a reason for hiding this comment

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

レビューありがとうございます。リアロケーションの議論はとても興味深く拝見しました。
オーダー記法による概算を超えた、Pythonの場合の各計算時間の勘所みたいなものがまだまだないので、気にしていきます。このあたりの時間感覚みたいなものを鍛えていきたいですね。
キャッシュミスについては、 「プログラマーですがなぜキャッシュメモリは早いのかといった物理的なことがネットで調べてもしっかり理解できまえせん」に関して #CPU - Qiitaあたりも読みました。
また、odaさんがいつか紹介されていた"Numbers Everyone Should Know" from Jeff Dean.の推移が書いてあるwebサイトも見つけました。Numbers Every Programmer Should Know By Year


空間計算量について
- `key`ひとつあたりの空間計算量は`O(k)`です。
- `word`ひとつあたりの空間計算量は`O(m)`です。
- よって、全体の(最悪)空間計算量は`O(n * (m + k))`となります。
- すべての`word`が異なる文字を持つ場合が最悪ケースです。


# 次に解く問題の予告
- [Implement Trie (Prefix Tree) - LeetCode](https://leetcode.com/problems/implement-trie-prefix-tree/description/)
- [Is Subsequence - LeetCode](https://leetcode.com/problems/is-subsequence/description/)
30 changes: 30 additions & 0 deletions 49_group_anagrams_medium/char_count_key.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#
# @lc app=leetcode id=49 lang=python3
#
# [49] Group Anagrams
#


# @lc code=start
from collections import defaultdict


class Solution:
def groupAnagrams(self, words: list[str]) -> list[list[str]]:
# manage anagrams as a hashmap whose key is "canonical key" and value is a list of anagrams
anagram_groups: dict[tuple[int, ...], list[str]] = defaultdict(list)

for word in words:
canonical_key = self._generate_anagram_key(word)
anagram_groups[canonical_key].append(word)
return list(anagram_groups.values())

def _generate_anagram_key(self, word: str) -> tuple[int, ...]:
char_count = [0] * 26
for char in word:
order = ord(char) - ord("a")
char_count[order] += 1
return tuple(char_count)


# @lc code=end
24 changes: 24 additions & 0 deletions 49_group_anagrams_medium/step1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#
# @lc app=leetcode id=49 lang=python3
#
# [49] Group Anagrams
#


# @lc code=start
from collections import defaultdict


class Solution:
def groupAnagrams(self, strs: list[str]) -> list[list[str]]:

# hasmap: standardized key -> list of anagrams
hashmap = defaultdict(list)
Copy link

Choose a reason for hiding this comment

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

変数名に型名を入れても、読み手にとってあまり有益な情報にはならないと思います。キーと値にどのような値が含まれているかを明示することをおすすめします。 sorted_to_anagrams などはいかがでしょうか?

Copy link
Owner Author

Choose a reason for hiding this comment

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

それはその通りですね。
sorted_to_anagrams はわかりやすいと感じました。dictは XX_to_YY のスタイルで書けないか、検討することにします。

for word in strs:
key = "".join(sorted(word))
hashmap[key].append(word)

return [anagrams for anagrams in hashmap.values()]


# @lc code=end
27 changes: 27 additions & 0 deletions 49_group_anagrams_medium/step2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#
# @lc app=leetcode id=49 lang=python3
#
# [49] Group Anagrams
#


# @lc code=start
from collections import defaultdict


class Solution:
def groupAnagrams(self, words: list[str]) -> list[list[str]]:
# manage anagrams as a hashmap whose key is "canonical key" and value is a list of anagrams
anagram_groups: dict[str, list[str]] = defaultdict(list)

for word in words:
canonical_key: str = self._generate_anagram_key(word)
anagram_groups[canonical_key].append(word)
return list(anagram_groups.values())

# canonical key of a word is generated by sorting each characters in it

Choose a reason for hiding this comment

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

関数やメソッドに関するコメントは def の次の行に docstring として書くことが一般的だと思います。
https://google.github.io/styleguide/pyguide.html#383-functions-and-methods

Copy link
Owner Author

Choose a reason for hiding this comment

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

たしかにその通りですね。style guideまで案内ありがとうございます。

def _generate_anagram_key(self, word: str) -> str:
return "".join(sorted(word))


# @lc code=end
24 changes: 24 additions & 0 deletions 49_group_anagrams_medium/step3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#
# @lc app=leetcode id=49 lang=python3
#
# [49] Group Anagrams
#


# @lc code=start
from collections import defaultdict


class Solution:
def groupAnagrams(self, strs: list[str]) -> list[list[str]]:
anagram_groups = defaultdict(list)
for word in strs:
canonical_key = self._generate_anagram_key(word)
anagram_groups[canonical_key].append(word)
return list(anagram_groups.values())

def _generate_anagram_key(self, word: str) -> str:
return "".join(sorted(word))


# @lc code=end