【SEOI2024】官方題解

Jefferyzzzz發表於2024-06-03

【SEOI2024】官方題解

摘要

作為市二程式設計社舉辦的第一場公開比賽,主題為資料的處理與分析,賽題主要圍繞基本演算法以及資料結構展開。難度被設計呈兩級分化趨勢,\(A\)題與\(B\)題及其簡單,\(DEF\)題則以部分分形式進行的難度分層,前一半在難度與\(A\)\(B\)相同,後一半則達到省選難度,出乎意料的是\(DEF\)賽時無人拿分,可能是因為時間不足,並且選手對輸入輸出不熟悉。也有可能是因為選手第一次接受到專業的資訊試題,在\(SEOI2025\)我們會對選手進行賽前的簡單培訓,讓選手能夠提前熟悉比賽環境從而對難度有更好的適應。

賽題連結

https://www.cnblogs.com/Jefferyz/p/18227791

註明

本題解若未宣告的記號均來源於原題記號,請儘量對照原題檢視題解。由於微信奇怪的限制,透過瀏覽器開啟本文章能獲得絕對更好的閱讀體驗。

【SEOI2024 A】二元運算器

這題沒什麼好說的,但是\(Python\)讀入會由於行末換行符而多讀入一個字元,導致直接進入\(math\ error\)分支或者直接\(RE\),幾乎所有\(Python\)選手都因為這點沒有拿到第一題的分數。

時間複雜度\(O(1)\),空間複雜度\(O(1)\)

參考程式碼:

#include <bits/stdc++.h>

#define I return
#define Love 0
#define Coding ;
using namespace std;

int main() {
    char opt;
    cin >> opt;
    int num1, num2;
    cin >> num1 >> num2;
    if (opt == '+') cout << num1 + num2;
    if (opt == '-') cout << num1 - num2;
    if (opt == '*') cout << num1 * num2;
    if (opt == '^') cout << (int) pow(num1, num2);
    if (opt == '%') {
        if (num2 == 0) printf("math error");
        else cout << num1 % num2;
    }
    if (opt == '/') {
        if (num2 == 0) printf("math error");
        else cout << (int) round(num1 * 1.0 / num2);
    }
    I Love Coding
}

【SEOI2024 B】Grievous

這題是唯一不是我出的題,一開始被排除在題單外,記數的值域為\(R\),因為這題\(Python\)根本無法使用素數篩,而開小資料會被其他程式語言\(O(n\sqrt R)\)暴力秒掉,而對於\(Python\)而言如果想卡掉這個根號,幾乎是不可能的,即使把百萬級的數塞進陣列就以及無法在最高時限\(5s\)中完成了。在賽前的幾個小時,衚衕學把這題

加了進來,我緊急把後面大樣例刪掉了,但這個決定最終被證明是明智的,至少讓兩支隊伍\(A\)掉拿到了\(100\)分,不至於全員爆零。

有關素數篩法可以前去檢視這篇我寫的文章:https://www.cnblogs.com/Jefferyz/p/18226687。

時間複雜度\(O(R)\),空間\(O(R)\)

參考程式碼:

#include <iostream>
#include <string>
#include <algorithm>

using namespace std;
const int N = 1e8 + 10, M = 2e2 + 10;
int isNotPrime[N];
int Prime[N], cnt, a[N];

int main() {
    int n;
    scanf("%d", &n);
    int maxm = -1;
    for (int i = 1; i <= n; ++i)
        scanf("%d", &a[i]), maxm = max(a[i], maxm);
    for (int i = 2; i <= maxm; ++i) {
        if (!isNotPrime[i]) {
            Prime[++cnt] = i;
        }
        for (int j = 1; j <= cnt; ++j) {
            if (Prime[j] * i > maxm)
                break;
            isNotPrime[Prime[j] * i] = 1;
            if (i % Prime[j] == 0)
                break;
        }
    }
    int ans = 0;
    for (int i = 1; i <= n; ++i)
        if (!isNotPrime[a[i]] && a[i] != 1)
            ++ans;
    printf("%d", ans);
    return 0;
}

【SEOI2024 C】修改向量

本次比賽陰間的開始,沒有人進行過嘗試,所有提交的人都輸出了\(-1\)\(2\)騙了\(5\sim15\)分。

我們記\(\delta = A\cdot B\),如果\(\delta = 0\)顯然無需進行修改操作,而對於\(\delta \neq 0\),我們對\(B_i\)進行修改時會對\(\delta\)產生\(|\Delta B\cdot A_i|\)的增減,而單次修改增減的最大值即為\(|m\cdot A_i|\),其中\(m\)為常數,提出得\(\Delta _{max}=|m|\cdot |A_i|\),我們為了最小化修改次數,顯然每次因選擇最大的\(|A_i|\),於是將\(A\)序列按\(|A|\)大小排序,每次取最大的\(|A|\),能夠使\(\delta =0\)時,該次數即為最小操作次數。

時間複雜度\(O(n\log_{2}n)\),在於排序;空間複雜度\(O(n)\)

參考程式碼:

#include <bits/stdc++.h>

#define int long long
using namespace std;
const int N = 5e5 + 10;
int A[N], B[N];

bool cmp(int a, int b) { return a > b; }

signed main() {
    int flag = 0;
    int n, m;
    scanf("%lld%lld", &n, &m);
    for (int i = 1; i <= n; ++i) scanf("%lld", &A[i]);
    for (int i = 1; i <= n; ++i) scanf("%lld", &B[i]);
    int tmp = 0;
    for (int i = 1; i <= n; ++i) tmp += A[i] * B[i];
    if (tmp == 0) {
        puts("0");
        return 0;
    }
    tmp = abs(tmp);
    for (int i = 1; i <= n; ++i) A[i] = abs(A[i]);
    sort(A + 1, A + n + 1, cmp);
    for (int i = 1; i <= n; ++i) {
        if (m * A[i] >= tmp) {
            printf("%lld", i);
            flag = i;
            break;
        } else tmp -= m * A[i];
    }
    if (!flag) puts("-1");
    return 0;
}

【SEOI2024 D】義大利麵序列

這題極其考驗選手對時間複雜度的最佳化,對於暴力,顯然存在\(O(n^3)\)的演算法,列舉左右端點\(O(n^2)\),計算左右端點最大值\(O(n)\)。對於最大值的計算我們可以透過\(ST\)表透過\(O(nlog_{2}n)\),的預處理達到\(O(1)\)查詢,總時間複雜度\(O(n^2)\)\(ST\)表寫的程式碼比標算長,賽時有一個人寫出來了,但是沒編譯透過很遺憾。

接下來考慮正解,我們發現\(O(n^3)\)\(ST\)表最佳化得到的\(O(n^2)\)瓶頸皆在於\(O(n^2)\)的列舉左右端點的,我們對於同一型別的劃分尋找相似性,不難發現如果左右端點一處被固定下來的話,一端的最大值也恆定了,此時滑動另一端點,對結果產生影響的只有另一端點在不同位置的最大值。

於是我們嘗試構造數列

\[f_i=\sum\limits_{j=1}^{i} max[A_1,A_i] \]

\[g_i=max[A_i,A_n] \]

即字首\(max\)和,於字尾\(max\),由於確定右端點後右端點貢獻恆定為從右端點到序列尾部的最大值,我們只需要對左端點之前每次分割位置得到的最大值做字首和,最後兩者相乘就能得到最終答案。

\[ans=\sum\limits_{i=2}^{n-1} f[i-1]\cdot g[i+1] \]

時間複雜度\(O(n)\),空間複雜度\(O(n)\)

透過大樣例的擬合,我們很容易發現第二問的數學期望逼近\(k^2\),具體數學證明歡迎大家來程式設計社討論。

參考程式碼:

#include <bits/stdc++.h>

#define int long long
const int N = 2e7 + 10;
int a[N], f[N], g[N];
using namespace std;

signed main() {
    int type;
    scanf("%lld", &type);
    if (type) {
        int k;
        scanf("%lld", &k);
        printf("%lld\n", 1ll * k * k);
        return 0;
    }
    int n;
    scanf("%lld", &n);
    int num = (1 + n - 2) * (n - 2) / 2;
    for (int i = 1; i <= n; ++i) scanf("%lld", &a[i]);
    f[1] = a[1];
    for (int i = 2; i <= n; ++i) f[i] = max(f[i - 1], a[i]);
    for (int i = 1; i <= n; ++i) f[i] += f[i - 1];
    g[n] = a[n];
    for (int i = n - 1; i >= 1; --i) g[i] = max(g[i + 1], a[i]);
    int sum = 0;
    for (int i = 2; i <= n - 1; ++i) sum += 1ll * f[i - 1] * g[i + 1];
    printf("%.9f", sum * 1.0 / num);
    return 0;
}

【SEOI2024 E】旅途的華章

非常有意思的一道思維題。暴力的話只要把迴圈操作寫明白即可。

這裡提供兩種思路,看到左右迴圈操作,是以下標為單位的整體移動問題,顯然可以透過二叉樹的下標進行分裂合併,關於節點前後關係可以採用平衡樹維護,如果是整體翻轉操作或者段修,只需要延後下傳,下傳時交換平衡二叉樹的左右節點即可,總時間複雜度\(O((n+q)log_2 n)\)\(O(nlog_2 n)\)建平衡樹,\(O(qlog_2 n)\)查詢,空間複雜度\(O((n+q)log_2 n)\)。排名分裂可以採用\(FHQ-Treap\)或者\(Splay\)樹。但是平衡樹碼量特別大,加上有兩種下傳標記和分裂合併會導致程式碼及其難寫,平衡樹常數也非常大,可能會被\(n=2\times 10^6\)的資料點卡常。

於是我們採用另外一種思路,觀察第二類資料點,翻轉顯然不用真正地進行,因為每次記錄翻轉,如果有翻轉標記,如果詢問就在\(i\)位置的“互補”位置\(n-i+1\)位置找值即可,兩次迴圈等價於原序列,我們用異或標籤維護翻轉,左右迴圈時間複雜度絕對是\(O(n)\)的,如果我們不在樹高被維護在\(O(log_2 n)\)級別的平衡樹上實時操作,無法有效地進行左右迴圈操作。但我們能不能不真正地進行左右移,而是記錄迴圈操作進行疊加,在查詢時直接查詢對應位置?。顯然單純的左右迴圈是可以累加的,\(n\)次迴圈等價於原序列,我們把最後迴圈次數對\(n\)取模即可。現在考慮同時存在翻轉和迴圈的情況,我們可以發現當序列被翻轉時,左右迴圈會被顛倒,這點很容易被證明,原來被向右挪移\(k\)位的數,經過翻轉後顯然相對原序列被向左挪移了\(k\)位。由於迴圈翻轉都是對於位置而言的,同時也只有單點修改,我們的思想只維護原序列,就是累加所有翻轉迴圈操作,在單點查詢時只需要找到在原序列中找到查詢位置被翻轉迴圈後的新位置的值即可。

我們可以透過上述思想得到以下策略,在查詢時以先迴圈後翻轉進行查詢,如果進行迴圈時序列處理翻轉狀態就向反方向記錄迴圈。同理,在修改時,我們由於是先記錄翻轉迴圈狀態查詢時統一進行位置處理從而找到對應位置,我們在修改一個位置時,由於該位置是對於已經進行翻轉迴圈的序列而言的,我們則需要逆著翻轉迴圈找到當前位置對應的未被翻轉迴圈前的位置,所以我們逆著查詢找位置發的方法,先翻轉後反向迴圈便可將操作對映到未被翻轉迴圈後的序列,從而在查詢時能夠得到正確的位置。我們只需要建一顆線段樹分治區間維護原序列的段修然後查詢時根據翻轉迴圈情況找位置即可。時間複雜度仍然是\(O((n+q)log_2 n)\),空間複雜度\(O((n+q)log_2 n)\)。但編碼難度和常數都比第一個方法少的多。

參考程式碼:

#include <stdio.h>
#include <string.h>

const int N = 2e6 + 10;

int read() {
    char c;
    bool flag = false;
    while ((c = getchar()) < '0' || c > '9') if (c == '-') flag = true;
    int res = c - '0';
    while ((c = getchar()) >= '0' && c <= '9') res = (res << 3) + (res << 1) + c - '0';
    return flag ? -res : res;
}

void write(int x, char ch) {
    if (x < 0) putchar('-'), x = -x;
    if (x > 9) write(x / 10, '\0');
    putchar(x % 10 + '0');
    if (ch != '\0') putchar(ch);
}

int ls(int p) { return p << 1; }

int rs(int p) { return p << 1 | 1; }

int tree[N << 2], tag[N << 2], reverse = 1, delta, n, m;

void pushdown(int p) {
    if (~tag[p]) {
        tag[ls(p)] = tag[rs(p)] = tag[p];
        tree[ls(p)] = tree[rs(p)] = tag[p];
        tag[p] = -1;
    }
}

void update(int p, int lp, int rp, int l, int r, int k) {
    if (lp >= l && rp <= r) {
        tree[p] = tag[p] = k;
        return;
    }
    pushdown(p);
    int mid = (lp + rp) >> 1;
    if (l <= mid) update(ls(p), lp, mid, l, r, k);
    if (r > mid) update(rs(p), mid + 1, rp, l, r, k);
}

int query(int p, int lp, int rp, int pos) {
    if (lp == rp && lp == pos) return tree[p];
    pushdown(p);
    int mid = (lp + rp) >> 1;
    if (pos <= mid) return query(ls(p), lp, mid, pos);
    else return query(rs(p), mid + 1, rp, pos);
}

void print(int s) {
    if (reverse == -1) s = n - s + 1;
    int d = -delta;
    int ans;
    if (d >= 0) {
        if (s + d > n) ans = query(1, 1, n, s + d - n);
        else ans = query(1, 1, n, s + d);
    } else {
        if (s + d <= 0) ans = query(1, 1, n, s + d + n);
        else ans = query(1, 1, n, s + d);
    }
    write(ans, '\n');
}

signed main() {
    n = read(), m = read();
    memset(tag, -1, sizeof tag);
    while (m--) {
        int opt = read();
        if (opt == 0) {
            int s = read();
            print(s);
        }
        if (opt == 1) {
            int l = read(), r = read(), k = read();
            int tmpl = l;
            if (reverse == -1) l = n - r + 1, r = n - tmpl + 1;
            int d = -delta;
            if (d > 0) {
                l = l + d;
                r = r + d;
                if (l > n) update(1, 1, n, l - n, r - n, k);
                else if (r > n) update(1, 1, n, l, n, k), update(1, 1, n, 1, r - n, k);
                else update(1, 1, n, l, r, k);
            } else if (d < 0) {
                l = l + d;
                r = r + d;
                if (r <= 0) update(1, 1, n, l + n, r + n, k);
                else if (l <= 0) update(1, 1, n, 1, r, k), update(1, 1, n, l + n, n, k);
                else update(1, 1, n, l, r, k);
            } else update(1, 1, n, l, r, k);
        }
        if (opt == 2) {
            int p = read();
            delta += p * reverse;
            if (delta >= n) delta %= n;
            if (delta <= -n) delta = -(-delta) % n;
        }
        if (opt == 3) {
            reverse = -reverse;
        }
    }
    return 0;
}

【SEOI2024 F】異色

作為本次比賽的壓軸題,我作為出題人仍然承襲了前幾題前易後難的特點。

對於第一類資料點,我們只需要離散化值域,對於每次詢問掃一便區間值域即可。時間複雜度\(O(nlog_2 n+qn)\),前半部分為離散化,後半部分為查詢,空間複雜度\(O(n)\)。對於本題\(q\)\(n\)同階的情況下,時間複雜度位\(O(n^2)\),不能忍受。

對於此類查詢\([l,r]\)的不可重複貢獻型問題時,我們常用莫隊演算法控制左右端點波動幅度從而將\(O(n)\)的整體震盪轉換為\(O(n)\)的塊間震盪,總時間複雜度\(O(n\sqrt n)\),對於有修改的情況我們可以使用類似思想的帶修莫隊達到\(O(n^{\frac{3}{2}})\)

然而本題查詢操作強制線上,你無法使用任何離線演算法包括莫隊演算法混到任何分數。

其實對於\([l,r]\)的點對貢獻型問題都可以轉換成等價的\(k\)維偏序關係從而使用\(k\)維數點(即透過維護\(k\)個維護的值域從而查詢各個維護滿足一定限制的點數量)的解決。對於本題查詢區間內不同的數顯然也可以進行如此轉換。

\(Pre_i\)表示上一個等於\(C_i\)的數的下標,\(Suf_i\)表示下一個等於\(C_i\)的數的下標。(如果不存在前驅\(Pre_i\),我們定義其為\(0\),如果不存在後繼\(Suf_i\)我們定義其不存在字尾。)

對於\([l,r]\),問題等價於我們需要找到滿足以下二維偏序關係的數\(a_i\)的數量:

\[\begin{cases}Pre_{i} < l\\l\le i \le r \end{cases}\tag{1} \]

透過將統計問題轉化成尋找滿足偏序關係的點,我們可以透過二維數點來解決這個問題,對於\(k\)維數點,如果可以離線我們可以使用\(CDQ\)分治,或者排序掃一維度,資料結構維護其他維護的方法。而對於強制線上,我們可以採用高維資料結構,例如各種樹套樹,\(K-d\)樹等等。而對於靜態情況,我們可以採用樹套樹,主席樹(可持久化線段樹)。帶修且強制線上的情況我們只能使用高維資料結構進行維護。其中\(K-d\)樹過於難編寫和維護平衡,線段樹套線段樹或者樹狀陣列套樹狀陣列空間難以忍受,對於二維的情況,我們可以採用樹狀陣列套動態開點線段樹來解決這個問題。其中樹狀陣列維護序列下標,每個樹狀陣列巢狀的動態開點線段樹維護在該樹狀陣列下標對應的若干個原序列下標的\(Pre\)資訊的值域。

至此,我們已經解決了靜態問題,而動態該怎麼辦?我們考慮每次修改可能對其他位置\(Pre\)值所產生的影響。

假設將\(i\)位置的數修改成\(val\),記\(R_i\)表示\(i\)位置的值域,\(R[i][j]\)表示\((R_i)_j\) 即第\(i\)個位置值域中的\(j\)值。(此處為表述方便,\(R_i\)並不表示樹狀陣列維護的若干個值域,而是但點值域資訊,值域中有且僅有一個點,但為了便於理解程式碼,還是以值域形式給出)。

一共有三類位置的\(Pre\)值可能會產生影響,分別為:原位置,修改前的後繼,修改後的後繼,我們分六種情況進行討論:

\(1^0\) 首先必定會對以下位置的\(Pre\)值產生以下影響,記\(Pre_{now}\)表示修改後\(i\)位置的\(Pre\)

\[(1) R[i][Pre_i]-1 \]

\[(2)R[i][Pre_{now}]+1 \]

\(2^0\) 如果修改前\(i\)位置存在後繼,記其為\(Suf_i\),則會對以下位置的\(Pre\)值產生以下影響

\[(3)R[Suf_i][Pre_{suf_i}]-1 \]

\[(4)R[Suf_i][i]+1 \]

\(3^0\) 如果將\(i\)位置修改成\(val\)後,對修改後的\(i\)點存在後繼\(s\),記其為\(Suf_s\),記\(Suf_s\)在修改前原來的前驅為\(Pre_p\)則會對以下位置的\(Pre\)值產生以下影響

\[(5)R[Suf_s][Pre_p]-1 \]

\[(6)R[Suf_s][i]+1 \]

我們對以上六種情況進行處理,在對應的下標透過修改包含被影響的下標的樹狀陣列中巢狀的動態開點線段樹中的值域資訊即可。

同時我們仍需維護動態的前驅後繼,我們採用平衡樹進行維護。

總時間複雜度\(O(nlog_2n+qlog_2^2n)\),空間複雜度\(O(n+qlog^2_2n)\),兩種複雜度加號前半部分為維護前驅後繼的複雜度後半部分為維護樹狀陣列套動態開點線段樹的複雜度。

至此壓軸題已經被完美解決,我們來計算一下這題需要的空間,一次最多產生\(6\)次修改,每次修改會多產生\(O(log^2_2 n)\)的節點。

最多需儲存約\(10^5+6\times 10^5\times log_2 10^5 \times log_2 10^5 \approx 165635260個int \approx 631.8484MiB\)

加上棧空間大約為\(650MiB\),而我們設定的空間限制只有\(512MiB\)(因為這是luogu最高空間限制),可能由於資料隨機生成導致修改操作減半,以及資料隨機情況下很少會有重複數,導致後兩類情況很少出現,但這完全能被特殊資料卡掉,這是我們出題人的不嚴謹所導致的。但事實證明這對比賽情況沒有任何影響,因為所有資料點的記憶體消耗都沒有超過\(200MiB\),最高的約為\(197.90MiB\),屬於是非常僥倖了(。

其實還可以對空間進行更大的最佳化,例如對先前\(Pre\)位置進行修改時先判斷修改前和修改後的\(Pre\)位置的值是否相等,如果不相等則不進行修改,這樣能在資料隨機的情況下極大程度的減少記憶體消耗,甚至可以使空間減半,大家可以自行嘗試一下。

參考程式碼:

#include <bits/stdc++.h>

using namespace std;
typedef pair<int, int> pii;
const int N = 4e7, M = 5e5 + 10;
int ls[N], rs[N], root[M], sum[N], tot, a[M], tree_rt;
int cnt;


struct Node {
    int ls, rs, pri;
    pii key;
} t[M];


int newNode(pii key) {
    ++cnt;
    t[cnt].pri = rand();
    t[cnt].key = key;
    return cnt;
}

void rotate(int &o, int d) {
    int k;
    if (d == 0) {
        k = t[o].rs;
        t[o].rs = t[k].ls;
        t[k].ls = o;
    } else {
        k = t[o].ls;
        t[o].ls = t[k].rs;
        t[k].rs = o;
    }
    o = k;
}

void insert(int &u, pii x) {
    if (!u) {
        u = newNode(x);
        return;
    }
    if (x > t[u].key) insert(t[u].rs, x);
    else insert(t[u].ls, x);
    if (t[u].ls && t[u].pri > t[t[u].ls].pri) rotate(u, 1);
    if (t[u].rs && t[u].pri > t[t[u].rs].pri) rotate(u, 0);
}

void del(int &u, pii x) {
    if (t[u].key == x) {
        if ((!t[u].ls) && !(t[u].rs)) {
            u = 0;
            return;
        }
        if ((!t[u].ls) || (!t[u].rs)) {
            u = t[u].ls + t[u].rs;
            return;
        }
        if (t[t[u].ls].pri > t[t[u].rs].pri) {
            rotate(u, 0);
            del(t[u].ls, x);
            return;
        } else {
            rotate(u, 1);
            del(t[u].rs, x);
            return;
        }
    }
    if (t[u].key > x) del(t[u].ls, x);
    else del(t[u].rs, x);
}

pii Pre(int u, pii x) {
    if (!u) return make_pair(0, 0);
    if (t[u].key >= x) return Pre(t[u].ls, x);
    pii tmp = Pre(t[u].rs, x);
    if (!tmp.second) return t[u].key;
    return tmp;
}

pii Suf(int u, pii x) {
    if (!u) return make_pair(0, 0);
    if (t[u].key <= x) return Suf(t[u].rs, x);
    pii tmp = Suf(t[u].ls, x);
    if (!tmp.second) return t[u].key;
    return tmp;
}


inline int read() {
    char c;
    bool flag = false;
    while ((c = getchar()) < '0' || c > '9') if (c == '-') flag = true;
    int res = c - '0';
    while ((c = getchar()) >= '0' && c <= '9') res = (res << 3) + (res << 1) + c - '0';
    return flag ? -res : res;
}

int lowbit(int x) { return x & -x; }

void pushup(int p) { sum[p] = sum[ls[p]] + sum[rs[p]]; }

void update(int &p, int lp, int rp, int pos, int val) {
    if (!p) p = ++tot;
    if (lp == rp && lp == pos) {
        sum[p] += val;
        return;
    }
    int mid = (lp + rp) >> 1;
    if (pos <= mid) update(ls[p], lp, mid, pos, val);
    else update(rs[p], mid + 1, rp, pos, val);
    pushup(p);
}

int query(int p, int lp, int rp, int l, int r) {
    if (!p) return 0;
    if (lp >= l && rp <= r) return sum[p];
    int mid = (lp + rp) >> 1, ans = 0;
    if (l <= mid) ans = query(ls[p], lp, mid, l, r);
    if (r > mid) ans += query(rs[p], mid + 1, rp, l, r);
    return ans;
}

int getPre(int pos) {
    pii tmp = Pre(tree_rt, make_pair(a[pos], pos));
    return ((tmp.first == a[pos]) ? tmp.second : 0);
}

int getSuf(int pos) {
    pii tmp = Suf(tree_rt, make_pair(a[pos], pos));
    return ((tmp.first == a[pos]) ? tmp.second : 0);
}

int main() {
    int n = read(), q = read(), ans = 0;
    for (int i = 1; i <= n; ++i) a[i] = read();
    for (int i = 1; i <= n; ++i) {
        int pr = getPre(i);
        for (int j = i; j <= n; j += lowbit(j)) update(root[j], 0, M, getPre(i), 1);
        insert(tree_rt, make_pair(a[i], i));
    }
    for (int i = 1; i <= q; ++i) {
        int opt = read();
        if (opt == 0) {
            int l = read(), r = read();
            l = (l + ans) % r + 1;
            int res = 0;
            for (int j = r; j; j -= lowbit(j)) res += query(root[j], 0, M, 0, l - 1);
            for (int j = l - 1; j; j -= lowbit(j)) res -= query(root[j], 0, M, 0, l - 1);
            printf("%d\n", res);
            ans = res;
        } else {
            int p = read(), k = read();
            int pre = getPre(p);
            for (int j = p; j <= n; j += lowbit(j)) update(root[j], 0, M, pre, -1);
            int suf = getSuf(p);
            if (suf) {
                for (int j = suf; j <= n; j += lowbit(j)) update(root[j], 0, M, pre, 1);
                for (int j = suf; j <= n; j += lowbit(j)) update(root[j], 0, M, p, -1);
            }
            del(tree_rt, make_pair(a[p], p));
            suf = ((Suf(tree_rt, make_pair(k, p)).first == k) ? Suf(tree_rt, make_pair(k, p)).second : 0);
            if (suf) {
                pre = getPre(suf);
                for (int j = suf; j <= n; j += lowbit(j)) update(root[j], 0, M, pre, -1);
            }
            a[p] = k;
            insert(tree_rt, make_pair(a[p], p));
            pre = getPre(p);
            for (int j = p; j <= n; j += lowbit(j)) update(root[j], 0, M, pre, 1);
            if (suf) {
                for (int j = suf; j <= n; j += lowbit(j)) update(root[j], 0, M, p, 1);
            }
        }
    }
    return 0;
}

總結

這次比賽中我們程式設計社幾乎全員參與,才能造就瞭如此專業程度之高的比賽。我們原創的賽題思維難度可以與省選媲美,我們在保證題目質量的情況下,也用心製造資料,分類資料點,讓經驗不足選手能夠從部分分中獲得更多的思路。在此期待\(SEOI2025\)——圖論主題賽能夠更完美的進行,也希望我們能舉辦更多更優質,體驗更好的比賽!

相關文章