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
11 changes: 11 additions & 0 deletions 35.SearchInsertPosition/for.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
for (int i = 0; i < nums.size(); i++) {
if (nums[i] >= target) {
return i;
}
}
return nums.size();
}
};
102 changes: 102 additions & 0 deletions 35.SearchInsertPosition/memo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
## ステップ1
制約にO(log 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.

二分探索についてはDiscord内で沢山議論がされているので、探してみると良さそうです。

自分が役に立ったなと思うのはこの辺
Yoshiki-Iwasa/Arai60#35 (comment)
Yoshiki-Iwasa/Arai60#35 (comment)

https://discord.com/channels/1084280443945353267/1084283898617417748/1282392271643345007
(上記のリンク先でOdaさんやnodchipさんが話してる内容)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

seal-azarashi/leetcode#39 (comment)
こういう風にコードを変えたときにどこは動いてどこは動かないかを分かっていれば、正しく頭の中でモデルが作れていると思います。

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.

@Yoshiki-Iwasa @oda
資料ありがとうございます。同ジャンルの問題を解いてみてこの辺よくわかっていないと気づいたのでじっくり落とし込んでみます。

acceptまで34分
制約がなければforループO(n)で実装していた

時間計算量
O (log n)
空間計算量
O(1)

## ステップ2
elseを使った方がスッキリしそう
・forループバージョンでも念の為書いてみた。すごくシンプルに書けるしLeetCode上でもacceptされた
・whileループバージョンでも書いてみた。

命名について
start endが個人的にしっくりきているが、right leftとどっちがいいのだろう
調べていたhighとlowも出てきた。middleを使うならこれが英語的にしっくりきそう。

left < right VS left <= rightはどちらも間違いではない
要素数が偶数の場合、前者は2つの真ん中の左側、後者は右側がmiddleとなる
>From a readability perspective, it might be slightly better (in my opinion) to use the exclusive one in languages with 0-based arrays and either one in languages with 1-based arrays, in order to minimise the number of -1's in the code. An argument could also be made to just stick to a single version in all languages, as to not require that people understand both versions or get confused between the two.
チーム間で決めておくことなのかな。
https://stackoverflow.com/questions/44231413/binary-search-using-start-end-vs-using-start-end

## ステップ3
**3回書き直しやりましょう、といっているのは、不自然なところや負荷の高いところは覚えられないからです。**

## 他の方の解法
自分は再帰で書いたけど、わざわざ再帰を使わずwhileで処理可能
https://github.com/Mike0121/LeetCode/pull/43

>CPU 周りでぱっと目に付いた単語はこんな感じなのですが、
パイプライン
スーパースカラー
マイクロアーキテクチャ
命令セット
マイクロコード
レジスタ
実行ユニット
キャッシュメモリ
アウトオブオーダー
マルチコア
同時マルチスレッディング
スループット
常識の範囲のようなので要チェック
https://github.com/Yoshiki-Iwasa/Arai60/pull/34

>課題の制約でnums contains distinct values sorted in ascending order.とあるので問題ないですが、たとえばnums = [3, 3, 3, 3], target = 3の場合とかの答えが違うくないですか?
middleを使わないforループの探索だとこの左端が回答となってしまう。。。ので今回はたまたまacceptされただけか

>条件を満たすものを1つ探している場合、
>条件を満たすもののうち左端を探している場合、
>条件を満たすところと満たさないところの切れ目を探している場合(上とほぼ同じ)、
>あと、閉区間か、開閉区間か、などがあって、これは、「条件を満たすもののうち左端を探している場合に満たすものを1つ見つかったら中止するコードがついた」
この辺りの目線はなかった。
今回はたまたまnumsがユニークであったので問題が無かったが実際は要件定義必須
https://github.com/fhiyo/leetcode/pull/42
https://github.com/sakupan102/arai60-practice/pull/42

## Discordなど

middleについれleet codeの解説より
>If left + right is greater than the maximum int value 2^31 −1, it overflows to a negative value. In Java, it would trigger an exception of ArrayIndexOutOfBoundsException, and in C++ it causes an illegal write, which leads to memory corruption and unpredictable results.

## step4.cpp
### 閉区間 [start, end]
* 探索空間、初期値の設定
start = 0、end = nums.size() - 1

* ループ終了条件
start == end

* 更新操作
nums[middle] < targetがtrueの場合[middle + 1, end]
nums[middle] < targetがfalseの場合[start, middle]

## 半開区間 [start, end) or (start, end]
[start, end)でstep5.cppに実装
* 探索空間、初期値の設定
start = 0, end = nums.size()

* ループ終了条件
start >= end

* 更新操作
nums[middle] < targetがtrueの場合[middle + 1, end]
nums[middle] < targetがfalseの場合[start, middle]
形で覚えるのではなく、下記の考え方を理解する
https://github.com/Yoshiki-Iwasa/Arai60/pull/35/commits/f279dd98a68111954a02344b20b47512ebffafc4#r1699552857


>二分探索を、 [false, false, false, ..., false, true, true, ture, ..., true] と並んだ配列があったとき、 false と true の境界の位置を求める問題、または一番左の true の位置を求める問題と捉えているか?
>位置を求めるにあたり、答えが含まれる範囲を狭めていく問題と捉えているか?
>範囲を考えるにあたり、閉区間・開区間・半開区間の違いを理解できているか?
>用いた区間の種類に対し、適切な初期値を、理由を理解したうえで、設定できるか?
>用いた区間の種類に対し、適切なループ不変条件を、理由を理解したうえで、設定できるか?
>用いた区間の種類に対し、範囲を狭めるためのロジックを、理由を理解したうえで、適切に記述できるか?
https://discord.com/channels/1084280443945353267/1196498607977799853/1269532028819476562
24 changes: 24 additions & 0 deletions 35.SearchInsertPosition/step1.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
return SearchInsertIndex(0, nums.size() - 1, nums, target);
}

private:
int SearchInsertIndex(int start, int end, vector<int>& nums, int target) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

これ、start と end はどういう変数であるかを意識していたらいいと思います。
「nums の start よりも左の要素はすべて target 未満」ということでしょうか。

Copy link
Copy Markdown
Owner Author

@Ryotaro25 Ryotaro25 Dec 16, 2024

Choose a reason for hiding this comment

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

@oda

start と end はどういう変数であるかを意識していたらいいと思います。

意識できていなかったです。
startが挿入位置になるように意識したものを追加しました。
049a0f6

// 端にいってしまった場合の処理
if (start > end) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

区間を閉区間としてとらえているため、 start > end となることはないと思います。終了条件は start == end となると思います。

Copy link
Copy Markdown
Owner Author

@Ryotaro25 Ryotaro25 Dec 16, 2024

Choose a reason for hiding this comment

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

@nodchip
レビューありがとうございます。
下記で頂いたコメントを元にstep4.cppを追加しました。

#47

コードの中に、フレームワークを用いてコメントを追記しました。

start > end ではダメな理由としては、以下のように理解で合っておりますでしょうか?
まずstart〜endの中に挿入位置が存在すること=閉区間
であるので、start > endを終了条件にすると、探索の範囲が[2, 1]であったり[0, -1]のような状態が発生しstart~endの中に挿入位置が存在することと矛盾するため。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1.問題のモデル化

の部分で、

  • 一番左端の true のいちを求める、もしくは false と true の問題とみなす。
  • 上記問題を解くにあたり、対象の位置を含む区間を定義する。
    が抜けているように思いました。

4.ループ不変条件の設定

一番書くべきことは

  • ループの不変条件を start < end とする。
    だと思います。

5.探索ロジックの設計
nums[mid] < targetがtrueなら、startをmiddle + 1に更新

この書き方ですと、 start を mid に設定してはいけない理由が分かりませんでした。
nums[mid] < target より、探したい対象が mid より右にあることが分かります。区間は閉区間で、区間の中に対象が含まれるので、 start は mid より右に設定してあげるのがよいはずです。そのため、 start を mid + 1 に設定します。
また、 start を mid に設定した場合、区間内の要素数が残り 2 個になったときに、無限ループとなります。

nums[mid] >= targetがtrueなら、endをmiddleに更新
endをmiddle - 1に更新してしまうと、start > endが発生しうる

最終的な挙動からボトムアップに思考しているように見え、違和感を感じました。二分探索の問題のモデルからトップダウンに考えていき、その内容を記述するのが良いと思います。
nums[mid] >= target の場合、 mid の位置に対象がある場合があるため、区間を狭めつつ mid を区間内に含めるため、 end = mid とすると考えるのが良いと思います。

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.

@nodchip
詳細に説明いただきありがとうございます。

一番書くべきことは
ループの不変条件を start < end とする。

ここですが start <= middle < end のような書き方でもよろしいでしょうか?
startとendの関係は、start < end変わらないとは思いますが、middleがある方が理解しやすいと思いました。

最終的な挙動からボトムアップに思考しているように見え、違和感を感じました。二分探索の問題のモデルからトップダウンに考えていき、その内容を記述するのが良いと思います。

指摘されるまで気づかなかったのですが、確かに答えから説明を作っていました。

頂いた指摘事項を元にstep4の説明を修正しました。また練習として半開区間を用いたstep5.cppを追加しました🙇‍♂️
お手隙の際に見ていただけると幸いです。
f1f955b

Copy link
Copy Markdown

@nodchip nodchip Dec 18, 2024

Choose a reason for hiding this comment

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

ここですが start <= middle < end のような書き方でもよろしいでしょうか?

閉区間で考えているため、 middle == end の場合もあります。そのため、 middle < end とはかけないと思います。 start < end、start <= middle、middle <= end と書くしかないように思います。

return start;
}
int middle = (start + end) / 2;
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 middle = start + (end - start) / 2;
と書くことをお勧めいたします。

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.

leetcodeにも開設ございました。
ありがとうございます。

if (nums[middle] == target) {
return middle;
}

if (nums[middle] < target) {
return SearchInsertIndex(middle + 1, end, nums, target);
}

return SearchInsertIndex(start, middle - 1, nums, target);
}
};
23 changes: 23 additions & 0 deletions 35.SearchInsertPosition/step2.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
return SearchInsertIndex(0, nums.size() - 1, nums, target);
}

private:
int SearchInsertIndex(int start, int end, vector<int>& nums, int target) {
if (start > end) {
return start;
}
int middle = (start + end) / 2;
if (nums[middle] == target) {
return middle;
}

if (nums[middle] < target) {
return SearchInsertIndex(middle + 1, end, nums, target);
} else {
return SearchInsertIndex(start, middle - 1, nums, target);
}
}
};
23 changes: 23 additions & 0 deletions 35.SearchInsertPosition/step3.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
return SearchInsertIndex(0, nums.size() - 1, nums, target);
}

private:
int SearchInsertIndex(int start, int end, vector<int>& nums, int target) {
if (start > end) {
return start;
}
int middle = (start + end) / 2;
if (nums[middle] == target) {
return middle;
}

if (nums[middle] < target) {
return SearchInsertIndex(middle + 1, end, nums, target);
} else {
return SearchInsertIndex(start, middle - 1, nums, target);
}
}
};
64 changes: 64 additions & 0 deletions 35.SearchInsertPosition/step4.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
1.問題のモデル化
numsの各要素をターゲット未満かターゲット以上であるかの2種類に分類する
ターゲット未満であればfalse、ターゲット以上であればtrueと見なす。

例えばnums = [1,3,5,6], target = 5 を用いると
nums = [false, false, true, true]と表すことができる
この中で一番左の true の位置を探す。

2.探索空間の定義
今回は閉区間として探索を行う。

3.初期値の設定
startを0、endを配列の最後の要素nums.size() - 1とする。

4.ループ不変条件の設定
startとendの真ん中をmiddleとして、ループの不変条件は
start < end、start <= middle、middle <= end

5.探索ロジックの設計
nums[middle] < target の場合、middleおよびその左側にtargetは存在しないので
startをmiddle + 1に更新する。

nums[mid] >= target の場合、 mid の位置に対象がある場合があるため、
区間を狭めつつ mid を区間内に含めるため、end = mid とする。

6.検証
・targetがnumsのいずれよりも小さい場合
nums[middle] < targetが常にfalseとなり、startは最初の位置のままendが狭まりstartの位置が解となる

・targetがnumsのいずれよりも大きい場合
nums[middle] < targetが常にtrueとなり、startがendに近づきstart + 1

・targetが複数存在する場合
最小のインデックスを返却

7.実行
leet codeにて動作確認

*/
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

なんとなく理解しているか不安を感じています。

「この関数の仕事を手作業でやっているとしましょう。シフト制で SearchInsertIndex の呼び出しが起きるごとに、人が交代します。

あなたは、当番で SearchInsertIndex の呼び出しがおきたという連絡を受けて、仕事につきます。
start, end, nums, target が与えられました。

ここまで働いている人たちがきちんと仕事をしていたら、start, end, nums, target についてどのようなことがいえますか。」

という質問に答えられますか。

まず、自分が呼び出した前任者がしていた仕事は3通りの可能性がありますね。

SearchInsertIndex(0, nums.size() - 1, nums, target);
SearchInsertIndex(middle + 1, end, nums, target);
SearchInsertIndex(start, middle, nums, target);

前任者たちが正しく仕事をしていたら、どういう条件のものが送られてきますか。

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.

@oda

なんとなく理解しているか不安を感じています。

紙に書いて処理を追ったり、なんとか言葉に落とし込んだりしているもののまだスッキリしておりません。
手作業だと下記のような感じでしょうか。

1番最初に作業をする人からは、確認作業の範囲と確認対象とターゲットの数字が引き継がれます。
SearchInsertIndex(0, nums.size() - 1, nums, target);
作業は始まったところであるため、範囲はnumsの先頭から1番最後まで取ります。

2番目作業者からは効率よく探索を行うためnums内のおおよそどこにターゲットがあるのかあたり付けをします。
startとendの情報を元に、numsの真ん中の数字を確認します。

この時にターゲットが真ん中より大きい場合は、下記の形で呼び出します。
SearchInsertIndex(middle + 1, end, nums, target);
次の作業者へ真ん中の数字より大きいためstart 〜 middle内にはターゲットが存在しないことを伝えます。
次の作業者の作業範囲はmiddleの次の位置から、numsの最後となります。

ターゲットが真ん中の数字以下の場合(真ん中より大きくない場合)は下記の形で呼び出します。
SearchInsertIndex(start, middle, nums, target);
これはstartからmiddleを含むどこかにターゲットが存在することを知らせております。
numsの始まりから真ん中を含む範囲を確認するよう依頼します。

3番目以降の作業者も効率よく作業をするため、前任から引き継がれた作業範囲を元に
真ん中を見つけて次の人へ作業範囲を分割して引き継ぎます。

これを作業範囲がなくなるまで繰り返します。

start, end, nums, target についてどのようなことがいえますか。

・startはtargetの位置に対して常に左側(startの位置とtargetの位置がイコールの場合もある)
・endはtargetの位置に対して常に右側
・前任者から引き継いだstartとendは、自分がmiddle分割したmiddle含むまでの範囲とmiddleの次以降から最後までを合わせたものと一致します。
2番目の作業者が分割したstart 〜 middleとmiddleの次 〜 end くっつけたものは、1番最初に作業をした人が調べたstart 〜 endと同じになります。
・nums, target は常に不変です。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ありがとうございます。そうですね。正しい方向に考えていると思います。

endはtargetの位置に対して常に右側

これなんですが、一番はじめのループのときだけ、これが成立していませんよね。
end = nums.size() - 1 としているので、同じ可能性があります。

これが最後に if で分岐をする羽目になった理由です。

Copy link
Copy Markdown
Owner Author

@Ryotaro25 Ryotaro25 Dec 18, 2024

Choose a reason for hiding this comment

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

@oda

これなんですが、一番はじめのループのときだけ、これが成立していませんよね。
end = nums.size() - 1 としているので、同じ可能性があります。

この部分に気づけませんでした。
小田さんと野田さんから頂いたコメントを元にstep4の説明を修正しました。
また練習として半開区間を用いたstep5.cppを追加しました🙇‍♂️
f1f955b

class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
return SearchInsertIndex(0, nums.size() - 1, nums, target);
}

private:
int SearchInsertIndex(int start, int end, vector<int>& nums, int target) {
if (start == end) {
if (nums[start] >= target) {
return start;
} else {
return start + 1;
}
}
int middle = start + (end - start) / 2;

if (nums[middle] < target) {
return SearchInsertIndex(middle + 1, end, nums, target);
} else {
return SearchInsertIndex(start, middle, nums, target);
}
}
};
59 changes: 59 additions & 0 deletions 35.SearchInsertPosition/step5.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
1.問題のモデル化
ターゲットより大きいのか、以下なのか2つの状態に分類することができるのでtrueとfalseの問題とみなす。
記問題を解くにあたり、対象の位置を含む区間をstartからendとして定義する。

2.探索空間の定義
探索範囲を0からnとして、半開区間とみなし探索を行う。
ターゲットより大きい場合をtrueとして[true)となる箇所を探す。

3.初期値の設定
startを0、endをnums.size()として探索する。

4.ループ不変条件の設定
startとendの真ん中をmiddleとして、ループの普遍条件は
start < end、start <= middle、middle <= end

5.探索ロジックの設計
nums[middle] < targetがtrueであれば、middleより左側にtargetは存在しないので
startをmiddle + 1に更新

nums[middle] >= targetがtrueであれば、middleを含めmiddle以下のどこかにtargetは存在するので
ループ不変条件を守るように[start, middle)となるように範囲を狭める。

6.検証
・targetがnumsのいずれよりも小さい場合
nums[middle] < targetが常にfalseとなり、startは最初の位置のままendが狭まりstartの位置が解となる

・targetがnumsのいずれよりも大きい場合
nums[middle] < targetが常にtrueとなり、startがendに近づく、start <= target < endとなっているので
startの位置が解となる

・targetが複数存在する場合
最小のインデックスを返却

7.実行
leet codeにて動作確認

*/
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
return SearchInsertIndex(0, nums.size(), nums, target);
}

private:
int SearchInsertIndex(int start, int end, vector<int>& nums, int target) {
if (start == end) {
return start;
}
int middle = start + (end - start) / 2;

if (nums[middle] < target) {
return SearchInsertIndex(middle + 1, end, nums, target);
} else {
// nums[middle] >= target
return SearchInsertIndex(start, middle, nums, target);
}
}
};
23 changes: 23 additions & 0 deletions 35.SearchInsertPosition/while.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int start = 0;
int end = nums.size();

while (start < end) {
int middle = (start + end) / 2;

if (nums[middle] == target) {
return middle;
}
if (nums[middle] < target) {
start++;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

こうすると、target が大きい場合、start が一つずつ増えていくので、全部舐めることになりますね。

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.

@oda
binary searchになっておりませんでした。他の数問といて戻ってくるとおかしいですね。
start = middle + 1としました。

}
if (nums[middle] > target) {
end = middle;
}
}

return end;
}
};
23 changes: 23 additions & 0 deletions 35.SearchInsertPosition/while_step2.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int start = 0;
int end = nums.size();

while (start < end) {
int middle = (start + end) / 2;

if (nums[middle] == target) {
return middle;
}
if (nums[middle] < target) {
start = middle + 1;
}
if (nums[middle] > target) {
end = middle;
}
}

return end;
}
};
Loading