Skip to content
Open
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
200 changes: 200 additions & 0 deletions 209MinimumSizeSubarraySum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
問題: https://leetcode.com/problems/minimum-size-subarray-sum/description/

### Step 1
- 大まかな方針はすぐに立ったが仕事の引き継ぎのイメージが甘くて詰まった
- 時間計算量: O(2n) = O(n)
- 空間計算量: O(1)
- target 以上の subarray が存在しない場合の処理をどうしようか迷った。
下記の方法以外には、result の他に found のようなフラグを用意する方法もある

```Go
func minSubArrayLen(target int, nums []int) int {
result := math.MaxInt
subarraySum := 0
left := 0
for right := range len(nums) {
subarraySum += nums[right]
if subarraySum < target {
Copy link

Choose a reason for hiding this comment

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

この if 文は削除しても同じ結果になると思います。

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
}
for ; target <= subarraySum && left <= right; left++ {
subarraySum -= nums[left]
result = min(result, right-left+1)
}
}
if result == math.MaxInt {
return 0
}
return result
}
```

### Step 2
#### 2a
- step1 の修正
- 答え(result)の初期値について
- https://github.com/olsen-blue/Arai60/pull/50/files#r2005904280
- 確かに答えの最大値は len(nums) なので初期値を len(nums) + 1 としても動く
- https://github.com/Yoshiki-Iwasa/Arai60/pull/43/files#r1709694524
- 定数を宣言すると丁寧
- https://github.com/olsen-blue/Arai60/pull/50/files#diff-6d4eb2707ed57d8037bfa2e5985424b237a407741a7602ba92b31e090d1cb096R163
- left == right であれば subarraySum は 0 になり、target <= subarraySum でなくなるので
`&& left <= right` のチェックは不要
- https://github.com/olsen-blue/Arai60/pull/50/files#diff-6d4eb2707ed57d8037bfa2e5985424b237a407741a7602ba92b31e090d1cb096R164-R165
- `min_length` と `prefix_sum` の更新順が自分のとは逆
Comment on lines +43 to +44
Copy link

@olsen-blue olsen-blue Apr 6, 2025

Choose a reason for hiding this comment

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

ここのコードの意図ですが、下記のようなイメージでした。
・まずprefix_sum >= targetが現状において保証されているので、真っ先にmin_lengthの更新をしてみる。
・もしかすると、まだまだ記録を更新できるかもなので、windowを小さくしてみる。つまりprefix_sumを削り、削ったのでleftを右に1個進める。
・ここまでを自分の仕事分として次の担当に引き継ぐ。

https://github.com/olsen-blue/Arai60/pull/50/files#diff-6d4eb2707ed57d8037bfa2e5985424b237a407741a7602ba92b31e090d1cb096R163-R166

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 でつまずいていたのは処理の理解が自然でなかったことが伺える
- https://github.com/olsen-blue/Arai60/pull/50/files#r2008742099
- nums に 0 が含まれる場合の挙動について

```Go
func minSubArrayLen(target int, nums []int) int {
const notFound = 0
result := notFound
subarraySum := 0
left := 0
for right := range len(nums) {
subarraySum += nums[right]
if subarraySum < target {
continue
}
for target <= subarraySum {
if result == notFound {
result = right - left + 1
} else {
result = min(result, right-left+1)
}
subarraySum -= nums[left]
left++
}
}
return result
}
```

#### 2b
- 二分探索を用いた解法
1. nums の累積和配列を作る
2. nums[i] が先頭となる部分配列で総和が target 以上となるものの中で最も要素数の少ないものを探せば良い
3. そこで登場するのが二分探索。(0~nums[i-1]の累積和) + target の insert position を探せば良い
- 時間計算量: O(n logn)
- 空間計算量: O(n)
- https://github.com/Yoshiki-Iwasa/Arai60/pull/43/files#r1712678111
- not found の場合の判定を prefixSums を作り終わった時にできる
- (追記) https://github.com/fhiyo/leetcode/pull/49/files#r1685369428
- 関数の一番最初に `sum(nums) < target` でも判定できるので 2d で実装
- https://github.com/Yoshiki-Iwasa/Arai60/pull/43/files#r1709772635
- この指摘の「prefix_sum の要素を詰め終わったら、nums は役割を全うして prefix_sum が代わりに進む準備ができたような気持ちになりました」に納得
- 「変数のスコープはできたら短くしたい」の部分についてはそこまで考慮できていなかった

```Go
func minSubArrayLen(target int, nums []int) int {
// prefixSums[0] = 0
// prefixSums[i] = nums[0] + ... + nums[i-1]
prefixSums := make([]int, len(nums)+1)
for i := range nums {
prefixSums[i+1] = prefixSums[i] + nums[i]
}
if prefixSums[len(prefixSums)-1] < target {
return 0
}
result := len(prefixSums)
for left := range nums {
right, _ := slices.BinarySearch(prefixSums, prefixSums[left]+target)
if right == len(prefixSums) {
break
}
result = min(result, right-left)
}
return result
}
```

#### 2c
- 2b の二分探索の区間を (left, len(prefixSums)) の開区間にしてみた
- `if left+right == len(prefixSums) { break }` の部分で可読性が下がったような気がする

```Go
func minSubArrayLen(target int, nums []int) int {
// prefixSums[0] = 0
// prefixSums[i] = nums[0] + ... + nums[i-1]
prefixSums := make([]int, len(nums)+1)
for i := range nums {
prefixSums[i+1] = prefixSums[i] + nums[i]
}
if prefixSums[len(prefixSums)-1] < target {
return 0
}
result := math.MaxInt
for left := range nums {
right, _ := slices.BinarySearch(prefixSums[left:], prefixSums[left]+target)
Copy link

Choose a reason for hiding this comment

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

前回の探索結果の right よりも右くらいまではいえますかね。あんまり速度には影響がないかもしれませんが。そうなると、right から右に走査するのも一つですね。(そうすると step 1 に近い物になるでしょう。)

Copy link
Owner Author

Choose a reason for hiding this comment

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

書いてみました。
BinarySearch で調べる区間を前回の探索結果よりも右にしたもの。積極的に書きたいコードではない。

func minSubArrayLen(target int, nums []int) int {
	prefixSums := make([]int, len(nums)+1)
	for i := range nums {
		prefixSums[i+1] = prefixSums[i] + nums[i]
	}
	if prefixSums[len(prefixSums)-1] < target {
		return 0
	}
	result := math.MaxInt
    lastRight := 0
	for left := range nums {
		subarrayRight, _ := slices.BinarySearch(prefixSums[lastRight+1:], prefixSums[left]+target)
		if lastRight+subarrayRight+1 == len(prefixSums) {
			break
		}
		result = min(result, (lastRight+subarrayRight+1)-left)
	}
	return result
}

right から右に走査する。二分探索の対象が prefixSums[0:right] と始点を固定できるのがありがたい。二分探索で見つけるべきは prefixSums[right] - target を挿入できる位置のすぐ左のインデックスなので、BisectRight を自作した。

func BisectRight(nums []int, target int) int {
    left := 0
    right := len(nums) - 1
    for left < right {
        middle := (left+right+1) / 2
        if nums[middle] <= target {
            left = middle
        } else {
            right = middle - 1
        }
    }
    return left
}

func minSubArrayLen(target int, nums []int) int {
    prefixSums := make([]int, len(nums)+1)
    for i := range nums {
        prefixSums[i+1] = prefixSums[i] + nums[i]
    }
    if prefixSums[len(prefixSums)-1] < target {
        return 0
    }
    result := math.MaxInt
    for right := 1; right < len(prefixSums); right++ {
        if prefixSums[right] < target {
            continue
        }
        left := BisectRight(prefixSums[:right], prefixSums[right]-target)
        result = min(result, right-left)
    }
    return result
}

Copy link

Choose a reason for hiding this comment

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

はい。前回の結果を覚えておくならば、Binary Search よりも線形で探したほうが速そうですね。

Copy link

@olsen-blue olsen-blue Apr 6, 2025

Choose a reason for hiding this comment

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

私は、prefix_sums全体を探索対象にして、target_indexを求めてしまってましたが、スライスした方が時間計算量は少なく済んでより効率的になりそうですね。
https://github.com/olsen-blue/Arai60/pull/50/files#diff-6d4eb2707ed57d8037bfa2e5985424b237a407741a7602ba92b31e090d1cb096R189

また、スライスすることで、rightが(おそらく)相対インデックスになるので、result = min(result, right)に直接使えるのもいいですね。

Copy link
Owner Author

Choose a reason for hiding this comment

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

後から見返したところ left は prefixSums 全体の中のインデックスで right はスライスされたものの中でのインデックスとなっており、対称性がないのがちょっと読み手にとってトラップかもしれません。

Choose a reason for hiding this comment

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

たしかに非対称なのが認知不可があるかもですね。
アイデアベースですが、leftfrom_indexrightlengthとかにしたらどうでしょう、みたいなことを思いました。subarray_lengthまで言っちゃっても良いかもですね。

if left+right == len(prefixSums) {
break
}
result = min(result, right)
}
return result
}
```

#### 2d
- https://github.com/fhiyo/leetcode/pull/49/files#diff-b4505bb135c82fffdf6750131a4227b712965b52787f0d78ace5983440d8b88aR139
- 左半開区間でやってみる
- 一番最初に not found の場合を処理する

```Go
func SumInt(nums []int) int {
sum := 0
for _, n := range nums {
sum += n
}
return sum
}

func minSubArrayLen(target int, nums []int) int {
if SumInt(nums) < target {
return 0
}
result := math.MaxInt
subarraySum := 0
left := -1
for right := range nums {
subarraySum += nums[right]
for target <= subarraySum {
result = min(result, right-left)
left++
subarraySum -= nums[left]
}
}
return result
}
```

### Step 3
- left を含めない subarray を考えた方が left が right を追い越す瞬間がなくてわかりやすかった

```Go
func minSubArrayLen(target int, nums []int) int {
const notFound = math.MaxInt
minLength := notFound
subarraySum := 0
left := -1
for right := range nums {
subarraySum += nums[right]
if subarraySum < target {
continue
}
for subarraySum >= target {
minLength = min(minLength, right-left)
left++
subarraySum -= nums[left]
}
}
if minLength == notFound {
return 0
}
return minLength
}
```

### CS