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
93 changes: 93 additions & 0 deletions src/bin/step1.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Step1
// 目的: 方法を思いつく

// 方法
// 5分考えてわからなかったら答えをみる
// 答えを見て理解したと思ったら全部消して答えを隠して書く
// 5分筆が止まったらもう一回みて全部消す
// 正解したら終わり

/*
問題の理解
- 整数nとkが与えられる。n,kは1から始まる。
n行の表を考えるとき、1行目,1番目の数字を0でスタートする。2行目から直前の行の数字を0->01,1->10に変更した数字を書き込む。
n=4,k=2のときoutput=1となる。
1行目: 0
2行目: 01
3行目: 0110
4行目: 01101001
となる。

入力の制約:
1 <= n <= 30
1 <= k <= 2 ^ (n - 1)

何を考えて解いていたか
- n行目だけわかればよいので表全体をメモリに保持しておく必要は無さそう。
- 入力の制約からkはi32の上限ギリギリの値を取り得るので、計算量に注意が必要そう。
- 1行増えるたびに行が持つ数値の数が倍になっていくので、k番目の数字を定数時間O(1)で取得するために配列で管理すると空間計算量が爆発しそう。
bit列で計算しても 2 ^ (29) で計算すると約536MBになるので、ナイーブな実装ではだめそう。
ただし、n行目の結果をn-1行目の結果から計算できれば 2 ^ (log n) となり空間計算量は問題ないと考えられる。
n行目のk番目の数字は、n-1行目の(k%2 == 1 then k - (k/2) else k/2 )番目の数字から求められる。
n行目k番目の数字はn-1行目の(k%2 == 1 then k - (k/2) else k/2 )番目の数字に対応している。
3,4 = 2
5,6 = 3
7,8 = 4
- n行目の値は、n-1行目のbit列にn-1行目の反転bitをくっつけている
- bitを渡すと、渡したbitを反転したbitをくっつけた全体のビット列を返す処理が必要。

ここまで考えてbit操作がよく分からず実装の手がとまったので解答を見る。

何がわからなかったか
- bit列の操作
- bit列を反転したbit列を反転前のbit列と結合する処理
- k番目のbit列を求める処理

解答の理解
https://leetcode.com/problems/k-th-symbol-in-grammar/solutions/4205266/100-recursive-bit-count-by-vanamsen-ctv2/
- 注目している点
- n行目のbit列の長さはn-1行目のbit列の長さの2倍になる。
- kがn行目の前半部分である場合、n-1行目のk番目とそのまま対応する。
- kがn行目の後半部分である場合、n-1行目のbit列を反転した値のk番目に対応する。
- 1 << (n - 2) が何をしているのか見てすぐに分からない。
- 1 << (n - 2) で 2 ^ (n - 2)と等価になる。
- 0001を左に(n-2)bitシフトしている。
- n=4のときbit列の長さは8になる。 1 << (n - 2) = 4となり、前後半の境界を求められる。
- 1 << (n - 2)によって求めたbit列の前後半で条件分岐している。
- k <= length: kが前半部分のとき、そのままn-1行目k番目の値を返している。
else: kが後半部分の時、k - lengthにより後半部分のk番目の値を1 - x により反転して返している。

正解してから気づいたこと
- bit演算に慣れていないせいか難しく感じる。k番目のbitを返しているという内容を実装から読み取るのが難しいという感覚。
- 問題の制約的にはあり得ないが、n,kが1以上であることを検証した方が良いと思った。

所感
- 他の人のコードを読んで写経するのが良さそう。
*/

pub struct Solution {}
impl Solution {
pub fn kth_grammar(n: i32, k: i32) -> i32 {
if n == 1 {
return 0;
}

let length = 1 << (n - 2);
if k <= length {
return Self::kth_grammar(n - 1, k);
}
1 - Self::kth_grammar(n - 1, k - length)
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn step1_test() {
assert_eq!(Solution::kth_grammar(1, 1), 0);
assert_eq!(Solution::kth_grammar(2, 1), 0);
assert_eq!(Solution::kth_grammar(2, 2), 1);
}
}
119 changes: 119 additions & 0 deletions src/bin/step2.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Step2
// 目的: 自然な書き方を考えて整理する

// 方法
// Step1のコードを読みやすくしてみる
// 他の人のコードを2つは読んでみること
// 正解したら終わり

// 以下をメモに残すこと
// 講師陣はどのようなコメントを残すだろうか?
// 他の人のコードを読んで考えたこと
// 改善する時に考えたこと

/*
コメント集、他の人のコードを読んで考えたこと
https://discord.com/channels/1084280443945353267/1200089668901937312/1216054396161622078
> 実は、ビットの数を調べるのは、たまに大事で、有名なアルゴリズムがあります。
> ビット演算だけでできる計算って実はかなり豊かなんですよね。
> https://stackoverflow.com/questions/109023/count-the-number-of-set-bits-in-a-32-bit-integer#109025
- x86アーキテクチャのCPUではハードウェアレベルのサポート(命令セットにpopcnt命令がある)があるとのこと。
ハードウェアサポートの有無に依存しないポータブルなソフトウェア実装のアルゴリズムが紹介されている。
「ハミング重み」とはbit列に出現する1の個数のこと。
https://ja.wikipedia.org/wiki/%E3%83%8F%E3%83%9F%E3%83%B3%E3%82%B0%E9%87%8D%E3%81%BF
popcntのRust実装があった。
https://github.com/BartMassey/popcount
READMEで言及されている「Hacker’s Delight」はnodchipさんのコメントでも翻訳版「ハッカーのたのしみ」として言及されていていわゆる名著なのかなと思った。
https://github.com/hroc135/leetcode/pull/44#discussion_r2007607576

https://github.com/hayashi-ay/leetcode/pull/46/changes
- 読みやすい。Binary Treeとして見るという視点はなかったので参考になった。

https://github.com/hayashi-ay/leetcode/pull/46#issuecomment-1986824146
https://github.com/olsen-blue/Arai60/pull/47/changes#r2002307405
- nに依存せずkから答えを求められる。

https://github.com/hroc135/leetcode/pull/44/changes#r2019738588
- 場合分けのときはif elseで書きたいという意見。
ifブロックの中身がreturnだけのときはネストを浅くしたいという気持ちもわかるし、場合分けの対称性が分かるようにあえてelseとするという方針も理解できる。
どちらも正しいと思うので一度コーディング規約で決めてしまって、この部分の議論に時間を使いたくないなという感じ。

改善する時に考えたこと
- 引数チェックの追加。

所感
- 問題を見てbit演算の考え方で効率的な解法が思いつかなくても、この解法は思いつきたいなと思える解法だった。シンプルかつ問題の答えとして十分だと感じたため。

一番読みやすく理解しやすいと思ったコードを参考にした。
https://github.com/hayashi-ay/leetcode/pull/46/changes
*/

pub struct Solution {}
impl Solution {
pub fn kth_grammar(n: i32, k: i32) -> i32 {
if n <= 0 || k <= 0 {
panic!("n and k must be greater than 0")
}

if n == 1 {
return 0;
}

let previous_value = Self::kth_grammar(n - 1, (k + 1) / 2);
if previous_value == 0 {
if k % 2 == 0 {
return 1;
} else {
return 0;
}
} else {
if k % 2 == 0 {
return 0;
} else {
return 1;
}
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn step2_test() {
assert_eq!(Solution::kth_grammar(1, 1), 0);
assert_eq!(Solution::kth_grammar(2, 1), 0);
assert_eq!(Solution::kth_grammar(2, 2), 1);
}

#[test]
#[should_panic]
fn kth_grammar_n1_k0_panics() {
Solution::kth_grammar(1, 0);
}

#[test]
#[should_panic]
fn kth_grammar_n0_k1_panics() {
Solution::kth_grammar(0, 1);
}

#[test]
#[should_panic]
fn kth_grammar_n0_k0_panics() {
Solution::kth_grammar(0, 0);
}

#[test]
#[should_panic]
fn kth_grammar_negative_n_panics() {
Solution::kth_grammar(-1, 0);
}

#[test]
#[should_panic]
fn kth_grammar_negative_k_panics() {
Solution::kth_grammar(0, -1);
}
}
86 changes: 86 additions & 0 deletions src/bin/step2a.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Step2a
// 目的: 別の解法を練習する

/*
https://github.com/hayashi-ay/leetcode/pull/46/changes#diff-da439603310f08640b8dab0ec6cfc15251b5669e04e4effc5795dbe1f506a8daR66
- 決定木として考える方針で何をしているのか理解できた。
n-1,(k+1)/2に対応する数値parent_valueを見たときに、
parent_valueが0のとき
kが偶数であれば1
kが奇数であれば0
parent_valueが1のとき
kが偶数であれば0
kが奇数であれば1
といった規則があることを利用している。
手元で決定木を書いてみたところ規則性が読み取れた。
n=3,k=4
0
/ \
0 1 <- parent_value=1
/ \ / \
0 1 1 0 <- k=4

所感
- 決定木として考えると分かりやすいと感じた。bit演算が登場しないからだと思った。
- かなりすっきりと書ける。情報を圧縮しすぎて何をしているのかわかりにくいかもとも思ったが別の解法でも、解法を理解していないと一見何をしているのかわからないのは変わらないと思った。
*/

pub struct Solution {}
impl Solution {
const BINARY_TREE_ROUTES: [[i32; 2]; 2] = [[0, 1], [1, 0]];

pub fn kth_grammar(n: i32, k: i32) -> i32 {
if n <= 0 || k <= 0 {
panic!("n and k must be greater than 0")
}

if n == 1 || k == 1 {
return 0;
}

let parent_value = Self::kth_grammar(n - 1, k - (k / 2));
Self::BINARY_TREE_ROUTES[parent_value as usize][((k + 1) % 2) as usize]
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn step2a_test() {
assert_eq!(Solution::kth_grammar(1, 1), 0);
assert_eq!(Solution::kth_grammar(2, 1), 0);
assert_eq!(Solution::kth_grammar(2, 2), 1);
}

#[test]
#[should_panic]
fn step2a_kth_grammar_n1_k0_panics() {
Solution::kth_grammar(1, 0);
}

#[test]
#[should_panic]
fn step2a_kth_grammar_n0_k1_panics() {
Solution::kth_grammar(0, 1);
}

#[test]
#[should_panic]
fn step2a_kth_grammar_n0_k0_panics() {
Solution::kth_grammar(0, 0);
}

#[test]
#[should_panic]
fn step2a_kth_grammar_negative_n_panics() {
Solution::kth_grammar(-1, 0);
}

#[test]
#[should_panic]
fn step2a_kth_grammar_negative_k_panics() {
Solution::kth_grammar(0, -1);
}
}
73 changes: 73 additions & 0 deletions src/bin/step2b.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Step2b
// 目的: 別の解法を練習する

/*
https://github.com/hayashi-ay/leetcode/pull/46#issuecomment-1986824146
https://github.com/olsen-blue/Arai60/pull/47/changes#r2002307405
- nに依存せずkから答えを求められる。数学パズルに近いとのことで写経だけする。

所感
- 上の2つの例をみて、なぜそうなるのかという数学的な証明は考えずにRustで書き換えることだけ考えながら書いたら動いた。
- 例が何をしているのかの理解
- k-1をbinaryで表した時に1がいくつあるかをカウントしている
- カウントした結果の偶奇性を結果として返している
- binary(2進数)において、LSB(一番右の最下位ビット)が1であれば奇数、0であれば偶数になる性質を利用して、0001とAND演算することによって偶奇性を確認している
- カウントした結果の偶奇性がなぜ答えと一致するのかが数学パズルな部分だと理解
- 偶奇性という言葉を初めて使ったので、調べたところ英語でparityということが分かった。
- 関連してparity bit という英語は聞いたことがあったものの意味は知らなかったので調べたところ、誤り検出の文脈で利用されている英語だった。
https://ja.wikipedia.org/wiki/%E3%83%91%E3%83%AA%E3%83%86%E3%82%A3%E3%83%93%E3%83%83%E3%83%88
- bit演算で何かしたいという時に、こういった数学的な特性を利用してショートカットする手法が存在することを知っておくことで、必要になったときに調べながら利用できる程度で良いと思った。(今自分が関与しているレイヤーでは。)
*/

pub struct Solution {}
impl Solution {
pub fn kth_grammar(n: i32, k: i32) -> i32 {
if n <= 0 || k <= 0 {
panic!("n and k must be greater than 0")
}

((k - 1).count_ones() & 1) as i32
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn step2b_test() {
assert_eq!(Solution::kth_grammar(1, 1), 0);
assert_eq!(Solution::kth_grammar(2, 1), 0);
assert_eq!(Solution::kth_grammar(2, 2), 1);
}

#[test]
#[should_panic]
fn step2b_kth_grammar_n1_k0_panics() {
Solution::kth_grammar(1, 0);
}

#[test]
#[should_panic]
fn step2b_kth_grammar_n0_k1_panics() {
Solution::kth_grammar(0, 1);
}

#[test]
#[should_panic]
fn step2b_kth_grammar_n0_k0_panics() {
Solution::kth_grammar(0, 0);
}

#[test]
#[should_panic]
fn step2b_kth_grammar_negative_n_panics() {
Solution::kth_grammar(-1, 0);
}

#[test]
#[should_panic]
fn step2b_kth_grammar_negative_k_panics() {
Solution::kth_grammar(0, -1);
}
}
Loading