Skip to content

373. Find K Pairs With Smallest Sums #9

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

hroc135
Copy link
Owner

@hroc135 hroc135 commented Jul 30, 2024

- ぱっと思いついた方法は、すべてのペアを生成し、各合計値を元にソートして最初のk個を取り出す方法。
- しかし、いかにも時間制限を超えそうなので他の方法を考えることにする
- ヒープを使えば多少マシになる?
- ただし、n^2通りのペアについて考えることに変わりはないのでそこまで改善されていない気はしつつも、とりあえず実装してみる
Copy link

Choose a reason for hiding this comment

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

時間計算量の観点で考えましたか?

Copy link
Owner Author

Choose a reason for hiding this comment

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

改めて考えてみました。
step1のヒープを使ったやり方だと二重ループの中でheap.Pushをしているので時間計算量はO(n1n2 logk)。空間計算量はO(k)。
反対に「すべてのペアを生成し、各合計値を元にソートして最初のk個を取り出す方法」だと、長さn1
n2の配列をソートする部分がボトルネックで、O(n1n2 log n1n2)。空間計算量はO(n1*n2)。
よって、結論としてはkの値が十分に小さければstep1の解法は時間・空間計算量はともに改善されている。

Copy link

Choose a reason for hiding this comment

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

Constraints:
1 <= nums1.length, nums2.length <= 105
1 <= k <= 104

で、時間計算量が O(|nums1| |nums2| log k) のとき、実行時間は最大で何秒程度になると推測できますか?

Copy link

@nodchip nodchip Jul 31, 2024

Choose a reason for hiding this comment

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

懸念している点を先回りして書きます。

プログラムを書くにあたり、実行時間を気にしていないように見受けられます。プログラムを書くときは、書く前または書いている最中に実行時間に気を配る事をお勧めいたします。また、実行時間を推定するために、時間計算量を常に意識することをお勧めいたします。時間計算量の式にデータの最大サイズを代入すると、おおよその計算ステップ数が見積もれます。そして計算ステップ数からおおよその実行時間が見積もれます。

C++ のような高速な言語ですと、 1 秒間に 1~10 億ステップ程度実行できます。 1~10 億ステップという数字は、 CPU の動作周波数などがボトルネックとなって決まります。 CPU の動作周波数は数 GHz 程度です。これは 1 秒間に数十億回機械語が実行されることを表します。 C++ をコンパイルして機械語に変換したときのオーバーヘッドを考慮すると、 1 秒間に 1~10 億ステップ程度という数字が出てきます。実際には IPC (Instructions per cycles) などによって値は変わってきますが、詳細すぎるので割愛します。

さらに、実行時間は言語によって大きな差があります。以下は各言語ごとの速度ベンチマークの例です。
https://github.com/niklas-heer/speed-comparison
https://benchmarksgame-team.pages.debian.net/benchmarksgame/box-plot-summary-charts.html
Go だと C++ の 2 倍遅い、 Java/C# だと C++ の 3 倍遅い、 Python (CPython) だと 100倍遅い、 Ruby だと 1000 倍遅いようです。主要な言語の速度感は覚えておくとよいと思います。

元の問題に戻り、

で、時間計算量が O(|nums1| |nums2| log k) のとき、実行時間は最大で何秒程度になると推測できますか?

について推測してみます。

|nums1| |nums2| log k = 105 * 105 * log 104 ≒ 1011 くらいになると思います。実行時間を推測するために、これを 1 億で割ると、 103 になります。結果、最悪ケースで 1000 秒程度の実行時間がかかると推測できます。 LeetCode の実行時間は数秒程度に制限されていると思いますので、 Time Limited Exceeded で誤答となると考えられます。

練習として step3 のコードの実行時間を推測してみていただけますか?

Copy link
Owner Author

Choose a reason for hiding this comment

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

step3では、ヒープの高さの最大値がlogkであることを考慮すると、時間計算量はO(k log k)になります。
kの最大値は10^4なので、k log k = 10^4 * log 10^4 ≒ 10^5 くらいになり、これを1億で割ると、10^-3 s = 1 ms。つまり、最悪ケースで1 ms程度の実行時間??自信のない数字が出てきてしまいました、、
ちなみにGoはC++より2倍遅いということなので×2して2msということになるのでしょうか?

Copy link

Choose a reason for hiding this comment

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

はい、おおよそ正しいと思います。 LeetCode 上での実行時間と比較してみるとよいと思います。

Copy link
Owner Author

Choose a reason for hiding this comment

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

Leetcode上での実行時間は150~230msになっています。今回の理論値との差もそうですし、同じコードでも振れ幅が大きいので、実行時間がどのように計測されているのか気になりました。調べてもleetcodeの実行時間は当てにするなという意見ばかり見つかりました

Copy link

Choose a reason for hiding this comment

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

ありがとうございます。プロセスの起動時間などのオーバーヘッドもありますので、正確な計測は難しいかもしれませんね。

```

- テーブルを使って下と右に攻めていく方法があるらしいとDiscordで知り、試してみた(集合を使わないver)
- 理解にかなり苦しんだ
Copy link

Choose a reason for hiding this comment

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

row int
col int
}

Choose a reason for hiding this comment

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

pairって名前からすると何かしらが組として入っているものをイメージします。
それに対して実際はsum含め3つ要素を持っているのは違和感を感じました。

Copy link
Owner Author

Choose a reason for hiding this comment

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

たしかにそうですね。当初の

type pair struct {
    pair [2]int
    sum int
}

という構造体ならpairという名前で良かったのですが、中身が変わっても同じ名前を使ってしまっておりました。step3で最終的に

type pair struct {
    sum int
    idx1 int
    idx2 int
}

という構造体に落ち着いたのですが、こちらでしたらpairSumAndIndicesみたいな感じですかね

}
p := pair{pair: [2]int{n1, n2}, sum: sum}
heap.Push(h, p)
if h.Len() > k {
Copy link

@Ryotaro25 Ryotaro25 Jul 31, 2024

Choose a reason for hiding this comment

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

Goは全く知らないのですが、質問です。heapとstackはどのように使い分けるのでしょうか?
今回の問題ではなぜheapなのでしょうか🙇

Copy link
Owner Author

Choose a reason for hiding this comment

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

ヒープはノード間の大小関係が定義されますが、スタックでは入った順番という情報しかありません。今回はペアの合計値の大小を知りたいのでヒープを使いました。
Goではヒープとスタックをそれぞれcontainer/heap, container/listというパッケージから使うことができ、ヒープを使うためにはheap.Interfaceをまず実装しなくてはいけません。
https://pkg.go.dev/container/heap#Interface

Copy link

@seal-azarashi seal-azarashi left a comment

Choose a reason for hiding this comment

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

Go だとこのように実装出来るのですね。勉強になりました。


func kSmallestPairs(nums1 []int, nums2 []int, k int) [][]int {
min := pair{sum: nums1[0] + nums2[0], idx1: 0, idx2: 0}
h := &pairSumMinHeap{min}

Choose a reason for hiding this comment

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

make() を使って capacity を設定してあげると多少パフォーマンスが良くなるかと思います。

kSmallestPairs := [][]int{}

for len(kSmallestPairs) < k {
top := heap.Pop(h).(pair)

Choose a reason for hiding this comment

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

heap を Init() 関数を使って初期化していないのが気になりました。今のままでも問題なく動作しますが、ライブラリの提供者が本来意図している使い方と少し違うのかなという印象を受けます。

Copy link
Owner Author

Choose a reason for hiding this comment

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

ご指摘の通りInitはしておくべきでしたね。[3,1,2]という配列を渡してInitするとheapifyして[1,3,2]のようにしてくれるのですが、今回は初期値として要素を一つしか入れていないのでInitをしなくても正常に動きます。とはいえ、Initした方がいいですね。

Comment on lines +231 to +242
func (h pairSumMinHeap) Len() int { return len(h) }
func (h pairSumMinHeap) Less(i, j int) bool { return h[i].sum < h[j].sum }
func (h pairSumMinHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }

func (h *pairSumMinHeap) Push(x any) { *h = append(*h, x.(pair)) }

func (h *pairSumMinHeap) Pop() any {
n := len(*h)
min := (*h)[n-1]
*h = (*h)[:n-1]
return min
}

Choose a reason for hiding this comment

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

とても細かいですが、Push() と Pop() の間には空白行が無い方が自然なように思えました。同じインターフェースに属する Len(), Less(), Swap() の3つは空白行なしで宣言されている一方、Push() と Pop() はそのようになっていないのが気になっています。

Comment on lines +259 to +274
if top.idx1 + 1 < l1 && top.idx2 == nextIdx2ForNums1[top.idx1 + 1] {
p := pair{
sum: nums1[top.idx1 + 1] + nums2[top.idx2],
idx1: top.idx1 + 1,
idx2: top.idx2,
}
heap.Push(h, p)
}
if top.idx2 + 1 < l2 && top.idx1 == nextIdx1ForNums2[top.idx2 + 1] {
p := pair{
sum: nums1[top.idx1] + nums2[top.idx2 + 1],
idx1: top.idx1,
idx2: top.idx2 + 1,
}
heap.Push(h, p)
}

Choose a reason for hiding this comment

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

ここの似た処理うまくまとめられないでしょうか??

Copy link
Owner Author

Choose a reason for hiding this comment

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

おっしゃる通り8行もの似ている処理があったら関数化したいなとは僕も思いましたが、当初この部分の理解にかなり苦労したので、関数化によって読み手の負担が増えたら嫌だなと思い、このままにしました。とはいえ、読み直すと冗長さが気になるのでstep4で関数化したいと思います。

}
```

- テーブルを使って下と右に攻めていく方法があるらしいとDiscordで知り、試してみた(集合を使わないver)
Copy link

Choose a reason for hiding this comment

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

テーブルの一番左の列の上からk個をまず候補としてheapに入れる方法もありますね。個人的にはこちらの方法の方が理解しやすいと思います。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants