750-E. New Year and Old Subsequence

Tags: automaton, segment-tree

出處:https://codeforces.com/contest/750/problem/E
提交:https://codeforces.com/contest/750/submission/106919267

問題:給定一個僅包含字元 ‘0’ 到 ‘9’ 之題幹字串 $s[1:n]$ 之長度 $n\in[4,200000]$,後面有 $q\in[1,200000]$ 筆詢問 $s[a:b]$ 這個子字串的醜度為何。所謂醜度就是最少必須被移除的字元數量使得該字串由左而右有跳著出現 ‘2’、‘0’、‘1’、‘7’ 的子序列,但不能跳著出現 ‘2’、
‘0’、‘1’、‘6’ 的子序列。無法做到則回傳 -1。

解法:看到區間查詢很明顯是使用線段樹,但因為我們不確定不同區間各自的刪除狀況,所以其實這題的每個節點都必須搭配一台自動機才能解出 [1]。自動機的設計理念大致如下:每個節點都記錄了它自己代表的子字串從狀態 i 走到狀態 j 所必須移除的最少字元數量,其中 $0\le i\le j\le 4$,而且必須遞迴計算,例如 $m_0[1,3] = \min\{m_1[1,1]+m_2[1,3],\ m_1[1,2]+m_2[2,3],\ m_1[1,3]+m_2[3,3]\}$,這邊就又有點類似 DP 的味道。至於最後的單字元字串該怎麼賦值呢?從下圖觀察即可,舉例來說,對於字串 “2” 而言,要維持在狀態 0 的話必須把自己刪掉 $m[0,0]=1$,但是從狀態 0 走到 1 以及其他狀態的停留轉移並不用刪除任何字元 $m[0,1]=0,\ m[i,i]=0\ \text{for}\ i>0$,至於其他狀態轉移都是不可能達到的,所以用無限大去表示。如此一來欲求某個子字串的醜度只要查詢對應節點的 $m[0,4]$ 即可,當出現無限大代表不可行就回傳 -1。

Example image

參考資料:
[1] https://codeforces.com/blog/entry/49412?#comment-334446

實作:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#include <bits/stdc++.h>
using namespace std;

#define loop(i,L,R) for (int i=L; i<R; i++)
#define left(p) ((p) << 1)
#define right(p) (((p) << 1) + 1)

char s[200001];
const int oo = INT_MAX / 2;

struct automaton {
    int m[5][5]; // 用一個 5 * 5 的矩陣代表一台自動機
    automaton (char c = 0) {
        loop(i,0,5) loop(j,i,5)
            m[i][j] = (i != j) * oo; // no cost if there is no movement
        switch (c) {
            case '2': m[0][1] = 0; m[0][0] = 1; break;
            case '0': m[1][2] = 0; m[1][1] = 1; break;
            case '1': m[2][3] = 0; m[2][2] = 1; break;
            case '7': m[3][4] = 0; m[3][3] = 1; break;
            case '6': m[3][3] = 1; m[4][4] = 1; break;
        }
    }
    automaton operator*(const automaton& o) const {
        automaton ans;
        loop(i,0,5) loop(j,i,5) { // 0 <= i <= j < 5
            ans.m[i][j] = oo;
            loop(k,i,j+1) ans.m[i][j] = min(ans.m[i][j], m[i][k] + o.m[k][j]);
        }
        return ans;
    }
};

struct segtree {
    vector<automaton> nodes; // 每個節點都儲存一台自動機,節點號碼從 1 開始,才能遵循 LeftChild * 2、RightChild * 2 + 1 原則
    automaton build(int p, int l, int r) { // 求出區間 [l-r] 的答案並儲存在號碼 p 的節點
        assert(l <= r); // 從中間剖分的過程中,左索引值不可能超過右索引值
        if (l == r) return nodes[p] = automaton(s[l]); // 區間為單點元素時只得自行計算出答案
        int m = (l + r) >> 1; // 取中點
        auto a1 = build(left(p), l, m); // 求出區間 [l-m] 的答案並儲存在號碼 2 * p 的節點
        auto a2 = build(right(p), m+1, r); // 求出區間 [(m+1)-r] 的答案並儲存在號碼 2 * p + 1 的節點
        return nodes[p] = a1 * a2; // 合併左右子樹的兩個答案以求出自己的答案,並存進節點中
    }
    int N; // 整串序列的長度
    segtree(int n): nodes(n << 2) { // 欲查詢長度為 N 的序列答案所對應到的線段樹最多需要 4N - 1 個節點
        N = n; build(1, 1, N); // https://cp-algorithms.com/data_structures/segment_tree.html
    }
    int L, R; // 用於 query 和 recursive,表示欲查詢區間為 [L-R]
    automaton query(int l, int r) {
        L = l; R = r; assert(L <= R); // 欲查詢區間 [L-R] 在計算過程中不會改變
        return rec(1, 1, N); // 給定大區間 [1-N] 內所有子區間的已知資料,如何求出恰為 [L-R] 區間的答案?
    }
    // 給定過程中已經有答案的節點區間 [l-r],最後會回傳此區間和欲查詢區間 [L-R] 交集之後的答案,注意這三個數字本應與 build 過程中所設定的一致
    automaton rec(int p, int l, int r) { // assert(l <= r); // "rec" stands for "recursive"
        if (r < L || R < l) return automaton(); // 如果節點區間與欲查詢區間沒有重疊,則希望此答案為乘法單位元素 (相乘之後不影響其他區間的答案)
        if (L <= l && r <= R) return nodes[p]; // 如果節點區間已經被完整包覆在欲查詢區間之內,則直接貢獻答案 (停止繼續遞迴)
        // 如果節點區間和欲查詢區間僅部分重疊,則把自己這個節點區間再切成兩半,分別繼續做 rec
        int m = (l + r) >> 1; // 取中點,注意到下面的 left(p), l, m 和 right(p), m+1, r 的 pattern 會與 build 的過程完全相同
        return rec(left(p), l, m) * rec(right(p), m+1, r); // pseudo multiplication
    }
};

int main() { ios_base::sync_with_stdio(false); cin.tie(nullptr); // IO 優化
    int n, q;
    scanf("%d%d", &n, &q);
    scanf("%s", s+1);
    segtree st(n);
    loop(i,0,q) {
        int a, b;
        scanf("%d%d", &a, &b);
        int ans = st.query(a, b).m[0][4];
        if (ans == oo) ans = -1;
        cout << ans << endl;
    }
    return 0;
}

結果: Example image