-
Notifications
You must be signed in to change notification settings - Fork 0
560 subarray sum equals k medium #26
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
base: main
Are you sure you want to change the base?
Conversation
…rove documentation
…y optimization suggestion
| return num_subarrays | ||
| ``` | ||
| - `sum_to_j`は`prefix_sum`の方が適切かも | ||
| - `subarray_sum_count` も `prefix_sum_count` の方が適切かも |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
自分なら、prefix_sum_to_countやcumsum_to_frequencyなどにします。
| num_subarrays = 0 | ||
| prefix_sum_counts = defaultdict(int) | ||
| prefix_sum = 0 | ||
| for index in range(len(nums)+1): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+, -などの二項演算子の前後は原則半角スペースを入れた方が良いと思います。
https://peps.python.org/pep-0008/#other-recommendations
https://peps.python.org/pep-0008/#whitespace-in-expressions-and-statements
| num_subarrays = 0 | ||
| prefix_sum_counts = defaultdict(int) | ||
| prefix_sum = 0 | ||
| for index in range(len(nums)+1): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
初期化時にprefix_sum_counts[0] += 1としておけば、indexを0から始める必要がなくなり、そのほうが可読性が高いと感じます。
また、indexはnumsへのアクセスにしか使われていないので、for num in nums:で良い気がします。
| This can be found as below: | ||
| for each j = 1, ..., len(nums), find the number of i (0<=i<j) that satisifes nums[:j] -k == nums[:i]. | ||
| Note that nums[:0] is defined as 0. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
これは実装の説明であって、関数を使う側には役に立たない情報であり、また下のコードを見れば十分読み取れる内容なため、あえてdocstringに書く必要はないと思いました。
| for i in range(len(nums)): | ||
| cumsums[i + 1] = cumsums[i] + nums[i] | ||
|
|
||
| hashmap: dict[int, int] = defaultdict(int) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hashmapという変数名は、要はdictと名付けているのと一緒で、あまり読み手の助けにならない気がします。(type hintがあるので情報量としてはゼロだと思います。)
自分ならcumsum_to_countなどにします。(自分はdictの変数名に対し大体は{key}_to_{value}とつけてます。)
| - 時間計算量:`O(1)` | ||
| - 空間計算量:`O(n)` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
入れ替わってますね...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
時間空間ともにO(n)じゃないでしょうか?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
全探索、単純な二重ループは時間計算量O(n^2)、空間計算量O(1)です。
hashmapの方法が時間計算量O(n)、空間計算量O(n)です。一重ループのため走査がn回です。各走査ごとのパターン網羅O(n)をhashmapでO(1)に高速化しています。ただしパターンを記憶するためにO(n)の空間容量が必要で、そこがトレードオフです。(時間のn倍を空間のn倍に、hashmapで変換しているイメージです。)
two pointersの方法が時間計算量O(n)、空間計算量O(1)です。理由はleftとrightが2回線形に舐めるので走査回数が2n前後だからです。単純な二重ループの枝刈りに相当します。
速度、空間の2軸では下2つがパレート最適な選択肢で、two pointersが最も優等生っぽく感じます。
| cumsums = [0] * (len(nums) + 1) | ||
| for i in range(len(nums)): | ||
| cumsums[i + 1] = cumsums[i] + nums[i] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
以下のようにも書けます。
import itertools
cumsums = [0] + list(itertools.accumulate(nums))https://docs.python.org/ja/3.13/library/itertools.html#itertools.accumulate
| num_subarrays += hashmap[cumsums[i] - k] | ||
| hashmap[cumsums[i]] += 1 | ||
| return num_subarrays | ||
| ``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
2-passで書かれてますが、こちらも1-passでも書けますね。
from collections import defaultdict
class Solution:
def subarraySum(self, nums: list[int], k: int) -> int:
cumsum_to_count = defaultdict(int)
cumsum_to_count[0] += 1
cumsum = 0
total_count = 0
for num in nums:
cumsum += num
total_count += cumsum_to_count.get(cumsum - k, 0)
cumsum_to_count[cumsum] += 1
return total_count| sum_to_j = 0 | ||
| # Add sum(nums[:0]) = 0 | ||
| subarray_sum_count[0] = 1 | ||
| for j in range(1, len(nums) + 1): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ループカウンタでiを使っていないのにjが先に出てくるのは違和感があります。(通例、jは2種類目のループカウンタ、という意味だと思うので。)
特別な意味がある場合はまず変数名を工夫すべきだと感じます。
| if not nums: | ||
| return 0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
これは省いたとしてもforループがskipされてそのまま0が返るので書かなくても良いと思います。(あったとしても不自然とまでは思いません。)
| def subarraySum(self, nums: list[int], k: int) -> int: | ||
| num_subarrays = 0 | ||
| # cumsums[i] = sum(nums[:i]) | ||
| # sum(nums[i:j]) = cumsums[i] - cumsums[j] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sum(nums[i:j]) = cumsums[j] - cumsums[i]でしょうか.
コード本体では正しく書けているのでタイポだと思いますが
| num_subarrays = 0 | ||
| # sum(nums[:i]) -> count | ||
| subarray_sum_count: dict[int, int] = defaultdict(int) | ||
| sum_to_j = 0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
「jってなんだろう?」と思ったので,for j in ...の直前にあると認知負荷が下がるかもしれません.
| count_end_with_index = prefix_sum_counts[prefix_sum-k] | ||
| num_subarrays += count_end_with_index |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
2行程度でこの後再利用するなどもないので,countでも十分意図は伝わると思いました.
| - 時間計算量:`O(1)` | ||
| - 空間計算量:`O(n)` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
時間空間ともにO(n)じゃないでしょうか?
| num_subarrays = 0 | ||
| # cumsums[i] = sum(nums[:i]) | ||
| # sum(nums[i:j]) = cumsums[i] - cumsums[j] | ||
| cumsums = [0] * (len(nums) + 1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cumsum という単語はやや分かりにくく感じました。 prefix_sum や cumulative_sum など、フルスペルで書いたほうが分かりやすいと思います。
問題へのリンク
560. Subarray Sum Equals K
言語
Python
自分の解法
numsに負の数が含まれなければ、two pointersでTC:O(n)/ SC:O(1)で解けるが、負の数が含まれるので、two pointersは使えない。O(n log n)という解法もある。-min(nums)など)を足して累積和を非負にする方法も考えたが、それでは累積和が「要素数×ずらした値」の分だけずれるので、うまくいかない。step1
二重ループを回す方法
nをnumsの長さとすると、時間計算量:
O(n^2)空間計算量:
O(n)この空間計算量は
O(1)にできるcumsumの求め方は以下のような方法もある。(ref: https://github.com/tokuhirat/LeetCode/pull/16/files?short_path=d4900f9#diff-d4900f989c6f9680b8e8144658ef8f10d6025523b2c0c63bed653dcdcc4fc290)
step2
空間計算量を
O(1)にする方法cumsumはcumsums[i] = sum(nums[:i])と定義すると、元の配列より1だけ長くなるので、添え字の管理が面倒になる点に注意する。特に、本解法のようにforループを1度だけ回す場合、rangeの範囲をどうするかがポイントになる&バグを生みやすい。cumsums[i] = sum(nums[:i+1])と定義すると、cumsumsの長さはlen(nums)と同じになるが、cumsums[i] - cumsums[i-1] = nums[i]がi=0のときに成り立たないので、条件分岐が余分に必要になる。step3
step3_1.py(20 min)sum_to_jはprefix_sumの方が適切かもsubarray_sum_countもprefix_sum_countの方が適切かもテストケース
nums=[1],k=1->1、nums=[1],k=0->0nums=[1, 1, 1],k=3->1、nums=[1, 1, 1],k=2->2nums=[1, 2, 3, -3, 3],k=3->4nums=[1, -1, 1, -1],k=0->4nums=[0, 1, 0],k=1->4nums=[-1, -1, -1],k=-2->2nums=[],k=0->0、nums=[],k=1->0step4 (FB)
別解・模範解答
ハッシュマップを使う方法
時間計算量を
O(n)にできる。Subarray自体は必要なくて、その数だけが必要であることがミソ。数だけなら、ハッシュマップで管理すれば、
O(1)でアクセスできる。cumsumsを使う解法ではSubarray自体が求まるただし、
cumsumsの解法の空間計算量をO(1)にした解法とは時間計算量と空間計算量のトレードオフの関係にあるので、どちらが良いかは場合による。時間計算量:
O(1)空間計算量:
O(n)numsが正の数のみを含む場合(two pointers)numsに0が含まれる場合は、結構複雑になる想定されるフォローアップ質問
defaultdictの代わりにdict.getを使えば、メモリ使用量を抑えられる可能性がある。次に解く問題の予告