置頂

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

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

熱門文章

2026年9月1日 星期二

ZeroJudge 解題筆記:s215.紙膠帶 (Tape)

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


ZeroJudge 題目連結:s215.紙膠帶 (Tape)

題目 pdf 檔連結:紙膠帶 (Tape)

解題想法


題目是多筆測資。每筆測資第一列是代表紙膠帶長度的整數 $N$,第二列有 26 個正整數代表每種動物(字母)數量上限,第三列是代表紙膠帶圖案的字串。題目規定一段連續的漂亮紙帶必須符合以下 2 個條件:
  1. 紙帶上正好有 3 種動物
  2. 紙帶上最多只能有 1 種動物數量超過上限
這題要找符合條件的連續子字串,很適合用滑動視窗 (sliding window) 解題。不過麻煩的地方在於計分方式有 2 種:
  1. 紙帶上沒有動物超過數量上限,以 3 種動物的最大數量計分。
  2. 紙帶上正好有 1 種物物超過數量上限,以超過上限的數量計分。
為了保證滑動視窗取對範圍,要分別對 2 種計分方式各跑一次滑動視窗,因為可能在一段較短的視窗內正好有 1 種物物超過數量上限,但是這個數量很大,分數反而可能很高。

Python 程式碼


使用時間約為 0.8 s,記憶體約為 10.9 MB,通過測試。
def solve():
    import sys
    
    def get_tokens():
        for line in sys.stdin:
            for token in line.split():
                yield token
        
    tokens = get_tokens()
    
    result = []
    while True:
        try:
            N = int(next(tokens))
            limits = [0] * 26
            for i in range(26):
                limits[i] = int(next(tokens))
            s = next(tokens)
        except StopIteration:
            break
        
        def get_score(max_exceed):
            # 自訂函式,用滑動視窗取連續子字串分數,代入可以有幾個字母超標
            left = 0  # 視窗左端點
            imax = 0  # 最高分數
            curr = set()  # 視窗中的字母索引值
            exceed = set()  # 超標的字母
            cnt = [0] * 26  # 視窗中的字母計數器
            for right in range(N):  # 右端點 0 ~ N-1
                ri_idx = ord(s[right]) - ord('a')  # 右端點字母索引值
                curr.add(ri_idx)  # ri_idx 加入 curr
                cnt[ri_idx] += 1  # ri_idx 數量加 1
                # 如果 ri_idx 超標,ri_idx 加入 exceed
                if cnt[ri_idx] == limits[ri_idx] + 1: exceed.add(ri_idx)
                # 左端點向右滑,直到視窗內字母種類等於 3 且超標數量等於 max_exceed
                while left < right and (len(curr) > 3 or len(exceed) > max_exceed):
                    le_idx = ord(s[left]) - ord('a')  # 左端點字母索引值
                    left += 1
                    cnt[le_idx] -= 1
                    # 如果 le_idx 降回數量上限,exceed 移除 le_idx
                    if cnt[le_idx] == limits[le_idx]: exceed.remove(le_idx)
                    # 如果 le_idx 降回數量歸零,curr 移除 le_idx
                    if cnt[le_idx] == 0: curr.remove(le_idx)
                
                # 依照 max_exceed 計分
                score = 0
                if len(curr) == 3:  # 有 3 種字母才有分數
                    if max_exceed == 1 and len(exceed) == 1:  # 只有一種超標,這個字母數量是分數
                        score = cnt[list(exceed)[0]]
                    elif max_exceed == 0 and len(exceed) == 0:  # 沒有字母超標,curr 之中 3 個字母數量最大值是分數
                        score = max(cnt[idx] for idx in curr)
                # 更新最高分數
                imax = max(imax, score)
            return imax
        # 用兩次滑動視窗找最高分
        ans = max(get_score(0), get_score(1))
        result.append(f"{ans:d}\n")
    sys.stdout.write("".join(result))

if __name__ == "__main__":
    solve()


LeetCode 解題筆記:3568. Minimum Moves to Clean the Classroom

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


LeetCode 題目連結:3568. Minimum Moves to Clean the Classroom

解題想法


中等難度題。題目給一個長度為 $m$ 的陣列 $classroom$,其中包含 $m$ 個長度為 $n$ 的字串,字串中只包含以下的字元:
  • S 代表這格是學生的初位置
  • L 代表這格有垃圾
  • R 代表可以恢復能量的格子
  • X 代表這格有障礙物,學生不能走到這格。
  • . 代表空格
另外給一個整數 $energy$ 代表學生一開始的能量。假設學生每走一格消耗 1 點能量,如果能量歸零時不是位在 R 的格子上,學生無法再移動。如果學生走到 L 的格子上,可以撿起垃圾。如果學生走到 R 的格子上,會將能量補到起始值 $energy$。題目要問學生撿起所有垃圾時需要移動的最少步數,如果無法撿起所有的垃圾則回傳 $-1$。

這題的下方有提示,要用 BFS 解題,待走訪佇列放入的資料為 (x 座標, y 座標, 已撿起的垃圾狀態 mask, 目前的能量 e, 已走的步數 step),並用一個三維陣列 bestEnergy 代表走到座標 (x, y) 時、狀態為 mask 的最高能量,並用 bestEnergy 剪枝。由於這題的垃圾數量上限為 10 個,可以先將每個位置的垃圾編號,用二進位制記錄這個編號的垃圾是否已被撿起來。詳細的 BFS 過程請參考程式碼中的註解。

Python 程式碼


Runtime: 1511 ms, beats 91.23%. Memory: 24.54 MB, beats 94.74%.
class Solution:
    def minMoves(self, classroom: List[str], energy: int) -> int:
        m, n = len(classroom), len(classroom[0])  # 教室尺寸 m*n
        # 1. 先找到起點 S 的位置、垃圾 L 的位置
        xi, yi = 0, 0  # S 的位置
        litter_pos = dict()  # 特定位置垃圾對應的編號
        litter_idx = 0   # 垃圾的編號
        for i in range(m):
            for j in range(n):
                ch = classroom[i][j]
                if ch == 'S':
                    xi, yi = i, j
                elif ch == 'L':
                    litter_pos[i, j] = litter_idx
                    litter_idx += 1
        
        total_litters = litter_idx  # 垃圾數量
        fullMask = (1 << total_litters) - 1  # 拿到所有垃圾的狀態
        
        # 特例,如果沒有垃圾,回傳 0
        if total_litters == 0: return 0
        
        # 2. BFS,待走訪佇列放入 (x 座標, y 座標, 狀態, 能量, 步數)
        que = deque([(xi, yi, 0, energy, 0)])
        # bestEnergy[x][y][mask] 代表走到 x, y, mask 狀態時最高的能量,預設為 -1
        bestEnergy = [[[-1] * (1 << total_litters) for _ in range(n)] for _ in range(m)]
        bestEnergy[xi][yi][0] = energy  # 起始狀態能量全滿
        # BFS
        while que:
            x, y, mask, e, step = que.popleft()
            # 如果 能量 e 為 0 且不在 R 上面,無法再移動
            if e == 0 and classroom[x][y] != 'R':
                continue
            # 四方位檢查
            for dx, dy in ((0, 1), (1, 0), (0, -1), (-1, 0)):
                nx, ny = x + dx, y + dy
                # 如果沒有出界也沒有遇到障礙物 X
                if 0 <= nx < m and 0 <= ny < n and classroom[nx][ny] != 'X':
                    ne = e - 1  # 能量減 1
                    if ne < 0: continue  # 能量不足
                    nxt_mask = mask  # 新的狀態
                    if classroom[nx][ny] == 'L':  # 撿垃圾
                        nxt_mask |= (1 << litter_pos[nx, ny])  # 更新狀態
                    elif classroom[nx][ny] == 'R':  # 能量全滿
                        ne = energy
                    
                    # 如果 nxt_mask == fullMask,找到答案,回傳 step + 1
                    if nxt_mask == fullMask: return step + 1
                    
                    # 如果 ne 大於之前走到 [nx][ny][mask] 的能量,才將下一步加入 que
                    if ne > bestEnergy[nx][ny][nxt_mask]:
                        bestEnergy[nx][ny][nxt_mask] = ne
                        que.append((nx, ny, nxt_mask, ne, step + 1))
        # 如果走完 BFS 還沒有找到 fullMask,無法達成目標,回傳 -1
        return -1