3. Longest Substring Without Repeating Characters#3
Conversation
| class Solution { | ||
| public: | ||
| int lengthOfLongestSubstring(string s) { | ||
| vector<int> char_code_to_last_index(256, -1); |
| int max_len = 0, unique_str_begin = 0; | ||
|
|
||
| for (int i = 0; i < s.size(); ++i) { | ||
| unsigned char char_code = s[i]; |
There was a problem hiding this comment.
There was a problem hiding this comment.
あ、すみません、charの話をコメントに書き忘れていました。
一旦僕の考えを書かせていただきます。
僕の認識では、charはsignedするコンパイラとunsignedとするコンパイラ両方があって、なのでunsigned charに強制しています。
<cctype>のstd::isdigit(int)等はintで受け取るので値が変換されてうまく文字判定を行えないことがあります。
とはいえ0x80以上の文字コードが使われなければ考えなくて良い話ではあります。
文字コードが1バイトで表せるエンコーディングは、0x80以上を使わないものなのでしょうか?
文字コードが1バイトで表せるものはASCIIぐらいしか知らず確証が持てなかったのでunsigned charにしてvectorに256要素用意しました。
There was a problem hiding this comment.
文字コードが1バイトで表せるエンコーディングは、0x80以上を使わないものなのでしょうか?
そういうことはないでしょう。
s consists of English letters, digits, symbols and spaces.
この問題ではcharの保持する値はASCIIと考えて問題ないでしょう。
There was a problem hiding this comment.
There was a problem hiding this comment.
https://cpprefjp.github.io/reference/string/basic_string.html
using string = basic_string<char>;
とありますので、 std::string の要素の方は char と考えて良いと思います。わざわざ unsigned char にキャストする必要はないと思います。
There was a problem hiding this comment.
私は、@philip82148 さんの説明を以下のように解釈しましたが、どんな文字エンコーディングを使っているか分からない状況は考えなくても良いと思いました。
charはASCII以外の文字エンコーディングを使っているかもしれないので、charの取りうる値は全て"English letters, digits, symbols and spaces"の可能性がある。charがsigned charだとすると、負の値が"English letters, digits, symbols and spaces"である可能性を否めないので、unsigned charにキャストして、vectorのインデックスとして使い、出現回数をカウントする。
There was a problem hiding this comment.
文字コードが1バイトで表せるエンコーディングは、0x80以上を使わないものなのでしょうか?
0x80 以上を使う代表的なものとして、
- JIS X 0201: ASCII に加えて、半角カタカナが表現できる。ひらがな漢字はできない。
- ISO-8859: 枝番で西欧だったり東欧だったりいくつかのバリエーションがある。
- EBCDIC: アルファベットが連続して並んでいないことで有名。また0x80以上のところに数字とアルファベットがある。
があります。
@nodchip
char_code_to_last_index の添字の範囲である 0-255 にしたいので unsigned にキャストするということは確かにそうです。
char が signed か unsigned かは implementation defined で、文字コードもそうです。
よって、EBCDIC 環境でも動くようにしたいというならば、こうすることになります。
@liquo-rice 私も考えなくていいに一票。
There was a problem hiding this comment.
本当に、EBCDIC 環境で動くようにすべてのソースコードを管理しているならばともかく、UTF-8 にも対応せず「C++ の仕様書上これは実装依存で、1バイトの文字コードならどれでもいけるように書いた」と主張するのはバランスを欠く気がしますね。
これ、基本的にバランスを取るゲームなので、ある状況において正しいことを強く主張しないほうがいいでしょう。
Python の話ですが、バランスについては下を参照。
https://discord.com/channels/1084280443945353267/1200089668901937312/1210619083385479258
There was a problem hiding this comment.
なるほどです!charとして扱って、128要素でやりたいと思います!
皆さんありがとうございます!
@oda 0x80以上を使うものの代表的な例ありがとうございます!
バランスをとる、今回の問題点がうまく言語化されていて面白いです。
今後も迷ったらその観点を考えてみたいと思います。
ありがとうございます!
nodchip
left a comment
There was a problem hiding this comment.
1.cpp しかないようですが、複数回繰り返しましたか?複数回繰り返す理由については、小田さんがコメントしていますので、探してみてください。
| class Solution { | ||
| public: | ||
| int lengthOfLongestSubstring(string s) { | ||
| vector<int> char_code_to_last_index(256, -1); |
There was a problem hiding this comment.
固定長かつ十分にサイズが小さいのであれば、 int char_code_to_last_index[256]; または int char_code_to_last_index[128]; としてよいと思います。これによりスタックメモリにメモリを確保し、ヒープメモリを確保する処理を省くことができます。
スタックメモリとヒープメモリの違いは分かりますか?
There was a problem hiding this comment.
スタックメモリとヒープメモリの違いは分かりますか?
はい、分かります。配列にしたいと思います!ありがとうございます!
ちなみに、unordered_mapよりデフォルトmapみたいな、スタックに置くかどうかについて、デフォルトのバイト/要素数の基準みたいなのはありますか?もちろん最終的には状況(再帰、スタックサイズ、…)によると思いますが
There was a problem hiding this comment.
はい、分かります。配列にしたいと思います!ありがとうございます!
念のため、違いを説明してみていただけますか?
ちなみに、unordered_mapよりデフォルトmapみたいな、スタックに置くかどうかについて、デフォルトのバイト/要素数の基準みたいなのはありますか?もちろん最終的には状況(再帰、スタックサイズ、…)によると思いますが
他の方のコードレビューにコメントがついていたと思います。そちらを参照されることをおすすめいたします。
他の方のコメントを調べていらっしゃらないのでしょうか。
There was a problem hiding this comment.
念のため、違いを説明してみていただけますか?
|テキスト領域|データ領域|ヒープ領域|空き・共有ライブラリ等|スタック領域|
※これは主な領域。他にも細々とした領域がある(?、/proc/$PID/smapを見た感じ)のとASLRにより領域は連続とは限らない(?、多分)
スタックはスタックポインタ(rspレジスタ)をずらす(四則演算)だけでメモリの確保ができます。速度が速い代わりに、あらかじめ与えられた領域以上は使えません。
ヒープ領域は、あらかじめ与えられた領域内でバディシステムか何かで管理されたメモリを切り売りすることで自由なサイズのメモリを確保・解放することが出来ます。
メモリが足りなくなったらOSに要求して(ほぼ無限に?(仮想メモリのサイズはLinuxでは最大128TB))メモリを確保することが出来ます。その代わり遅いです。
他の方のコードレビューにコメントがついていたと思います。そちらを参照されることをおすすめいたします。
すみません…、見つかりません…。申し訳ないのですが、どこにあるでしょうか?
他の方のコメントを調べていらっしゃらないのでしょうか。
回答は見ていたのですが、レビューコメントまで見てませんでした。
これからちゃんと見るようにしますmm
There was a problem hiding this comment.
|テキスト領域|データ領域|ヒープ領域|空き・共有ライブラリ等|スタック領域|
この書式は何を表そうとしたのですか?
速度が速い
ここで言う速度とは何を表していますか?スループットですか?レイテンシーですか?何と比べて速度が速いのですか?なぜ速度が速いのですか?本当に速いのですか?
ほぼ無限に?
実利用環境においては、サイズは何によって制限されますか?
その代わり遅いです。
何と比べて遅いのですか?なぜ遅いのですか?本当に遅いのですか?
すみません…、見つかりません…。申し訳ないのですが、どこにあるでしょうか?
自分の勘違いでした。失礼いたしました。
スタックオーバーフローしないよう十分に余裕を持ちつつ確保するのがよいと思います。代表的なコンパイラー/リンカーのデフォルトでのスタックサイズは調べておいても良いと思います。
There was a problem hiding this comment.
この書式は何を表そうとしたのですか?
仮想アドレス空間0~0x7fffffffffffのメモリの使われ方を先頭から順に左から書きました。
速度が速い
メモリを確保する処理速度が速いということです。引き算命令一個でメモリを確保することが出来ます。
(すみません、スループットに当たるのが何かわからないのですが、レイテンシーになるかと思います(スループットも早い気がする)。)
(CPUキャッシュの観点ではヒープより参照の局所性が効きそうです。が、CPUキャッシュの仕組みをまだちゃんと理解していないので差が見積もれません)
その代わり遅いです。
対してヒープでは、バディシステムの場合管理用のリンクリストを走査して空き領域を見つける動作があります。
(もっと具体的な値が必要でしたら、これは僕のマジで適当な予想なのですが、早くて(すぐに見つかった時)少なくとも3命令ぐらい、多いとき100命令超えそう、そしてOSに要求するとなったら際はAPI呼び出しでもっとかかる、みたいなスピードかな、と思っています。)
(ネットワークやディスクほど遅い訳ではありません)
ほぼ無限に?
物理メモリに乗っているという意味では他のプロセスのメモリの量と、それらがどれだけアクティブであるか(ページアウトしないか)によると思います。それからプロセステーブル等、カーネル側で物理メモリに常駐させたいものがあるじゃないかと思います。
仮想メモリという意味では、ヒープ以外の要素(とASLRでずらしたオフセットの分だけ)が制限になります。
が、128TBもあるのでこれはほぼ無視できると思うので、それよりもストレージのサイズということになるんでしょうか(ページアウトした分を載せるストレージ)。
それから、ここら辺はあまりよく分かっていないのですが、多分ulimitで指定したリソース量にも影響を受けるんですかね。
スタックオーバーフローしないよう十分に余裕を持ちつつ確保するのがよいと思います。
代表的なコンパイラー/リンカーのデフォルトでのスタックサイズは調べておいても良いと思います。
承知しました。ありがとうございます!
ちなみに、スタックサイズの設定方法があまりよく分かっていなかったので質問が一点あります。
僕は今まで実行ファイルにスタックサイズが書き込まれていると思っていたのですが、僕の環境ではデフォルトだと書き込まれていませんでした。(これが正しいスタックサイズの調べ方か分かりませんがreadelfしてGNU_STACKと書かれたところを読んでいます)
ChatGPTに聞いたところ(いい日本語記事が見つけられなかった)、通常は書きこまれていないが、コンパイラで指定することもできる(実行ファイルに書き込まれる)。だが、最終的には(ulimit等のシステム設定などを考慮して)OSが実行時に決める。
とのことだったのですが、この認識はあっているのでしょうか?
一応確かにコンパイラでスタックサイズ=256MBを指定してGNU_STACKが書き変わっているのは確認したのですが、実行した時のスタックサイズは/proc/$PID/smapで見たら136KBのままで変わっていませんでした。
There was a problem hiding this comment.
スタックとヒープで割り当てられた変数の生存期間の違いやマルチスレッド環境での変数の共有についてはどういう認識でしょうか?
スタックはブロックスコープを抜ければ勝手に解放されます。(スタックポインタがずれて範囲外に出るだけ)
ヒープは手動で開放(free, delete)しなければなりません。
マルチスレッドの時のスタックがどこに確保されるのか曖昧だったので調べたのですが、ヒープ上みたいですね。
マルチスレッドにおける変数ですが、ヒープの場合はアクセス自体はミューテックスやセマフォとかでロックして行う、確保や解放はどこか一つのスレッドがきちんと担当するか、参照カウンタ(これ自体のアクセスもスレッドセーフにする)を作ってカウントすればいいのかなと思っています(経験ないので予想です)。
スタックの方の共有は、そもそもスタックの変数がローカルスコープなので共有すべきではない気がしますが、
別のスレッド間で共有した変数にアドレスを渡すことで一応共有することが出来ます。
もしやるなら、その変数が他スレッドで使われている間はそのスコープを抜けないように気を使って設計をすることが必要になると思います。(アクセス方法はヒープと同じようにミューテックス等を使います。)
There was a problem hiding this comment.
仮想アドレス空間0~0x7fffffffffffのメモリの使われ方を先頭から順に左から書きました。
元の話題はスタックメモリとヒープメモリの違いでした。仮想アドレス空間の話はレイヤーが異なっており、その話が出てくることに違和感を感じました。
メモリを確保する処理速度が速いということです。
これは明示的に書いていただかないと分かりませんでした。
(すみません、スループットに当たるのが何かわからないのですが、レイテンシーになるかと思います(スループットも早い気がする)。)
スループットとレイテンシーは、メモリからデータを読み込むときの話です。速度と言われてメモリの読み込みにかかる時間を連想しました。メモリの確保の時間を連想する人は少ないかもしれません。
(CPUキャッシュの観点ではヒープより参照の局所性が効きそうです。が、CPUキャッシュの仕組みをまだちゃんと理解していないので差が見積もれません)
局所性が効くとどうなりますか?
(もっと具体的な値が必要でしたら、これは僕のマジで適当な予想なのですが、早くて(すぐに見つかった時)少なくとも3命令ぐらい、多いとき100命令超えそう、そしてOSに要求するとなったら際はAPI呼び出しでもっとかかる、みたいなスピードかな、と思っています。)
実機で計測するにはどのようにしますか?
物理メモリに乗っているという意味では他のプロセスのメモリの量と、それらがどれだけアクティブであるか(ページアウトしないか)によると思います。
物理メモリの容量と、仮想メモリに使用するストレージの容量の両方について話さないことに違和感を感じました。
とのことだったのですが、この認識はあっているのでしょうか?
そのあたりは自分は詳しくありません。ご自身でお調べいただき、ここで共有いただくのがよいと思います。
There was a problem hiding this comment.
元の話題はスタックメモリとヒープメモリの違いでした。仮想アドレス空間の話はレイヤーが異なっており、その話が出てくることに違和感を感じました。
スタックとヒープがどこで確保されるかを示したつもりでした。まあ確かに関係ないですかね。
(スタックメモリやヒープメモリの名前の由来がこれらの領域みたいな感覚があったのですが、よくよく考えたら逆ですかね。単にスタックメモリやヒープメモリをそこに置いてるからそういう名前がついてるんでしょうか。まあともかく、スタックは端の方から(アドレス降順に)とってくる、ヒープは反対側の広い領域にある、です)
これは明示的に書いていただかないと分かりませんでした。
スループットとレイテンシーは、メモリからデータを読み込むときの話です。
なるほどです。勉強になります!
局所性が効くとどうなりますか?
こちらは確保にかかる時間ではなく、アクセスにかかる時間が早くなります。CPUキャッシュに載っている確率が高くなるからです。
実機で計測するにはどのようにしますか?
ヒープメモリの確保速度は、それまでにどれだけのメモリが使われたか、それらのメモリがそれぞれ要求されたサイズはどれくらいか、その順番、等によると思います。
これらのファクターを実際に使う範囲に似せて実験するのが良いと思いますが、一般的な差を測る場合は例えば以下のような感じでしょうか。
- ヒープに要求する最小メモリサイズから最大メモリサイズの間で、乱数を振って確保することを繰り返す。
- 合計サイズがある程度のサイズになったら全部解放して、それをN回繰り返す。
- 同じプログラムで、要求先をスタックに変更して(int[変数]で確保。gccの拡張か何かでこの文法が使えたはず。ある程度のサイズを超えるとヒープに要求し始めるかもしれないのでそこは考慮する必要がある)実験します。
- そしてかかった時間が何倍違うかで見積もります。
- 繰り返す回数Nは、ある程度目に見える差がでる大きさに設定すればよいです。
- ちなみに、最悪ケースという意味では、最小メモリサイズを何度も要求しまくるときになると思います。
- (なおやったことがあるわけではなく他の部分(乱数振るところなど)で結構時間取られるかもしれないので、その場合は臨機応変に実験内容を変える必要があります)
今回の場合だったら、どちらも要求メモリ量は128バイトに設定して、確保直後に解放を繰り返せばいい(というか配列+-1フィルとvector<int>(128, -1)で比較すればいい)ですね。
物理メモリの容量と、仮想メモリに使用するストレージの容量の両方について話さないことに違和感を感じました。
すみません、物理メモリサイズの方は別にコンテナになってて容量が調整可能みたいなことを考えていたわけではないのですが、単純に当たり前だったので言及を忘れていました。
(ちなみにストレージサイズにも疑問符がついているのは経験的にストレージサイズよりも前にシステムがクラッシュするかスローダウンするかしていたのでストレージサイズが制限となりうることに確信が持てなかったためです。何か別の要因があるのではないかと。ページアウトしている量が多ければディスク読み書きが発生してスローダウンの原因となると思うのですが、結局それがメモリサイズがストレージサイズによる制限を受ける前に発生する制限(その前にスローダウンするかクラッシュする)という感じなんですかね)
そのあたりは自分は詳しくありません。ご自身でお調べいただき、ここで共有いただくのがよいと思います。
承知しました。あとで調べた結果を書きたいと思います!
There was a problem hiding this comment.
これらのファクターを実際に使う範囲に似せて実験するのが良いと思いますが、一般的な差を測る場合は例えば以下のような感じでしょうか。
実際に実験してみて、結果を提示してみていただけますか?
ヒープに要求する最小メモリサイズから最大メモリサイズの間で、乱数を振って確保することを繰り返す。
実験をするのであれば、最もシンプルな条件から始めるのがよいと思います。自分なら、
- 2^0 バイトを N 回確保する時間を計測する。
- 2^1 バイトを N 回確保する時間を計測する。
- 2^2 バイトを N 回確保する時間を計測する。
- ...
と順々に増やしていき、バイト数と確保にかかる時間の相関を調べると思います。
要求先をスタックに変更して
alloca() 関数でスタックメモリに領域を確保することができます。ただし、この機能は現在は推奨されていないようです。
そしてかかった時間が何倍違うかで見積もります。
その方法ですと、ループの実行にかかった時間が結果に含まれてしまい、メモリの確保と解放にかかる時間のみを計測したことにならないと思います。メモリの確保と解放にかかる時間のみを計測するにはどうしますか?
さらに言えば、
(もっと具体的な値が必要でしたら、これは僕のマジで適当な予想なのですが、早くて(すぐに見つかった時)少なくとも3命令ぐらい、多いとき100命令超えそう、そしてOSに要求するとなったら際はAPI呼び出しでもっとかかる、みたいなスピードかな、と思っています。)
と言っているのですから、「何倍違う」ではなく、何命令もしくは何 ns 程度かかるかを結果として出力するのが妥当ではないでしょうか。
最悪ケース
これはどういう意味で最悪なのですか?
今回の場合だったら、どちらも要求メモリ量は128バイトに設定して、確保直後に解放を繰り返せばいい(というか配列(+-1フィル)とvector(128, -1)で比較すればいい)ですね。
-1 で埋める操作が含まれるため、メモリの確保と解放にかかる時間のみを計測できていないように思います。メモリの確保と解放にかかる時間のみを計測するにはどうしますか?
| int max_len = 0, unique_str_begin = 0; | ||
|
|
||
| for (int i = 0; i < s.size(); ++i) { | ||
| unsigned char char_code = s[i]; |
There was a problem hiding this comment.
https://cpprefjp.github.io/reference/string/basic_string.html
using string = basic_string<char>;
とありますので、 std::string の要素の方は char と考えて良いと思います。わざわざ unsigned char にキャストする必要はないと思います。
| vector<int> ascii_to_last_index(256, -1); | ||
| int max_len = 0, unique_string_begin = 0; | ||
| vector<int> char_code_to_last_index(256, -1); | ||
| int max_len = 0, unique_str_begin = 0; |
There was a problem hiding this comment.
好みの問題ですが、このくらいのスコープであれば、unique_str_beginはもう少し単純な変数名で良いかとも思いました。(start, window_start など)
There was a problem hiding this comment.
好みの問題ですが、
僕も長すぎないかと気になっていたところです
短くしたいと思います。ありがとうございます!
https://discord.com/channels/1084280443945353267/1084283898617417748/1192721655160639569 すみません、ちゃんと読んでませんでした。 |
| public: | ||
| int lengthOfLongestSubstring(string s) { | ||
| int ascii_to_last_index[128]; | ||
| for (int i = 0; i < 128; ++i) ascii_to_last_index[i] = -1; |
There was a problem hiding this comment.
fill(ascii_to_last_index, ascii_to_last_index + 128, -1)
There was a problem hiding this comment.
fill、そうでした…
ありがとうございます!
| if (ascii_to_last_index[s[i]] >= begin) { | ||
| begin = ascii_to_last_index[s[i]] + 1; | ||
| } |
There was a problem hiding this comment.
begin = max(begin, ascii_to_last_index[s[i]] + 1)
There was a problem hiding this comment.
全然気づかなかったです
ありがとうございます!
注意
ファイルが複数ある場合、<番号>.cppの<番号>が最も大きいものが最新版です!
最新版のみレビューお願いします!(もちろん過去のものをレビューしていただいても構いませんmm)
問題
https://leetcode.com/problems/longest-substring-without-repeating-characters/description/?envType=problem-list-v2&envId=xo2bgr0r