【SEOI2024】官方题解

【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=\frac{2\cdot \sum\limits_{i=2}^{n-1} f[i-1]\cdot g[i+1]}{(n-1)(n-2)} \]

时间复杂度\(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\)并不表示树状数组维护的若干个值域,而是\(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\)——图论主题赛能够更完美的进行,也希望我们能举办更多更优质,体验更好的比赛!

posted @ 2024-06-03 02:00  Jefferyzzzz  阅读(55)  评论(0编辑  收藏  举报