置頂

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

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

熱門文章

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


C++ 程式碼


使用指標及 new,速度很慢。Runtime: 1259 ms, beats 5.37%. Memory: 527.82 MB, beats 5.37%.
/* 自訂節點類別 */
class Node {
public:
    int pre_len;  // 從左端點開始的連續相同字元長度
    int suf_len;  // 從右端點開始的連續相同字元長度
    int max_len;  // 區間內最大連續長度
    int total_len;  // 區間總長度
    char left_char;  // 區間左端點的字元
    char right_char;  // 區間右端點的字元
    
    Node(int pre=1, int suf=1, int maxL=1, char left_ch=' ', char right_ch=' ', int tot=1) {
        pre_len = pre;
        suf_len = suf;
        max_len = maxL;
        left_char = left_ch;
        right_char = right_ch;
        total_len = tot;
    }
};

/* 自訂線段樹類別 */
class SegmentTree {
private:
    int n;  // 長度
    string s;  // 字串
    vector<Node*> tree;  // 樹的內容
    
    void _build(int idx, int start, int end) {
        // 內部函式,索引值 idx,左端點 start,右端點 end
        // 遞迴出口,左、右端點重合,建立新的節點
        if (start == end) {
            tree[idx] = new Node(1, 1, 1, s[start], s[start], 1);
            return;
        }
        // 一般狀況,用遞迴建立左、右子節點
        int mid = (end - start) / 2 + start;  // 中點
        _build(2 * idx, start, mid);  // 遞迴,建立左子節點
        _build(2 * idx + 1, mid + 1, end);  // 遞迴,建立右子節點
        tree[idx] = _merge(tree[2 * idx], tree[2 * idx + 1]);  // 合併左、右子節點成為父節點
    }
    
    Node* _merge(Node* left, Node* right) {
        // 內部函式,合併左、右子節點
        Node* res = new 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) {
            int 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
    }
    
    void _update(int idx, int start, int end, int target, char new_char) {
        // 內部函式,單點更新,目前處理節點索引值 idx,左端點 left,右端點 right,目標索引值 target,新的字元 new_char
        // 遞迴出口,左、右端點重合,更新 self.tree[idx]
        if (start == end) {
            tree[idx] = new Node(1, 1, 1, new_char, new_char, 1);
            return;
        }
        // 一般狀況,遞迴,找到 target 之後再往上層更新父節點
        int mid = (end - start) / 2 + start;  // 中點
        if (target <= mid) {  // 目標在左側
            _update(2 * idx, start, mid, target, new_char);
        } else {  // 目標在右側
            _update(2 * idx + 1, mid + 1, end, target, new_char);
        }
        tree[idx] = _merge(tree[2 * idx], tree[2 * idx + 1]);  // 合併
    }
    
public:
    SegmentTree(const string& t) {
        // 初始化,輸入字串 t,建立線段樹的內容
        n = (int)t.size();
        s = t;
        tree.assign(4*n, new Node());
        _build(1, 0, n-1);
    }
    
    void update(int target, char new_char) {
        // 外部函式,呼叫 _update
        _update(1, 0, n - 1, target, new_char);
    }
    
    int query_max() {
        // 回傳根節點的最大連續重複長度
        return tree[1]->max_len;
    }
};

class Solution {
public:
    vector<int> longestRepeating(string s, string queryCharacters, vector<int>& queryIndices) {
        // 初始化線段樹物件
        SegmentTree* tree = new SegmentTree(s);
        int m = (int)queryIndices.size();
        vector<int> ans (m, 0);
        // 處理更新及查詢
        for(int i = 0; i < m; i++) {
            char new_char = queryCharacters[i];
            int target = queryIndices[i];
            // 單點更新
            tree->update(target, new_char);
            // 查詢目前整體的最長連續重複字元長度
            ans[i] = tree->query_max();
        }
        return ans;
    }
};


修改後的程式碼,速度快很多。Runtime: 164 ms, beats 61.75%. Memory: 110.38 MB, beats 50.34%.
/* 自訂節點結構體 */
struct Node {
    int pre_len;  // 從左端點開始的連續相同字元長度
    int suf_len;  // 從右端點開始的連續相同字元長度
    int max_len;  // 區間內最大連續長度
    int total_len;  // 區間總長度
    char left_char;  // 區間左端點的字元
    char right_char;  // 區間右端點的字元
    // 建構子
    Node(int pre=1, int suf=1, int maxL=1, char left_ch=' ', char right_ch=' ', int tot=1)
        : pre_len(pre), suf_len(suf), max_len(maxL), left_char(left_ch), right_char(right_ch), total_len(tot) {}
};

/* 自訂線段樹類別 */
class SegmentTree {
private:
    int n;  // 長度
    string s;  // 字串
    vector<Node> tree;  // 樹的內容,不要用指標,速度比較快
    
    void _build(int idx, int start, int end) {
        // 內部函式,索引值 idx,左端點 start,右端點 end
        // 遞迴出口,左、右端點重合,建立新的節點
        if (start == end) {
            tree[idx] = Node(1, 1, 1, s[start], s[start], 1);
            return;
        }
        // 一般狀況,用遞迴建立左、右子節點
        int mid = (end - start) / 2 + start;  // 中點
        _build(2 * idx, start, mid);  // 遞迴,建立左子節點
        _build(2 * idx + 1, mid + 1, end);  // 遞迴,建立右子節點
        tree[idx] = _merge(tree[2 * idx], tree[2 * idx + 1]);  // 合併左、右子節點成為父節點
    }
    
    Node _merge(const Node& left, const Node& right) {
        // 內部函式,合併左、右子節點,不要用指標
        Node res;  // 最後要回傳的節點,不要用 new
        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) {
            int 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
    }
    
    void _update(int idx, int start, int end, int target, char new_char) {
        // 內部函式,單點更新,目前處理節點索引值 idx,左端點 left,右端點 right,目標索引值 target,新的字元 new_char
        // 遞迴出口,左、右端點重合,更新 self.tree[idx]
        if (start == end) {
            tree[idx] = Node(1, 1, 1, new_char, new_char, 1);
            return;
        }
        // 一般狀況,遞迴,找到 target 之後再往上層更新父節點
        int mid = (end - start) / 2 + start;  // 中點
        if (target <= mid) {  // 目標在左側
            _update(2 * idx, start, mid, target, new_char);
        } else {  // 目標在右側
            _update(2 * idx + 1, mid + 1, end, target, new_char);
        }
        tree[idx] = _merge(tree[2 * idx], tree[2 * idx + 1]);  // 合併
    }
    
public:
    SegmentTree(const string& t) {
        // 初始化,輸入字串 t,建立線段樹的內容
        n = (int)t.size();
        s = t;
        tree.resize(4*n);
        _build(1, 0, n-1);
    }
    
    void update(int target, char new_char) {
        // 外部函式,呼叫 _update
        _update(1, 0, n - 1, target, new_char);
    }
    
    int query_max() {
        // 回傳根節點的最大連續重複長度
        return tree[1].max_len;
    }
};

class Solution {
public:
    vector<int> longestRepeating(string s, string queryCharacters, vector<int>& queryIndices) {
        // 初始化線段樹物件
        SegmentTree tree(s);  // 不要用 new
        int m = (int)queryIndices.size();
        vector<int> ans (m, 0);
        // 處理更新及查詢
        for(int i = 0; i < m; i++) {
            char new_char = queryCharacters[i];
            int target = queryIndices[i];
            // 單點更新
            tree.update(target, new_char);
            // 查詢目前整體的最長連續重複字元長度
            ans[i] = tree.query_max();
        }
        return ans;
    }
};


沒有留言:

張貼留言