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
12 changes: 12 additions & 0 deletions Arai60/52. Is Subsequence/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
NAME = a.out
SRC = main.cpp
CXX = g++
CXXFLAGS = -Wall -Wextra -Werror -std=c++23

all: $(NAME)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

個人用なので気にしてないとかだったらすみません
ターゲット(コロンの左側)がファイル名ではない場合は以下のようにするといいです。
ターゲットが疑似ターゲットでファイルでないことを示します(runもファイルでないので同じようにします)

.PHONY: all
all: $(NAME)
...

(また、一番上のターゲットはデフォルトターゲットでmakeだけで実行できるので(このような練習用リポジトリだと僕だったらrunを一番上にします(もしくは.DEFAULT_GOALを指定することでもできそう)))

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

ありがとうございます。おっしゃる通り、.PHONY等は省略しています。
また、実行は明示的に行いたいので、自分は一番上にはしないことにしています。


$(NAME): $(SRC) step1.hpp step2.hpp step3.hpp
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

また、普通はヘッダーファイルはコンパイラに渡さずソースファイル(.cpp)から#includeのみします

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

コンパイルコマンドは$(CXX) -o $(NAME) $(SRC) $(CXXFLAGS)ですので、ヘッダーファイルはコンパイラに渡していません。
ヘッダーファイルに変更があったときに再コンパイルされるようにしたいので、$(NAME)ターゲットに対するprerequisitesとして指定しています。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

あ、すみません、誤読してましたmm

$(CXX) -o $(NAME) $(SRC) $(CXXFLAGS)

run: $(NAME)
./$(NAME)
25 changes: 25 additions & 0 deletions Arai60/52. Is Subsequence/main.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#include <iostream>
#include <string>

#include "step2.hpp"

#define test(subsequence, text, expected) \
[] { \
bool actual = Solution4().isSubsequence(subsequence, text); \
std::cout << "Test: " << #subsequence << " in " << #text << "\" : "; \
if (actual == expected) { \
std::cout << "OK" << std::endl; \
} else { \
std::cout << "Failed" << std::endl \
<< "Expected: " << expected << ", but got: " << actual \
<< std::endl; \
} \
}()

int main(void) {
test("abc", "ahbgdc", true);
test("axc", "ahbgdc", false);
test("", "ahbgdc", true);
test("a", "", false);
test("a", "b", false);
}
35 changes: 35 additions & 0 deletions Arai60/52. Is Subsequence/step1.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#ifndef STEP1_HPP
#define STEP1_HPP

/*
何がわからなかったか
- N/A
何を考えて解いていたか
- tを前から舐めていけばいい
正解してから気づいたこと
- Follow upに対応するためには、tを毎回舐めると無駄なので、前処理をしておいて
ある文字がtのなかのどこにあるかを高速に調べられるようにしておくといい
*/
#include <string>

class Solution {
public:
// Naive solution
bool isSubsequence(const std::string &subsequence, const std::string &text) {
if (subsequence.empty()) {
return true;
}
size_t i = 0;
for (auto c : text) {
if (subsequence[i] == c) {
++i;
if (subsequence.size() == i) {
return true;
}
}
}
return false;
}
};

#endif // STEP1_HPP
246 changes: 246 additions & 0 deletions Arai60/52. Is Subsequence/step2.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
/*
講師陣はどのようなコメントを残すだろうか?
-
他の人のコードを読んで考えたこと
- https://github.com/philip82148/leetcode-arai60/pull/7
関数プロトタイプとかは与えられたものと考えるのではなく、より好ましいものに
変えちゃった方がいい
- https://github.com/Yoshiki-Iwasa/Arai60/pull/62
Edit distanceの問題として捉えることもできるのか.
https://en.wikipedia.org/wiki/Levenshtein_distance
DPでも解ける, なるほどやってみよう
改善する時にかんがえたこと
- 同じtextに対してqueryが来ないなら初期化したcharacterPositionsをメンバ変数で
持ってもいいし、Constructorで初期化してもいい。
- Constructorで初期化する場合関数プロトタイプが変わってテストをそのためだけに
書き直したり用意するのが面倒なのでここではやってない。
- 複数のtextに対してsubsequenceをチェックする場合、textごとに初期化する。
ただし、textが大きい場合はmapのkeyとしてtextも保存する羽目になるので、
メモリを食ってしまうかもしれない。衝突を許容して少ない数のtextだったら
key自体もhashにしてしまってもいいかもしれない?
- `auto positions = characterPositions[c];`と元々はしていたけれど
referenceにしておかないと意図せぬcopyが発生してしまう
*/
#ifndef STEP2_HPP
#define STEP2_HPP

#include <functional>
#include <map>
#include <string>
#include <vector>

class Solution1 {
// Follow up
/* Suppose there are lots of incoming s, say s1, s2, ..., sk where k >= 109,
* and you want to check one by one to see if t has its subsequence. In this
* scenario, how would you change your code?
*/
public:
bool isSubsequence(const std::string &subsequence, const std::string &text) {
std::map<char, std::vector<size_t>> characterPositions;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

vectorの代わりにsetでもいいんじゃないでしょうか(L49のlower_boundの書き方がすっきりします)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

なるほど、要素がuniqueであることが保証されているためsetでも良いのですね。
確かに、この方がスッキリと書けますね。

positions.lower_bound(searchPos);

https://en.cppreference.com/w/cpp/container/set
https://en.cppreference.com/w/cpp/container/set/lower_bound

setを使う場合はcharacterPositionsの初期化がO(N)ではなくO(NlogN)になりそうかなと思ったのですが、今回はそもそも挿入時にソート済みであることが確定しているためヒント付きの挿入操作を使うことでO(N)にすることができそうなので、この方が好ましいと思いました。ありがとうございます。

      auto &positions = characterPositions[c];
      positions.insert(positions.end(), i);

https://en.cppreference.com/w/cpp/container/set/insert

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

setを使う場合はcharacterPositionsの初期化がO(N)ではなくO(NlogN)になりそうかなと思ったのですが、今回はそもそも挿入時にソート済みであることが確定しているためヒント付きの挿入操作を使うことでO(N)にすることができそうなので、この方が好ましいと思いました。

あー確かに…。すみません、そこ何も考えてませんでした(笑)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

現実的には log N の部分は、定数と同程度にしか気にしないと思います。N が 1T としても40とかですからね。

for (size_t i = 0; i < text.size(); ++i) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

講師の方に聞きたいんですが、こういう時に選ぶiの型って左辺がsize_tである限り、(多くの場合は)何も考えずにsize_tを選ぶで大丈夫なんですかね?
自分がintをデフォルトで書いているので気になりました。
(intがだいたい32bit、size_tがだいたい64bitであるゆえに、intで値が表現できない場合を除いてです。
制約がtext.size()<=1e4となっているので)
(chromium code searchだと両方がありました)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

私はこれはどちらでも許容です。状況依存かもしれません。
int はその処理系で一番扱いやすい整数という意味ですね。size_t の方が新しいです。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@oda なるほどです!ありがとうございます!

char c = text[i];
characterPositions[c].push_back(i);
}
size_t searchPos = 0;
for (char c : subsequence) {
auto &positions = characterPositions[c];
auto found =
std::lower_bound(positions.begin(), positions.end(), searchPos);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

二分探索が使えるときはだいたい尺取り法が使えることが多く、今回の場合そちらの方が読みやすいかなと思います

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

おっしゃる通りですね。

step1は尺取り方で解いているのですが、こちらの解法はコメントにもあるとおりFollow Upに対するものになっています。
Follow upで聞かれているような状況(多数のクエリ(例えば 10^9 個以上)が与えられる場合)では、毎回 t 全体を線形走査するのは非効率になる可能性があります。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

あ、いえ、僕が言っているのはこちらです!

philip82148/leetcode-swejp#7 (comment)

@usatie さんの回答だと O(max(subsequence.size() * log (text.size()), text.size())) じゃないかなと思うのですが、こちらだとO(max(subsequence.size(), text.size()))になるかと。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

あ、すみません、勘違いしてましたmm
characterPositionsの初期化を省ける前提なら、O(subsequence.size() * log (text.size())で処理できるということですか。
なるほどです

if (found == positions.end()) {
return false;
}
searchPos = *found;
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

当初はssize_t searchPos = -1std::upper_boundで書いていたのですが、lower_boundに変更した際にsearchPos = *found + 1と変更しないといけないところを見落としていました。

以下、Solution1〜Solution3はすべて、同じ間違いをしています。

}
return true;
}
};

// If text is the same all the time, we only need to initialize the data once.
class Solution2 {
private:
std::map<char, std::vector<size_t>> characterPositions;

public:
bool isSubsequence(const std::string &subsequence, const std::string &text) {
if (characterPositions.empty()) {
for (size_t i = 0; i < text.size(); ++i) {
char c = text[i];
characterPositions[c].push_back(i);
}
}
Comment on lines +66 to +71
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

※講師役の方感覚が違ったら訂正していただきたいです

本当はこういうのはコンストラクタで初期化したいところなんですが、LeetCodeの性質上引数として渡されてしまいます。
つまり、「コンストラクタ(事前処理)で」「characterPositionsを初期化する」というのが「textが前と同じだったら」「characterPositionsを初期化する」となっているので、その思考のままで行くと、僕だったらif文内を別のメソッド(initCharacterPositions())に分けます。

if (条件式) initCharacterPositions(text);

条件式のところはcharacterPositions.empty()で十分だと思いますが、もしロジックが変わっても(「textが前と同じだったら」をより丁寧に表現する必要がでてきた場合等)、ここを関数にして別にすればいいので分かりやすい/変更しやすいと思っています。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

確かにその方がいいですね。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

副作用のある関数であるという点のほうが気になります。この関数はマルチスレッドで呼ばれた場合に競合状態になる可能性があります。競合状態を避けるため、副作用のある書き方は現実的な範囲でできるだけ避けたほうがよいと思います。また、どうしても副作用のある書き方をしなければならない場合は、適切にロックを取る必要があるかを考慮するのが良いと思います。

size_t searchPos = 0;
for (char c : subsequence) {
auto &positions = characterPositions[c];
auto found =
std::lower_bound(positions.begin(), positions.end(), searchPos);
if (found == positions.end()) {
return false;
}
searchPos = *found;
}
return true;
}
};

// If we expect multiple but small texts, and massive subsequence queries,
// we can initialize the data for each text.
class Solution3 {
private:
typedef std::map<char, std::vector<size_t>> character_positions_t;
std::map<std::string, character_positions_t> initialized_data;
void initialize(const std::string &text) {
auto &characterPositions = initialized_data[text];
for (size_t i = 0; i < text.size(); ++i) {
char c = text[i];
characterPositions[c].push_back(i);
}
}

public:
bool isSubsequence(const std::string &subsequence, const std::string &text) {
if (initialized_data.count(text) == 0) {
initialize(text);
}
Comment on lines +102 to +104
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

initialized_dataじゃ何か分からないのでtext_to_character_positionsとかがいいと思います

auto &characterPositions = initialized_data[text];
size_t searchPos = 0;
for (char c : subsequence) {
auto &positions = characterPositions[c];
auto found =
std::lower_bound(positions.begin(), positions.end(), searchPos);
if (found == positions.end()) {
return false;
}
searchPos = *found;
}
return true;
}
};

// Dynamic Programming
class Solution4 {
public:
bool isSubsequence(const std::string &subsequence, const std::string &text) {
size_t m = subsequence.size();
size_t n = text.size();
// is_subseq[i][j] : subsequence[0..i) is a subsequence of text[0..j)
std::vector<std::vector<bool>> is_subseq(m + 1,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

std::vector については何度か議論があったと思います。それらを意識したうえで使うのであれば大丈夫だと思います。過去のコメントを探してみることをお勧めいたします。

std::vector<bool>(n + 1, false));
for (size_t j = 0; j <= n; ++j) {
is_subseq[0][j] = true;
}
for (size_t i = 1; i <= m; ++i) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

レーベンシュタインの編集距離を思い出しました。

for (size_t j = 1; j <= n; ++j) {
if (subsequence[i - 1] == text[j - 1]) {
is_subseq[i][j] = is_subseq[i - 1][j - 1];
} else {
is_subseq[i][j] = is_subseq[i][j - 1];
}
}
}
return is_subseq[m][n];
}
};

// Recursive
class Solution5 {
public:
// ラムダでの再帰関数がうまく書けなくて、std::functionを使うことになった
// 書ける場合と書けない場合の違いは何だろうか?
bool isSubsequence(const std::string &subsequence, const std::string &text) {
std::function<bool(size_t, size_t)> is_subseq = [&](size_t i,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ちょっと詳しい方がいたら触れてもらおうと思っていたのですが、もしかしたら触れられずに終わってしまうかもしれないので一応コメントしておきます。

std::functionを使う場合、auto func = ...の場合と違ってキャプチャされた変数を保存するメモリが動的に(ヒープに)確保される可能性があります。
それに、呼び出しも少しオーバーヘッドがあります。

なので使えるときはauto func = ...の形式がいいと思います(なおこれで再帰を書くには引数に渡すしかありません)。

https://stackoverflow.com/questions/46163607/avoid-memory-allocation-with-stdfunction-and-member-function
https://uchan.hateblo.jp/entry/2019/03/17/070736
https://timsong-cpp.github.io/cppwp/n4950/function.objects (規格書)

※良いリファレンスが見つけられずすみません。実装を読むのが早いかもしれません。

Copy link
Copy Markdown

@philip82148 philip82148 Feb 9, 2025

Choose a reason for hiding this comment

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

ちなみに多分ですが、再帰が書けるか否かは左辺と右辺が厳密には違うものであることに起因しているんじゃないですかね。
L151の右辺は関数オブジェクトですが、左辺はそれを参照する関数ポインタですので。
(L151は右辺を第一引数にfunctionのコンストラクタを呼び出す、という文の糖衣構文になっています。)
auto func = ...だと左辺と右辺が同じものになります。

型推論に起因する問題な気がしてきました。

※C++は言語仕様巨大すぎて、どんなにC++学んでも初心者を抜け出せないというミームがあるくらいなので、最初の方はここら辺はあんまり理解していなくてもいいんじゃないかと個人的には思っています。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

型推論の問題のようですね。
https://www.dev0notes.com/intermediate/recursive_lambdas.html

size_t j) -> bool {
if (i == 0) {
return true;
}
if (j == 0) {
return false;
}
if (subsequence[i - 1] == text[j - 1]) {
return is_subseq(i - 1, j - 1);
} else {
return is_subseq(i, j - 1);
}
};
return is_subseq(subsequence.size(), text.size());
}
};

// C++17 way to write recursive lambda
// Recursive (Lambda), only works with C++17
// If auto is used as a type of a parameter or an explicit template parameter
// list is provided(since C++20), the lambda is a generic lambda. (since C++14)
// https://en.cppreference.com/w/cpp/language/lambda
class Solution6 {
public:
// 引数で自身を渡してあげれば書けるというのがC++17の機能だった模様
// というか、引数でautoを使える(=Generic Lambda)のがC++17から
// https://github.com/usatie/leetcode/pull/4/files#diff-43e2749181b31eb4f4bf1fd95048e0d7f2fb65e9b44c84eefaf3905a22349a90R77
//
// `auto &&self`というのはどこからか引っ張ってきたコードだったけど、
// `auto self`でも`auto &&self`でも動いた. それぞれの挙動の違いが違いが
// わかっていない. `&`にしないと関数オブジェクト?のコピーが発生するのはわかる
// 気がするが`&&`は普段使ったことがない. また、cppreferenceのサンプルコードも
// `auto`だったので、referenceにする必要があるのかもわからない
// https://stackoverflow.com/questions/29859796/c-auto-vs-auto
//
// ChatGPT-o3-mini-highに質問してみて、だんだんと違いがわかってきた。
// 色々と理解が曖昧なものがあったが、さらにドキュメントを読む必要がありそう
// lvalue, rvalue, move constructor, move semantics, perfect forwarding,
// type erasure, std::function
// https://chatgpt.com/share/67a67173-839c-8000-913d-ca67b6aa6859
//
// たくさんあるのでひとまず積読ですが、徐々に読んでみます。
// https://chatgpt.com/share/67a67173-839c-8000-913d-ca67b6aa6859
// https://en.cppreference.com/w/cpp/language/auto
// https://en.cppreference.com/w/cpp/language/reference
// https://en.cppreference.com/w/cpp/language/value_category
// https://en.cppreference.com/w/cpp/utility/move
// https://en.cppreference.com/w/cpp/language/move_constructor
// https://en.cppreference.com/w/cpp/language/move_assignment
// https://en.cppreference.com/w/cpp/utility/forward
// https://en.cppreference.com/w/cpp/utility/functional/function
//
// Type Erasure
// https://cplusplus.com/articles/oz18T05o/
// https://davekilian.com/cpp-type-erasure.html
bool isSubsequence(const std::string &subsequence, const std::string &text) {
auto is_subseq = [&](auto &&self, size_t i, size_t j) -> bool {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ラムダ関数にラムダ関数を渡すのはエンジニアのコード(競プロ以外)ではあまり見ないパターンという認識なんですが、どうですかね??
僕も再帰ラムダ関数使った方がすっきりすると良く思うんですけど、実際にはちゃんとしたメソッドや関数として書くべきなのかなと思っています
※講師役の方感覚が違ったら訂正していただきたいです

それは気にしない前提で話しますと、ラムダ関数というのはoperator()()メソッド(演算子のオーバーロード)を持った無名クラスのオブジェクトで、フィールドとしてキャプチャした変数を持ちます。
なので、もし渡すなら&&(右辺値参照)じゃなくて&(左辺値参照)で十分かなと思っています。
なお、もし&&(右辺値参照)にする際は関数呼び出しの際にself(move(self), ...)とするのがいいです。

以下右辺値と左辺値の説明

右辺値と左辺値はとてもややこしい問題なので簡単には説明できないです…
頑張って簡単に説明すると、左辺値は普通の変数など、右辺値は0,1,2,3...(数値)、true/false(boolean値)、'a'/"abc..."(文字、文字列)などです。
そして、変数に代入する前の関数の戻り値も右辺値になります。

auto /* resultは左辺値 */ result = /* ここでは右辺値 */  func1(/* ここでは右辺値 */  func2());

この違いがあるのは、右辺値は自分しか使わないので破壊したり変更したりしてもいいもの、左辺値は他の人が使う可能性があるので破壊したり変更したりできないもの、みたいなのがざっくりとした説明です。
そしてややこしいのはそれを直接扱わず(メモリを誰かに持ってもらって)参照するのが右辺値参照や左辺値参照ですが、参照自体は変数なので全て左辺値になることです。(笑)

auto /* selfは右辺値参照だけど左辺値 */ &&self = <右辺値>

なので、右辺値参照という左辺値を右辺値として渡すために、move()による変換が必要になります。

self(move(self), ...)

より正確な説明は色々記事を見てみるといいと思います
https://qiita.com/luftfararen/items/1de032bc6e3eb69ca672

Copy link
Copy Markdown

@philip82148 philip82148 Feb 8, 2025

Choose a reason for hiding this comment

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

あ、僕が一つ間違えていましたが、auto &&は右辺値参照じゃなくてユニバーサル参照というものですね。
なので今回の場合はauto &&は左辺値参照扱いになっているという...
なのでこのままでも大丈夫ですね…(多分?)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

はい、大丈夫だと思います。
Qiitaの記事もいいですが、cppreferenceなどのドキュメントを読まれることをお勧めします。(私も読んでいる最中ですが!)

右辺値参照で渡したほうがラムダのコピーのオーバーヘッドがないので、こうした方が良いのか?
とも思い実行してみたところ一応動くようなのですが、これは引数が評価されるタイミングでis_subseqのmoveが行われ、その後にis_subseqを呼ぶことになっているので未定義動作なのでは?とも感じます。
だとすれば、そもそもauto const &selfという宣言でいいのではという気にもなってきますね。

return is_subseq(std::move(is_subseq), subsequence.size(), text.size());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

あ、ちなみに大丈夫というのはエンジニアが見慣れたコードかという意味で大丈夫かという話だったのですが…。
(個人的に左辺値参照で足りるのにユニバーサル参照にしているのが気持ち悪い。けど気にしすぎかもしれない)
※講師役の方にコメントを願いたいです。

右辺値参照も左辺値参照も、どちらも参照なのでコピーは通常されないですよ
(コンストラクタを呼び出して新しくオブジェクトを作るときは別で、左辺値で渡したらコピーされて、右辺値で渡したらムーブします)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

あ、すみません、完全に間違えました。
「ラムダのコピーのオーバーヘッドがないので、」という部分はauto selfとしていた時に考えていたことで、先ほどはそれとごっちゃになってしまっていました。

わかります。

左辺値参照で足りるのにユニバーサル参照にしているのが気持ち悪い。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

なるほどです
あれ、そしたら疑問は解決した感じなんですかね??

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

自分自身を引数に与えるのは、ラムダ計算の理論的な文脈では見ますが、プロダクションコードで見たことはない気がしますね。
もっとも、私が C++ 書いていた頃は、まだ C++ lambda があまり一般的ではない時代と場所だったので、そもそも複雑なラムダを書かないという発想でした。

C++23 から explicit object parameter が使えるようで、LeetCode も対応しているようです。

auto を含む lambda は generic lambda なのでユニバーサル参照になるが、使っていないので普通のリファレンスでいいのではということですね。(そもそも、私はあまり複雑なラムダ書きたくないですが、これは古い人だからですね。)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@oda ありがとうございます!
ちなみに、細かくてすみませんが、ユニバーサル参照(forwarding reference)ってgeneric lambdaの文脈に限らず、auto &&varの形式だったらローカル変数でも言うんじゃないでしょうか。

https://en.cppreference.com/w/cpp/language/reference#:~:text=foo()%20may%20be%20lvalue%20or%20rvalue%2C%20vec%20is%20a%20forwarding%20reference

@usatie 複雑なlambdaはそもそもC++ではあまり書かれないようです。

philip82148/leetcode-swejp#12 (comment)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

あ、そうですね。

if (i == 0) {
return true;
}
if (j == 0) {
return false;
}
if (subsequence[i - 1] == text[j - 1]) {
return self(self, i - 1, j - 1);
} else {
return self(self, i, j - 1);
}
};
return is_subseq(is_subseq, subsequence.size(), text.size());
}
};

// C++23 way to write recursive lambda
// https://en.cppreference.com/w/cpp/language/member_functions#Explicit_object_member_functions
class Solution7 {
public:
bool isSubsequence(const std::string &subsequence, const std::string &text) {
auto is_subseq = [&](this auto self, size_t i, size_t j) -> bool {
if (i == 0) {
return true;
}
if (j == 0) {
return false;
}
if (subsequence[i - 1] == text[j - 1]) {
return self(i - 1, j - 1);
} else {
return self(i, j - 1);
}
};
return is_subseq(subsequence.size(), text.size());
}
};
#endif // STEP2_HPP
41 changes: 41 additions & 0 deletions Arai60/52. Is Subsequence/step3.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
3問連続で正解するのにかかった時間 : 6:30
subsequence.size() : M
text.size() : N
時間計算量: O(N + kMlogN)
空間計算量: O(N)
*/
#ifndef STEP3_HPP
#define STEP3_HPP

#include <map>
#include <string>
#include <vector>

class Solution {
private:
std::map<char, std::vector<int>> character_positions;

public:
bool isSubsequence(const std::string &subsequence, const std::string &text) {
if (character_positions.empty()) {
for (size_t i = 0; i < text.size(); ++i) {
char c = text[i];
character_positions[c].push_back(i);
}
}
size_t search_pos = 0;
for (char c : subsequence) {
auto &positions = character_positions[c];
auto found =
std::lower_bound(positions.begin(), positions.end(), search_pos);
if (found == positions.end()) {
return false;
}
search_pos = *found;
}
return true;
}
};

#endif // STEP3_HPP