731-D. 80-th Level Archeology

Tags: difference-array

出處:https://codeforces.com/contest/731/problem/D
提交:https://codeforces.com/contest/731/submission/107192106

問題:給定 $n\in[2,500000]$ 個長度介於 $[1,500000]$ 的字串,每個字串至多由 $c\in[1,1000000]$ 種字元 (1 ~ c) 構成。定義一種旋轉 $x$ 會讓每個字串的每個字元往後移 $x$,超過上界則回補 [1]。請問有沒有合法的旋轉 $x$ 可以讓原本的字串們由上而下會滿足字典序 [2],若無則輸出 -1。

解法:
因為字典序是一種傳遞關係 (transitive relation),也就是當 $a \le b$ 且 $b \le c$ 時,會連帶導致 $a \le c$,因此吾人只要由上往下依序檢查相鄰兩字之間有哪些旋轉 $x$ 可以讓兩字滿足字典序,再依序取交集,最後就是答案了。檢查的過程中,由下方的程式碼一開始的註解可以注意到,其實會有兩種情況,一種是上方的字元大於下方的字元而你希望透過旋轉讓上方小於下方,另外一種是原本上方已經小於下方,但你怕旋轉之後順序會倒過來,因此這個時候也必須加上限制使得字串即使經過旋轉還是能保持原本的順序。至於當上方的字元等於下方的字元,這個時候不管怎麼旋轉都無法改變順序,因此不用理會這種情況。

這題的賣點藏在取交集的實作!如果是第一種情況,也就是多個一整段連續的區間作交集,很簡單,左端點取最右邊的,右端點取最左邊的,就沒事了。但第二種情況有離散的區間並沒辦法這樣做,所以我們只能使用差分陣列 (difference array) 技巧,對於每次有覆蓋到的區間元素都加 1,由於這種情況每次作交集的時候都一定會覆蓋到 0 這個元素,所以最後在算 prefix sum 的時候其實只要檢查誰和 0 有一樣的覆蓋次數,就可以知道它們都是篩選過後留下來的元素。

心得:這題板主曾經嘗試過用 memset 的方式對沒覆蓋到的元素的 bit 設為 0,最後竟然吃 TLE,哈,看來有時候我們對 system call 效率的直覺不一定是對的。

附註:
[1] 以精確的數學語言來描述,字元 $c_i$ 往後移 $x$ 的結果為 $((c_i - 1 + x)\ \%\ c) + 1$。
[2] 所謂滿足字典序的意思與一般大家認知的相同,上方的字串由前往後掃的過程中如果第一次遇到一個字元與對應到下方字串之同一個位置的字元值不同,那麼上方的字元必須小於下方的字元;如果過程中每個字元都相同,那麼上方字串的長度不可以超過下方字串。

實作:

 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
#include <bits/stdc++.h>
using namespace std;

// [0,1,2,...,...,a,(c-2),(c-1)]
// [0,1,2,...,b,...,(c-2),(c-1)]
// 此時位移必須落在 [c-a, c-1-b] 位數
// 單純單塊區間多次的交集可以單純由 left end 和 right end 兩個端點維護,省時又省空間!

// [0,1,2,...,a,...,(c-2),(c-1)]
// [0,1,2,...,...,b,(c-2),(c-1)]
// 此時位移必須落在 [0, c-1-b] U [c-a, c-1] 位數
// 複雜雙塊區間多次的交集必須使用 difference array (prefix sum) 方法,記錄哪些區間確實被所有數字滿足,需要線性時間與空間。

int sum[1000001];
int arr[2][1000001]; // the last element in arr[i] indicates the size of the array!

int main() { ios_base::sync_with_stdio(false); cin.tie(nullptr); // IO 優化
    bool fail = false; // avoid double output of -1
    int n, c, insetL=0, insetR;
    cin >> n >> c; insetR=c-1;
    for (int z=0; z<n; z++) {
        cin >> arr[z&1][1000000];
        for(int i=0; i<arr[z&1][1000000]; i++)
            cin >> arr[z&1][i];
        if (z >= 1) { // 此時 arr[(z&1)^1] 是之前的,而 arr[z&1] 才是後來的
            bool needCompareLength = true;
            for(int i=0; i<min(arr[0][1000000], arr[1][1000000]); i++) {
                int a = arr[(z&1)^1][i]-1, b = arr[z&1][i]-1;
                needCompareLength &= (a==b);
                if (a != b) {
                    if (a > b) {
                        insetL = max(insetL, c-a);
                        insetR = min(insetR, c-1-b);
                    } else {
                        sum[0] += 1;
                        sum[c-b] -= 1;
                        sum[c-a] += 1;
                        sum[c] -= 1;
                    }
                    break; // needless to see other characters if this character should be satisfied.
                }
            }
            fail = needCompareLength && arr[(z&1)^1][1000000]>arr[z&1][1000000] || insetL>insetR;
            if (fail) break;
        }
    }
    if (fail) cout << -1 << endl;
    else {
        fail = true;
        for(int i=1; i<=c; i++)
            sum[i] += sum[i-1];
        for(int i=insetL; i<=insetR; i++) {
            if (sum[i] == sum[0]) {
                cout << i << endl;
                fail = false;
                break;
            }
        }
        if (fail) cout << -1 << endl;
    }
    return 0;
}

結果: Example image