Try an interactive version of this dialog: Sign up at solve.it.com, click Upload, and pass this URL.
import sys
sys.path.append('.')
from leet_utils import lc_setup, get_leetcode_problem, lc_export
lc_setup("https://leetcode.com/problems/valid-anagram/", add_cells=False)
from IPython.display import HTML
problem = get_leetcode_problem("https://leetcode.com/problems/valid-anagram/")
HTML(problem["content"])
Given two strings s and t, return true if t is an anagram of s, and false otherwise.
Example 1:
Input: s = "anagram", t = "nagaram"
Output: true
Example 2:
Input: s = "rat", t = "car"
Output: false
Constraints:
1 <= s.length, t.length <= 5 * 104sandtconsist of lowercase English letters.
Follow up: What if the inputs contain Unicode characters? How would you adapt your solution to such a case?
import string
class Solution:
NUM_CHARS = len(string.ascii_lowercase)
BASE_ORD = ord("a")
def count_lowers(self, s: str) -> list[int]:
counter = [0] * self.NUM_CHARS
for ch in s:
counter[ord(ch) - self.BASE_ORD] += 1
return counter
def isAnagram(self, s: str, t: str) -> bool:
return self.count_lowers(s) == self.count_lowers(t)
class Solution:
def count_chars(self, s: str) -> dict[str, int]:
counter = {}
for ch in s:
if ch not in counter:
counter[ch] = 0
counter[ch] += 1
return counter
def isAnagram(self, s: str, t: str) -> bool:
if len(s) != len(t):
return False
remaining_counts = self.count_chars(s)
for ch in t:
if remaining_counts.get(ch, 0) == 0:
return False
remaining_counts[ch] -= 1
return True
🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠
3つの実装、どれもよく考えられていますね!いくつかコメントします。
実装1について
良い点:
- 固定長配列を使うのは、この制約下では最適解
- クラス変数で定数を定義しているのは良い習慣
改善案:
len(s) != len(t)の早期リターンを入れると、長さが違う場合にカウント処理をスキップできる
def isAnagram(self, s: str, t: str) -> bool:
if len(s) != len(t):
return False
return self.count_lowers(s) == self.count_lowers(t)
実装2について
良い点:
- Unicode対応への拡張として適切な選択
Counterを使わず自前実装する練習、良いと思います
細かい点:
counter[ch] = counter.get(ch, 0) + 1と1行で書くスタイルもある(好みの問題)
実装3について
良い点:
- early returnの発想は実践的
- 実際に「ほとんど不一致」なケースでは効果的
気になる点:
remaining_counts[ch] -= 1で値が0になっても辞書に残る。問題はないが、メモリ的にはdelする選択肢もある(ただし可読性との兼ね合い)
全体として、問題の制約をよく理解した上で複数のアプローチを試しているのが素晴らしいです。何か特定の実装について深掘りしたい点はありますか?
- LLMレビューの感想
counter[ch] = counter.get(ch, 0) + 1も悪くないが、こう書くくらいならdefaultdictに変更して+= 1を維持したい。
- コメント集:空
- PR
- https://github.com/ryosuketc/leetcode_grind75/pull/7
- https://github.com/eito2002/LeetCode/pull/2
- https://github.com/huyfififi/coding-challenges/pull/7
- https://github.com/azriel1rf/leetcode-prep/pull/2
- https://github.com/rihib/leetcode/pull/5
- https://github.com/kzhra/Grind41/pull/8
- https://github.com/NobukiFukui/Grind75-ProgrammingTraining/pull/21
🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠
7つのPRを確認しますね。順番に読み込んでいきます。
{
"id": "toolu_011WpAKjrpDwSaJHSPfrM84P",
"call": {
"function": "read_url",
"arguments": {
"url": "https://github.com/ryosuketc/leetcode_grind75/pull/7"
}
},
"result": "'Skip to content\\n\\n## Navigation Menu\\n\\nToggle navigation\\n\\n[ ](/)\\n\\n[ Sign in ](/login?return_t<TRUNCATED>"
}
{
"id": "toolu_01VwxRFBFi6baLoSkfsoGk6b",
"call": {
"function": "read_url",
"arguments": {
"url": "https://github.com/eito2002/LeetCode/pull/2"
}
},
"result": "'Skip to content\\n\\n## Navigation Menu\\n\\nToggle navigation\\n\\n[ ](/)\\n\\n[ Sign in ](/login?return_t<TRUNCATED>"
}
{
"id": "toolu_012w15UWiPkXPK3uzX6Qiiir",
"call": {
"function": "read_url",
"arguments": {
"url": "https://github.com/huyfififi/coding-challenges/pull/7"
}
},
"result": "'Skip to content\\n\\n## Navigation Menu\\n\\nToggle navigation\\n\\n[ ](/)\\n\\n[ Sign in ](/login?return_t<TRUNCATED>"
}
{
"id": "toolu_01SCG5MfaKMnRkXkFv6kEDjP",
"call": {
"function": "read_url",
"arguments": {
"url": "https://github.com/azriel1rf/leetcode-prep/pull/2"
}
},
"result": "'Skip to content\\n\\n## Navigation Menu\\n\\nToggle navigation\\n\\n[ ](/)\\n\\n[ Sign in ](/login?return_t<TRUNCATED>"
}
{
"id": "toolu_01A5eeTL14cQxVwdBzZDkUPk",
"call": {
"function": "read_url",
"arguments": {
"url": "https://github.com/rihib/leetcode/pull/5"
}
},
"result": "'Skip to content\\n\\n## Navigation Menu\\n\\nToggle navigation\\n\\n[ ](/)\\n\\n[ Sign in ](/login?return_t<TRUNCATED>"
}
{
"id": "toolu_01Mvfch1JmDMbATmKKKCwxb1",
"call": {
"function": "read_url",
"arguments": {
"url": "https://github.com/kzhra/Grind41/pull/8"
}
},
"result": "'Skip to content\\n\\n## Navigation Menu\\n\\nToggle navigation\\n\\n[ ](/)\\n\\n[ Sign in ](/login?return_t<TRUNCATED>"
}
{
"id": "toolu_01PU62DRUbvQersym7RzXxEP",
"call": {
"function": "read_url",
"arguments": {
"url": "https://github.com/NobukiFukui/Grind75-ProgrammingTraining/pull/21"
}
},
"result": "'Skip to content\\n\\n## Navigation Menu\\n\\nToggle navigation\\n\\n[ ](/)\\n\\n[ Sign in ](/login?return_t<TRUNCATED>"
}
各PRの内容をまとめます。
1. ryosuketc/leetcode_grind75 PR #7 (C++)
- 解法:
unordered_mapでカウント - 主なコメント:
- Structured binding (
for (const auto& [s_char, s_char_count] : ...)) はC++17から(oda) - ソート後に
s == tで比較する方法もある(Apo-Matchbox) - 参照渡し・RVO・ポインタに関する深い考察をメモに記載
- Structured binding (
2. eito2002/LeetCode PR #2 (C++)
- 解法: hash map、ソート、固定長配列(26要素)
- 主なコメント:
- ぶら下がりif(
if (...) return false;)は事故が起きやすいので避けるべき(huyfififi) - 変数名を
dict→char_to_countのように説明的にすべき(huyfififi) ++iとi++の違い(前置の方がコピーを避けられる)- EBCDICでは
s[i] - 'a'が動かない可能性(nodchip) - CodeRabbit(AI)のレビューはこの勉強会では不要という議論
- ぶら下がりif(
3. huyfififi/coding-challenges PR #7 (Python)
- 解法: 配列カウント、
defaultdict、collections.Counter - 主なコメント:
- 同じ処理を2度したら関数にすることを考える(oda)
- 引き算で解くとSpaceがn+mではなくnかmで済む(sasanquaneuf)
- 早期リターンできるパターンに気を配る
cntよりcountと省略しない方が良い(Google Style Guide)- Step3でも同じコードでも貼った方が良い
4. azriel1rf/leetcode-prep PR #2 (Python)
- 解法:
defaultdictでカウント、Counter - 主なコメント:
- 変数名は
lower_snakeが標準(PEP8, Google Style Guide) cntではなくcountと略さず書くreturn cnt1 == cnt2で辞書比較可能(内部でearly returnされる)Counter(s) == Counter(t)で1行で書ける- 引き算して負になったらFalseで抜ける方法も(liquo-rice)
- 1ループで2つの文字列を処理するより、2ループに分けた方が良い(oda)
- 変数名は
5. rihib/leetcode PR #5 (Go)
- 解法:
map[rune]intでカウント(Unicode対応) - 主なコメント:
- Unicode/サロゲートペア/結合文字/合字 についての非常に詳細な調査(本人)
runeを使っても結合文字(例:「が」=「か」+濁点)は正しく処理できない(sasanquaneuf)- グラフィムクラスタを使えばほぼ対応可能だが、旗シーケンスなど例外あり
- 文字コードは魔境(oda)
6. kzhra/Grind41 PR #8 (C++)
- 解法:
map/unordered_mapでカウント、ソート - 主なコメント:
return count_letter_1 == count_letter_2;で比較可能- 変数名は
letter_to_countのようにkey→valueを表す命名が良い(nodchip) unordered_mapvsmap:要素数が少ないとハッシュ化のオーバーヘッドで差がないこともfor (auto [letter, count] : ...)で回した方が読みやすいifの後にスペースを入れる(Google Style Guide)
7. NobukiFukui/Grind75-ProgrammingTraining PR #21 (Python)
全体的な傾向:
- 変数名:
char_to_count、letter_to_countなど説明的な命名が推奨 - 比較: 辞書/mapは
==で直接比較可能 - 早期リターン: 長さチェックや負のカウントで早く抜ける
- Unicode: 結合文字やサロゲートペアの問題は深い(特にrihib PR)
- コーディング規約: PEP8、Google Style Guideへの言及多数
🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠
では、PRレビューで出てきた主要トピックを深掘りしていきます。
1. 変数名の命名(char_to_count スタイル)
辞書/mapの変数名で key_to_value の形式が推奨される理由:
# 悪い例
dict = {} # 組み込み型を上書き
d = {} # 意味不明
cnt = {} # 何のカウント?
# 良い例
char_to_count = {} # 文字 → 出現回数
letter_to_frequency = {}
この命名により:
- 辞書のキーと値が何かが一目でわかる
char_to_count[ch]と書くと「文字chの出現回数」と自然に読める- コードレビューや後からの保守が楽になる
Google Style Guideでは略語を避けることも推奨されています(cnt → count)。
2. 辞書/mapの == 比較
Python/C++/Goいずれでも、辞書型は == で直接比較可能です:
# Python
{"a": 1, "b": 2} == {"b": 2, "a": 1} # True(順序は関係ない)
# 内部的には:
# 1. len(d1) == len(d2) をチェック
# 2. 各キーの存在と値を比較
# → 不一致が見つかった時点でFalseを返す(early return)
// C++
std::unordered_map<char, int> m1, m2;
m1 == m2; // OK
つまり、自分でループを書いて比較する必要はありません。
3. 早期リターン(Early Return)
不一致を早く検出することで無駄な処理を省けます:
def isAnagram(self, s: str, t: str) -> bool:
# Level 1: 長さチェック
if len(s) != len(t):
return False
# Level 2: カウントしながら検出
char_to_count = {}
for ch in s:
char_to_count[ch] = char_to_count.get(ch, 0) + 1
for ch in t:
if ch not in char_to_count:
return False # sに無い文字が出現
char_to_count[ch] -= 1
if char_to_count[ch] < 0:
return False # sより多く出現
return True
引き算アプローチ(sasanquaneufさんのコメント):
- 2つの辞書を作る → Space: O(n + m)
- 1つの辞書で引き算 → Space: O(n) または O(m)
4. Unicode・サロゲートペア・結合文字
これはrihib PRで非常に深く議論されていたトピックです。
レベル1: ASCII
len("abc") # 3 → 問題なし
レベル2: BMP(Basic Multilingual Plane)内のUnicode
len("あいう") # 3 → 問題なし
レベル3: サロゲートペア(BMP外の文字)
len("𠮷") # Python3では1(正しく処理)
# JavaScriptやJavaでは2になることがある(UTF-16の2つのコードユニット)
Goの rune はUnicodeコードポイント単位なので、サロゲートペアは正しく処理されます。
レベル4: 結合文字(ここが魔境)
s1 = "が" # 1文字として入力(U+304C)
s2 = "か" + "\u3099" # 「か」+ 濁点(U+304B + U+3099)
len(s1) # 1
len(s2) # 2 ← 見た目は同じ「が」なのに!
s1 == s2 # False
これを正しく処理するには Unicode正規化 が必要:
import unicodedata
s1_nfc = unicodedata.normalize('NFC', s1)
s2_nfc = unicodedata.normalize('NFC', s2)
s1_nfc == s2_nfc # True
レベル5: グラフィムクラスタ(さらに深い)
絵文字の例:
flag = "🇯🇵" # 日本国旗
len(flag) # 2(Regional Indicator Symbol 2つの組み合わせ)
family = "👨👩👧👦" # 家族絵文字
len(family) # 7(ZWJで結合された複数のコードポイント)
結論: 「文字」の定義は複雑。問題の制約が「lowercase English letters」なら気にしなくて良いが、「Unicode対応」と言われたら正規化まで考慮が必要。
5. コーディング規約
Python(PEP8 / Google Style Guide):
- 変数名:
lower_snake_case - 略語を避ける:
cnt→count - 行の長さ: 79文字(PEP8)/ 80文字(Google)
C++(Google Style Guide):
ifの後にスペース:if (condition)✓ /if(condition)✗- ぶら下がりif(braceなし)は避ける:
Copied!
// 危険 if (condition) return false; // 安全 if (condition) { return false; }
++ivsi++: イテレータでは前置++iの方がコピーを避けられる
C++17 Structured Binding:
// C++17以降
for (const auto& [key, value] : map) { ... }
// C++11/14
for (const auto& pair : map) {
auto key = pair.first;
auto value = pair.second;
}
どのトピックについてさらに詳しく知りたいですか?特にUnicode周りは奥が深いので、必要であればもっと掘り下げられます。
🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠
面白いチャレンジですね!本気でUnicode対応するコードを書いてみます。
"""
Unicode-aware anagram checker.
This module provides anagram detection that correctly handles:
- Unicode normalization (NFC)
- Grapheme clusters (e.g., 👨👩👧👦 as a single unit)
- Combining characters (e.g., か + ゙ = が)
"""
import unicodedata
from collections import Counter
from typing import Iterator
# graphemeライブラリがあればより正確、なければフォールバック
try:
import grapheme
_HAS_GRAPHEME = True
except ImportError:
_HAS_GRAPHEME = False
def iterate_graphemes(text: str) -> Iterator[str]:
"""
Iterate over user-perceived characters (grapheme clusters).
Examples:
>>> list(iterate_graphemes("café"))
['c', 'a', 'f', 'é']
>>> list(iterate_graphemes("👨👩👧")) # family emoji
['👨👩👧']
>>> list(iterate_graphemes("🇯🇵")) # flag
['🇯🇵']
"""
if _HAS_GRAPHEME:
yield from grapheme.graphemes(text)
else:
# Fallback: コードポイント単位(不完全だが標準ライブラリのみ)
yield from text
def normalize(text: str) -> str:
"""
Normalize Unicode string to NFC form.
NFC (Canonical Decomposition, followed by Canonical Composition):
- 'é' (U+0065 U+0301) → 'é' (U+00E9)
- 'が' (U+304B U+3099) → 'が' (U+304C)
"""
return unicodedata.normalize('NFC', text)
def count_graphemes(text: str) -> Counter[str]:
"""
Count occurrences of each grapheme cluster in normalized text.
"""
normalized = normalize(text)
return Counter(iterate_graphemes(normalized))
def is_anagram(s: str, t: str) -> bool:
"""
Check if two strings are anagrams of each other.
Handles Unicode correctly:
- Normalizes both strings (NFC)
- Compares by grapheme clusters, not code points
Examples:
>>> is_anagram("anagram", "nagaram")
True
>>> is_anagram("rat", "car")
False
>>> is_anagram("café", "cafe\u0301") # different representations of é
True
>>> is_anagram("がぎぐ", "ぐぎが")
True
"""
return count_graphemes(s) == count_graphemes(t)
# LeetCode提出用(制約がASCII lowercase前提なので簡潔版)
class Solution:
"""LeetCode submission wrapper."""
def isAnagram(self, s: str, t: str) -> bool:
return is_anagram(s, t)
設計のポイント:
| 観点 | 対応 |
|---|---|
| 正規化 | NFC形式に統一(か+濁点 → が) |
| グラフィムクラスタ | graphemeライブラリで対応(絵文字・国旗など) |
| フォールバック | ライブラリがなければコードポイント単位(明示的に不完全) |
| 可読性 | 各関数が単一責任、docstringで意図を明確に |
| テスタビリティ | 各関数が独立してテスト可能 |
テストコード:
import pytest
class TestIsAnagram:
"""Test cases for Unicode-aware anagram detection."""
# Basic ASCII
def test_basic_anagram(self):
assert is_anagram("listen", "silent")
def test_basic_not_anagram(self):
assert not is_anagram("hello", "world")
def test_different_length(self):
assert not is_anagram("ab", "abc")
# Unicode normalization
def test_nfc_normalization(self):
# é as single char vs e + combining acute
s1 = "café"
s2 = "cafe\u0301"
assert is_anagram(s1, s2)
def test_japanese_combining(self):
# が as single char vs か + combining dakuten
ga_single = "\u304C" # が (U+304C)
ga_combined = "\u304B\u3099" # か + ゙
assert is_anagram(ga_single, ga_combined)
def test_japanese_anagram(self):
assert is_anagram("あいう", "ういあ")
# Grapheme clusters (requires grapheme library)
@pytest.mark.skipif(not _HAS_GRAPHEME, reason="grapheme library not installed")
def test_emoji_flag(self):
# 🇯🇵 is two code points but one grapheme
assert is_anagram("🇯🇵🇺🇸", "🇺🇸🇯🇵")
@pytest.mark.skipif(not _HAS_GRAPHEME, reason="grapheme library not installed")
def test_emoji_zwj_sequence(self):
# Family emoji: 👨👩👧 is multiple code points joined by ZWJ
family1 = "👨👩👧"
family2 = "👨👩👧"
assert is_anagram(family1, family2)
# Edge cases
def test_empty_strings(self):
assert is_anagram("", "")
def test_single_char(self):
assert is_anagram("a", "a")
assert not is_anagram("a", "b")
graphemeライブラリなしでの限界:
標準ライブラリだけでは以下が正しく処理できません:
# 国旗(Regional Indicator Symbols)
"🇯🇵" # len() = 2, でも視覚的には1文字
# ZWJ (Zero Width Joiner) シーケンス
"👨👩👧👦" # len() = 7, でも視覚的には1文字
# 肌色修飾子
"👋🏽" # len() = 2, でも視覚的には1文字
本番環境でUnicode完全対応が必要なら pip install grapheme を推奨します。
なぜNFCを選んだか:
| 正規化形式 | 説明 | 用途 |
|---|---|---|
| NFC | 合成形式(できるだけ1文字に) | Web標準、一般的な比較 |
| NFD | 分解形式(基底文字+結合文字) | 検索、ソート |
| NFKC/NFKD | 互換文字も統一(①→1など) | 検索向け(意味が変わる可能性) |
アナグラム判定では「見た目が同じなら同じ」が自然なので、NFCを選択しました。
"""
Unicode-aware anagram checker.
This module provides anagram detection that correctly handles:
- Unicode normalization (NFC)
- Grapheme clusters (e.g., 👨👩👧👦 as a single unit)
- Combining characters (e.g., か + ゙ = が)
"""
import unicodedata
from collections import Counter
from typing import Iterator
# graphemeライブラリがあればより正確、なければフォールバック
try:
import grapheme
_HAS_GRAPHEME = True
except ImportError:
_HAS_GRAPHEME = False
def iterate_graphemes(text: str) -> Iterator[str]:
"""
Iterate over user-perceived characters (grapheme clusters).
Examples:
>>> list(iterate_graphemes("café"))
['c', 'a', 'f', 'é']
>>> list(iterate_graphemes("👨👩👧")) # family emoji
['👨👩👧']
>>> list(iterate_graphemes("🇯🇵")) # flag
['🇯🇵']
"""
if _HAS_GRAPHEME:
yield from grapheme.graphemes(text)
else:
# Fallback: コードポイント単位(不完全だが標準ライブラリのみ)
yield from text
def normalize(text: str) -> str:
"""
Normalize Unicode string to NFC form.
NFC (Canonical Decomposition, followed by Canonical Composition):
- 'é' (U+0065 U+0301) → 'é' (U+00E9)
- 'が' (U+304B U+3099) → 'が' (U+304C)
"""
return unicodedata.normalize('NFC', text)
def count_graphemes(text: str) -> Counter[str]:
"""
Count occurrences of each grapheme cluster in normalized text.
"""
normalized = normalize(text)
return Counter(iterate_graphemes(normalized))
def is_anagram(s: str, t: str) -> bool:
"""
Check if two strings are anagrams of each other.
Handles Unicode correctly:
- Normalizes both strings (NFC)
- Compares by grapheme clusters, not code points
Examples:
>>> is_anagram("anagram", "nagaram")
True
>>> is_anagram("rat", "car")
False
>>> is_anagram("café", "cafe\u0301") # different representations of é
True
>>> is_anagram("がぎぐ", "ぐぎが")
True
"""
return count_graphemes(s) == count_graphemes(t)
# LeetCode提出用(制約がASCII lowercase前提なので簡潔版)
class Solution:
"""LeetCode submission wrapper."""
def isAnagram(self, s: str, t: str) -> bool:
return is_anagram(s, t)
def run_tests():
"""Simple test runner for Jupyter."""
passed = 0
failed = 0
def test(name: str, s: str, t: str, expected: bool):
nonlocal passed, failed
result = is_anagram(s, t)
if result == expected:
print(f"✓ {name}: is_anagram({s!r}, {t!r}) = {result}")
passed += 1
else:
print(f"✗ {name}: is_anagram({s!r}, {t!r}) = {result}, expected {expected}")
failed += 1
# Basic ASCII
test("basic_anagram", "listen", "silent", True)
test("basic_not_anagram", "hello", "world", False)
test("different_length", "ab", "abc", False)
# Unicode normalization
test("nfc_normalization (café)", "café", "cafe\u0301", True)
test("japanese_combining (が)", "\u304C", "\u304B\u3099", True)
test("japanese_anagram", "あいう", "ういあ", True)
# Grapheme clusters
if _HAS_GRAPHEME:
test("emoji_flag (🇯🇵🇺🇸)", "🇯🇵🇺🇸", "🇺🇸🇯🇵", True)
test("emoji_zwj_sequence", "👨👩👧", "👨👩👧", True)
test("skin_tone_emoji", "👋🏽👋🏻", "👋🏻👋🏽", True)
else:
print("⚠ Skipping grapheme tests (install: pip install grapheme)")
# Edge cases
test("empty_strings", "", "", True)
test("single_char", "a", "a", True)
test("single_char_different", "a", "b", False)
print(f"\n{passed} passed, {failed} failed")
run_tests()
✓ basic_anagram: is_anagram('listen', 'silent') = True
✓ basic_not_anagram: is_anagram('hello', 'world') = False
✓ different_length: is_anagram('ab', 'abc') = False
✓ nfc_normalization (café): is_anagram('café', 'café') = True
✓ japanese_combining (が): is_anagram('が', 'が') = True
✓ japanese_anagram: is_anagram('あいう', 'ういあ') = True
✓ emoji_flag (🇯🇵🇺🇸): is_anagram('🇯🇵🇺🇸', '🇺🇸🇯🇵') = True
✓ emoji_zwj_sequence: is_anagram('👨\u200d👩\u200d👧', '👨\u200d👩\u200d👧') = True
✓ skin_tone_emoji: is_anagram('👋🏽👋🏻', '👋🏻👋🏽') = True
✓ empty_strings: is_anagram('', '') = True
✓ single_char: is_anagram('a', 'a') = True
✓ single_char_different: is_anagram('a', 'b') = False
12 passed, 0 failed
🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠
はい、その理解で正確です!整理すると:
NFC正規化で解決するもの:
- 結合文字の統一:
か+゙→が - アクセント記号:
e+́→é - 同じ見た目の異なる表現を統一
NFC正規化では解決しないもの(grapheme必要):
- 国旗:
🇯🇵= Regional Indicator J + P(2コードポイント、合成されない) - ZWJ絵文字:
👨👩👧= 複数絵文字 + ZWJ(結合しても1コードポイントにならない) - 肌色修飾:
👋🏽= 👋 + 🏽(修飾子は独立したコードポイント)
import unicodedata
# NFCで解決 ✓
s = "か\u3099" # か + 濁点
unicodedata.normalize('NFC', s) # → "が" (1文字)
# NFCでは解決しない ✗
flag = "🇯🇵"
unicodedata.normalize('NFC', flag) # → "🇯🇵" (まだ2コードポイント)
len(unicodedata.normalize('NFC', flag)) # → 2
結論:
| ケース | NFC | grapheme |
|---|---|---|
| ASCII | 不要 | 不要 |
| 日本語・アクセント | ✓ | 不要 |
| 絵文字・国旗 | 不十分 | ✓ |
実用上は「NFC正規化だけで95%のケースは対応できる」という感じですね。絵文字をユーザー入力で扱うSNS系サービスなどではgraphemeまで必要になります。
from typing import Iterator
import unicodedata
from collections import Counter
import grapheme
class Solution:
def iter_graphemes(self, s: str) -> Iterator[str]:
normalized = unicodedata.normalize('NFC', s)
return grapheme.graphemes(normalized)
def isAnagram(self, s: str, t: str) -> bool:
s_graphemes = self.iter_graphemes(s)
t_graphemes = self.iter_graphemes(t)
return Counter(s_graphemes) == Counter(t_graphemes)
class Solution:
def isAnagram(self, s: str, t: str) -> bool:
if len(s) != len(t):
return False
s_char_to_count = {}
for ch in s:
if ch not in s_char_to_count:
s_char_to_count[ch] = 0
s_char_to_count[ch] += 1
remaining_count = s_char_to_count
for ch in t:
if remaining_count.get(ch, 0) == 0:
return False
remaining_count[ch] -= 1
return True
from IPython.display import Markdown
md = lc_export(
next_url="https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-search-tree/",
dry_run=True,
desc="",
notebook_url="https://share.solve.it.com/d/31dccb7f2a9aa3998533a6acccf04c65"
)
# Markdown(md)
=== DRY RUN: 0242-valid-anagram/0242-valid-anagram.md ===
# 242. Valid Anagram
- URL: https://leetcode.com/problems/valid-anagram/
- Difficulty: Easy
- Tags: Hash Table, String, Sorting
- Notebook: https://share.solve.it.com/d/31dccb7f2a9aa3998533a6acccf04c65
## 問題文
<p>Given two strings <code>s</code> and <code>t</code>, return <code>true</code> if <code>t</code> is an <span data-keyword="anagram">anagram</span> of <code>s</code>, and <code>false</code> otherwise.</p>
<p> </p>
<p><strong class="example">Example 1:</strong></p>
<div class="example-block">
<p><strong>Input:</strong> <span class="example-io">s = "anagram", t = "nagaram"</span></p>
<p><strong>Output:</strong> <span class="example-io">true</span></p>
</div>
<p><strong class="example">Example 2:</strong></p>
<div class="example-block">
<p><strong>Input:</strong> <span class="example-io">s = "rat", t = "car"</span></p>
<p><strong>Output:</strong> <span class="example-io">false</span></p>
</div>
<p> </p>
<p><strong>Constrain...
=== 22 cells would be exported ===
=== PR Title: 242. Valid Anagram ===
=== Notebook: https://share.solve.it.com/d/31dccb7f2a9aa3998533a6acccf04c65 ===