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
110 changes: 110 additions & 0 deletions 0973.K-Closest-Points-to-Origin/memo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# 973. K Closest Points to Origin

## step1

まずheapを使った解法。7mほど。

n = len(points)として

時間計算量: O(n+klog n)、空間計算量: 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.

heapの長さをkで絞りつつpushしていけば、空間計算量はO(k)で済ませることができそうです。その場合、時間計算量はO(n log k)ですかね。(下の方に書いてありました)
Arai60の範囲でヒープ等を使ってk番目までを取る問題があった気がするので、発想はそれと同じような気がします。

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.

Arai60の問題は以下だと思います。
https://leetcode.com/problems/kth-largest-element-in-a-stream/description/

heapの長さをkで保つ解法の発想も持っておこうと思います。今回は他の方のコードを見るまで思い浮かびませんでした。


他の解法として、ソートを思いつく。

時間計算量: O(nlogn)、空間計算量 O(k) (in-placeならO(1))

## step2

Solution の Topic、Quick select を見てもう一つの解法に気づく。

時間計算量: 平均O(n)、最悪O(n^2)、空間計算量: O(1)

partitionの実装方法に自信がなくwikipediaを見た

https://ja.wikipedia.org/wiki/%E3%82%AF%E3%82%A4%E3%83%83%E3%82%AF%E3%82%BB%E3%83%AC%E3%82%AF%E3%83%88


https://github.com/huyfififi/coding-challenges/pull/28/changes#diff-ab6bba3ca897c3d2d653f49bad85d2a075662bc1eba6f421b9689a7e80aa58f9

ヒープの使い方が異なる。こちらは時間計算量O(nlogk)、空間計算量O(k)

pointsを書き換えない場合に最も省メモリ。

> どちらにせよ必要な処理を条件分岐から出すことで、若干私の脳への収まりがよくなったが、どちらの方が人気だろう。

heappushpopは知らなかった

https://docs.python.org/3/library/heapq.html#heapq.heappushpop

一度heappushをしてから条件分岐をするかどうかについて。個人的にはメソッドとなっているheappushpopを使う方効率的なので良いと思う。heapの順序入れ替えの操作が一度で済む。

heapq.nsmallest()を使う解法

```python
import heapq


class Solution:
def kClosest(self, points: list[list[int]], k: int) -> list[list[int]]:
return heapq.nsmallest(k, points, key=lambda p: pow(p[0], 2) + pow(p[1], 2))
```

> pow() 関数を呼び出すより、 step2 のように、 x * x + y * y としたほうが読みやすく、処理が軽そうなイメージがあります。ただ、処理の重さ軽さについては、 Python のインタープリター自体が重いため、あまり気にしなくても良いかもしれません。


disライブラリというものがあるらしい。disassemblerのライブラリ。自分でも触ってみる。

https://docs.python.org/ja/3/library/dis.html

```python
def pow(a, b):
"Same as a ** b."
return a ** b
```

```text
# x * x
4 0 RESUME 0

5 2 LOAD_FAST 0 (x)
4 LOAD_FAST 0 (x)
6 BINARY_OP 5 (*)
10 RETURN_VALUE

# x**2
8 0 RESUME 0

9 2 LOAD_FAST 0 (x)
4 LOAD_CONST 1 (2)
6 BINARY_OP 8 (**)
10 RETURN_VALUE

# pow(x, 2)
12 0 RESUME 0

13 2 LOAD_GLOBAL 1 (NULL + pow)
12 LOAD_FAST 0 (x)
14 LOAD_CONST 1 (2)
16 CALL 2
24 RETURN_VALUE
```

これを見ても x * xが良さそう

## その他

https://github.com/ryosuketc/leetcode_grind75/pull/28


https://github.com/naoto-iwase/leetcode/pull/74/changes

「YAGNI」という知らない用語があった。

> 「YAGNI(ヤグニ)」とは、ソフトウェア開発における非常に有名な原則(設計思想)の一つで、「You Aren't Gonna Need It(どうせそれ、必要にならないよ)」の頭文字を取ったもの

https://ja.wikipedia.org/wiki/YAGNI



## step3

クイックセレクトに馴染みがないのでこれを練習する。
18 changes: 18 additions & 0 deletions 0973.K-Closest-Points-to-Origin/step1_heap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import heapq


class Solution:
def kClosest(self, points: list[list[int]], k: int) -> list[list[int]]:
def square_distance_to_origin(point):
return point[0] ** 2 + point[1] ** 2

heap = [(square_distance_to_origin(point), i) for i, point in enumerate(points)]
heapq.heapify(heap)

k_closest = []
while k > 0:
_, i = heapq.heappop(heap)
k_closest.append(points[i])
k -= 1

return k_closest
7 changes: 7 additions & 0 deletions 0973.K-Closest-Points-to-Origin/step1_sort.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class Solution:
def kClosest(self, points: list[list[int]], k: int) -> list[list[int]]:
def square_distance_to_origin(point):
return point[0] ** 2 + point[1] ** 2

ordered_points = sorted(points, key=square_distance_to_origin)
return ordered_points[:k]
21 changes: 21 additions & 0 deletions 0973.K-Closest-Points-to-Origin/step2_max_heap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import heapq


class Solution:
def kClosest(self, points: list[list[int]], k: int) -> list[list[int]]:
def square_distance_to_origin(point):
return point[0] ** 2 + point[1] ** 2

max_heap = []
for point in points:
if len(max_heap) < k:
heapq.heappush_max(
max_heap,
(square_distance_to_origin(point), point),
)
else:
heapq.heappushpop_max(
max_heap, (square_distance_to_origin(point), point)
)

return [point for _, point in max_heap]
40 changes: 40 additions & 0 deletions 0973.K-Closest-Points-to-Origin/step2_quick_select.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import random


class Solution:
def kClosest(self, points: list[list[int]], k: int) -> list[list[int]]:
def square_distance(point):
return point[0] ** 2 + point[1] ** 2

def get_pivot_index(start, last):
return start + random.randint(0, last - start)

def partition(start, last, pivot_index):
pivot_value = square_distance(points[pivot_index])

points[pivot_index], points[last] = points[last], points[pivot_index]
len_fixed = start
for i in range(start, last):
if square_distance(points[i]) < pivot_value:
points[i], points[len_fixed] = points[len_fixed], points[i]
len_fixed += 1

points[last], points[len_fixed] = points[len_fixed], points[last]
return len_fixed

def k_closest_helper(start, last):
if start >= last:
return

pivot_index_before = get_pivot_index(start, last)
pivot_index_after = partition(start, last, pivot_index_before)

if pivot_index_after == k:
return
elif pivot_index_after < k:
return k_closest_helper(pivot_index_after + 1, last)
else:
return k_closest_helper(start, pivot_index_after - 1)

k_closest_helper(0, len(points) - 1)
return points[:k]
46 changes: 46 additions & 0 deletions 0973.K-Closest-Points-to-Origin/step3_quick_select.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import random


class Solution:
def kClosest(self, points: list[list[int]], k: int) -> list[list[int]]:
def square_distance(point):
return point[0] * point[0] + point[1] * point[1]

def get_pivot_index(start, last):
return start + random.randint(0, last - start)

def partition(start, last, pivot_index):
pivot_value = square_distance(points[pivot_index])
points[pivot_index], points[last] = points[last], points[pivot_index]

len_fixed = 0
for i in range(start, last):
if square_distance(points[i]) < pivot_value:
points[i], points[start + len_fixed] = (
points[start + len_fixed],
points[i],
)
len_fixed += 1

points[last], points[start + len_fixed] = (
points[start + len_fixed],
points[last],
)
return start + len_fixed

def k_closest_helper(start, last):
if start >= last:
return

pivot_index_before = get_pivot_index(start, last)
pivot_index_after = partition(start, last, pivot_index_before)

if pivot_index_after == k:
return
elif pivot_index_after < k:
return k_closest_helper(pivot_index_after + 1, last)
else:
return k_closest_helper(start, pivot_index_after - 1)

k_closest_helper(0, len(points) - 1)
return points[:k]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

念のため、ぐらいのコメントです。
このアルゴリズムは練習・選択肢の幅としてはよいと思いますが、副作用が「常に完全にpointsをソートするわけではなく、この問題において必要な範囲のみpointsを並び替える」という内容なので、pointsが使い捨ての想定でなければかなり関数として気持ち悪いとは思います。
(もちろん、一時変数に対してこういった処理を適用することはよくあるとは思います。関数としてのこの問題の想定解は長さkのheapを使うものかなという気がします。)

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.

pointsが使い捨ての想定でなければかなり関数として気持ち悪い

このような感覚は持っていませんでした。こういったコードを俯瞰するような視点を持てるようになりたいです。