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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
start_new_problem.sh
main.go
go.mod
go.sum
*.go
211 changes: 211 additions & 0 deletions 779K-thSymbolInGrammar.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
問題: https://leetcode.com/problems/k-th-symbol-in-grammar/description/

### Step 1
- コンパイラの授業の文脈自由文法の話に似ている作業
- プッシュダウンオートマトンと再帰を使ったという朧げな記憶
- まず愚直に記号列を作っていく方法を思いつく
- 他にはnとkを再帰的に減らしていく方法も思いついたが、一旦ナイーブな方法で実装する
- 記号列をstring型で作る方法も考えられたが、
Goのstringはイミュータブルで余計なコピー作成が発生するのでスライスを使うことに
- 時間計算量: O(2^n)
- currentRowSymbols のサイズは2^kなので内側のループは2^kであることより、
1+2+4+...+2^n という一般項2^nの等比数列の和を求めてO(2^n)
- 空間計算量: O(2^n)
- ヒープメモリ切れでクラッシュした
Copy link

Choose a reason for hiding this comment

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

「ヒープメモリ切れ」という言い方はあまり聞きません。「メモリ溢れ」「メモリ不足」あたりをよく使うと思います。


```Go
func kthGrammar(n int, k int) int {
currentRowSymbols := []int{0}
for i := 1; i <= n; i++ {
nextRowSymbols := []int{}
for _, s := range currentRowSymbols {
switch {
case s == 0:
nextRowSymbols = append(nextRowSymbols, 0)
nextRowSymbols = append(nextRowSymbols, 1)
case s == 1:
nextRowSymbols = append(nextRowSymbols, 1)
nextRowSymbols = append(nextRowSymbols, 0)
}
}
currentRowSymbols = nextRowSymbols
}
return currentRowSymbols[k-1]
}
```

- 紙に書いたところ、以下のような法則性があることに気がついた
- n行目の前半はn-1行目と一致する
- n行目の後半はn-1行目の0と1を入れ替えたもの一致する
- テストケース
- n=1, k=1 -> 0
- n=2, k=1 -> 0
- n=2, k=2 -> 1
- n=3, k=1 -> 0
- n=3, k=2 -> 1
- n=3, k=3 -> 1
- n=3, k=4 -> 0
- n=4, k=1 -> 0
- n=4, k=5 -> 1
- 時間計算量: O(n)
- 空間計算量: O(n)

```Go
func kthGrammar(n int, k int) int {
switch {
case n == 1:
return 0
case n == 2 && k == 1:
return 0
case n == 2 && k == 2:
return 1
Comment on lines +58 to +61
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.

はい、不要でした。

}
rowSizeHalf := int(math.Pow(2, float64(n-2)))
if k <= rowSizeHalf {
return kthGrammar(n-1, k)
} else {
// if kthGrammar(n-1, k-rowSizeHalf) == 0 then return 1
// if kthGrammar(n-1, k-rowSizeHalf) == 1 then return 0
return (kthGrammar(n-1, k-rowSizeHalf) + 1) & 1
Comment on lines +67 to +69

Choose a reason for hiding this comment

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

好みですが、+1に何か意図があるように見えるので、反転処理は 1 - kthGrammar(n-1, k-rowSizeHalf)と書く方が好きです。
ちなみにPythonでは、not使うと反転できたんですが、bool 値になるため int 変換が必要になり、読みづらかったので、1 - self.kthGrammar(n-1, k - half_num_elements)って最終的に書きたくなりました。
https://github.com/olsen-blue/Arai60/pull/47/files#diff-da439603310f08640b8dab0ec6cfc15251b5669e04e4effc5795dbe1f506a8daR9:~:text=%2D%20https%3A//atmarkit,%E3%81%8C%E5%BF%85%E8%A6%81%E3%81%9D%E3%81%86%E3%80%82

Copy link

@olsen-blue olsen-blue Mar 29, 2025

Choose a reason for hiding this comment

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

あ、下で直ってますね。失礼しました。

}
}
```

### Step 2
- https://github.com/hayashi-ay/leetcode/pull/46/files#diff-da439603310f08640b8dab0ec6cfc15251b5669e04e4effc5795dbe1f506a8daR13
- O(2^n)の空間計算量でどのくらいのメモリ領域が使われるのかについて見積もっていたので自分もやってみる
- メモリ使用量は2^{n-1}でleetcodeの制約はn <= 30
- int64は8Bなので 2^29 ÷ 8 ≒ 2^30 ÷ 10 ≒ 1000^3 ÷ 10 = 10^8 = 100MB
Copy link

Choose a reason for hiding this comment

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

8で割っているのがよく分かりません。
log_10 2 ~ 0.301 を使うと少し速いです。

Copy link
Owner Author

Choose a reason for hiding this comment

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

スライスの最大要素数が2^29で各要素がint64で8Bなので、×8でした。
$2^{29} × 8 = 2^{32} = (10^{\log_{10}2})^{32} \approx (10^{0.301})^{32} \approx 10^9$ で1GB程度になりそうです。

- 0か1かなのでint64で64bitまとめて表す方法もある
- ただし、メモリ使用量はたかだか1/64
- https://github.com/hayashi-ay/leetcode/pull/46/files#diff-da439603310f08640b8dab0ec6cfc15251b5669e04e4effc5795dbe1f506a8daR16

#### 2a
- step1の修正
- 0なら1に、1なら0にの反転はもっと簡単にできる
Copy link

Choose a reason for hiding this comment

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

true false で not であるという考え方もあります。

Copy link
Owner Author

Choose a reason for hiding this comment

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

書いてみました。

func kthGrammar(n int, k int) int {
	var kthGrammarHelper func(n int, k int) bool
	kthGrammarHelper = func(n int, k int) bool {
		if n == 1 {
			return false
		}
		rowHalfSize := int(math.Pow(2, float64(n-2)))
		if k > rowHalfSize {
			return !kthGrammarHelper(n-1, k-rowHalfSize)
		}
		return kthGrammarHelper(n-1, k)
	}

	if kthGrammarHelper(n, k) {
		return 1
	}
	return 0
}

- https://github.com/fhiyo/leetcode/pull/47/files#diff-518e9507bea66eabe2b96ca4930b3dfa228b17d6c3e68dd05b92ab1c7de3228dR49
- bits.Reverseを使っても良いが、0か1かだけなので上記で良さそう
- nが2の場合はなくても良い
- ifでreturnしているのでわざわざelseを書く必要がない
- pythonのようにキャッシュデコレータがあれば2の累乗を自作再帰関数で行ってキャッシュすれば若干早くなりそうだが、Goにはない
- O(logn)時間の計算がO(1)で行える場合が出るが、メモリを消費することと実装難易度を考えるとわざわざやるほどでもない

```Go
func kthGrammar(n int, k int) int {
if n == 1 {
return 0
}
rowHalfSize := int(math.Pow(2, float64(n-2))) // 2**(n-1) / 2
Copy link

Choose a reason for hiding this comment

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

rowLength = int(math.Pow(2, float64(n-1)))
で変数を置いたあと、if k > rowLength/2 と分岐した方がわかりやすい気もしますが、好みかもしれません

if k > rowHalfSize {
return 1 - kthGrammar(n-1, k-rowHalfSize)
}
return kthGrammar(n-1, k)
Comment on lines +99 to +102
Copy link

@olsen-blue olsen-blue Mar 29, 2025

Choose a reason for hiding this comment

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

好みですが、等価なことを別々にやる時は if/else を使い、早期returnや特別な場合をキャッチするという時だけ if 単独で書きたい、みたいな気持ちがあります。
(また、elseは、残り全部丸投げという投げやりな気持ちで使う時もありますね。)

Copy link
Owner Author

Choose a reason for hiding this comment

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

使い分けの気持ちわかります。
今回は中身が return 文だけなのに if/else で書くと冗長に見える気がしてインデントを下げました。3項演算子が欲しくなるところですが、残念ながら Go にはありません。最近3項演算子は目読みにくいと言う意見を複数人から伺いましたが、自分は Python は左から右に読めないので使用を控え、条件式 : trueの場合 ? falseの場合 と言う形の言語では使いたくなります。

if k > rowHalfSize {
	return 1 - kthGrammar(n-1, k-rowHalfSize)
} else {
	return kthGrammar(n-1, k)
}

}
```

#### 2b
- 二分木があると想像して葉から根までたどって戻ってくる方法
- https://github.com/hayashi-ay/leetcode/pull/46/files#diff-da439603310f08640b8dab0ec6cfc15251b5669e04e4effc5795dbe1f506a8daR66

```Go
func kthGrammar(n int, k int) int {
patterns := map[int][2]int{
0: {0, 1},
1: {1, 0},
}

var kthGrammarHelper func(n int, k int) int
kthGrammarHelper = func(n int, k int) int {
if n == 1 {
return 0
}
previousSymbol := kthGrammarHelper(n-1, (k+1)/2)
return patterns[previousSymbol][(k-1)%2]
Comment on lines +122 to +123
Copy link

Choose a reason for hiding this comment

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

この辺の(k+1)/2, (k-1)%2の計算が結構追いにくいですが、代案は思い浮かばないし難しいですね

Comment on lines +122 to +123

Choose a reason for hiding this comment

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

子ノード(n, k)の親ノードは(n-1, (k+1) // 2)の位置のノードになりますね。
k の偶奇によって、親の値を反転するかどうかを選択する感じでも書けました。(Step3です)
https://github.com/olsen-blue/Arai60/pull/47/files

}
return kthGrammarHelper(n, k)
}
```

#### 2c
- kのpopcntから導けるらしい。なんだと
- https://github.com/hayashi-ay/leetcode/pull/46#issuecomment-1986824146
- https://github.com/olsen-blue/Arai60/pull/47/files#r2003238004
- この説明がわかりやすかった
- 0 -> 01, 1 -> 10 を二分木として見て観察すると、
左側に行く場合は親と同じで右側に行く場合は反転する
- 0 -> 01 -> 0110 を二分木として見て、
0-indexの2番の葉に辿り着くまでに根から順に右、左と移動したので
ビットの反転は1度だけ起きる。
根が0でビットの反転が1度(奇数回)起きると最終的なビットは1
- https://discord.com/channels/1084280443945353267/1200089668901937312/1216054396161622078
- こんな話があったので読んでみる
- メモは最下部CS欄に記載
- Goだとpopcntに相当する計算をbits.OnesCountで行えるらしい
- Goは未使用の変数を宣言するとエラーが出るので、
引数nを使わないとエラーが出るかなと思ったが、unusedparamsチェッカーが文句を言ってきただけだった

```Go
func kthGrammar(n int, k int) int {
return bits.OnesCount(uint(k-1)) & 1
}
```

### Step 3
```Go
func kthGrammar(n int, k int) int {
return bits.OnesCount(uint(k-1)) & 1
}
```

#### Step 4
- bits.OnesCountの内部実装確認
- uint型がマシン環境によって32bitになっているか64bitになっているかを調べてbits.OnesCount32かOnesCount64を呼び出している
- 32bitマシンか64bitマシンかどうかの調べ方が勉強になった
- `const uintSize = 32 << (^uint(0) >> 63)`
- `^uint(0)`は0のXORを取るので32bitマシンなら1が32個、
64bitマシンなら64個並ぶ
- それを63個右シフトしたら32bitマシンなら全部0で、
64bitマシンなら末尾に1が残る
- OnesCount32, OnesCount64の中では下記リンク先で記載されていることと同じことをやっている
- https://stackoverflow.com/questions/109023/count-the-number-of-set-bits-in-a-32-bit-integer#109025

```Go
const uintSize = 32 << (^uint(0) >> 63)
const UintSize = uintSize

func OnesCount(x uint) int {
if UintSize == 32 {
return bits.OnesCount32(uint32(x))
}
return bits.OnesCount64(uint64(x))
}
```

- uintSizeの調べ方と似ているものに、MaxIntがある
- MinIntは`MinInt = 1 << (intSize - 1)`でも得られるかと思ったが、overflowしてしまうらしい

```Go
const MaxInt = 1<<(intSize-1) - 1
const MinInt = -1 << (intSize - 1)
```

### CS
- Hamming Weight
- 0のみからなる文字列に対するハミング距離みたいなもの
- つまり、ビット列で1が出現する回数
- popcnt
- x86などにあるhamming weightを計算する命令
- Population Count の略らしい
- SWAR Algorithm
- Hamming Weight を計算するアルゴリズム
- SWAR: SIMD within a Register
- アルゴリズムの中身はあまり理解できなかったが、
ざっくり言うとビットのシフトと0101、0011みたいなビット列とのアンドを繰り返していく感じ
Copy link

Choose a reason for hiding this comment

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

ああ、これは2桁ずつ切って、そこの中で2桁の整数を使ってビットの数を数える、4桁ずつ4桁、8桁ずつ8桁……としています。

Copy link
Owner Author

Choose a reason for hiding this comment

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

なるほどです!改めて手元で8bitで実験したところ、2桁ずつ、4桁ずつ、8桁ずつとビット数が計算されていく様子がわかりました。ありがとうございます。

Copy link

Copy link
Owner Author

Choose a reason for hiding this comment

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

こういう計算を生業にしている方々がいると思うと尊敬です

- 0x5555 -> 0101...
- 0x3333 -> 00110011...
- 0x0F0F -> 00010001...
- 0x0101 -> 0000000100000001...
- みたいな規則性のあるビット列になる16進数たち
- SIMD: Single Instruction, Multiple Data
- 日本語読みは「シムディー」
- 単一の命令で複数のデータにアクセスすること