From 5a63918ad2f5702ed5078c7c4dc8c2feed64fafa Mon Sep 17 00:00:00 2001 From: t9a Date: Sun, 11 Jan 2026 13:25:10 +0900 Subject: [PATCH] solve: 779.K-th Symbol in Grammar --- src/bin/step1.rs | 93 ++++++++++++++++++++++++++++++++++++ src/bin/step2.rs | 119 ++++++++++++++++++++++++++++++++++++++++++++++ src/bin/step2a.rs | 86 +++++++++++++++++++++++++++++++++ src/bin/step2b.rs | 73 ++++++++++++++++++++++++++++ src/bin/step3.rs | 90 +++++++++++++++++++++++++++++++++++ src/bin/step3a.rs | 94 ++++++++++++++++++++++++++++++++++++ 6 files changed, 555 insertions(+) create mode 100644 src/bin/step1.rs create mode 100644 src/bin/step2.rs create mode 100644 src/bin/step2a.rs create mode 100644 src/bin/step2b.rs create mode 100644 src/bin/step3.rs create mode 100644 src/bin/step3a.rs diff --git a/src/bin/step1.rs b/src/bin/step1.rs new file mode 100644 index 0000000..db4e9bb --- /dev/null +++ b/src/bin/step1.rs @@ -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); + } +} diff --git a/src/bin/step2.rs b/src/bin/step2.rs new file mode 100644 index 0000000..0887bd4 --- /dev/null +++ b/src/bin/step2.rs @@ -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); + } +} diff --git a/src/bin/step2a.rs b/src/bin/step2a.rs new file mode 100644 index 0000000..8723a93 --- /dev/null +++ b/src/bin/step2a.rs @@ -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); + } +} diff --git a/src/bin/step2b.rs b/src/bin/step2b.rs new file mode 100644 index 0000000..6ebf1a8 --- /dev/null +++ b/src/bin/step2b.rs @@ -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); + } +} diff --git a/src/bin/step3.rs b/src/bin/step3.rs new file mode 100644 index 0000000..c34a59d --- /dev/null +++ b/src/bin/step3.rs @@ -0,0 +1,90 @@ +// Step3 +// 目的: 覚えられないのは、なんか素直じゃないはずなので、そこを探し、ゴールに到達する + +// 方法 +// 時間を測りながらもう一度解く +// 10分以内に一度もエラーを吐かず正解 +// これを3回連続でできたら終わり +// レビューを受ける +// 作れないデータ構造があった場合は別途自作すること + +/* + n = n + 時間計算量: O(n) + 空間計算量: O(n) <- 最初1スタックフレームあたりがO(1)なのでO(1)になると思ったが違った。再帰処理でn回呼び出すのでスタックフレームをn個積む。 +*/ + +/* + 1回目: 1分55秒 + 2回目: 2分09秒 + 3回目: 2分21秒 +*/ + +/* +n=3,k=4 + 0 + / \ + 0 1 <- parent_value=1 + / \ / \ + 0 1 1 0 <- k=4 +*/ + +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 { + 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 step3_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 step3_kth_grammar_n1_k0_panics() { + Solution::kth_grammar(1, 0); + } + + #[test] + #[should_panic] + fn step3_kth_grammar_n0_k1_panics() { + Solution::kth_grammar(0, 1); + } + + #[test] + #[should_panic] + fn step3_kth_grammar_n0_k0_panics() { + Solution::kth_grammar(0, 0); + } + + #[test] + #[should_panic] + fn step3_kth_grammar_negative_n_panics() { + Solution::kth_grammar(-1, 0); + } + + #[test] + #[should_panic] + fn step3_kth_grammar_negative_k_panics() { + Solution::kth_grammar(0, -1); + } +} diff --git a/src/bin/step3a.rs b/src/bin/step3a.rs new file mode 100644 index 0000000..72ae709 --- /dev/null +++ b/src/bin/step3a.rs @@ -0,0 +1,94 @@ +// Step3a +// 目的: 再帰処理をループに書き換えて実装する。 + +/* + n = n + 時間計算量: O(n) + 空間計算量: O(1) +*/ + +/* + 所感 + - step3の空間計算量を見積もった時にループに書き換えるバージョンを書いていないことに気付いた。 + - 自力でループに書き換えられなかったのでGPT-5.2に聞いて写経した。 + + 解法の理解 + - 決定木の右の子は親の値を反転したものになる + - 決定木の右の子はkが偶数になる性質がある + - kが偶数のときは自身を反転した値が親の値(parent_value)になる。 +*/ + +/* +n=3,k=4 + 0 + / \ + 0 1 <- parent_value=1 + / \ / \ + 0 1 1 0 <- k=4 +*/ + +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"); + } + + let mut current_n = n; + let mut current_k = k; + let mut parent_value = 0; + + while 1 < current_n { + if current_k % 2 == 0 { + parent_value = 1 - parent_value; + } + + current_k = (current_k + 1) / 2; + current_n -= 1; + } + + parent_value + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn step3a_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 step3a_kth_grammar_n1_k0_panics() { + Solution::kth_grammar(1, 0); + } + + #[test] + #[should_panic] + fn step3a_kth_grammar_n0_k1_panics() { + Solution::kth_grammar(0, 1); + } + + #[test] + #[should_panic] + fn step3a_kth_grammar_n0_k0_panics() { + Solution::kth_grammar(0, 0); + } + + #[test] + #[should_panic] + fn step3a_kth_grammar_negative_n_panics() { + Solution::kth_grammar(-1, 0); + } + + #[test] + #[should_panic] + fn step3a_kth_grammar_negative_k_panics() { + Solution::kth_grammar(0, -1); + } +}