置頂

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

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

熱門文章

2026年10月20日 星期二

LeetCode 解題筆記:3886. Sum of Sortable Integers

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


LeetCode 題目連結:3886. Sum of Sortable Integers

解題想法


困難題。題目給一個長度為 $n ~(1 \leq n \leq 10^5)$ 的整數陣列 $nums ~(1 \leq nums[i] \leq 10^5)$,如果將 $nums$ 切成長度為 $k$ 的子陣列,如果可以將子陣列以滾動的方式使得 $nums$ 由小到大排序,求所有符合條件的 $k$ 值加總。

這題下方有提示,基本上就是按照題示寫程式碼。寫一個檢驗 $k$ 是否符合條件的自訂函式 $check$,定義前一個子陣列的最小值 prev_max,預設為小於題目測資範圍的 $-100000000$。用一個 for 迴圈檢查分割 $nums$ 為長度為 $k$ 的子陣列,定義子陣列最小值 sub_min、最大值 sub_max,兩者皆預設為$nums[i]$,子陣列遞減次數 $drop$ 預設為 $0$。裡面再用一個 for 迴圈,檢查長度為 $k$ 的區間內部。如果子陣列可以用滾動的方式由小到大排列,則子陣列中遞減的次數最多只有 1 次;如果遞減次數為 1 次,則子陣列的首項必須大於、等於末項;這個子陣列的最小值必須大於、等於前一個子陣列的最大值。

由於題目要將 $nums$ 分割為長度均為 $k$ 的子陣列,因此 $k$ 一定是 $n$ 的因數,再加上 $n$ 的最大值為 $100000$,要測試的因數 $k$ 最大為 $\sqrt{n} \approx 316$,如果在這個範圍內找到因數 $k$,同時再檢查對應的因數 $p = n / k$,需要檢查的次數並不多,可以用 for 迴圈暴力解。

Python 程式碼


Runtime: 2965 ms, beats 29.67%. Memory: 31.36 MB, beats 69.23%.
class Solution:
    def sortableIntegers(self, nums: list[int]) -> int:
        n = len(nums)

        # 1. 自訂函式,檢驗 nums 切成長度 k 的子陣列是否能排序
        def check(k):
            prev_max = -float('inf')  # 前一個子陣列的最小值
            for i in range(0, n, k):  # 分割 nums 為長度 k 的子陣列
                sub_min = sub_max = nums[i]  # 子陣列最小值、最大值,預設為 nums[i]
                drop = 0  # 遞減次數
                # 檢查長度 k 的區間內部
                for j in range(i, i + k - 1):
                    if nums[j] > nums[j + 1]:  # 遞減
                        drop += 1
                    # 更新 sub_min, sub_max
                    if nums[j + 1] < sub_min:
                        sub_min = nums[j + 1] 
                    elif nums[j + 1] > sub_max:
                        sub_max = nums[j + 1]
                
                # 條件1,如果遞減次數大於1,無法排序
                if drop > 1: return False
                # 條件2,如果遞減1次,子陣列末項必須小於、等於首項
                if drop == 1 and nums[i + k - 1] > nums[i]:
                    return False
                # 條件3,這一區最小值大於、等於前一區最大值
                if sub_min < prev_max: return False

                # 更新 prev_max
                prev_max = sub_max
            
            # 通過測試,回傳 True
            return True
        
        # 2. 找出 n 的所有因數 k,呼叫 check 測試因數 k
        ans = 0  # 答案
        for k in range(1, int(n**0.5) + 1):
            if n % k == 0:
                # 驗證因數 k
                if check(k): ans += k
                # 驗證對稱的因數 n // k,且 k != n // k
                if k != n // k:
                    if check(n // k): ans += n // k
        return ans


2026年10月19日 星期一

ZeroJudge 解題筆記:f435.10267-Graphical Editor

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


ZeroJudge 題目連結:f435.10267-Graphical Editor

解題想法


這題要模擬圖片編輯的操作過程,共有 8 個操作指令,雖然每個部分都不難,但是寫起來很長。指令開頭皆為一個大寫英文字母,後面再接上整數或字母。指令及功能分別為
  1. I M N:創造 $M \times N$ 的表格且每個像素的顏色為大寫英文字母 O。需要特別注意,題目的軸方向不一樣,這裡指的是表格有 N 列、M 欄。可以用二維陣列或是 C++ vector 搭配 string 儲存表格。
  2. C:使目前的表格顏色全為大寫英文字母 O,可以用迴圈或是 C++ vector.assign 處理。
  3. L X Y C:使 $(X,Y)$ 的像素顏色為 C。需要特別注意,題目的軸方向不一樣,而且是從 1 開始計算位置,這裡指的是二維陣列索引值 $[Y - 1][X - 1]$ 的格子。
  4. V X Y1 Y2 C:使 $(X, Y1) \dots (X, Y2)$ 的顏色為 C。需要特別注意,題目的軸方向不一樣,而且是從 1 開始計算位置,這裡指的是二維陣列索引值 $[Y1 - 1][X]$ 到 $[Y2 - 1][X]$ 的格子。
  5. H X1 X2 Y C:使 $(X1, Y) \dots (X2, Y)$ 的顏色為 C。需要特別注意,題目的軸方向不一樣,而且是從 1 開始計算位置,這裡指的是二維陣列索引值 $[Y][X1 - 1]$ 到 $[Y][X2 - 1]$ 的格子。
  6. K X1 Y1 X2 Y2 C:使由 $(X1, Y1)$ 為左上角、$(X2, Y2)$ 為右下角組成的矩形中所有的像素顏色為 C,可以用二層 for 迴圈處理。需要特別注意,題目的軸方向不一樣,而且是從 1 開始計算位置,這裡指的是二維陣列索引值 $[Y1 - 1][X1 - 1]$ 到 $[Y2 - 1][X2 - 1]$ 的格子。
  7. F X Y C:使 $(X, Y)$ 附近與它同顏色的像素顏色為 C,可以用 BFS 處理。需要特別注意,如果這格的顏色與 C 相同,不需要處理。題目的軸方向不一樣,而且是從 1 開始計算位置,這裡指的是二維陣列索引值 $[Y - 1][X - 1]$ 的格子。
  8. S Name:輸出 Name 之後輸出目前的表格。
  9. X:停止操作


Python 程式碼


使用時間約為 0.2 s,記憶體約為 11.4 MB,通過測試。
def solve():
    import sys
    from collections import deque

    result = []
    data = sys.stdin.read().split()
    ptr = 0
    
    graph = []  # 圖片
    m, n = 0, 0  # 圖片尺寸 n 列 m 欄
    
    while ptr < len(data):
        op = data[ptr]
        ptr += 1
        if op == "I":  # 建立 n 列 m 欄的圖片,內容皆為 O
            m = int(data[ptr])
            n = int(data[ptr + 1])
            ptr += 2
            graph = [["O"] * m for _ in range(n)]
        elif op == "C":  # 重置 n 列 m 欄的圖片,內容皆為 O
            graph = [["O"] * m for _ in range(n)]
        elif op == "L":  # 改變 graph[y][x] 為指定的顏色
            x = int(data[ptr]) - 1
            y = int(data[ptr + 1]) - 1
            ch = data[ptr + 2]
            ptr += 3
            graph[y][x] = ch
        elif op == "V":  # 改變 graph[y1][x] ~ graph[y2][x] 為指定的顏色
            x = int(data[ptr]) - 1
            y1 = int(data[ptr + 1]) - 1
            y2 = int(data[ptr + 2]) - 1
            ch = data[ptr + 3]
            ptr += 4
            for r in range(y1, y2 + 1):
                graph[r][x] = ch
        elif op == "H":  # 改變 graph[y][x1] ~ graph[y][x2] 為指定的顏色
            x1 = int(data[ptr]) - 1 
            x2 = int(data[ptr + 1]) - 1
            y = int(data[ptr + 2]) - 1
            ch = data[ptr + 3]
            ptr += 4
            graph[y][x1 : x2 + 1] = [ch] * (x2 - x1 + 1)
        elif op == "K":  # 改變 graph[y1][x1] ~ graph[y2][x2] 的矩形區域為指定的顏色
            x1 = int(data[ptr]) - 1
            y1 = int(data[ptr + 1]) - 1
            x2 = int(data[ptr + 2]) - 1
            y2 = int(data[ptr + 3]) - 1
            ch = data[ptr + 4]
            ptr += 5
            for r in range(y1, y2 + 1):
                for c in range(x1, x2 + 1):
                    graph[r][c] = ch
        elif op == "F":  # 將 graph[y][x] 及相鄰同色的格子改為指定的顏色
            x = int(data[ptr]) - 1
            y = int(data[ptr + 1]) - 1 
            ch = data[ptr + 2]
            ptr += 3
            target = graph[y][x]  # 目標顏色
            if target == ch: continue  # 目標顏色與新的顏色相同,不需要改變
            graph[y][x] = ch
            que = deque([(y, x)])
            while que:  # BFS
                r, c = que.popleft()
                for dr, dc in ((0, 1), (1, 0), (0, -1), (-1, 0)):
                    nr, nc = r + dr, c + dc
                    if 0 <= nr < n and 0 <= nc < m and graph[nr][nc] == target:
                        graph[nr][nc] = ch
                        que.append((nr, nc))
        elif op == "S":  # 儲存圖片,印出檔名及目前的圖片
            name = data[ptr]
            result.append(f"{name}\n")
            for row in graph:
                res = "".join(map(str, row)) + "\n"
                result.append(res)
        elif op == "X":  # 結束操作
            continue
    # 輸出答案
    sys.stdout.write("".join(result))

if __name__ == "__main__":
    solve()


LeetCode 解題筆記:2089. Find Target Indices After Sorting Array

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


LeetCode 題目連結:2089. Find Target Indices After Sorting Array

解題想法


簡單題。題目給一個長度為 $n ~(1 \leq n \leq 100)$ 的正整數陣列 $nums ~(1 \leq nums[i] \leq 100)$,以及一個正整數 $target ~(1 \leq target \leq 100)$,要找出 $nums$ 由小到大排序之後 $target$ 的索引值。

這題最直接的想法就是將 $nums$ 排序,再用線性搜尋法或是二分搜尋法找出 $target$ 在 $nums$ 之中的索引值,由於 $n$ 最多為 $100$,線性搜尋法的速度很快。但其實這題根本不需要排序,只要用一個 for 迴圈掃過 $nums$,記錄小於 $target$ 的數字數量 $small$ 以及等於 $target$ 的數字數量 $equal$,再建立一個長度為 $equal$、內容為 $small$ 到 $small + equal - 1$ 的陣列即可。

Python 程式碼


線性搜尋法。Runtime: 0 ms, beats 100.00%. Memory: 19.38 MB, beats 24.20%.
class Solution:
    def targetIndices(self, nums: List[int], target: int) -> List[int]:
        nums.sort()
        ans = []
        for i, num in enumerate(nums):
            if num == target:
                ans.append(i)
        return ans


二分搜尋法。Runtime: 1 ms, beats 33.47%. Memory: 19.41 MB, beats 24.20%.
from bisect import bisect_left

class Solution:
    def targetIndices(self, nums: list[int], target: int) -> list[int]:
        n = len(nums)  # 長度
        nums.sort()  # 排序
        ans = []  # 答案
        # 用二分搜尋法從 nums 之中找第一個大於、等於 target 的索引值
        idx = bisect_left(nums, target)
        if idx < n:  # 有找到索引值,要再向後找,只找等於 target 的項目
            while idx < n and nums[idx] == target:
                ans.append(idx)
                idx += 1
        return ans


計算數量。Runtime: 0 ms, beats 100.00%. Memory: 19.21 MB, beats 62.71%.
class Solution:
    def targetIndices(self, nums: list[int], target: int) -> list[int]:
        small = 0  # 小於 target 的數量
        equal = 0  # 等於 target 的數量
        for num in nums:
            if num < target:
                small += 1
            elif num == target:
                equal += 1
        return list(range(small, small + equal))


LeetCode 解題筆記:826. Most Profit Assigning Work

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


LeetCode 題目連結:826. Most Profit Assigning Work

解題想法


中等難度題。題目給兩個長度為 $n ~(1 \leq n \leq 10^4)$ 的正整數陣列 $difficulty ~(1 \leq difficulty[i] \leq 10^5)$ 及 $profit ~(1 \leq profit[i] \leq 10^5)$,分別代表工作的困難度及利潤,以及一個長度為 $m ~(1 \leq n \leq 10^4)$ 的正整數陣列 $worker ~(1 \leq worker[i] \leq 10^5)$,代表工人的能力值。如果這個工人的能力值大於等於某一項工作的困難度,則這個工人可以承接這項工作,並取得對應的利潤。每個工人只能承接一項工作,同一項工作可以由多個工人承接,則最大利潤為何?

這題最直接的想法是先將 $profit$ 及 $difficulty$ 組成 tuple,先依照利潤由大到小排序,再依照困難度由小到大排序。接著用兩層 for 迴圈計算答案 $ans$,外層迴圈跑每個工人的能力 $w$,內層迴圈跑工作的利潤 $p$ 及困難度 $d$,如果某一項工作符合 $w \geq d$,可以承接這項工作,$ans += p$,中止迴圈。不過這樣跑的時間複雜度為 $O(n^2)$,很慢。

換一種想法,先將 $difficulty$ 及 $profit$ 組成 tuple,由小到大排序。再建立兩個陣列 $diff$ 及 $prof$,分別代表不重複的工作難度及對應的最大利潤,並且先放入 $0$ 避免空陣列造成的問題。用一個變數 $pmax$ 記錄目前的最高利潤。再用一層 for 迴圈填入 $diff$ 及 $prof$ 對應的資料。最後用一層 for 迴圈跑每個工人的能力 $w$,用內建的二分搜尋法工具從 $diff$ 之中找第一個大於、等於 $w$ 的工作索引值 $idx$,如果 $idx == len(diff)$ 代表這個工人可以承接任何工作,答案 $ans$ 加上利潤最大值;反之,再用一個 while 迴圈往回找第一個等於 $w$ 的工作,$ans$ 加上工作對應的利潤。這個寫法的時間複雜度為 $O(n \log n)$,速度快很多。

Python 程式碼


$O(n^2)$. Runtime: 4643 ms, beats 5.13%. Memory: 22.53 MB, beats 16.49%.
class Solution:
    def maxProfitAssignment(self, difficulty: list[int], profit: list[int], worker: list[int]) -> int:
        arr = [(p, d) for p, d in zip(profit, difficulty)]
        arr.sort(key = lambda x : (-x[0], x[1]))
        
        ans = 0
        for w in worker:
            for p, d in arr:
                if w >= d:
                    ans += p
                    break
        return ans


$O(log n)$. Runtime: 36 ms, beats 62.99%. Memory: 22.10 MB, beats 79.16%.
from bisect import bisect_left

class Solution:
    def maxProfitAssignment(self, difficulty: list[int], profit: list[int], worker: list[int]) -> int:
        arr = sorted((d, p) for d, p in zip(difficulty, profit))
        diff, prof = [0], [0]  # 不重複的工作難度及對應的最高利潤
        pmax = 0  # 目前任務可得的最高利潤
        for d, p in arr:
            pmax = max(pmax, p)  # 更新最高利潤
            if d > diff[-1]:  # 較高的難度
                diff.append(d)
                prof.append(pmax)
            elif d == diff[-1]:  # 相同的難度
                prof[-1] = pmax
        
        # 用工人的能力於 diff 之中搜尋可執行的工作及最高利潤
        n = len(diff)  # 長度
        ans = 0  # 答案
        for w in worker:
            idx = bisect_left(diff, w)
            if idx == n:  # 可以執行最難的工作
                ans += prof[-1]
            else:
                while diff[idx] > w: idx -= 1  # 往回找到小於、等於 w 的值
                ans += prof[idx]
        return ans


2026年10月18日 星期日

ZeroJudge 解題筆記:o628.天秤 (Libra)

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


ZeroJudge 題目連結:o628.天秤 (Libra)

題目 pdf 檔連結:天秤 (Libra)

解題想法


這題是TOI練習賽202409潛力組第3題,這組題目當中最簡單的一題。題目是多組測資。每組測資第1列為1個整數 $n (2 \leq n \leq 100)$,代表有 $n$ 個砝碼,第2列有 $n$ 個正整數 $w_1, w_2, w_3, \dots, w_n ~(w_i \leq 10^3)$,代表這 $n$ 個砝碼的重量。題目要檢測,是否能將 $n$ 個砝碼分為總重量相同的兩組。

先計算砝碼總重量 $wsum$,如果 $wsum$ 是奇數,不可能平分,直接輸出 $0$。接下來要湊出 $target = wsum // 2$,因為每個砝碼只有選或不選兩種可能性,是 0/1 背包問題,可以用一維陣列 $dp$ 記錄狀態,$dp[j]$ 是否可以湊出總重量 $j$,也可以用 bitset 取代陣列。 。

Python 程式碼


使用時間約為 0.4 s,記憶體約為 9.9 MB,通過測試。
def solve():
    import sys

    result = []
    data = sys.stdin.read().split()
    ptr = 0
    while ptr < len(data):
        n = int(data[ptr])
        ptr += 1
        weights = list(map(int, data[ptr:ptr+n]))
        ptr += n
        wsum = sum(weights)
        if wsum%2 == 1:  # 不可能平分,輸出 0
            result.append("0\n")
            continue
        target = wsum//2  # 目標
        dp = [False]*(target+1)  # 0/1 背包問題,是否能湊出 target
        dp[0] = True
        for w in weights:
            for j in range(target, w-1, -1):
                dp[j] |= dp[j-w]
        result.append("1\n" if dp[target] else "0\n")
    sys.stdout.write("".join(result))

if __name__ == "__main__":
    solve()


使用時間約為 14 ms,記憶體約為 9.6 MB,通過測試。
def solve():
    import sys

    result = []
    data = sys.stdin.read().split()
    ptr = 0
    while ptr < len(data):
        n = int(data[ptr])
        ptr += 1
        weights = list(map(int, data[ptr:ptr+n]))
        ptr += n
        wsum = sum(weights)
        if wsum%2 == 1:  # 不可能平分,輸出 0
            result.append("0\n")
            continue
        target = wsum//2  # 目標
        dp = 1  # 0/1 背包問題,是否能湊出 target
        for w in weights:
            dp |= (dp << w)
        result.append("1\n" if (dp >> target) & 1 else "0\n")
    sys.stdout.write("".join(result))

if __name__ == "__main__":
    solve()


LeetCode 解題筆記:11. Container With Most Water

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


LeetCode 題目連結:11. Container With Most Water

解題想法


中等難度題。題目給一個長度為 $n ~(2 \leq n \leq 10^5)$ 的整數陣列 $height ~(0 \leq height[i] \leq 10^4)$,代表位置 $i$ 有一面 $(i, 0)$ 到 $(i, height[i])$ 的牆壁,如果取其中兩面牆壁作為容器,求最大的容積為何?

由於 $n$ 不大,用兩個 for 迴圈跑所有的牆壁組合也能通過,但是這樣寫很慢。提示有說要用雙指針 (two pointer),假設 $left$、$right$ 分別指向左側、右側牆壁的位置,由兩側向中間移動找最大容積,接下來選擇當時較低的牆壁往中間移動,直到 $left$、$right$ 重合為止。

Python 程式碼


Runtime: 56 ms, beats 62.51%. Memory: 29.36 MB, beats 97.91%.
class Solution:
    def maxArea(self, height: list[int]) -> int:
        left = 0  # 左側牆壁索引值
        right = len(height) - 1  # 右側牆壁索引值
        ans = 0  # 答案

        # 由兩側往中間移動牆壁找答案
        while left < right:
            lh = height[left]  # 左側牆壁高度
            rh = height[right]  # 右側牆壁高度
            vol = min(lh, rh) * (right - left)  # 目前的體積,高度取 lh, rh 較小值
            ans = max(ans, vol)  # 更新答案
            # lh, rh 較小的一側向中間移動
            if lh < rh: left += 1
            else: right -= 1
        
        return ans


2026年10月17日 星期六

LeetCode 解題筆記:3551. Minimum Swaps to Sort by Digit Sum

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


LeetCode 題目連結:3551. Minimum Swaps to Sort by Digit Sum

解題想法


中等難度題。題目給一個長度為 $n ~(1 \leq n \leq 10^5)$ 的整數陣列 $nums ~(1 \leq nums[i] \leq 10^9)$,如果要將 $nums$ 的數字依照數字加總、數字大小由小到大排序,最少要交換 $nums$ 之中任意兩個數字幾次?這題的提示1,先將 $nums$ 依照數字加總、數字大小由小到大排序,再找出排序後的數字在原來的 $nums$ 之中的索引值。提示2,答案等於 $n - $ 環的數量。

對於提示1,我們可以寫一個自訂函式 digit_sum,用來計算整數的數字加總。將 $nums$ 之中的數字加總、數字、索引值組成 tuple 存入另一個陣列 $arr$,再用內建的 sort 將 $arr$ 排序。由於 sort 會自動依照 tuple 的內容及順序由小到大排序,不需要寫比較式。為了找出排序後的數字於 $nums$ 之中對應的索引值,我一開始是用 dict 記錄數字對索引值的資料,但是這樣寫在 C++ 會很慢。後來改成開一個長度 $n$ 的一維陣列 $target$,$target[i]$ 為 $arr[i]$ 在 $nums$ 之中的索引值,這樣速度快很多。

對於提示2,我們可以假設 $nums$ 之中有 $m$ 個環,每個環的長度分別為 $L_1, L_2, \dots, L_m$。如果要將長度為 $L$ 的環排序,交換任意兩個數字 $1$ 次可以將 $1$ 個數字歸位,共要交換 $L - 1$ 次。因此所有環交換的次數為 $$ ans = (L_1 - 1) + (L_2 - 1) + \dots + (L_m - 1) = (L_1 + L_2 + \dots + L_m) - m = n - m $$

Python 程式碼


Runtime: 885 ms, beats 94.69%. Memory: 45.08 MB, beats 75.22%.
class Solution:
    def minSwaps(self, nums: List[int]) -> int:
        # 自訂函式計算數字加總
        def digit_sum(x):
            dsum = 0
            while x:
                dsum += x % 10
                x //= 10
            return dsum

        # 將 {數字加總、num、索引值} 組成 tuple 存入 arr,再由小到大排序
        n = len(nums)  # 數量
        arr = [(digit_sum(nums[i]), nums[i], i) for i, num in enumerate(nums)]
        arr.sort()

        # 舊的索引值對應到新的索引值
        target = [-1] * n
        for i in range(n):
            target[arr[i][2]] = i
        
        # 找出同組的數字,如果同組有 m 個數字,要交換 m - 1 次
        cycle = 0  # 環的數量
        visited = [False] * n  # nums[i] 是否已經找過
        for i in range(n):
            if visited[i]: continue
            cycle += 1  # 新的環
            curr = i
            while not visited[curr]:
                visited[curr] = True
                curr = target[curr]
        return n - cycle


ZeroJudge 解題筆記:o627.校外教學分組 (Trip)

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


ZeroJudge 題目連結:o627.校外教學分組 (Trip)

題目 pdf 檔連結:校外教學分組 (Trip)

解題想法


這題是TOI練習賽202409潛力組第2題,這題也很難。題目是多組測資。每組測資第1列為2個整數 $n (3 \leq n \leq 16)$、$m (m \leq n (n-1) / 2)$,代表有 $n$ 個人,編號 $1$ 到 $n$,有 $m$ 組朋友關係。接下來 $m$ 列,每列有2個相異整數 $u, v$,代表 $u, v$ 是朋友,不能不在同一組。如果要將所有人分成3組,朋友不能分在同一組,求這3組中最多人數減去最少人數的最小值。如果無法分成3組,回傳 $-1$。

由於這題的 $n$ 最大為 16,可以用二進位制的整數 $bitmask$ 代表這 $n$ 個人所有的組合,如果 $mask$ 第 $i$ 位是 $1$,代表編號 $i$ 的人在這個分組之中,但是題目的編號是從 $1$ 開始,為了配合位元運算,需要將編號改成從 $0$ 開始。解題過程主要分為 3 步:
  1. 開一個長度為 $n$ 的陣列 $adj$,並用 $bitmask$ 儲存每個人的好友名單。
  2. 找出所有合法的組合及人數。所有的組合數 $total = 1 << n$,開一個長度為 $total$ 的陣列 $valid$ 記錄每個組合是否合法,另一個長度為 $total$ 的陣列 $sz$ 記錄每個組合的人數。
  3. 窮舉所有可能的組合,要求各組人數 $s1 \geq s2 \leq s3$,因為題目只要求分成 3 組,並沒有規定哪個組別的人數最多,加上這個條件可以少算很多狀況。
另外可以再加上一個已經找到理論最佳解的剪枝條件,如果已經找到最佳解,之後不可能再找到更好的解,可以直接輸出答案不需要再跑。如果 $n$ 是 $3$ 的倍數,可以分為人數相等的 $3$ 組,理論最佳解為 $0$;反之,理論最佳解為 $1$。在第 3 個步驟之前,可以將答案 $ans$ 定義成超出題目上限的值,例如 $1000$,如果在第 3 個步驟中沒有找到解,$ans$ 仍為預設值,代表無解,輸出 $-1$。

Python 程式碼


使用時間約為 0.5 s,記憶體約為 10.8 MB,通過測試。
def solve():
    import sys
    
    result = []
    data = sys.stdin.read().split()
    ptr = 0
    while ptr < len(data):
        n = int(data[ptr])  # n 個人
        m = int(data[ptr + 1])  # m 組關係
        ptr += 2

        # 1. 用 bitmask 儲存朋友關係
        adj = [0] * n
        for _ in range(m):
            u = int(data[ptr]) - 1  # u, v 是朋友,改成 0-index
            v = int(data[ptr + 1]) - 1
            ptr += 2
            adj[u] |= (1 << v)
            adj[v] |= (1 << u)

        # 2. 找出所有合法的組合及人數
        total = 1 << n  # 所有的組合數
        valid = [True] * total  # 組合是否合法
        sz = [0] * total  # 各組合的人數
        for mask in range(1, total):
            sz[mask] = mask.bit_count()  # 計算 mask 之中 1 的數量,就是人數
            u = (mask & -mask).bit_length() - 1  # 取 mask 的 lowbit 計算二進位制長度 - 1,即最後的 1 索引值
            prev = mask ^ (1 << u)  # 去除 u 之後的組合
            # 如果 prev 不合法,或者 mask 之中有人是 u 的朋友,mask 不合法
            if not valid[prev] or mask & adj[u]:
                valid[mask] = False

        # 3. 窮舉所有可能的組合,要求各組人數 s1 >= s2 >= s3
        ans = 1000  # 答案預設為超過上限的值
        full = total - 1  # n 個人同一組的 mask
        best = 0 if n%3 == 0 else 1  # 理論上的最佳解,如果 n 是 3 的倍數,可以平分成 3 組,最佳解為 0;否則最佳解為 1
        found = False  # 是否找到理論最佳解
        for s1 in range(1, total):
            # 如果 s1 不合法,找下一個
            if not valid[s1]: continue
            # 剪枝1,如果 s1 人數小於 n//3,找下一個
            if sz[s1] * 3 < n: continue
            # 用 XOR 找出剩下的人
            rest = full ^ s1
            # 窮舉 s2,逐步縮小 s2
            s2 = rest
            while s2 > 0:
                # 如果 s2 合法
                if valid[s2]:
                    # 用 XOR 找出第 3 組 s3
                    s3 = rest ^ s2
                    # 如果 s3 合法
                    if valid[s3]:
                        # 剪枝2,要求人數 s1 >= s2 >= s3
                        if sz[s1] >= sz[s2] >= sz[s3]:
                            ans = min(ans, sz[s1] - sz[s3])
                            # 剪枝3,找到理論最佳解,提早終止
                            if ans == best:
                                result.append(f"{best:d}\n")
                                found = True
                                break
                # 縮小 s2,不加這行會變成無窮迴圈
                s2 = (s2 - 1) & rest
            # 已經找到理論最佳解,中止迴圈
            if found: break
        # 如果沒有找到理論最佳解,ans == 1000,無解
        if not found:
            if ans == 1000: result.append("-1\n")
            else: result.append(f"{ans:d}\n")
    # 一次性輸出答案
    sys.stdout.write("".join(result))

if __name__ == "__main__":
    solve()


2026年10月16日 星期五

LeetCode 解題筆記:695. Max Area of Island

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


LeetCode 題目連結:695. Max Area of Island

解題想法


中等難度題。題目給一個 $m \times n$ 的二維陣列 $grid$,其中只有代表陸地的 $1$ 以及代表水域的 $0$,並且假設 $grid$ 四周都是 $0$。陸地可以往右、下、左、上等四個方向連接。要找出 $grid$ 之中面積最大的島嶼,也就是由最多的 $1$ 組成的連通區塊。連通區塊的題目,用 BFS 或 DFS 都可以解,由於我是以 Python 為主要的解題語言,我比較習慣用 BFS,如果用 DFS 經常會遇到遞迴深度過深的問題。這題我兩個方法都有寫,如果用 Python 解題 BFS 跑比較快,如果用 C++ 解題 DFS 跑比較快。

Python 程式碼


BFS. Runtime: 11 ms, beats 96.45%. Memory: 19.38 MB, beats 94.88%.
from collections import deque

class Solution:
    def maxAreaOfIsland(self, grid: list[list[int]]) -> int:
        m, n, imax = len(grid), len(grid[0]), 0
        for i in range(m):
            for j in range(n):
                if grid[i][j] == 0:
                    continue
                grid[i][j] = 0
                area = 1
                que = deque([(i, j)])
                while que:
                    r, c = que.popleft()
                    for dr, dc in ((0, 1), (1, 0), (0, -1), (-1, 0)):
                        nr, nc = r + dr, c + dc
                        if 0 <= nr < m and 0 <= nc < n and grid[nr][nc] == 1:
                            grid[nr][nc] = 0
                            area += 1
                            que.append((nr, nc))
                imax = max(imax, area)
        return imax


DFS. Runtime: 17 ms, beats 67.40%. Memory: 20.39 MB, beats 64.21%.
class Solution:
    def maxAreaOfIsland(self, grid: list[list[int]]) -> int:
        m, n, imax = len(grid), len(grid[0]), 0
        area = 0
        
        # 自訂 dfs 函式,找連通區塊面積同時將走過的格子標記為 0
        def dfs(r, c):
            nonlocal area
            area += 1
            grid[r][c] = 0
            for dr, dc in ((0, 1), (1, 0), (0, -1), (-1, 0)):
                nr, nc = r + dr, c + dc
                if 0 <= nr < m and 0 <= nc < n and grid[nr][nc] == 1:
                    dfs(nr, nc)
        
        # 掃過每一格,跳過 0,遇到 1 呼叫 dfs
        for i in range(m):
            for j in range(n):
                if grid[i][j] == 0:
                    continue
                # area 歸零之後再呼叫 dfs 找連通區塊面積
                area = 0
                dfs(i, j)
                imax = max(imax, area)
        return imax


ZeroJudge 解題筆記:o626.檔案順序 (Files)

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


ZeroJudge 題目連結:o626.檔案順序 (Files)

題目 pdf 檔連結:檔案順序 (Files)

解題想法


這題是TOI練習賽202409潛力組第1題,這題跟同組題目的第2題〈o627.校外教學分組 (Trip)〉都很難,第3題〈o628.天秤 (Libra)〉比較簡單。題目是多組測資。每組測資第1列為2個整數 $n (1 \leq n \leq 2 \times 10^5)$、$k (1 \leq k \leq 2 \times 10^5)$,分別代表有 $n$ 個檔案、$k$ 次操作。接下來 $n$ 列,每列有 4 個整數,分別代表檔案的名稱、修改時間、類型、大小。最後一列有 $k$ 個 1 到 4 的數字,分別代表用檔案的名稱、修改時間、類型、大小作為排序依據。如果有多個檔案這個屬性的值相同,則這幾個檔案保持前一次排序後的順序,也就是用 stable sort。

如果前一次操作與這次操作用的屬性不同,則這次操作為升序排序 (ascending order),也就是由小到大排序;如果重複點同樣的屬性第二次,會改成降序排序 (descending order),也就是由大到小排序。這點在解題時特別重要,因為 $n, k$ 的數值極大,如果真的模擬每一次的排序過程絕對會超時。但由於這個操作上的特性,我們可以用一個串列 $ops$ 記錄操作的索引值 $idx$ 以及目前為升序 (1) 或降序 (-1) 排列,如果連續使用相同的屬性排序,只要更新升序或降序即可;如果用不同屬性排序,於 $ops$ 最後面接上 $[idx, 1]$。

另一個要利用的特性是操作過程中如果先用了某個屬性排序,再用另一個屬性排序,之後再回來用前面用過的屬性排序,只有最後面這次的排序有效。例如先用名稱,再用修改時間,之後再回來用名稱排序,則第一次排序的效果會被後面同樣用名稱排序的效果蓋掉。因此,我們可以倒序讀取 $ops$,取出用這 4 個屬性排序的索引值及升序、降序,資料存入串列 sort_key。將 sort_key 組成 tuple,只排序一次,將排序後的檔案依序輸出。

不過這題的記憶體限制極小,只有 64 MB,我用 Python 解題最多只能通過 50% 的測資就遇到 MemoryError,目前看排行還沒有人用 Python 過關。用 C++ 則是 0.2 s、9.4 MB 過關。

Python 程式碼


只能通過 40% 的測資。
def solve():
    import sys

    result = []
    data = sys.stdin.read().split()
    ptr = 0
    while ptr < len(data):
        n = int(data[ptr])
        k = int(data[ptr + 1])
        ptr += 2
        files = []  # (name, time, type, size)
        for _ in range(n):
            file = tuple(map(int, data[ptr : ptr + 4]))
            ptr += 4
            files.append(file)
        
        operations = []  # idx of operation
        state = [0] * 5  # 1 ascending, -1 descending, 0 none
        for _ in range(k):
            idx = int(data[ptr])
            ptr += 1
            operations.append(idx)
            if state[idx] == 0:
                state[idx] = 1
            else:
                state[idx] *= -1
        
        sort_key = []  # (idx, 1 or -1)
        used = [False] * 5
        for idx in operations[::-1]:
            if not used[idx]:
                used[idx] = True
                sort_key.append((idx, state[idx]))
        
        def mykey(file):
            return tuple(file[idx - 1] * val for idx, val in sort_key)
        
        files.sort(key = mykey)
        
        for file in files:
            res = " ".join(map(str, file)) + "\n"
            result.append(res)
        
    # 輸出答案
    sys.stdout.write("".join(result))

if __name__ == "__main__":
    solve()