Skip to content

Conversation

@Kaichi-Irie
Copy link
Owner

問題へのリンク

Group Anagrams - LeetCode

言語

Python

自分の解法

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

strsの配列の長さをn、各文字列の長さの最大値をmとする。
本問では、0<= n <= 10^40 <= m <= 100である。

  • 時間計算量:O(n * m log(m))
  • 空間計算量:O(n * m)

step2

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

別解・模範解答(char_count_key.py

もし、strsの各文字列の長さが長い場合、ソートにかかる時間が大きくなるため、キーを文字列のカウントのタプルにする方法も考えられる。
ここで、ハッシュマップのキーに使えるのは、イミュータブルなオブジェクトである必要があるため、リストではなくタプルを使う。

strsの配列の長さをn、各文字列の長さの最大値をm、各文字列の含む文字の種類数をkとすると
本問では、k= 26(英小文字のみ)である。

  • 時間計算量:O(n * k)
  • 空間計量:O(n * m)

次に解く問題の予告

return list(anagram_groups.values())

# canonical key of a word is generated by sorting each characters in it
def generate_anagram_key(word: str) -> str:

Choose a reason for hiding this comment

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

self を引数に書き忘れていますね。

Copy link
Owner Author

Choose a reason for hiding this comment

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

レビューありがとうございます。おっしゃるとおりですね、きちんと通るかを確認してpushします。

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まで案内ありがとうございます。

anagram_groups[canonical_key].append(word)
return list(anagram_groups.values())

def generate_anagram_key(self, word: str) -> tuple[str]:

Choose a reason for hiding this comment

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

返り値は tuple[int, ...] でしょうか。

Copy link
Owner Author

Choose a reason for hiding this comment

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

これも同じくおっしゃるとおりです。
古いtype hintが残ったまま、更新できていませんでした。

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

Choose a reason for hiding this comment

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

L32とL33は逆でしょうか。

Copy link
Owner Author

Choose a reason for hiding this comment

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

ご指摘ありがとうございます。結論としては、どちらの計算量も間違っており、正しくは

  • 時間計算量:O(n * m)
  • 空間計量:O(n * (m + k))

でした。以下、この理由を説明します。

まず、別解ではkeysorted_word: strから (cnt_1, cnt_2, ..., cnt_k): tuple[int, ...] の形式にすることを考えています。

時間計算量について
wordの文字数をm、文字の種類数をkとします。(小文字英字のみを考えると、k = 26です。)

  • wordに対するkeyの生成一回あたりの時間計算量は、wordの各文字を見ていくのでO(m)です。
  • よって、全体の時間計算量はO(n * m)となります。

空間計算量について

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

wordの文字数が大きすぎると、各keyの長さが大きくなり無駄が多くなるのではないか、という課題感からこの別解を考えましたが、実際に改善されるのはキーのソートが無くなる部分のlog(m)だけで、あまり改善されないことがわかりました。ご指摘いただきありがとうございました。

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[str], list[str]] = defaultdict(list)
Copy link

Choose a reason for hiding this comment

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

anagram_groups: dict[tuple[int, ...], list[str]] = defaultdict(list) でしょうか。

Copy link
Owner Author

Choose a reason for hiding this comment

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

毎度レビューありがとうございます。おっしゃるとおりです。
古いtype hintが残ったまま、更新できていませんでした。本末転倒ですね。

anagram_groups[canonical_key].append(word)
return list(anagram_groups.values())

def generate_anagram_key(self, word: str) -> tuple[str]:
Copy link

Choose a reason for hiding this comment

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

他のクラスから呼ばれない protected な関数のため、 _generate_anagram_key とするとよいと思います。

Copy link
Owner Author

Choose a reason for hiding this comment

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

確かにその通りですね。アドバイスありがとうございます。

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 のスタイルで書けないか、検討することにします。

Copy link
Owner Author

@Kaichi-Irie Kaichi-Irie left a comment

Choose a reason for hiding this comment

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

レビューとご指摘ありがとうございます。

anagram_groups[canonical_key].append(word)
return list(anagram_groups.values())

# canonical key of a word is generated by sorting each characters in it
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まで案内ありがとうございます。

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

Choose a reason for hiding this comment

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

ご指摘ありがとうございます。結論としては、どちらの計算量も間違っており、正しくは

  • 時間計算量:O(n * m)
  • 空間計量:O(n * (m + k))

でした。以下、この理由を説明します。

まず、別解ではkeysorted_word: strから (cnt_1, cnt_2, ..., cnt_k): tuple[int, ...] の形式にすることを考えています。

時間計算量について
wordの文字数をm、文字の種類数をkとします。(小文字英字のみを考えると、k = 26です。)

  • wordに対するkeyの生成一回あたりの時間計算量は、wordの各文字を見ていくのでO(m)です。
  • よって、全体の時間計算量はO(n * m)となります。

空間計算量について

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

wordの文字数が大きすぎると、各keyの長さが大きくなり無駄が多くなるのではないか、という課題感からこの別解を考えましたが、実際に改善されるのはキーのソートが無くなる部分のlog(m)だけで、あまり改善されないことがわかりました。ご指摘いただきありがとうございました。

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[str], list[str]] = defaultdict(list)
Copy link
Owner Author

Choose a reason for hiding this comment

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

毎度レビューありがとうございます。おっしゃるとおりです。
古いtype hintが残ったまま、更新できていませんでした。本末転倒ですね。

anagram_groups[canonical_key].append(word)
return list(anagram_groups.values())

def generate_anagram_key(self, word: str) -> tuple[str]:
Copy link
Owner Author

Choose a reason for hiding this comment

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

これも同じくおっしゃるとおりです。
古いtype hintが残ったまま、更新できていませんでした。

anagram_groups[canonical_key].append(word)
return list(anagram_groups.values())

def generate_anagram_key(self, word: str) -> tuple[str]:
Copy link
Owner Author

Choose a reason for hiding this comment

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

確かにその通りですね。アドバイスありがとうございます。

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

# hasmap: standardized key -> list of anagrams
hashmap = defaultdict(list)
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 のスタイルで書けないか、検討することにします。

…nts and update README with complexity analysis
時間計算量について
`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

@Kaichi-Irie Kaichi-Irie changed the title Solve 49_group_anagrams_medium 49 group anagrams medium Sep 10, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants