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

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

/*
問題の理解
- 英数記号と空白で構成される文字列sが与えられる。
重複する文字の存在しない最長部分文字列の長さを返す。
部分文字列の定義として空白でない文字が連続していること。

何を考えて解いていたか
- 手作業でやることを考えた時に思いついた実装方法としてはs[i]からスタートして重複する文字または空白を見つけるまで出現した文字を数え上げる方法。
s.len()としたときに、時間計算量O(n ^ 2)になる。入力の制約から文字列の最大長さは(5 * 10 ^ 4) ^ 2 = 2,500,000,000となり計算量が爆発する。
ナイーブな実装ではTime Limit Exceededとなりそう。
他の方法が思いつかないのでナイーブな実装だけしてみる。
空白の扱いでWrong Answerとなった。
「A substring is a contiguous non-empty sequence of characters within a string.」 のnon-emptyが空白文字に言及していると思っていた。
修正してAcceptedとなった。
事前に頭の中で思い描いていた実装とは異なり、s[i..j].contains[&s[j]]により時間計算量がO(N^3)となったものの採点システム側では時間計算量として許容範囲内だった理由が分からなかった。

何がわからなかったか
- 時間計算量の見積もり自体は行えたが、求めた時間計算量から組み合わせ爆発が起きると予想したがそうではなかった。
- LeetCodeの採点システムがどの程度の実行時間を許容するかが事前に分からないという点は置いておいて、どの程度の実行時間がかかるかを事前に見積もれれば組み合わせ爆発ではないことが判断できたかどうか。

正解してから気づいたこと
- Big-O記法における時間計算量の見積もりはできていたが、実際に値を当てはめるときにミスをしていた。GPT-5.2に聞きながらまとめた。
入力の制約からsの長さは 0 <= s.length < 5 * 10 ^ 4となるが、sは英数記号と空白文字なので多く見積もってprintable ASCIIの文字種類数95と見積もれる。
https://www.ascii-code.com/characters/printable-characters
n = s.len(), m = 95 として、自分の実装の時間計算量を計算すると、O(n * (m ^ 2) / 2) となる。
二重ループの内側のcontainsによる計算は等差数列の和なので、95(95 - 1) / 2 となり、大体 (95 ^ 2) / 2と考えられる。
ループがbreakされるまでの重複のない文字が連続するのが95であるため、jは最長で95になる。
時間計算量の概算の数式に当てはめると、(5 * (10 ^ 4)) * ((95 ^ 2) / 2) / 10 ^ 8 = 約2.3秒
実行時間としては長いものの、2.3秒であれば現実的な実行時間ではあるかと思った。
LeetCode採点システムでTime Limit Exceededしそうな実行時間ではあると思った。
ただし、かなり大雑把な見積もりで2.3秒という値が出てきた時にどう判断するのかがまだ良くわからない。
今回は秒あたり1億ステップ(10 ^ 8)として見積もったが、秒あたり10億ステップ(10 ^ 9)とすると0.23秒 = 230msとなるので、Time Limit Exceededとはならないように見える。

10 ^ 8 は秒あたり1億ステップ命令を実行できるという概算。(RustなのでC++に近い速度がでるという想定)
https://github.com/Yuto729/LeetCode_arai60/pull/16#discussion_r2602118324

実行時間の概算見積もりについて、コメント集などから参考にしたもの
- https://github.com/Yuto729/LeetCode_arai60/pull/16#discussion_r2602118324
- https://discord.com/channels/1084280443945353267/1200089668901937312/1235490680592273410

簡易的なベンチマークとの比較
手元の環境(Apple M4(10 cores))での簡易的なベンチマーク(デバッグビルド)は平均59.874msとなった。
秒あたり10億ステップの概算では230msとなったので、ベンチマーク結果と4倍程度の差となった。
見積もろうとしている計算時間のスケールを考えると、概算にしては良いところまで見積もれているように思った。
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

一般論として、時間の見積もりは安全側に倒したいので(間に合わないと大きな問題だが、速すぎても大きな問題ではないことが多い)数倍くらい長めにでているくらいがちょうどいいだろうと思います。

あんまりにもぎりぎりそうだったら、「時間を測ってみたい」と感じることも大事です。


所感
- 文字種類のような値が時間計算量に関わってくると難しく感じる。
- Time Limit ExceededによるWrong Answerだと思っていたが、たまたまAcceptedになったという感じだった。
- 通らないと思っていたコードが採点システムを通ったので横道にそれてしまったがSliding window自体を知らないのでこの解法による実装を写経した方が良さそう。
*/

use std::time::Instant;

pub struct Solution {}
impl Solution {
pub fn length_of_longest_substring(s: String) -> i32 {
if s.len() <= 1 {
return s.len() as i32;
}

let s = s.as_bytes();
let mut max_length_of_substring = 1;

for i in 0..s.len() {
let mut unique_characters_substring_length = 1;

for j in i + 1..s.len() {
if s[i..j].contains(&s[j]) {
break;
}

unique_characters_substring_length += 1;
max_length_of_substring =
max_length_of_substring.max(unique_characters_substring_length);
}
}

max_length_of_substring
}
}

// GPT-5.2によるベンチマークコード
// “最悪寄せ”入力(表示可能ASCIIのみ)
fn gen_adversarial_printable(n: usize) -> String {
let sigma = 128usize; // ASCII 0..127 は1バイトUTF-8として合法
let mut bytes = Vec::<u8>::with_capacity(n);

while bytes.len() < n {
for k in 0..sigma {
// 巡回シフトしたユニーク列(長さ sigma)
for t in 0..sigma {
bytes.push(((k + t) % sigma) as u8);
if bytes.len() == n {
break;
}
}
if bytes.len() == n {
break;
}

// “最後の1バイト”をもう一回(末尾に重複を置く)
bytes.push(((k + sigma - 1) % sigma) as u8);
if bytes.len() == n {
break;
}
}
}

String::from_utf8(bytes).unwrap()
}

// GPT-5.2によるベンチマークコード
fn main() {
// 入力サイズ(LeetCode制約上限)
let n = 50_000usize;

// ベンチ回数:1回だけだとブレるので数回回して平均を見る
let iters = 5usize;

// 入力生成(ここは計測に入れない)
let s = gen_adversarial_printable(n);
println!("input length = {}", s.len());

// ウォームアップ(最適化・キャッシュ等のため)
let warm = Solution::length_of_longest_substring(s.clone());
println!("warmup answer = {}", warm);

// 計測開始
let start = Instant::now();

// 同じ入力で複数回(clone は計測に入るが、関数シグネチャが String なので仕方ない)
// clone のコストも含めた「実際の提出に近い」時間になる
let mut sum = 0i64;
for _ in 0..iters {
let ans = Solution::length_of_longest_substring(s.clone());
sum += ans as i64; // 最適化で消されないように使う
}

let elapsed = start.elapsed();

// 結果表示
println!("iters = {}", iters);
println!("sum(ans) = {}", sum);
println!("elapsed = {:.3?}", elapsed);
println!("avg/iter = {:.3?}", elapsed / (iters as u32));
}

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

#[test]
fn step1_test() {
assert_eq!(
Solution::length_of_longest_substring("abcabcbb".to_string()),
3
);
assert_eq!(
Solution::length_of_longest_substring("bbbbb".to_string()),
1
);
assert_eq!(
Solution::length_of_longest_substring("pwwkew".to_string()),
3
);
assert_eq!(
Solution::length_of_longest_substring("ab abc ab ".to_string()),
4
);
assert_eq!(Solution::length_of_longest_substring("a".to_string()), 1);
assert_eq!(Solution::length_of_longest_substring("".to_string()), 0);
}
}
90 changes: 90 additions & 0 deletions src/bin/step1a.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Step1a
// 目的: 別の解法を実装してみる

/*
問題の理解
- 英数記号と空白で構成される文字列sが与えられる。
重複する文字の存在しない最長部分文字列の長さを返す。
部分文字列の定義として空白でない文字が連続していること。

解法の理解
https://leetcode.com/problems/longest-substring-without-repeating-characters/solutions/7417480/rust-implementation-by-rajeshkumarrobert-23fo/
- 2ポインタで見ている区間(window)の始点と終点を管理しているイメージ。
- 区間には重複した文字がない状態を維持している。
- start = -1 とすることで開区間での初期化になっている。
- end - start で区間の文字数を数えられる
- start = start.max(i) とした時にiは過去に重複した文字の位置が入る。startは開区間として扱っているのでiをそのまま代入してもその位置の文字は区間に含まないといった扱いになると理解。
| = startの境界位置
- [|'a','b','a'] <- end = 0,start = -1
- [|'a','b','a'] <- end = 1,start = -1
- ['a',|'b','a'] <- end = 2, start = 0
- HashMapで出現した文字cとsにおける位置を記録する。
- 文字列sの文字cを先頭から見ていく。
- 文字cを毎回HashMapにinsertする。HashMap.insertはキーが存在する時、値を更新する。更新前の値が戻り値となる。
- 過去に文字cが登録されていて、その位置が区間内であれば、文字cの位置まで区間を狭める。(start = start.max(i))
- 区間内の文字数を数えて最大文字数を更新する。
- 不変条件として区間内には重複する文字が存在しない。
*/

use std::collections::HashMap;

pub struct Solution {}
impl Solution {
pub fn length_of_longest_substring(s: String) -> i32 {
let mut character_to_index = HashMap::new();
let mut max_substring_length = 0;
let mut start = -1isize;

for (end, c) in s.chars().enumerate() {
if let Some(i) = character_to_index.insert(c, end) {
start = start.max(i as isize);
}
max_substring_length = max_substring_length.max(end as isize - start);
}

max_substring_length as i32
}
}

#[cfg(test)]
mod tests {
use std::collections::HashSet;

use super::*;

#[test]
fn step1a_playground() {
let character_types = HashSet::<_>::from_iter("abca".chars().into_iter()).len();
assert_eq!(character_types, 3);

let mut maps: HashMap<char, i32> =
HashMap::from_iter([('a', 0), ('b', 1), ('c', 2), ('d', 3)]);
// HashMap::insertは重複するキーがある場合は値が更新される。戻り値として、更新前の値が返される。
assert_eq!(maps.insert('a', 4), Some(0));
// 更新されている。
assert_eq!(maps.get(&'a'), Some(&4));
}

#[test]
fn step1a_test() {
assert_eq!(
Solution::length_of_longest_substring("abcabcbb".to_string()),
3
);
assert_eq!(
Solution::length_of_longest_substring("bbbbb".to_string()),
1
);
assert_eq!(
Solution::length_of_longest_substring("pwwkew".to_string()),
3
);
assert_eq!(
Solution::length_of_longest_substring("ab abc ab ".to_string()),
4
);
assert_eq!(Solution::length_of_longest_substring("ab".to_string()), 2);
assert_eq!(Solution::length_of_longest_substring("a".to_string()), 1);
assert_eq!(Solution::length_of_longest_substring("".to_string()), 0);
}
}
91 changes: 91 additions & 0 deletions src/bin/step2.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Step2
// 目的: 自然な書き方を考えて整理する

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

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

/*
他の人のコードを読んで考えたこと
https://github.com/naoto-iwase/leetcode/pull/49/changes#diff-3b6e163021d78fcc9ee1d093525ad3943834e1362abebdf326ea2d7e3d5129f0R72
- 区間start側を閉区間として扱う実装。与えられた文字列sに存在する文字cを見るのであれば、区間の両端は閉区間にしてしまったほうが素直だという感覚は分かると思った。

https://github.com/Ryotaro25/leetcode_first60/pull/52/changes#diff-f8174339e096bcd076272c3e7cd89b087025459c8fd6c42938166e2f87636a9aR1
- 問題の制約からASCII文字しか出現しないのでサイズ128の配列を初期化して管理する方法。実装の幅として良いと思った。
実際に使うようなメソッドでこの実装をするのであれば、メソッド名でASCII文字にしか対応していないことを示唆する命名にするかなと考えた。

https://github.com/Ryotaro25/leetcode_first60/pull/52/changes#r2003497726
> ご参考までに共有ですが、リーダブルコードには、begin/endが [begin, end)半開区間、start/lastが [start, last]閉区間のイメージということが記載されていました。
- 特定の変数名と端点の開閉状態は関係がないように見えるので、個人的には一般化しないほうが良いと思った。
たとえば、Pythonのstr,findではstart/endが[start,end]で閉区間に対応している。
https://docs.python.org/ja/3.13/library/stdtypes.html#str.find
PythonのRangeでは、start/stopが[start,stop)なleft-close,right-openな半開区間になっている。
https://docs.python.org/ja/3.13/library/stdtypes.html#range
RustのRangeでは、start/endが[start,end)なleft-close,right-openな半開区間になっている。
https://doc.rust-lang.org/std/ops/struct.Range.html
ここまで書いていて思ったが、コーディング規約のようなもので端点の開閉状態に合わせてある程度変数名を統一しましょうという温度感であればむしろ好ましいのかとも思った。
制約が無く、自由度が高すぎるためにコードレビューで本質的では無い点(2ポインタの変数命名)に時間を使うよりは最初からコーディング規約による制約があるほうが良いという観点。

改善する時に考えたこと
- start側を閉区間として扱う実装を行ってみる。

所感
- 閉区間で扱うとstart,endともにusizeで扱えるので、区間の中からある値を探すみたいな場合では閉区間の方が自然だと思った。
- Binary Searchの問題で区間について時間をかけて理解しようとしたおかげで、区間で見るという視点でSliding windowをいい感じに理解できているように感じた。
2ポインタによる探索範囲(区間)の管理はポインタの更新条件が異なるものの、Binary Searchでの探索範囲管理と同じように見える。
*/

use std::collections::HashMap;

pub struct Solution {}
impl Solution {
pub fn length_of_longest_substring(s: String) -> i32 {
let mut character_to_index = HashMap::new();
let mut max_substring_length = 0;
let mut start = 0;

for (end, c) in s.chars().enumerate() {
if let Some(i) = character_to_index.insert(c, end) {
start = start.max(i + 1);
}
max_substring_length = max_substring_length.max((end - start) + 1);
}

max_substring_length as i32
}
}

#[cfg(test)]
mod tests {

use super::*;

#[test]
fn step2_test() {
assert_eq!(
Solution::length_of_longest_substring("abcabcbb".to_string()),
3
);
assert_eq!(
Solution::length_of_longest_substring("bbbbb".to_string()),
1
);
assert_eq!(
Solution::length_of_longest_substring("pwwkew".to_string()),
3
);
assert_eq!(
Solution::length_of_longest_substring("ab abc ab ".to_string()),
4
);
assert_eq!(Solution::length_of_longest_substring("ab".to_string()), 2);
assert_eq!(Solution::length_of_longest_substring("a".to_string()), 1);
assert_eq!(Solution::length_of_longest_substring("".to_string()), 0);
}
}
Loading