1. 程式人生 > 其它 >CCF 202012-5星際旅行(20~100分)

CCF 202012-5星際旅行(20~100分)

前置知識

線段樹:通過懶惰標記,可實現區間處理,和區間詢問皆為\(O(logn)\)時間複雜度的資料結構,是一種二叉樹。因此對於一個節點\(st\),其左兒子節點為\(st*2\),右節點為\(st*2+1\),為方便整潔,程式碼中使用巨集定義\(ls(st<<1)\)\(rs(st<<1|1)\)

離散化:將無法通過開陣列標記的大資料變為小資料的一種手段。

分析

\(20\)分攻略

演算法:模擬

由於\(n,m\leq 1000\),此時\(O(nm)\)的演算法是允許的,所以對於每一種操作用迴圈模擬即可。

\(40\)分攻略

演算法 :差分+樹狀陣列 \(OR\) 線段樹+懶惰標記\(*1\)

\(n\leq 100000,m\leq 40000\),且只包含\(1,4\)操作。需要使用時間複雜度帶\(log\)的演算法,使用資料結構樹狀陣列+差分或者線段樹+懶惰標記即可。

線段樹傳遞懶惰標記時,要乘以區間長度。由於有三維,所以開三個線段樹即可,時間複雜度\(O(mlogn)\)

struct node {
    int lazS, lazM, sum;
} t[3][maxn << 3];

\(60\)分攻略

演算法:線段樹+懶惰標記\(*2\)

\(n\leq 100000,m\leq 40000\),且只包含\(1,2,4\)操作。同樣需要使用時間複雜度帶\(log\)的演算法,但是此時有兩種操作,樹狀陣列不能應用於如此複雜的情況。

使用線段樹時,需要考慮兩個懶惰標記相互的影響。設加法懶惰標記為\(lazS\),乘法懶惰標記為\(lazM\)。由於標記的實際含義是保留左節點和右節點的未操作資訊,所以懶惰標記下傳時,由乘法分配律,\(lazS\)要乘以\(lazM\),這樣可以保證在\(lazS\)下傳的時候,不會丟失\(lazM\)的資訊。

考慮標記下傳時是先進行乘法操作還是先進行加法操作。

  • 若是先進行加法操作,會出現將乘法操作後的新的加法標記同時乘上乘法標記。
  • 若是先進行乘法操作,將乘法操作先作用於原有的和上,然後再將加法操作作用上去即可。
#define ls st<<1
#define rs st<<1|1
void push_down(int st);
t[i][ls].sum = ((t[i][ls].sum * t[i][st].lazM) % mod + t[i][st].lazS * len[ls]) % mod;//先進行乘法操作,再進行加法操作
t[i][rs].sum = ((t[i][rs].sum * t[i][st].lazM) % mod + t[i][st].lazS * len[rs]) % mod;//先進行乘法操作,再進行加法操作
t[i][ls].lazS = ((t[i][ls].lazS * t[i][st].lazM) % mod + t[i][st].lazS) % mod;//注意加法標記要先做乘法,再做加法
t[i][rs].lazS = ((t[i][rs].lazS * t[i][st].lazM) % mod + t[i][st].lazS) % mod;//注意加法標記要先做乘法,再做加法
t[i][ls].lazM = t[i][ls].lazM * t[i][st].lazM % mod;//乘法標記直接操作即可
t[i][rs].lazM = t[i][rs].lazM * t[i][st].lazM % mod;//乘法標記直接操作即可
t[i][st].lazM = 1;//清除懶惰標記
t[i][st].lazS = 0;//清除懶惰標記

時間複雜度\(O(mlogn)\),此時題目為洛谷\(P3372\)

\(80\)分攻略

演算法:線段樹+懶惰標記\(*3\)

\(n\leq 100000\),引入轉換操作,同樣需要使用時間複雜度帶\(log\)的演算法。

首先先考慮沒有\(1,2\)操作時應該如何做,注意到每次操作由原來的\(x,y,z\)變為\(y,z,x\)相當於原來的三維元素向左平移一維,那麼如果向左平移三次,就相當於沒有平移,所以轉換的轉移標記\(lazS\)進行\(\%3\)操作即可。

再考慮有\(1,2\)操作時應該如何做,注意到加法乘法其實和轉換操作沒有關係,所以只需要考慮數字運算(加法和乘法)和轉換運算這兩種運算是如何相互影響的。

再次考慮懶惰標記的實際含義,是將左節點和右節點尚未處理的資訊儲存起來,那麼轉換時也需將儲存的資訊同時轉換。

if (lazC[rt] == 1) {//向左平移一維
    node x = t[0][st], y = t[1][st], z = t[2][st];
    t[0][st] = y;
    t[1][st] = z;
    t[2][st] = x;
} else if (lazC[rt] == 2) {//向左平移兩維
    node x = t[0][st], y = t[1][st], z = t[2][st];
    t[0][st] = z;
    t[1][st] = x;
    t[2][st] = y;
}

再考慮是先進行轉換運算還是先進行數字運算,當操作子節點的時候,父節點已經進行了轉換,所以為了讓父節點的標記操作能夠對應上子節點的標記,所以要先進行轉換,再進行數字運算。

\(100\)分攻略

演算法:線段樹+懶惰標記\(*3\)+區間離散化

\(n≤1,000,000,000\),原有線段樹空間複雜度為\(O(n)\),但現在這個資料範圍不支援這個複雜度,考慮離散化。

注意到\(m\leq 40000\),那麼操作的區間個數最多為\(40000\)個,端點最多為\(80000\)個,那麼離散化後進行操作空間複雜度為\(O(m)\),注意到線段樹開點應開總端點的\(4\)倍,所以總共需要開\(8m\)個點,即為\(m<<3\)

使用區間離散化的技巧,將原有的\([L,R]\)區間變為\([L,R+1)\),這樣滿足區間可加性,舉個反例體會下這個操作的必要性。

提供三個操作\([1,4],[1,1],[4,4]\),直接離散化後,區間為\([1,2],[1,1],[2,2]\),後兩個操作合併等於第一個操作,而原有操作中的端點\(2\)\(3\)丟失了。而轉換為左閉右開區間後,區間為\([1,5),[1,2),[4,5)\),離散化後為\([1,4),[1,2),[3,4)\),此時資訊並未丟失,儲存每一個區間的長度\(len[maxn << 3]\)即可。

原線段樹的葉子節點理解為每一個端點,離散化區間後的葉子節點可以理解為以第\(i\)個端點為開頭的左閉右開區間。比如說上述轉換區間後為\([1,5),[1,2),[4,5)\),那麼葉子節點會變為\([1,2),[2,4),[4,5)\),一共出現過\(4\)個不同的區間端點,顯然最右邊的端點不需要儲存資訊,所以一共有三個葉子節點,第\(1\)個葉子節點表示以\(1\)開頭的左閉右開區間,第\(2\)個葉子節點表示以\(2\)為開頭的左閉右開區間,第\(3\)個葉子節點表示以\(4\)為開頭的左閉右開區間(程式碼中最後一個端點也有一個節點,長度為0)。

考慮操作,考慮第一個操作\([1,5)\)(注意離散化後區間為\([1,4)\)),他需要的是\(5\)之前的端點的操作,即要操作\(1,2,4\)(離散化後為\(1,2,3\))開頭的區間,那麼由於線段樹保留的是以第\(i\)個端點為開頭的左閉右開區間,所以操作時要將離散化後的右端點\(4\)減一再進行操作。

#include <bits/stdc++.h>

#define start ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
#define ll long long
#define int ll
#define ls st<<1//左子樹,st*2
#define rs st<<1|1//右子樹,st*2+1
using namespace std;
const int maxn = (ll) 4e5 + 5;
const int mod = 1e9 + 7;
int T = 1;
vector<int> v;
struct node {
    int lazS, lazM, sum;
} t[3][maxn << 3];
int lazC[maxn << 3];
int len[maxn << 3];
int q[maxn][6];

void build(int st, int l, int r) {
    for (int i = 0; i <= 2; ++i)t[i][st].lazM = 1;
    /*
     * 最右區間長度為0
     * 由於保留左閉右開區間,所以長度為v[l + 1] - v[l]
     */
    if (l == r) {
        if (l == v.size() - 1)
            len[st] = 0;
        else
            len[st] = v[l + 1] - v[l];
        return;
    }
    int mid = (l + r) >> 1;
    build(ls, l, mid);
    build(rs, mid + 1, r);
    len[st] = len[ls] + len[rs];
}

void change(int rt, int st) {
    if (lazC[rt] == 1) {//向左平移一維
        node x = t[0][st], y = t[1][st], z = t[2][st];
        t[0][st] = y;
        t[1][st] = z;
        t[2][st] = x;
    } else if (lazC[rt] == 2) {//向左平移兩維
        node x = t[0][st], y = t[1][st], z = t[2][st];
        t[0][st] = z;
        t[1][st] = x;
        t[2][st] = y;
    }
}

void push_down(int st) {
    if (lazC[st]) {//先進行轉換操作
        change(st, ls);
        change(st, rs);
        lazC[ls] = (lazC[ls] + lazC[st]) % 3;
        lazC[rs] = (lazC[rs] + lazC[st]) % 3;
        lazC[st] = 0;
    }
    for (int i = 0; i <= 2; ++i) {
        t[i][ls].sum = ((t[i][ls].sum * t[i][st].lazM) % mod + t[i][st].lazS * len[ls]) % mod;//先進行乘法操作,再進行加法操作
        t[i][rs].sum = ((t[i][rs].sum * t[i][st].lazM) % mod + t[i][st].lazS * len[rs]) % mod;//先進行乘法操作,再進行加法操作
        t[i][ls].lazS = ((t[i][ls].lazS * t[i][st].lazM) % mod + t[i][st].lazS) % mod;//注意加法標記要先做乘法,再做加法
        t[i][rs].lazS = ((t[i][rs].lazS * t[i][st].lazM) % mod + t[i][st].lazS) % mod;//注意加法標記要先做乘法,再做加法
        t[i][ls].lazM = t[i][ls].lazM * t[i][st].lazM % mod;//乘法標記直接操作即可
        t[i][rs].lazM = t[i][rs].lazM * t[i][st].lazM % mod;//乘法標記直接操作即可
        t[i][st].lazM = 1;//清除懶惰標記
        t[i][st].lazS = 0;//清除懶惰標記
    }
}

void add(int st, int l, int r, int L, int R, int a[]) {
    if (L <= l && r <= R) {
        for (int i = 0; i <= 2; ++i) {
            t[i][st].lazS = (t[i][st].lazS + a[i]) % mod;
            t[i][st].sum = (t[i][st].sum + a[i] * len[st]) % mod;//注意lazS保留的僅僅是加法,以便對於不同區間根據長度判定加和
        }
        return;
    }
    push_down(st);
    int mid = (l + r) >> 1;
    if (L <= mid)
        add(ls, l, mid, L, R, a);
    if (R > mid)
        add(rs, mid + 1, r, L, R, a);
    for (int i = 0; i <= 2; ++i)t[i][st].sum = (t[i][ls].sum + t[i][rs].sum) % mod;
}

void mul(int st, int l, int r, int L, int R, int val) {
    if (L <= l && r <= R) {
        for (int i = 0; i <= 2; ++i) {
            t[i][st].sum = (t[i][st].sum * val) % mod;
            t[i][st].lazM = (t[i][st].lazM * val) % mod;
            t[i][st].lazS = (t[i][st].lazS * val) % mod;//由乘法分配律,lazS也需要乘
        }
        return;
    }
    push_down(st);
    int mid = (l + r) >> 1;
    if (L <= mid)
        mul(ls, l, mid, L, R, val);
    if (R > mid)
        mul(rs, mid + 1, r, L, R, val);
    for (int i = 0; i <= 2; ++i)t[i][st].sum = (t[i][ls].sum + t[i][rs].sum) % mod;
}

void change(int st, int l, int r, int L, int R) {
    if (L <= l && r <= R) {
        lazC[st] = (lazC[st] + 1) % 3;//轉換操作轉換三次後就等於不轉換,所以取模
        node x = t[0][st], y = t[1][st], z = t[2][st];//lazC標記實際含義為保留子節點的資訊,所以父節點要進行操作
        t[0][st] = y;
        t[1][st] = z;
        t[2][st] = x;
        return;
    }
    push_down(st);
    int mid = (l + r) >> 1;
    if (L <= mid)
        change(ls, l, mid, L, R);
    if (R > mid)
        change(rs, mid + 1, r, L, R);
    for (int i = 0; i <= 2; ++i)t[i][st].sum = (t[i][ls].sum + t[i][rs].sum) % mod;
}

void query(int st, int l, int r, int L, int R, int a[]) {
    if (L <= l && r <= R) {
        for (int i = 0; i <= 2; ++i) {
            a[i] = (a[i] + t[i][st].sum) % mod;
        }
        return;
    }
    push_down(st);
    int mid = (l + r) >> 1;
    if (L <= mid)
        query(ls, l, mid, L, R, a);
    if (R > mid)
        query(rs, mid + 1, r, L, R, a);
}

void solve() {
    //注意本程式碼已#define int long long
    int n, m;
    cin >> n >> m;
    //為離散化,先讀入,後操作
    for (int i = 1; i <= m; ++i) {
        cin >> q[i][0] >> q[i][1] >> q[i][2];
        ++q[i][2];//形成左閉右開區間
        v.push_back(q[i][1]);
        v.push_back(q[i][2]);
        if (q[i][0] == 1) {
            for (int j = 3; j <= 5; ++j)cin >> q[i][j];
        } else if (q[i][0] == 2) {
            cin >> q[i][3];
        }
    }
    v.push_back(LLONG_MIN);//放入一個最小值,保證陣列以1開始,也可以開一個數組進行離散化
    sort(v.begin(), v.end());//離散化需要先排序
    v.erase(unique(v.begin(), v.end()), v.end());//將多餘的數字去除
    for (int i = 1; i <= m; ++i) {
        //通過二分離散化
        q[i][1] = lower_bound(v.begin(), v.end(), q[i][1]) - v.begin();
        q[i][2] = lower_bound(v.begin(), v.end(), q[i][2]) - v.begin();
    }
    int tot = v.size() - 1;//線段樹的葉子結點個數,即操作區間的最右端點
    build(1, 1, tot);
    for (int i = 1; i <= m; ++i) {
        //由於操作左閉右開區間,所以每次操作右端點需要-1
        if (q[i][0] == 1) {
            int x[] = {q[i][3], q[i][4], q[i][5]};
            add(1, 1, tot, q[i][1], q[i][2] - 1, x);
        } else if (q[i][0] == 2) {
            mul(1, 1, tot, q[i][1], q[i][2] - 1, q[i][3]);
        } else if (q[i][0] == 3) {
            change(1, 1, tot, q[i][1], q[i][2] - 1);
        } else {
            int x[] = {0, 0, 0};
            query(1, 1, tot, q[i][1], q[i][2] - 1, x);
            int ans = 0;
            for (int j = 0; j <= 2; ++j) {
                ans = (ans + x[j] * x[j] % mod) % mod;
            }
            cout << ans << '\n';
        }
    }
}

signed main() {
    start;//關同步
    while (T--)
        solve();
    return 0;
}

總結

本題難點在於三個標記的影響以及區間離散化。

對於前兩個標記在洛谷OJ中有原題,轉換則需要考慮多種組合,以及不正確轉換的反例,比如說轉換和運算的先後,是否帶標記轉換還是隻轉換\(sum\),轉換根據本節點的標記還是父節點的標記等等。題解給出的是正確的轉換方式,但是非正確的轉換方式很容易干擾思路,所以請完全理解為什麼如此轉換。

而區間離散化首先需要理解離散化的思想,其次理解為什麼要將區間變為左閉右開區間,最後理解每一個節點代表什麼。