置頂

我的 VPython 教學文件 (HackMD 版本)

VPython 教學文件目錄 安裝及測試 基本語法 等速度直線運動 自由落下 終端速度 水平抛射 使用For迴圈計算水平抛射資料 斜向抛射 圓周運動 簡諧運動 單擺 木塊彈簧系統分離 重力及簡諧 行星運動 相疊木塊 雙重簡諧運動 一維彈性碰撞 ...

熱門文章

2026年8月18日 星期二

LeetCode 解題筆記:3471. Find the Largest Almost Missing Integer

作者:王一哲
日期:2026年8月18日


LeetCode 題目連結:3471. Find the Largest Almost Missing Integer

解題想法


簡單題。題目給一個長度為 $n$ 的陣列 $nums$ 及整數 $k$,要從 $nums$ 之中找出長度為 $k$ 的連續子序列,子序列之中有一個數字只出現一次,回傳這些數字中的最大值。我一開始用的寫法非常直接,先找出所有長度為 $k$ 的連續子序列,將子序列存成 set,再將 set 存入 list 之中。接下來再依序從 $nums$ 讀取數字 $num$,檢查 $num$ 是否在所有的子序列中只出現一次而且 $num$ 大於目前的答案 $ans$,如果條件成立就更新 $ans$。這個寫法在 Python 的速度還可以,但是在 C++ 就很糟糕了。

比較好的寫法應該是列出以下 3 種狀況:
  1. $k = n$,子序列就是 $nums$,回傳 $nums$ 之中的最大值。
  2. $k = 1$,子序列就是 $nums$ 之中的每個數字,找出只在 $nums$ 之中出現一次的數字最大值。
  3. $k \neq n, k \neq 1$,只需要找 $nums[0]$ 與 $nums[n-1]$,因為中間的數字至少會出現在 2 個子序列之中。答案有 4 種:
    1. $nums[0]$ 與 $nums[n-1]$ 都只出現一次,回傳較大者。
    2. $nums[0]$ 只出現一次,$nums[n-1]$ 出現 2 次以上,回傳 $nums[0]$。
    3. $nums[n-1]$ 只出現一次,$nums[0]$ 出現 2 次以上,回傳 $nums[n-1]$。
    4. 以上條件皆不成立,回傳 $-1$。

Python 程式碼


方法1,Runtime: 3 ms, beats 62.30%. Memory: 19.15 MB, beats 93.85%.
class Solution:
    def largestInteger(self, nums: List[int], k: int) -> int:
        n = len(nums)
        subs = [set() for _ in range(n-k+1)]
        for i in range(n-k+1):
            subs[i] = set(nums[i:i+k])

        ans = -1
        for num in nums:
            cnt = 0
            for sub in subs:
                if num in sub: cnt += 1
                if cnt >= 2: break
            if cnt == 1 and num > ans:
                ans = num
        return ans


方法2,Runtime: 1 ms, beats 70.90%. Memory: 19.36 MB, beats 35.66%.
class Solution:
    def largestInteger(self, nums: List[int], k: int) -> int:
        n = len(nums)  # 長度
        # Case 1. k == n,回傳 nums 的最大值
        if k == n: return max(nums)

        # Case 2. k == 1,回傳只出現一次的數字最大值
        cnt = Counter(nums)  # 計數器
        if k == 1:
            ans = -1  # 答案預設為 -1
            for num in nums:
                if cnt[num] == 1 and num > ans:
                    ans = num
            return ans
        
        # Case 3. 一般狀況,只需要考慮 nums[0] 及 nums[n-1],因為其它數字至少會出現在子序列之中 2 次
        first, last = nums[0], nums[-1]
        # first, last 次數都是 1,回傳較大者
        if cnt[first] == 1 and cnt[last] == 1:
            return max(first, last)
        # first 次數 1,last 次數大於 1,回傳 first
        if cnt[first] == 1 and cnt[last] > 1:
            return first
        # first 次數大於 1,last 次數 1,回傳 last
        if cnt[first] > 1 and cnt[last] == 1:
            return last
        # 沒有答案,回傳 -1
        return -1


2026年8月17日 星期一

LeetCode 解題筆記:1563. Stone Game V

作者:王一哲
日期:2026年8月17日


LeetCode 題目連結:1563. Stone Game V

解題想法


困難題。題目給一個長度為 $n$ 的陣列 $stoneValue$ 代表每個石頭的分數,個回合 Alice 可以選擇一個分割點,將這列石頭分成左、右半邊,Bob 會將總分較高的半邊丢掉,Alice 可以獲得留下半邊石頭的總分,題目要問 Alice 最多可以拿幾分。由於這個題目需要不斷地計算區問和,需要先建立前綴和陣列 $psum$。接下來用動態規畫解題,定義大小為 $n \times n$ 的二維陣列 $dp$,$dp[i][j]$ 代表 Alice 在區間 i ~ j 能獲得的最高分,最後答案會在 $dp[0][n-1]$。填滿 $dp$ 的方法有兩種,第一種較簡單但是時間複雜度為 $O(n^3)$,用 Python 會超時,C 與 C++ 可以過關,但是時間排名很後面;第二種較複雜但是時間複雜度為 $O(n^2)$,用 Python、C、C++ 都能過關。

Python 程式碼


方法1,超時。
class Solution:
    def stoneGameV(self, stoneValue: List[int]) -> int:
        n = len(stoneValue)  # 數量
        
        # 1. 建立前綴和,之後可以用來查詢區間和
        psum = [0] * (n+1)  # pusm 的索引值比 stoneValue 多 1
        for i in range(1, n+1):
            psum[i] = psum[i-1] + stoneValue[i-1]
        
        # 2. 建立動態規畫陣列,dp[i][j] 代表在區間 [i...j] 之間 Alice 所能獲得的最高分
        dp = [[0]*n for _ in range(n)]
        
        # 3. 動態規畫
        for length in range(2, n+1):  # 區間長度 2 ~ n
            for i in range(0, n - length + 1):  # 起點 0 ~ n - length
                j = i + length - 1  # 終點
                for k in range(i, j):  # 分割點 i ~ j-1
                    lsum = psum[k+1] - psum[i]  # stoneValue[i] ~ stoneValue[k]
                    rsum = psum[j+1] - psum[k+1]  # stoneValue[k+1] ~ stoneValue[j]
                    if lsum > rsum:  # 左半邊總分較多,剩下右半邊
                        dp[i][j] = max(dp[i][j], rsum + dp[k+1][j])
                    elif lsum < rsum:  # 右半總分較多,剩下左半邊
                        dp[i][j] = max(dp[i][j], lsum + dp[i][k])
                    else:  # 兩側分數相同,Alice 選 dp 區間較高分
                        dp[i][j] = max(dp[i][j], lsum + max(dp[i][k], dp[k+1][j]))
        # 答案在 dp[0][n-1]
        return dp[0][n-1]


方法2,Runtime: 619 ms, beats 77.25%. Memory: 33.17 MB, beats 65.49%.
class Solution:
    def stoneGameV(self, stoneValue: List[int]) -> int:
        n = len(stoneValue)  # 數量
        
        # 1. 建立前綴和,之後可以用來查詢區間和
        psum = [0] * (n+1)  # pusm 的索引值比 stoneValue 多 1
        for i in range(1, n+1):
            psum[i] = psum[i-1] + stoneValue[i-1]
        
        # 2. 建立動態規畫陣列
        # dp[i][j] 代表在區間 [i...j] 之間 Alice 所能獲得的最高分
        dp = [[0]*n for _ in range(n)]
        # lmax[i][j] = dp[i][k] + sum(dp[i:k+1]) 的最大值
        lmax = [[0]*n for _ in range(n)]
        # rmax[i][j] = dp[k][j] + sum(dp[k:j+1]) 的最大值
        rmax = [[0]*n for _ in range(n)]
        # 初始化 lmax, rmal,長度 1 
        for i in range(n):
            lmax[i][i] = stoneValue[i]
            rmax[i][i] = stoneValue[i]
        
        # 3. 動態規畫,由短至長
        for i in range(n-1, -1, -1):  # i = n-1 ~ 0
            mid = i - 1  # 分割點
            for j in range(i+1, n):  # j = i+1 ~ n-1
                total = psum[j+1] - psum[i]  # stoneValue[i] + ... + stoneValue[j]
                # 找出左半邊和 L 大於右半邊和 R 的分割點
                # 如果 mid + 1 這格還是不符合條件,再向右移動1格
                # L >= R => L + L >= L + R => 2*L >= total
                # 2 * (psum[mid + 2] - psum[i]) >= total
                while mid + 1 < j and 2 * (psum[mid + 2] - psum[i]) <= total:
                    mid += 1
                
                res = 0
                # 狀況1,左半邊總分 > 右半邊總分 
                if mid >= i:
                    res = max(res, lmax[i][mid])
                    # mid 左半邊總分 == 右半邊總分,可以留下右半邊
                    if 2 * (psum[mid + 1] - psum[i]) == total:
                        res = max(res, rmax[mid + 1][j])
                # 狀況2,左半邊總分 < 右半邊總分
                if mid + 2 <= j:
                    res = max(res, rmax[mid + 2][j])
                # 更新 dp, lmax, rmax
                dp[i][j] = res
                lmax[i][j] = max(lmax[i][j-1], dp[i][j] + total)
                rmax[i][j] = max(rmax[i+1][j], dp[i][j] + total)
        # 答案在 dp[0][n-1]
        return dp[0][n-1]


2026年8月16日 星期日

LeetCode 解題筆記:2029. Stone Game IX

作者:王一哲
日期:2026年8月16日


LeetCode 題目連結:2029. Stone Game IX

解題想法


中等難度題。題目給一個整數陣列 $stones$ 代表一列石頭各自的分數,Alice 和 Bob 輪流拿石頭,如果目前行動的玩家拿走石頭時所有被移除的石頭總分為 3 的倍數,目前行動的玩家輸掉比賽;如果所有的石頭都拿光了,Alice 輸掉比賽。如果 Alice 能夠獲勝回傳 True,反之回傳 False。這題真正困難的地方在於找出 Alice 獲勝的條件,程式碼反而很簡短。由於題目只關心被移除的石頭總分是否為 3 的倍數,所以我們不需要計算總分,只要計算移除的石頭分數對 3 的餘數。先計算石頭分數對 3 取餘數為 0、1、2 的數量分別為 $a, b, c$,Alice 獲勝的狀況有以下 2 種:
  1. $a$ 為偶數且 $b > 0, c > 0$,Alice 第一回合可以拿走一顆餘數 1 或 2 的石頭,Bob 就算用餘數 0 的石頭拖時間,最後還是會拿到將總分湊成 3 的倍數的石頭。
  2. $a$ 為奇數且 $abs(b - c) > 2$,Alice 第一回合可以拿走餘數 1 及 2 的石頭之中數量較多者,Bob 就算用餘數 0 的石頭拖時間,Alice 還能夠拿一顆與第一回合相同的石頭。


Python 程式碼


Runtime: 55 ms, beats 45.80%. Memory: 30.49 MB, beats 88.55%.
class Solution:
    def stoneGameIX(self, stones: List[int]) -> bool:
        # 石頭的分數對 3 取餘數,餘數 0、1、2 的數量
        a, b, c = 0, 0, 0
        for num in stones:
            rem = num % 3
            if rem == 0: a += 1
            elif rem == 1: b += 1
            else: c += 1
        # 狀況1,a 是偶數,如果 b, c 都大於 0,第1回合可以任意選 b 或 c,Alice 勝
        # 狀況2,a 是奇數,如果 b, c 相差大於 2,Alice 勝
        if a % 2 == 0:
            return b > 0 and c > 0  
        else:
            return abs(b - c) > 2


2026年8月15日 星期六

LeetCode 解題筆記:3702. Longest Subsequence With Non-Zero Bitwise XOR

作者:王一哲
日期:2026年8月15日


LeetCode 題目連結:3702. Longest Subsequence With Non-Zero Bitwise XOR

解題想法


中等難度題。題目給一個整數陣列 $nums$,取任意長度的子陣列使其中所有的數字 XOR 不等於 0,求最大長度。這題看起來很像 0/1 背包問題,因為每個數字只有選或不選兩種可能性,但是這題的數字最大為 $10^9$,如果用 0/1 背包問題的方式處理會超時。這題需要用到 XOR 的數學性質,假設 $nums$ 的長度為 $n$,答案可能是以下 3 種狀況
  1. 如果所有的數字取 XOR 的結果 $total$ 不為 $0$,直接回傳 $n$。
  2. 如果 $total$ 為 $0$,且 $nums$ 之中有任意一個數字不為 $0$,刪除一個不為 $0$ 的數字可以使 $total$ 不為 $0$,回傳 $n-1$。
  3. 如果 $total$ 為 $0$,且所有的數字為 $0$,回傳 $0$。
範例測資 2 就是狀況 2,$nums = [2, 3, 4] = [10_2, 11_2, 100_2]$,如果取 $[2, 3, 4]$ 計算 XOR $$ 10 \oplus 11 \oplus 100 = 1 \oplus 100 = 0 $$ 刪除任意一個數字再取 XOR $$ 10 \oplus 11 = 1 ~~~~~ 10 \oplus 100 = 110 ~~~~~ 11 \oplus 100 = 101 $$ 如果用數學證明狀況 2,可以用反證法。如果陣列 $a$ 所有的元素取 XOR 為 $0$ $$ a_0 \oplus a_1 \oplus a_2 \oplus \dots \oplus a_{n-1} = 0 $$ 假設移除其中一項不為 $0$ 的元素 $a_i$ 可以使 $a$ 所有的元素取 XOR 仍然為 $0$,因為 $a_i \oplus a_i = 0$,所以 $$ (a_0 \oplus a_1 \oplus a_2 \oplus \dots \oplus a_{n-1}) \oplus a_i = 0 ~\Rightarrow~ 0 \oplus a_i = 0 $$ 但是 $$ 0 \oplus a_i = a_i $$ 兩個結果互相矛盾,假設錯誤,移除其中一項不為 $0$ 的元素 $a_i$ 可以使 $a$ 所有的元素取 XOR 不為 $0$。

Python 程式碼


Runtime: 27 ms, beats 74.19%. Memory: 33.18 MB, beats 66.13%.
class Solution:
    def longestSubsequence(self, nums: List[int]) -> int:
        n = len(nums)  # 數量
        non_zero = False  # 是否有任意一個非 0 的數字
        # 先取所有數字的 XOR
        total = 0
        for num in nums:
            total ^= num
            if num > 0: non_zero = True
        # 狀況1,所有數字的 XOR 不等於 0
        if total > 0: return n
        # 狀況2,所有數字的 XOR 等於 0,至少有一個非 0 的數字
        if total == 0 and non_zero: return n-1
        # 狀況3,所有數字都是 0
        return 0


2026年8月14日 星期五

LeetCode 解題筆記:3090. Maximum Length Substring With Two Occurrences

作者:王一哲
日期:2026年8月14日


LeetCode 題目連結:3090. Maximum Length Substring With Two Occurrences

解題想法


簡單題。題目給一個字串 $s$,要找出每個字母最多只會出現兩次的最長子字串長度,基本上就是 2958. Length of Longest Subarray With at Most K Frequency 的簡化版。這題很適合用滑動視窗 (sliding window) 解題。
  1. $s$ 的長度為 $n$,視窗左端點 $left$ 起始值為 $0$,答案 $ans$ 預設為 $0$,用表格或字典 $cnt$ 記錄視窗範圍內的數字數量。
  2. 用一層 for 迴圈跑視窗右端點 $right = 0$ 到 $right = n-1$,$cnt[s[right]] += 1$。
  3. 再用一層 while 迴圈,如果 $left < right$ 且 $cnt[s[right]] > 2$ 繼續執行,移除左端點的字母 $cnt[s[left]] -= 1$,左端點向右移 1 格 $left += 1$。
  4. 跑完 while 迴圈時,$s[right]$ 到 $s[left]$ 之間的字母數量都小於等於 $2$,視窗長度為 $right - left + 1$,這可能是新的答案,更新 $ans$。


Python 程式碼


使用預設的字典計數。Runtime: 2 ms, beats 81.56%. Memory: 19.1 MB, beats 98.37%.
class Solution:
    def maximumLengthSubstring(self, s: str) -> int:
        cnt = dict()
        n, ans, left = len(s), 0, 0
        for right in range(n):
            if s[right] not in cnt:
                cnt[s[right]] = 1
            else:
                cnt[s[right]] += 1
            while left < right and cnt[s[right]] > 2:
                cnt[s[left]] -= 1
                left += 1
            ans = max(ans, right - left + 1)
        return ans


使用 defaultdict 計數。Runtime: 3 ms, beats 76.16%. Memory: 19.2 MB, beats 60.10%.
class Solution:
    def maximumLengthSubstring(self, s: str) -> int:
        cnt = defaultdict(int)
        n, ans, left = len(s), 0, 0
        for right in range(n):
            cnt[s[right]] += 1
            while left < right and cnt[s[right]] > 2:
                cnt[s[left]] -= 1
                left += 1
            ans = max(ans, right - left + 1)
        return ans


2026年8月13日 星期四

LeetCode 解題筆記:2213. Longest Substring of One Repeating Character

作者:王一哲
日期:2026年8月13日


LeetCode 題目連結:2213. Longest Substring of One Repeating Character

解題想法


困難題。題目給一個字串 $s$,長度為 $k$ 的字串 $queryCharacters$,長度為 $k$ 的整數陣列 $queryIndices$,第 $i$ 次查詢時會將 $s[queryIndices[i]]$ 改成 $queryCharacters[i]$,找出修改後的字串 $s$ 之中最長連續相同字母的子字串長度。由於這題 $s$ 最長為 $10^5$,查詢次數最多也是 $10^5$,如果每次修改 $s$ 之後都要從頭再找一次答案,這樣一定會超時。可以用線段樹 (segment tree) 解題,我一開始寫出來的 C++ 版本速度不快,將程式碼丟給 Gemini 詢問如何改寫程式碼才能加速,發現問題出在指標及配置記憶體花費太多時間,修改後速度快很多。

定義節點 Node class 或 struct,其中儲存了
  • pre_len 從左端點開始的連續相同字元長度
  • suf_len 從右端點開始的連續相同字元長度
  • max_len 區間內最大連續長度
  • total_len 區間總長度
  • left_char 區間左端點的字元
  • right_char 區間右端點的字元
定義自訂線段樹 class,初始化時設定字串 $s$,字串長度 $n$,樹的內容 $tree$,資料格式為 Node,長度為 $4n$。類別中再定義以下的函式
  • _build 內部函式,建立樹的內容。
  • _merge 內部函式,合併節點。
  • _update 內部函式,單點更新。
  • update 外部函式,用來呼叫 _update。
  • query_max 外部函式,回傳根節點的最大連續長度。


Python 程式碼


Runtime: 4167 ms, beats 16.40%. Memory: 85.56 MB, beats 31.15%.
class Node:
    # 自訂節點類別
    def __init__(self, pre_len=0, suf_len=0, max_len=0, left_char='', right_char='', total_len=0):
        self.pre_len = pre_len  # 從左端點開始的連續相同字元長度
        self.suf_len = suf_len  # 從右端點開始的連續相同字元長度
        self.max_len = max_len  # 區間內最大連續長度
        self.left_char = left_char  # 區間左端點的字元
        self.right_char = right_char  # 區間右端點的字元
        self.total_len = total_len  # 區間總長度

class SegmentTree:
    # 自訂線段樹類別
    def __init__(self, s):
        self.n = len(s)  # 長度
        self.s = s  # 字串
        self.tree = [Node() for _ in range(4 * self.n)]  # 樹的內容
        # 呼叫內部函式 _build 建立樹,根節點索引值 1,左端點 0,右端點 n-1
        self._build(1, 0, self.n - 1)

    def _build(self, idx, start, end):
        # 內部函式,索引值 idx,左端點 start,右端點 end
        # 遞迴出口,左、右端點重合,建立新的節點
        if start == end:
            self.tree[idx] = Node(1, 1, 1, self.s[start], self.s[start], 1)
            return
        # 一般狀況,用遞迴建立左、右子節點
        mid = (start + end) // 2  # 中點
        self._build(2 * idx, start, mid)  # 遞迴,建立左子節點
        self._build(2 * idx + 1, mid + 1, end)  # 遞迴,建立右子節點
        self.tree[idx] = self._merge(self.tree[2 * idx], self.tree[2 * idx + 1])  # 合併左、右子節點成為父節點
    
    def _merge(self, left, right):
        # 內部函式,合併左、右子節點
        res = Node()  # 最後要回傳的節點
        res.total_len = left.total_len + right.total_len  # 更新線長度
        res.left_char = left.left_char  # 左端點字元
        res.right_char = right.right_char  # 右端點字元
        res.pre_len = left.pre_len  # 左端前綴長度
        res.suf_len = right.suf_len  # 右端後綴長度
        res.max_len = max(left.max_len, right.max_len)  # 更新最大長度
        # 如果左右交界處字元相同,進行跨區合併
        if left.right_char == right.left_char:
            cross_len = left.suf_len + right.pre_len
            res.max_len = max(res.max_len, cross_len)
            # 如果左子節點是同一個字元,更新前綴長度
            if left.pre_len == left.total_len:
                res.pre_len = left.total_len + right.pre_len
            # 如果右子節點是同一個字元,更新後綴長度
            if right.suf_len == right.total_len:
                res.suf_len = right.total_len + left.suf_len
        return res  # 回傳 res

    def _update(self, idx, start, end, target, new_char):
        # 內部函式,單點更新,目前處理節點索引值 idx,左端點 left,右端點 right,目標索引值 target,新的字元 new_char
        # 遞迴出口,左、右端點重合,更新 self.tree[idx]
        if start == end:
            self.tree[idx] = Node(1, 1, 1, new_char, new_char, 1)
            return
        # 一般狀況,遞迴,找到 target 之後再往上層更新父節點
        mid = (start + end) // 2  # 中點
        if target <= mid:  # 目標在左側
            self._update(2 * idx, start, mid, target, new_char)
        else:  # 目標在右側
            self._update(2 * idx + 1, mid + 1, end, target, new_char)
        self.tree[idx] = self._merge(self.tree[2 * idx], self.tree[2 * idx + 1])  # 合併
    
    def update(self, target, new_char):
        # 外部函式,呼叫 self._update
        self._update(1, 0, self.n - 1, target, new_char)
    
    def query_max(self):
        # 回傳根節點的最大連續重複長度
        return self.tree[1].max_len

class Solution:
    def longestRepeating(self, s: str, queryCharacters: str, queryIndices: List[int]) -> List[int]:
        # 初始化線段樹物件
        tree = SegmentTree(s)
        ans = []
        # 處理更新及查詢
        for new_char, target in zip(queryCharacters, queryIndices):
            # 單點更新
            tree.update(target, new_char)
            # 查詢目前整體的最長連續重複字元長度
            ans.append(tree.query_max())
        return ans


2026年8月12日 星期三

LeetCode 解題筆記:2958. Length of Longest Subarray With at Most K Frequency

作者:王一哲
日期:2026年8月12日


LeetCode 題目連結:2958. Length of Longest Subarray With at Most K Frequency

解題想法


中等難度題。題目給一個整數陣列 $nums$ 及一個整數 $k$,要找出 $nums$ 之中的最長連續子陣列,且子陣列之中每個數字出現的次數小於等於 $k$。這題很適合用滑動視窗 (sliding window) 解題。
  1. $nums$ 的長度為 $n$,視窗左端點 $left$ 起始值為 $0$,答案 $ans$ 預設為 $0$,用字典 $cnt$ 記錄視窗範圍內的數字數量。
  2. 用一層 for 迴圈跑視窗右端點 $right = 0$ 到 $right = n-1$,$cnt[nums[right]] += 1$。
  3. 再用一層 while 迴圈,如果 $left < right$ 且 $cnt[nums[right]] > k$ 繼續執行,移除左端點的數字 $cnt[nums[left]] -= 1$,左端點向右移 1 格 $left += 1$。
  4. 跑完 while 迴圈時,$nums[right]$ 到 $nums[left]$ 之間的數字數量都小於等於 $k$,視窗長度為 $right - left + 1$,這可能是新的答案,更新 $ans$。


Python 程式碼


用 defaultdict 比較方便。Runtime: 262 ms, beats 44.51%. Memory: 35.44 MB, beats 15.77%.
class Solution:
    def maxSubarrayLength(self, nums: List[int], k: int) -> int:
        n = len(nums)  # 長度
        left = 0  # 視窗左邊界
        ans = 0  # 答案
        cnt = defaultdict(int)  # 視窗範圍內數字計數器
        for right in range(n):  # 視窗右邊界依序為 0 ~ n-1
            cnt[nums[right]] += 1  # 右邊界數字數量加 1
            # 如果左邊界小於右邊界,右邊界數字數量大於 k
            while left < right and cnt[nums[right]] > k:
                cnt[nums[left]] -= 1  # 左邊界數字數量減 1
                left += 1  # 左邊界向右移 1 格
            ans = max(ans, right - left + 1)  # 更新答案
        return ans

用預設的 dict,更新 $nums[right]$ 的數量時需要先檢查 $nums[right]$ 是否在 $cnt$ 之中,比較麻煩一點。Runtime: 262 ms, beats 44.51%. Memory: 35.20 MB, beats 94.91%.
class Solution:
    def maxSubarrayLength(self, nums: List[int], k: int) -> int:
        n = len(nums)  # 長度
        left = 0  # 視窗左邊界
        ans = 0  # 答案
        cnt = dict()  # 視窗範圍內數字計數器
        for right in range(n):  # 視窗右邊界依序為 0 ~ n-1
            # 右邊界數字數量加 1
            if nums[right] not in cnt:
                cnt[nums[right]] = 1
            else:
                cnt[nums[right]] += 1
            # 如果左邊界小於右邊界,右邊界數字數量大於 k
            while left < right and cnt[nums[right]] > k:
                cnt[nums[left]] -= 1  # 左邊界數字數量減 1
                left += 1  # 左邊界向右移 1 格
            ans = max(ans, right - left + 1)  # 更新答案
        return ans


2026年8月11日 星期二

LeetCode 解題筆記:2996. Smallest Missing Integer Greater Than Sequential Prefix Sum

作者:王一哲
日期:2026年8月11日


LeetCode 題目連結:2996. Smallest Missing Integer Greater Than Sequential Prefix Sum

解題想法


簡單題。我一直覺得我沒有弄清楚題目的意思,但是不小心就過關了,以下是我對題目的解釋。題目給一個索引值為 0 開頭的陣列 $nums$。如果 $nums[0]$ 到 $nums[i]$ 符合條件 $nums[j] = nums[j-1] + 1, ~ 1 \leq j \leq i$,則 $nums[0]$ 到 $nums[i]$ 為連續的 (sequential)。其中的特例為 $nums[0]$,只有 $nums[0]$ 也符合連續的條件。題目要回傳一個最小的整數 $x$,且 $x$ 大於、等於最長連續前綴 (longest sequential prefix)。我的想法是先從 $i = 0$ 開始找最長連續前綴加總 $psum$,接下來將 $nums$ 轉成集合 $num\_set$,用一個 while 迴圈檢查 $psum$ 是否在 $num\_set$ 之中,如果條件成立就將 $psum$ 加 $1$,用線性搜尋的方式找答案。

Python 程式碼


Runtime: 0 ms, beats 100.00%. Memory: 19.22 MB, beats 51.82%.
class Solution:
    def missingInteger(self, nums: List[int]) -> int:
        n, psum = len(nums), nums[0]
        for i in range(1, n):
            if nums[i] == nums[i-1] + 1:
                psum += nums[i]
            else:
                break
        
        num_set = set(nums)
        while psum in num_set:
            psum += 1
        return psum


2026年8月10日 星期一

LeetCode 解題筆記:1510. Stone Game IV

作者:王一哲
日期:2026年8月10日


LeetCode 題目連結:1510. Stone Game IV

解題想法


困難題。題目給一個正整數 $n, ~ 1 \leq n \leq 10^5$,代表這堆石頭的數量,兩位玩家 Alice、Bob 輪流取走任意平方數的石頭,如果輪到自己時已經沒有石頭則輸掉比賽,固定由 Alice 先行動,回傳 Alice 的勝負狀態。這題用動態規畫解題,定義長度為 $n+1$ 的一維陣列 $dp$,$dp[i]$ 代表石頭數量為 $i$ 時 Alice 的勝負狀態,$dp$ 預設值皆為 False。用一個 for 迴圈更新 $i = 1$ 到 $i = n$,裡面再用一個 while 迴圈,更新 $j = 1$ 到 $j^2 \leq i$,如果任何一個 $dp[i - j*j] = false$,代表 $dp[i] = true$,就可以中止內層的 while 迴圈。最後回傳 $dp[n]$。

Python 程式碼


Runtime: 596 ms, beats 56.67%. Memory: 19.92 MB, beats 85.15%.
class Solution:
    def winnerSquareGame(self, n: int) -> bool:
        # dp[i] 代表數量為 i 時 Alice 的勝負狀態,0 顆必敗,dp[0] = False
        dp = [False] * (n+1)
        # 跑 i = 1 ~ n,檢查所有小於等於 i 的平方數 j,如果 dp[i - j*j] = False,則 dp[i] = True
        for i in range(1, n+1):
            j = 1
            while j*j <= i:
                if not dp[i - j*j]:
                    dp[i] = True
                    break
                j += 1
        return dp[n]


C++ 程式碼


Runtime: 31 ms, beats 81.44%. Memory: 8.85 MB, beats 90.65%.
class Solution {
public:
    bool winnerSquareGame(int n) {
        // dp[i] 代表數量為 i 時 Alice 的勝負狀態,0 顆必敗,dp[0] = False
        vector<bool> dp (n+1, false);
        // 跑 i = 1 ~ n,檢查所有小於等於 i 的平方數 j,如果 dp[i - j*j] = False,則 dp[i] = True
        for(int i = 1; i <= n; i++) {
            int j = 1;
            while(j*j <= i) {
                if (!dp[i - j*j]) {
                    dp[i] = true;
                    break;
                }
                j++;
            }
        }
        return dp[n];
    }
};


2026年8月9日 星期日

ZeroJudge 解題筆記:s573. 多項式 - 判斷正負

作者:王一哲
日期:2026年8月9日


ZeroJudge 題目連結:s573. 多項式 - 判斷正負

解題想法


這題與 s562. 多項式 - 湊出 a_n 幾乎一樣。題目定義 $$ (1 - a)(1 - b)(1 - c)(1 - d) \dots = 1 - a - b + ab - c + ac + bc - abc - d + \dots $$ 測資第一行為 $t$,代表接下來有 $t$ 行數字 $n$,要回傳第 $n$ 項的正負號。實際上這題考的是二進位,先計算 $n$ 的二進位制之中有幾個 $1$,如果。$1$ 的數量為偶數輸出 $+$,數量為奇數輸出 $-$。

Python 程式碼


解題時間約為 0.2 s,使用記憶體約為 8.4 MB。
def solve():
    import sys

    t = int(sys.stdin.readline())
    for _ in range(t):
        n = int(sys.stdin.readline())
        b = n.bit_count()
        if b % 2 == 0:
            sys.stdout.write("+\n")
        else:
            sys.stdout.write("-\n")

if __name__ == "__main__":
    solve()