Zubora Code

NeetCode 150 全問の解き方とポイントまとめ Sliding Window

Published: 29 August, 2024
Revised: 29 August, 2024

Sliding Window

本記事は以下記事の一部です。

NeetCode 150 全問の解き方とポイントまとめ & 感想

引き続き取っつやすい分野ではあるけど、Two Pointers とかと比較するとちょっと難易度上がるなという印象。

29. Best Time to Buy And Sell Stock

Link

https://neetcode.io/problems/buy-and-sell-crypto

Related Problem

Difficulty (Easy, Medium, Hard)

Easy

Time Complexity

O(N)

Space Complexity

O(1)

The personal need for review (1 ~ 10)

1

Memo

  • 単純にループの中で過去の最小値を更新しつつ利益があればmax_profitを更新していけば良い。
  • もちろん二重ループ(O(N^2)でも解けるけど、O(N)の考え方もかなりシンプルだと思う。
  • 実は「Best Time to Buy And Sell Stock」は確かIVとかまでシリーズがあって、これ以降はがっつりDP(動的計画法)の問題になりかなり難しくなる。(Top Interview Questions 150 の方にはあるが、NeetCodeにはなかった)

Problem

You are given an integer array prices where prices[i] is the price of NeetCoin on the ith day.

You may choose a single day to buy one NeetCoin and choose a different day in the future to sell it.

Return the maximum profit you can achieve. You may choose to not make any transactions, in which case the profit would be 0.

Example 1:

Input: prices = [10,1,5,6,7,1]

Output: 6

Explanation: Buy prices[1] and sell prices[4], profit = 7 - 1 = 6.

Example 2:

Input: prices = [10,8,7,5,2]

Output: 0

Explanation: No profitable transactions can be made, thus the max profit is 0.

Constraints:

1 <= prices.length <= 100

0 <= prices[i] <= 100

Solution

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        max_profit = 0
        min_price = math.inf

        for price in prices:
            if price > min_price:
                max_profit = max(max_profit, price - min_price)
            min_price = min(min_price, price)
        
        return max_profit

30. Longest Substring Without Repeating Characters

Link

https://neetcode.io/problems/longest-substring-without-duplicates

Related Problem

Difficulty (Easy, Medium, Hard)

Medium

Time Complexity

O(N)

Space Complexity

O(N)

The personal need for review (1 ~ 10)

4

Memo

  • いつもこういう問題を解く時は芋虫みたいなイメージなんだけど、右のポインターrと、左のポインターlを特定の条件に基づいて移動させていく
  • rは普通に1ずつincrementさせつつhash_setに文字を追加していって、すでにhash_setに存在する値に当たったら、今度はlを1つずつincrementさせてhash_setから当該文字が消えるまでremoveし続ける。終わったらまたrを動かして追加していく。
  • 「window size: r - l + 1」これめっちゃ出てくる。別にシンプルな話だけどパッと出てくるようにしておきたい。

Problem

Given a string s, find the length of the longest substring without duplicate characters.

A substring is a contiguous sequence of characters within a string.

Example 1:

Input: s = "zxyzxyz"

Output: 3

Explanation: The string "xyz" is the longest without duplicate characters.

Example 2:

Input: s = "xxxx"

Output: 1

Constraints:

0 <= s.length <= 1000

s may consist of printable ASCII characters.

Solution

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        l = 0
        hash_set = set()
        longest = 0

        for r in range(len(s)):
            while s[r] in hash_set:
                hash_set.remove(s[l])
                l+=1
                
            hash_set.add(s[r])
            longest = max(longest, r - l + 1)
        
        return longest


31. Longest Repeating Character Replacement

Link

https://neetcode.io/problems/longest-repeating-substring-with-replacement

Related Problem

Difficulty (Easy, Medium, Hard)

Medium

Time Complexity

O(N)

Space Complexity

O(N)

The personal need for review (1 ~ 10)

4

Memo

  • 最適解は比較的難しいと思う。
  • いつも通りlとrのポインターを動かす中で、windowの中の文字をカウントしていき、window size(r - l + 1) から最も頻度の高い文字のカウントを引いた値がk以下なら、最長のsubstringの長さとして使える。
  • window sizeを縮小する時にmax_freqを更新する必要がない、という点が直感的に分かりづらかった。

Problem

You are given a string s consisting of only uppercase english characters and an integer k. You can choose up to k characters of the string and replace them with any other uppercase English character.

After performing at most k replacements, return the length of the longest substring which contains only one distinct character.

Example 1:

Input: s = "XYYX", k = 2

Output: 4

Copy

Explanation: Either replace the 'X's with 'Y's, or replace the 'Y's with 'X's.

Example 2:

Input: s = "AAABABB", k = 1

Output: 5

Copy

Constraints:

  • 1 <= s.length <= 1000
  • 0 <= k <= s.length

Solution

from collections import defaultdict

class Solution:
    def characterReplacement(self, s: str, k: int) -> int:
        longest = 0
        l = 0
        max_freq = 0
        char_count_map = defaultdict(int)

        for r in range(len(s)):
            char_count_map[s[r]] += 1
            if char_count_map[s[r]] > max_freq:
                max_freq = char_count_map[s[r]]
            
            if (r - l + 1) - max_freq <= k:
                # window size から最大頻度を引いた値が
                # K以下の場合、重複を含まないsubstringとして使える
                longest = max(longest, r-l+1)
            else:
                # window size から最大頻度を引いた値が
                # Kより大きい場合、windowを縮小しなきゃいけない
                char_count_map[s[l]] -= 1
                l += 1
                
                # ここで再度char_count_map全体を見て
                # max_freqの値も更新する必要がある気がするが、
                # ここで更新したってlongestの値がもっと長くなることはないのと、
                # もっと長くなるチャンスがある時はmax_freqもちゃんと更新されるので、不要
        
        return longest


32. Permutation In String

Link

https://neetcode.io/problems/permutation-string

Related Problem

Difficulty (Easy, Medium, Hard)

Medium

Time Complexity

O(N)

Space Complexity

O(N)

The personal need for review (1 ~ 10)

5

Memo

  • 最適解を考えると、Mediumの中では難しい方だと思う。
  • Permutation of s1 exists as a substring of s2 とは、s2の中でs1と同じ長さ(window size)のsubstringを見ていった時に、その中に登場する文字列の数が等しい状態を指す。
  • substringは連続した文字列のこと。(sub sequenceは途中の文字を削除したものでも良いが、substringは連続してないとダメ)
  • s1のwindow sizeでs2をイテレートして毎回文字列のカウントを数えてs1と一致するかを確認していくのが一番直感的だが、O(N^2)の時間がかかる。O(N)の方法を考えたい。
  • Both strings only contain lowercase letters.」なので、最大でも26種類しかない。文字列のカウントを0~25のindexで保持した配列をそれぞれ持っておいて、それらを増減させつつぴったりのものがあればTrueを返してあげればO(N)で解ける。
  • 途中matchesの更新が若干ややこしい感じがするが、これが一番速いし、この後のHard問題でも似たようなテクを使うのでやっておいて損はない。ただし、26文字同士の比較であれば配列全部なめてもO(1)扱いで良いので、それでも問題はないはず。

Problem

You are given a string s consisting of only uppercase english characters and an integer k. You can choose up to k characters of the string and replace them with any other uppercase English character.

After performing at most k replacements, return the length of the longest substring which contains only one distinct character.

Example 1:

Input: s = "XYYX", k = 2

Output: 4

Copy

Explanation: Either replace the 'X's with 'Y's, or replace the 'Y's with 'X's.

Example 2:

Input: s = "AAABABB", k = 1

Output: 5

Copy

Constraints:

  • 1 <= s.length <= 1000
  • 0 <= k <= s.length

Solution

class Solution:
    def checkInclusion(self, s1: str, s2: str) -> bool:
        if len(s1) > len(s2):
            return False
        
        s1_ascii_array = [0] * 26
        s2_ascii_array = [0] * 26

        for i in range(len(s1)):
            ascii_s1 = ord(s1[i]) - ord('a')
            ascii_s2 = ord(s2[i]) - ord('a')

            s1_ascii_array[ascii_s1] += 1
            s2_ascii_array[ascii_s2] += 1
        
        matches = 0
        for i in range(26):
            if s1_ascii_array[i] == s2_ascii_array[i]:
                matches += 1
        
        l = 0
        for r in range(len(s1), len(s2)):
            if matches == 26:
                # 文字列のカウント数が等しいのでTrueを返す
                return True
            
            # 新しい文字のカウントを足す
            ascii_r = ord(s2[r]) - ord('a')
            s2_ascii_array[ascii_r] += 1

            # 新しく足した文字のカウントがs1の文字のカウントと等しければmatchesをincrement
            if s2_ascii_array[ascii_r] == s1_ascii_array[ascii_r]:
                matches += 1
            # 元々等しかった場合はmatchesを減らす
            elif s2_ascii_array[ascii_r] - 1 == s1_ascii_array[ascii_r]:
                matches -= 1

            # window_sizeを縮小する際にlの文字カウントを減らす
            ascii_l = ord(s2[l]) - ord('a')
            s2_ascii_array[ascii_l] -= 1

            # 減らした文字のカウントがs1の文字のカウントと等しければmatchesをincrement
            if s2_ascii_array[ascii_l] == s1_ascii_array[ascii_l]:
                matches += 1
            # 元々等しかった場合はmatchesを減らす
            elif s2_ascii_array[ascii_l] + 1 == s1_ascii_array[ascii_l]:
                matches -= 1
            
            l += 1
        
        # 最後の文字に到達した後の比較はまだできてないので、ここで比較する
        return matches == 26


33. Minimum Window Substring

Link

https://neetcode.io/problems/minimum-window-with-characters

Related Problem

https://neetcode.io/problems/permutation-string

Difficulty (Easy, Medium, Hard)

Hard

Time Complexity

O(N)

Space Complexity

O(N)

The personal need for review (1 ~ 10)

6

Memo

  • なかなか実装がややこしい。
  • 一つ前の問題はsubstringが完全に一致するかだったけど、今回はtをsのsubstringが含めば良いことになっているので、matches == 26 だと判定できない。sのsubstringの各文字の登場頻度がtの各文字の登場頻度を上回れば良い。
  • tに登場するユニークな文字をNEEDとして持っておく(例えばXYZの場合は3, XXZの場合は2になる)
  • sのsubstringの中に登場する文字で、tに登場するユニークな文字のカウントを上回っている数をHAVEとして持つ。NEED == HAVE となったタイミングがあれば、その文字列を記録しておく。一番短い文字列が答えなので、より短いものが登場すれば更新する。
  • カウントが等しいではなく、上回ることでHAVEの値が増え、下回ることで減るという点がややこしいポイント。初めてカウントが等しくなった時だけインクリメントする。ここさえ気をつければMediumの問題とほぼ同じ考え方で解ける。

Problem

Given two strings s and t, return the shortest substring of s such that every character in t, including duplicates, is present in the substring. If such a substring does not exist, return an empty string "".

You may assume that the correct output is always unique.

Example 1:

Input: s = "OUZODYXAZV", t = "XYZ"

Output: "YXAZ"

Copy

Explanation: "YXAZ" is the shortest substring that includes "X", "Y", and "Z" from string t.

Example 2:

Input: s = "xyz", t = "xyz"

Output: "xyz"

Copy

Example 3:

Input: s = "x", t = "xy"

Output: ""

Copy

Constraints:

  • 1 <= s.length <= 1000
  • 1 <= t.length <= 1000
  • s and t consist of uppercase and lowercase English letters.

Solution

from collections import Counter, defaultdict

class Solution:
    def minWindow(self, s: str, t: str) -> str:
        if len(t) > len(s):
            return ''
        
        t_counter = Counter(t)
        s_counter = defaultdict(int)

        # ユニークな文字列の数が必要な数
        NEED = len(t_counter.keys())

        # tの文字のカウントを上回る文字がwindowの中にあれば+1
        HAVE = 0

        for i in range(len(t)):
            s_counter[s[i]] += 1

            if s_counter[s[i]] == t_counter[s[i]]:
                HAVE += 1
        
        # sの最初のtの長さまでの文字とtの文字カウントが等しいのでそれを返す
        if HAVE == NEED:
            return s[:len(t)]

        l = 0
        shortest = ''
        shortest_len = math.inf

        for r in range(len(t), len(s)):
            s_counter[s[r]] += 1
            if s_counter[s[r]] == t_counter[s[r]]:
                HAVE += 1
            
            while HAVE == NEED:
                if (r - l + 1) < shortest_len:
                    shortest = s[l:r+1]
                    shortest_len = r - l + 1
                
                s_counter[s[l]] -= 1
                if s_counter[s[l]] + 1 == t_counter[s[l]]:
                    HAVE -= 1
                l += 1
        
        return shortest


34. Sliding Window Maximum

Link

https://neetcode.io/problems/sliding-window-maximum

Related Problem

Difficulty (Easy, Medium, Hard)

Hard

Time Complexity

O(N)

Space Complexity

O(N)

The personal need for review (1 ~ 10)

6

Memo

  • Hardにしては実装がシンプル。(easyだというわけではない)
  • decreasing monotonic queue というらしい。左から順に単調減少する値をqueueに入れる。
  • queueの右端(queueの中だと一番小さい)よりも大きい値が出てきたらpopし続ける
  • window size(r - l + 1) が kと等しくなったらresにqueueの左端の値を追加する。
  • window size(r - l + 1) が k 以上になったらqueueの左端にある値のindexがlと等しければpopする。

Problem

You are given an array of integers nums and an integer k. There is a sliding window of size k that starts at the left edge of the array. The window slides one position to the right until it reaches the right edge of the array.

Return a list that contains the maximum element in the window at each step.

Example 1:

Input: nums = [1,2,1,0,4,2,6], k = 3

Output: [2,2,4,4,6]

Explanation: 
Window position            Max
---------------           -----
[1  2  1] 0  4  2  6        2
 1 [2  1  0] 4  2  6        2
 1  2 [1  0  4] 2  6        4
 1  2  1 [0  4  2] 6        4
 1  2  1  0 [4  2  6]       6

Copy

Constraints:

  • 1 <= nums.length <= 1000
  • -1000 <= nums[i] <= 1000
  • 1 <= k <= nums.length

Solution

from collections import deque
class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
        queue = deque() # index

        res = []
        l = 0
        for r in range(len(nums)):
            while queue and nums[queue[-1]] < nums[r]:
                queue.pop()
            
            queue.append(r)

            # window sizeがkと等しくなったらresに最大値を追加する
            # 最大値はqueueの左端にある値
            # window sizeがk以上になったら、
            # queueの左端のindexがlと等しければpopleftしてlをincrement
            if r - l + 1 >= k:
                res.append(nums[queue[0]])

                if l == queue[0]:
                    queue.popleft()
                l += 1
        
        return res

Toshimitsu Kugimoto

Software Engineer

仕事では決済やメディアのWebやスマホアプリのBE開発、WebのFE開発をやっています。 JavaとTypeScriptをよく使います。プライベートではFlutterでのアプリ開発にも挑戦中です。