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
91 changes: 91 additions & 0 deletions 125_valid_palindrome/memo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# 125. Valid Palindrome

https://leetcode.com/problems/valid-palindrome/

## Comments

### step1

* とりあえず最小限のリファレンスで解いてみた。
* https://cpprefjp.github.io/reference/cctype/isalnum.html
* signature が `int isalnum(int ch);` で、int を返すことになっているが、bool じゃないんだ。C の名残?
* https://cpprefjp.github.io/reference/locale/tolower.html
* 個人的には、最初のループで alnum でないやつの除去 + 小文字変換をやってから、2 回目のループで palindrome check をしたい。
* 除去 + 小文字変換は private な関数に切り出して string_view? を返すとかしたい。

### step2

* https://github.com/colorbox/leetcode/pull/7/files
* `+=` しながら `checker` 文字列を作っている。Python だと文字列コピーが発生する (ただし最適化の実装による) のでやりたくない感じなんだが、C++ だとどうなの?
Copy link

Choose a reason for hiding this comment

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

C++ では文字列は mutable です。

* 上記だと `checker` を reverse している。
* step1 でのコメントも踏まえて `step2/Solution` はとりあえず書いてみた。関数では文字列コピーして返しているから冗長な感じはする。できればコピーはせずに使いたい気もする (できるのかな?無理そう)
Copy link

Choose a reason for hiding this comment

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

まず、これは named return value optimization が働いてコピーが自動的に消える可能性があります。
コピーしたくなければ、リファレンスかポインターを引数に渡します。

Copy link
Owner Author

Choose a reason for hiding this comment

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

std::string to_lower_alnum(std::string& s) {
    ...
}
bool isPalindrome(string s) {
    std::string lower_alnum_string = to_lower_alnum(s);
    ...

のような感じでしょうかね。(N)RVO の話はあまり考慮していませんでした。
https://cpprefjp.github.io/lang/cpp17/guaranteed_copy_elision.html
ちょっとフォローしきれていないので別途勉強します。

もともとここで意図していたのは、isPalindrome の引数 s の非連続 (である可能性がある) な部分文字列がlower_alnum_string であり、lower_alnum_string の中の文字 (char) はすべて s に含まれているはずなので、s のメモリを再利用して lower_alnum_string を (string_view のようなイメージで) 作れないのか、というものでした。
非連続な部分文字列なのでやはり難しい気がしました。

Copy link

Choose a reason for hiding this comment

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

void to_lower_alnum(std::string& from, std::string* to);
std::string lower_alnum_string;
to_lower_alnum(s, &lower_alnum_string);

こうです。

長い文字列から数文字を消去した文字列を空間をできるだけ再利用して表現したいということですね。

Rope というデータ構造を思い出します。これは、木構造で文字列を管理して編集されていない部分木を共有します。split, concatenate が O(log n) でできます。
Piece Table はテキストエディターなどで使われるもので、変更履歴を追いかけていくものです。

一般的に連続したメモリーに対しての処理は速いので普通はコピーしたほうがいいでしょうが、テキストの編集は頻繁に行われてきたのでよく研究されています。

Copy link

Choose a reason for hiding this comment

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

概ね連続したメモリーのコピーは 1 clock で4-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.

ありがとうございます!

こちらでもコメントいただいている箇所ですね。先に書き込み先を用意して関数にそこに書き込んでもらう、というのが馴染みないですが、慣れていきたいと思います。

#5 (comment)

string を引数に値渡しで渡すと、コピーが発生します。stringをコピーする場合、元の文字列が格納できるサイズのメモリをヒープに確保し、文字列をコピーします。できればこのストは避けたいです。 const string& と const 参照渡ししたほうが良いと思います。
また、戻り値を string の値型で返すと、同じようにコピーが発生することが多いと思います。引数に参照型の引数を追加し、そこに出力を渡すのが良いと思います。
サイズが十分小さかったり、定数倍の速度が求められないのであれば、値渡しをしたり、値型を返しても良いかもしれません。

Copy link
Owner Author

Choose a reason for hiding this comment

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

概ね連続したメモリーのコピーは 1 clock で4-8バイトくらいできるのではないかと思います。
木構造をたどるとどうしてもたどるたびに数クロックかかるでしょう。

なるほど…このあたりの数字感覚がなかったのですが、どのようなロジックで推論されるものでしょうか。

追記: 後のコメントから辿って多少理解しました。32 or 64 ビット CPU の想定から 4-8 バイトと見ているんですね (ただのコピーなので機械語的にも 1 命令で、特にオーバーヘッドがない処理のはずなので)。
Ryotaro25/leetcode_first60#66 (comment)

結局、切り替えることによってどれくらい実際に速くなるかを見積もって、別のライブラリーを使う必要がでてくるデメリットなどと勘案するという比較衡量をして欲しいですね。

今回であれば 1 <= s.length <= 2 * 10^5 という制約で、ASCII で 1 char / 1 byte と仮定するなら、200KB くらいなので、丸ごとコピーすることになるとすると 4 bytes / clock と仮定して 50K clocks くらい必要。昨今の CPU なら数 GHz / sec くらいなので、1/100 秒 (10ms)とかそのくらいでしょうか。まあこのくらいならいいかな…という気がしてきます。

Copy link

Choose a reason for hiding this comment

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

先に書き込み先を用意して関数にそこに書き込んでもらう

Google のスタイルガイドもともとこうだったんですが、最近はしないようになっているみたいです。NRVO が効く前提なんでしょうか。

Copy link

Choose a reason for hiding this comment

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

数字感覚
実行時間の見積もりは、具体的な CPU によって異なる+上から抑えたいためかなり適当です。

  • 2000年頃のアーキテクチャでも浮動小数点 double は64ビットなので64ビットは一命令で動かせるでしょう。これは遅い見積もりです。
  • 変更なしでメモリーをコピーするならば SIMD 命令などではるかに速いでしょう。
  • サイズによっては L1/L2 キャッシュが効く。ないとメモリー帯域は 10 GB/s の桁。

Jeff Dean の表(2012年)は Read 1 MB sequentially from memory 250 us としていました。

なんていうか、見積もりは色々な傍証から「こんなものだよな」とやっているのでそんなにロジカルなものではないです。機械語でどうなりそうかを考えてアーキテクチャの知識と照らし合わせています。

* https://cpprefjp.github.io/reference/string_view/basic_string_view.html
* > 文字配列の参照する先頭文字へのポインタと、文字数の2つをメンバ変数として持つ。
* とあるので今回のように非連続な文字列になる場合は view 使えないのかな。それなら vector に push して一番最後で string に変換? (Python の append -> join と同じ)
* 追記: そもそも `std::string` 自体 `vector<char>` 的なものとして実装されているので、Python のような処理は不要。
* https://qiita.com/LdCqh1/items/92f286ceb0ab96dc3c09
* https://zenn.dev/reputeless/books/standard-cpp-for-competitive-programming/viewer/string
* > C++ 標準ライブラリは、任意の文字型 Char に対して、便利な文字列処理を提供するためのクラステンプレート std::basic_string<Char> を定義しています。それを char 型に対して特殊化(std::basic_string<char>)したものが std::string です。
* `Char` と `char` って C++ だと違うものな?(TODO)
Copy link

Choose a reason for hiding this comment

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

ここでの Char は一般的に Char と書けば通じるものではありません。
template <class Char> で表現するように、wchar_t などを代入するという意味(変数 x みたいな意味)です。

* 軽く検索してみたがよくわからん
* `union` (TODO) とかあるんだ。。
* `size_t` という型がある
Copy link

Choose a reason for hiding this comment

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

sizeof の返す型。vector size もこの型のことが多いです。
「符号なし」なので引き算をするとたまにあふれてはまります。

* https://cpprefjp.github.io/reference/cstddef/size_t.html
* https://rinatz.github.io/cpp-book/ch07-12-unions/
* 共用体というやつか。聞いたことある。
* > 構造体の大きさがメンバーのすべての型の大きさの総和にパディングなどを足したものであったのに対して、 共用体ではメンバーの型の大きさの最大値にパディングなどを足したものとなります。 結果としてメモリーを節約することができるので、複数の型のどれかを格納したいという場合にはよく用いられます。

```cpp
// Cited from https://zenn.dev/reputeless/books/standard-cpp-for-competitive-programming/viewer/string#1.3-std%3A%3Astring-%E3%81%AE%E5%86%85%E9%83%A8%E6%A7%8B%E9%80%A0-(3)
struct string
{
char* m_ptr; // 確保したメモリの先頭を指すポインタ
size_t m_size; // 格納している文字列の長さ

static constexpr size_t LocalCapacity = 15; // 動的確保不要で格納できる文字列の最大の長さ

union
{
char m_local_buffer[LocalCapacity + 1]; // 文字列の長さが LocalCapacity 以下の場合、動的確保の代わりに使う配列
size_t m_allocated_capacity; // 動的確保した配列に格納できる文字列の最大の長さ
} m_storage;
};
```

* contd.
* > 2.2 文字列リテラルは std::string 型ではない
* まじか。ややこしい。
* > 3.3 別の std::string から構築する
* C++ だとこれはコピーになるのか。値渡しをしているということだと思うけど、`std::string` は mutable なのかな。

```cpp
std::string s = "abc";
std::string t = s; // s の値をコピーする
```

* contd.
* > std::string 型のデフォルトコンストラクタは、要素数が 0 である空の std::string を構築します。
* なるほど、`int` だと宣言だけした場合未定義動作になると思うけど、`std::string` は定義されているのか。
* > C++20 から、範囲ベースの for 文に初期化式を追加できるようになりました。
* `for (int i = 0; const auto& ch : s)`
* へーこんなのもできるんだ
* イテレータ、逆イテレータ
* `std::views::reverse`
* `std::string_view` とは異なる
* パイプを使うんだ。この文法知らん。アダプタ?(TODO)。まあシェルの模倣だろう。
* https://onihusube.hatenablog.com/entry/2022/04/24/010041
* 実際どう実装されているのかはよくわからん。
* https://cpprefjp.github.io/reference/ranges/drop_view.html
* drop が drop する個数と view を引数に取る関数で、view の方はパイプで渡される、という意味合いかと思ったけど合ってるのかな。違う気がする。
* 上記 (https://onihusube.hatenablog.com/entry/2022/04/24/010041) の後半で解説されている気もするが、ちょっと時間かかりそうなので一旦先へ進む (TODO)
* > 11. std::string への追加
* `char` の配列として実装されているなら末尾への追加は O(1) だろうと思っていたがやはりそうだった。ただ `push_back` と `+=` の違いについては特に言及がなかった (もちろん `push_back` は `char` を取り、`+=` は string を取るという違いはあるが)
* https://cpprefjp.github.io/reference/string/basic_string/op_plus_assign.html
* ここには `append(str) と等価。` と記載がある。
* https://cpprefjp.github.io/reference/string/basic_string/append.html
* いずれにせよ、C++ は末尾に追加する分には Python みたいなことは気にしなくてよいらしい。`push_back` と `+=` の違い (char を末尾に追加する場合) はまだよくわからないが、とりあえず `push_back` を使っておこう。

### step3

* `std::char` にしたら怒られた。
* 関数に分けるパターンを採用
27 changes: 27 additions & 0 deletions 125_valid_palindrome/step1.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#include <cctype>

class Solution {
public:
bool isPalindrome(string s) {
int left = 0;
int right = s.size() - 1;
while (left < right) {
if (!std::isalnum(s[left])) {
++left;
continue;
}
if (!std::isalnum(s[right])) {
--right;
continue;
}
char c_left = std::tolower(s[left]);
char c_right = std::tolower(s[right]);
if (c_left != c_right) {
return false;
}
++left;
--right;
}
return true;
}
};
29 changes: 29 additions & 0 deletions 125_valid_palindrome/step2.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#include <cctype>

class Solution {
private:
string to_lower_alnum(string s) {
Copy link

Choose a reason for hiding this comment

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

string を引数に値渡しで渡すと、コピーが発生します。stringをコピーする場合、元の文字列が格納できるサイズのメモリをヒープに確保し、文字列をコピーします。できればこのストは避けたいです。 const string& と const 参照渡ししたほうが良いと思います。
また、戻り値を string の値型で返すと、同じようにコピーが発生することが多いと思います。引数に参照型の引数を追加し、そこに出力を渡すのが良いと思います。
サイズが十分小さかったり、定数倍の速度が求められないのであれば、値渡しをしたり、値型を返しても良いかもしれません。

Copy link
Owner Author

Choose a reason for hiding this comment

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

ありがとうございます。こちらの oda さんのコメントと合わせて確認しました。
https://github.com/ryosuketc/leetcode_grind75/pull/5/files#r2274872702

string lower_alnum_string = "";
for (char c : s) {
if (std::isalnum(c)) {
lower_alnum_string.push_back(std::tolower(c));
}
}
return lower_alnum_string;
}

public:
bool isPalindrome(string s) {
string lower_alnum_string = to_lower_alnum(s);
int left = 0;
int right = lower_alnum_string.size() - 1;
while (left < right) {
if (lower_alnum_string[left] != lower_alnum_string[right]) {
return false;
}
++left;
--right;
}
return true;
}
};
27 changes: 27 additions & 0 deletions 125_valid_palindrome/step3.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
class Solution {
private:

Choose a reason for hiding this comment

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

Google guideではpublicを先に宣言することがお勧めされております!

Copy link

Choose a reason for hiding this comment

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

一応、プロダクションでは分割コンパイルをするというのを意識しておきましょう。
https://docs.google.com/document/d/11HV35ADPo9QxJOpJQ24FcZvtvioli770WWdZZDaLOfg/edit?tab=t.0#heading=h.cxz8lxsufnbn

Copy link
Owner Author

Choose a reason for hiding this comment

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

ありがとうございます。このあたりを確認しました。分割コンパイルについての意識はまだ低いのでもう少し C++ に慣れたら、どういうヘッダファイルが想定されるか考えてみようと思います (production のコードでちょくちょく読んでいるので読む方の認識はあるのですが、書くほうがまだ今ひとつ)。

元の質問に戻ると、class 定義は .h に書かれてどういうクラスであるだけが書かれているので、外からのインターフェースが前で(サイズを決めるのに必要な) private 情報は後ろに来ます。ただ、普通は実体がさらに後ろに回っているので、半分は肯定ですね。

std::string to_lower_alnum(std::string s) {

Choose a reason for hiding this comment

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

Suggested change
std::string to_lower_alnum(std::string s) {
std::string to_lower_alnum(const std::string& s) {

それかmodern C++ ではstring_viewを渡すことが多いかと思います。

Copy link
Owner Author

Choose a reason for hiding this comment

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

あ、なるほど、string_view はこういうので使えるんですね。

std::string lower_alnum_string;

Choose a reason for hiding this comment

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

もしsのサイズが大きれば、ここは先にcapacityをreserveするのも良いかと思います。

Copy link

Choose a reason for hiding this comment

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

reserve の時間見積もりはこのあたりにあります。一行を加える管理コストに足るメリットがあるかという良し悪しを評価するという方向に頭を動かしたかということが大事です。
https://docs.google.com/document/d/11HV35ADPo9QxJOpJQ24FcZvtvioli770WWdZZDaLOfg/edit?tab=t.0#heading=h.isflp7vsmzk2

Copy link
Owner Author

Choose a reason for hiding this comment

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

reserve をそもそも知らなかったのですがこれですね。
https://cpprefjp.github.io/reference/string/basic_string/reserve.html

「計算速度の見積もり」の項は何度か見ているものの、すべて身についている感じはしないので折に触れて確認していきたいと思います。
https://docs.google.com/document/d/11HV35ADPo9QxJOpJQ24FcZvtvioli770WWdZZDaLOfg/edit?tab=t.0#heading=h.xbcr3241jgv8

for (char c : s) {
if (std::isalnum(c)) {
lower_alnum_string.push_back(std::tolower(c));
}
}
return lower_alnum_string;
}

public:
bool isPalindrome(string s) {
std::string lower_alnum_string = to_lower_alnum(s);
int left = 0;
int right = lower_alnum_string.size() - 1;
while (left < right) {
if (lower_alnum_string[left] != lower_alnum_string[right]) {
return false;
}
++left;
--right;
}
return true;
}
};