Skip to content

Conversation

@nittoco
Copy link
Owner

@nittoco nittoco commented Aug 6, 2024

問題文: https://leetcode.com/problems/find-k-pairs-with-smallest-sums/description/

You are given two integer arrays nums1 and nums2 sorted in non-decreasing order and an integer k.

Define a pair (u, v) which consists of one element from the first array and one element from the second array.

Return the k pairs (u1, v1), (u2, v2), ..., (uk, vk) with the smallest sums.

問題文: https://leetcode.com/problems/find-k-pairs-with-smallest-sums/description/

You are given two integer arrays nums1 and nums2 sorted in non-decreasing order and an integer k.

Define a pair (u, v) which consists of one element from the first array and one element from the second array.

Return the k pairs (u1, v1), (u2, v2), ..., (uk, vk) with the smallest sums.

- ジェネレータの再帰をどうするか最初わからなかったが、イテレータになるのでfor文を使えばよかった
- TLEになった。時間計算量を考えたけどわからない、、、
- 再帰の計算量を考えるのが苦手
Copy link

Choose a reason for hiding this comment

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

リンク先は yield pair(0, j) によって高速化を図っています。

generate_smallest_pairs の再帰の木構造を想像してください。一列に並んでいますね。
index2 が大きい数字は、出力までに index2 回 yield されますね。
だから、ここに自乗が付きます。

Copy link

Choose a reason for hiding this comment

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

えー、例え話をしましょう。
generate_smallest_pairs という名前の関所があり、並ぶところが2つあります。
左の列から来た人と右の列から来た人が、それぞれ関所の番人に紙を渡します。番人は和が小さい方を通します。

generate_smallest_pairs(0) という関所は、[nums1[x], nums2[0]] が書かれた紙を持っている人たちが左に並んでいて、右に並んでいる人たちは generate_smallest_pairs(1) という関所を通ってきた人たちです。
generate_smallest_pairs(1) という関所は、[nums1[x], nums2[1]] が書かれた紙を持っている人たちが左に並んでいて、右に並んでいる人たちは generate_smallest_pairs(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.

ありがとうございます。関所のたとえ分かりやすかったです。
とすると、まだちょっと混乱しているのですが、index2が大きいものが比較されるときにたくさん関所を通るので時間がかかるということですよね。ただgenerate_smallest_pairs(0)に辿り着くまでに途中で勝負に負けるかもしれなくて、そこを計算するとどうなるのかがいまいちわかってないです
そのコードも面白そうなのでできれば読んでみます👀

Copy link

Choose a reason for hiding this comment

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

はい。えー、k 人関所0から出てきたとしましょう。
その人達が元々いた場所を確認すると、index2 + 1 回関所でお金を払ってますね。まあ、負けたことがあるかもしれませんが、そのときは勝った人が通行料を払っています。たしかに、何回か勝ったけれどもまだ関所0から出てきていない人というのはいますね。ただ Generator なので、引っかかっている人の数は関所の数で抑えられそうです。

つまり、支払われた通行料全体は、各関所での支払いの和であり、各人(出てきたかどこかの関所で引っかかっているか)の支払いの和ですね。あとは、k 人の出自がどうなっていると通行料全体が大きくなるかです。index2 が大きい人はできるだけ多くしたいんですが、しかし、j1 < j2 ならば nums1[x] + nums2[j1] <= nums1[x] + nums2[j2] という制約がありますね。

Copy link

Choose a reason for hiding this comment

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

はい。
len(nums2) を n としましょう。
k 人通過した後、関所に一回でも券を見せたことがある人は通行料を払って外に出ましょう。
(関所は、Generator ですから関所の外から人を一人寄越せといわれるまでは止まっていて、必要となったら2人を呼び比較します。だから、各関所の外に一人ずつ待機しています。ただし、最後の関所は1人しか呼ばないので待機は0人です。)
合計、n - 1 + k 人が外に出ますね。

さて、この時に、どの列から来ているかを考えると、すべての列から1人ずつは来ているはずです。また、index2 が小さい列のほうが多いとも少なくない人数が来ています。通行料の総額が最大になるためには、後ろに寄せたほうがいいですね。

そして、k 人が通過した後、最後の n - 1 人が払った金額の合計は 1 ~ n - 1 までの和です。

だからこの差をとれば、通行料の総額の最大値が出ます。

k が十分に大きければ、(k / n) × (1 ~ n までの和)
n が十分に大きければ、n - k + (1 ~ k までの和)
でしょうか。

Copy link
Owner Author

@nittoco nittoco Aug 10, 2024

Choose a reason for hiding this comment

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

少し時間かかりましたが、上の説明と下のコードでprintデバッグしてなんとか理解できました。(つもり)(nums1 = [i*1000 for i in range(10)] , nums2 = [i for i in range(10)]や、その逆で実験)
nが大きい場合のn - kの項が入る理由が最初混乱していましたが、最初は「自分の後ろの関所までの中で最も小さい値」がわからないので、最後の関所まで確認する必要がありますね。
横入りがたくさん起きると、次回「自分の後ろの関所までの中で最も小さい値」を確認しなくてすむので、通行料があんまり取れないですね。

ありがとうございました。

class Solution:
    def kSmallestPairs(self, nums1: list[int], nums2: list[int], k: int) -> list[list[int]]:
        def generate_smallest_pairs(index2):
            if index2 == len(nums2):
                yield [inf, inf]
                return
            index1 = 0
            for next_pair in generate_smallest_pairs(index2 + 1):
                while index1 < len(nums1) and nums1[index1] + nums2[index2] < sum(next_pair):
                    print(f'関所{index2}{next_pair=}は詰まる、横入りした{nums1[index1]=}, {nums2[index2]=}が通過')
                    yield [nums1[index1], nums2[index2]]
                    index1 += 1
                print(f'関所{index2}{next_pair=}が通過', end='、')
                if index1 >= len(nums1):
                    print('横入りは在庫切れ')
                else:
                    print(f'{nums1[index1]=}, {nums2[index2]=}は横入り失敗')
                yield next_pair
        return list(islice(generate_smallest_pairs(0), k))
        ```
        

Copy link
Owner Author

Choose a reason for hiding this comment

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

ちなみにGoogle社内の方だと、コード見てすぐにわかる範囲(常識の範囲)なのでしょうか

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.

なるほど、ありがとうございます、確かに変わった実装だなあとは思いました。(ただ選択肢に入れとくのは読む能力を上げるのでいいかなあと思いました。)


class Solution:
def kSmallestPairs(self, nums1: List[int], nums2: List[int], k: int) -> List[List[int]]:
num_pairs = 0
Copy link

Choose a reason for hiding this comment

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

num_pairs は一度も使われていないため、消したほうが良いと思います。

Comment on lines +171 to +172
for diff in diffs:
diff1, diff2 = diff

Choose a reason for hiding this comment

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

for diff1, diff2 in diffs:

で良いかもしれません。

Copy link
Owner Author

Choose a reason for hiding this comment

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

確かに、気づいてませんでした、ありがとうございます

- 関数化が意図を伝えられるというのなるほど。もうちょっと関数化して、意図を伝えたほうが良さそう?(今までのやつあんまりしてこなかったけど)
- sum_and_index_heapはデータの中身をまあ表してはいるけど、読む立場の時にcandidatesの方が意図が分かりやすそう
- len(nums1)*len(nums2)がkを超えない場合の処理も考えてみる
- 上記に気を付けて、結構関数化してみた(やり過ぎ?)

Choose a reason for hiding this comment

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

確かに処理の流れがわかりやすい気がします(バイアスがかかっているかもしれない)
他の人の意見も聞いてみたいですね。

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.

ありがとうございます。
自分なりの主観的基準で一般的かどうかわからないのですが、他のメソットにも使えそうなある程度汎用的だと思ったら外、そうでないなら中にしてます。

def is_possible_candidate(index1, index2):
if index1 == 0 or index2 == 0:
return True
return (index1 - 1, index2) in index_pairs_in_result\

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.

これスペースあるのは普通なんですね!ありがとうございます

result_pairs = []
index_pairs_in_result = set()
while len(result_pairs) < k:
pair_sum, index1, index2 = heapq.heappop(candidates)

Choose a reason for hiding this comment

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

sumでも十分かもと思いました

Copy link
Owner Author

Choose a reason for hiding this comment

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

sumは組み込みでsum関数があるので、個人的に単体ではあんまり使いたくないと思ってこうしました

Choose a reason for hiding this comment

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

sum が組み込み関数なんですね (。_ 。 )
知りませんでした

Comment on lines +149 to +150
def is_valid_index(index1, index2):
return 0 <= index1 < len(nums1) and 0 <= index2 < len(nums2)

Choose a reason for hiding this comment

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

一度しか呼び出されていない返り値が boolean の関数ですが、これらは関数名を見れば判定内容がある程度理解出来るようにする目的で作られていますでしょうか。もしそうなら、代わりに同じ名前の変数を宣言したり、コメントを残すようにしてもいいかなと思いました。

Copy link
Owner Author

Choose a reason for hiding this comment

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

なるほど、確かにぱっと見で分かりにくかったかもしれません。
コメントを残しておきます。ありがとうございます

Comment on lines +175 to +178
if not is_valid_index(new_index1, new_index2):
continue
if not is_possible_candidate(new_index1, new_index2):
continue

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.

これは個人的には、一つずつcontinueで可能性を潰していくイメージなので、分けたままにしたいかなあと思いました。(別問題で、 continueをこの時は自分は分けますというodaさんのコメントがあった気がしたが、どの問題か忘れてしまった)

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.

8 participants