月赛题解

出题人给的理论最优开题顺序:G->HI->DFC->BE->A
AI给的顺序
image

A. WA 了个WA

答案是全都不在 s 内的和 t 相同的子串数加只有尾部在 s 内的子串数加只有头部在 s 内的子串数加穿过 s 的子串数和 s 内的和 t 相同的子串数。

全都不在 s 内和都在 s 内可以直接算。

只有尾部在 s 内(只有头部在 s 内类似):

可以维护 s 前的字符串匹配 t 的最长前缀然后跳 fail 得到所有可以匹配的前缀。

对于 s 的开头可以维护匹配 t 的最长后缀。

可以发现所有相同长度的最长前缀答案是相同的,所以可以直接先预处理。

只有头部在 s 内和只有尾部在 s 内基本相同。

\(len(s)>len(t)\) 时没有穿过 s 的答案。

穿过 s 的答案:

对于一个 s 前的前缀 a 与一个 s 后的后缀 b,只有它们的长度之和是 \(len(t)-len(s)\) 并且 \(t(len(a)+1,len(a)+len(s))=s\) 才可以形成一个答案。

建出正向的 fail 树和反向的 fail 树。

可以发现一个 s 前的前缀 a 与一个 s 后的后缀 b的答案就是正向 fail 树的节点 a 的所有祖先和反向 fail 树的节点 b 所有祖先的可行组合的个数。

可以离线下来,dfs 正向的 fail 树,跑到某个节点时维护它的所有正向树祖先的可行后缀,查询某个后缀时就是查询这个后缀的所有反向树祖先的和,于是直接子树加单点查就好了。

\(O(n+qlogn)\)

不知道用一个串的所有 border 可以分成小于 log 个等差数列再等差数列求交可不可以做。

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
char s1[1000010],s2[1000010],s3[1000010],s[1000010];
int t1[1000010],t2[1000010],sz[1000010],p1,p2,n,m,ipx;
basic_string<int>e1[1000010],e2[1000010];int q,ln;
struct sd{int p,v;};basic_string<sd>c[1000010];
int ip[1000010],t[1000010],res[1000010];bool can[1000010];
int hd[1000010],tl[1000010],cs1[1000010],cs2[1000010];
int ans1[1000010],ans2[10000010],to[1000010],ans;
void ad(int p,int x){for(;p<=ipx;p+=p&-p)t[p]+=x;}
void add(int l,int r,int x){ad(l,x),ad(r+1,-x);}
int qry(int p){int ret=0;for(;p;p-=p&-p)ret+=t[p];return ret;}
void dfs(int p){
	ip[p]=++ipx,sz[p]=1;
	for(int x:e2[p])dfs(x),sz[p]+=sz[x];
}
void cfs(int p){
	if(can[p]&&p&&p<ln)add(ip[ln-p],ip[ln-p]+sz[ln-p]-1,1);
	for(int x:e1[p])cfs(x);
	for(sd x:c[p])res[x.p]+=qry(ip[x.v]);
	if(can[p]&&p&&p<ln)add(ip[ln-p],ip[ln-p]+sz[ln-p]-1,-1);
}
int main(){
	scanf("%s%s%d",s2+1,s1+1,&q);
	n=strlen(s1+1),m=strlen(s2+1),ln=n-m;
	for(int i=1;i<=n;i++)s3[i]=s1[n-i+1];
	for(int i=2,j=0;i<=n;i++){
		while(j&&s1[j+1]!=s1[i])j=t1[j];
		if(s1[j+1]==s1[i])j++;t1[i]=j;
	}
	for(int i=1,j=0;i<=m;i++){
		while(j&&s1[j+1]!=s2[i])j=t1[j];
		if(s1[j+1]==s2[i])j++;if(j==n)j=t1[j],ans++;
		if(i==m)p2=j;
	}
	for(int i=2,j=0;i<=m;i++){
		while(j&&s2[j+1]!=s2[i])j=to[j];
		if(s2[j+1]==s2[i])j++;to[i]=j;
	}
	for(int i=1,j=0;i<=n;i++){
		while(j&&s2[j+1]!=s1[i])j=to[j];
		if(s2[j+1]==s1[i])j++;
		if(j==m)j=to[j],can[i-m]=true;
	}
	for(int i=2,j=0;i<=n;i++){
		while(j&&s3[j+1]!=s3[i])j=t2[j];
		if(s3[j+1]==s3[i])j++;t2[i]=j;
	}
	for(int i=m,j=0;i;i--){
		while(j&&s3[j+1]!=s2[i])j=t2[j];
		if(s3[j+1]==s2[i])j++;if(j==n)j=t2[j];
		if(i==1)p1=j;
	}
	while(p2)cs2[n-p2]++,p2=t1[p2];while(p1)cs1[n-p1]++,p1=t2[p1];
	for(int i=1;i<=n;i++)cs2[i]+=cs2[t2[i]],cs1[i]+=cs1[t1[i]];
	for(int i=1;i<=n;i++)e1[t1[i]]+=i,e2[t2[i]]+=i;
	for(int _=1,a,b;_<=q;_++){
		scanf("%d",&a);
		hd[_]=hd[_-1],tl[_]=tl[_-1];
		ans1[_]=ans1[_-1],ans2[_]=ans2[_-1];
		if(a==1||a==2){
			scanf("%s",s+1),b=strlen(s+1);
			if(a==1){
				for(int i=1,j=hd[_];i<=b;i++){
					while(j&&s1[j+1]!=s[i])j=t1[j];
					if(s1[j+1]==s[i])j++;
					if(j==n)j=t1[j],ans1[_]++;
					if(i==b)hd[_]=j;
				}
			}else{
				for(int i=b,j=tl[_];i;i--){
					while(j&&s3[j+1]!=s[i])j=t2[j];
					if(s3[j+1]==s[i])j++;
					if(j==n)j=t2[j],ans2[_]++;
					if(i==1)tl[_]=j;
				}
			}
		}else{
			scanf("%d",&b);
			if(a==3)hd[_]=hd[b],ans1[_]=ans1[b];
			else tl[_]=tl[b],ans2[_]=ans2[b]; 
		}
		res[_]=ans1[_]+ans2[_]+cs1[hd[_]]+cs2[tl[_]],c[hd[_]]+=(sd){_,tl[_]};
	}
	if(ln>0)dfs(0),cfs(0);
	for(int i=1;i<=q;i++)printf("%d\n",res[i]+ans);
    return 0;
}

B. 神代共鸣

题意简述

定义序列 \(E(n)\)

\[E(1) = 1,\quad E(2) = P,\quad E(n) = P \cdot E(n-1) + Q \cdot E(n-2)\ (n \ge 3) \]

\(T\) 组询问,每组给定 \(P, Q, n, m\),求 \(\gcd(E(n), E(m)) \bmod 998244353\)

关键性质

对于形如 \(E(n) = P \cdot E(n-1) + Q \cdot E(n-2)\) 的线性递推数列,有重要性质:

\[\gcd(E(n), E(m)) = E(\gcd(n, m)) \]

证明思路:这类递推数列与矩阵幂运算密切相关,可以通过矩阵形式的欧几里得算法证明。类似斐波那契数列的 \(\gcd(F_n, F_m) = F_{\gcd(n,m)}\) 的性质可以推广到更一般的二阶线性递推(当系数满足一定条件时)。

算法步骤

  1. 对于每组询问,计算 \(g = \gcd(n, m)\)
  2. 计算 \(E(g) \bmod 998244353\)
  3. 输出结果

矩阵快速幂求 \(E(k)\)

递推式可以写成矩阵形式:

\[\begin{bmatrix} E(n) \\ E(n-1) \end{bmatrix} = \begin{bmatrix} P & Q \\ 1 & 0 \end{bmatrix} \cdot \begin{bmatrix} E(n-1) \\ E(n-2) \end{bmatrix} \]

因此:

\[\begin{bmatrix} E(n) \\ E(n-1) \end{bmatrix} = \begin{bmatrix} P & Q \\ 1 & 0 \end{bmatrix}^{n-2} \cdot \begin{bmatrix} E(2) \\ E(1) \end{bmatrix} = \begin{bmatrix} P & Q \\ 1 & 0 \end{bmatrix}^{n-2} \cdot \begin{bmatrix} P \\ 1 \end{bmatrix} \]

特别地,\(E(k)\) 就是 \(M^{k-1}\) 的左上角元素(当 \(k \ge 1\) 时)。

时间复杂度

  • 每次询问需要 \(O(\log \min(n, m))\) 的矩阵快速幂
  • 总复杂度 \(O(T \log \max(n, m))\),可以轻松通过

代码实现

#include <bits/stdc++.h>
using namespace std;
using int64 = long long;
const int MOD = 998244353;

struct Matrix {
    int a[2][2];
    Matrix() { memset(a, 0, sizeof a); }
    Matrix operator*(const Matrix& o) const {
        Matrix r;
        for (int i = 0; i < 2; ++i)
            for (int k = 0; k < 2; ++k)
                for (int j = 0; j < 2; ++j)
                    r.a[i][j] = (r.a[i][j] + (int64)a[i][k] * o.a[k][j]) % MOD;
        return r;
    }
};

int calcE(int64 k, int P, int Q) {
    // 计算 E(k) mod MOD, 其中 E(1)=1, E(0)=0
    // 使用矩阵快速幂: M = [[P, Q], [1, 0]]
    // E(k) = (M^{k-1})_{0,0}
    if (k == 0) return 0;
    if (k == 1) return 1 % MOD;
    Matrix res, base;
    res.a[0][0] = res.a[1][1] = 1;  // 单位矩阵
    base.a[0][0] = P % MOD;
    base.a[0][1] = Q % MOD;
    base.a[1][0] = 1;
    base.a[1][1] = 0;
    int64 exp = k - 1;
    while (exp) {
        if (exp & 1) res = res * base;
        base = base * base;
        exp >>= 1;
    }
    return res.a[0][0];  // 第一行第一列
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int T;
    cin >> T;
    while (T--) {
        int P, Q;
        int64 n, m;
        cin >> P >> Q >> n >> m;
        int64 g = __gcd(n, m);
        int ans = calcE(g, P, Q);
        cout << ans << '\n';
    }
    return 0;
}

E. 时空炼金阵

题意简述

\(n\) 个能量节点,初始每个节点的能量值为 \(1\)。定义斐波那契数列:

\[F_1 = F_2 = 1,\quad F_i = F_{i-1} + F_{i-2}\ (i > 2) \]

对于 \(i \le 0\),扩展定义为 \(F_i = F_{i+2} - F_{i+1}\)(等价于 \(F_0 = 0,\ F_{-1} = 1,\ F_{-2} = -1\) 等)。

需要维护三种操作:

  1. 区间加\([l, r]\) 内每个节点的能量值增加 \(k\)
  2. 区间赋值\([l, r]\) 内每个节点的能量值设为 \(x\)
  3. 区间查询:询问 \(\sum_{i=l}^r F_{a_i} \bmod (10^9+7)\)

关键观察

斐波那契数列满足线性递推。对于区间加操作 \(a_i \leftarrow a_i + k\),我们需要快速从 \(F_{a_i}\) 转移到 \(F_{a_i + k}\)

斐波那契的矩阵表示

定义转移矩阵:

\[M = \begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix} \]

则有:

\[\begin{bmatrix} F_{n+1} & F_n \\ F_n & F_{n-1} \end{bmatrix} = M^n \]

更常用的形式:

\[\begin{bmatrix} F_{n+1} \\ F_n \end{bmatrix} = M \cdot \begin{bmatrix} F_n \\ F_{n-1} \end{bmatrix} \]

由此可得:

\[\begin{bmatrix} F_{n+k} \\ F_{n+k-1} \end{bmatrix} = M^k \cdot \begin{bmatrix} F_n \\ F_{n-1} \end{bmatrix} \]

处理负指数

对于负整数 \(k\),我们需要 \(M^k\) 的定义。由于 \(M\) 可逆,\(M^{-1}\) 存在:

\[M^{-1} = \begin{bmatrix} 0 & 1 \\ 1 & -1 \end{bmatrix} \]

实际上 \(M^{-k} = (M^{-1})^k\)

因此对于任意整数 \(k\)(正或负),我们可以用矩阵快速幂计算 \(M^k\)

数据结构设计

我们需要维护每个位置的矩阵值而非直接的数值。设每个位置维护一个矩阵 \(T_i\),使得:

\[\begin{bmatrix} F_{a_i+1} \\ F_{a_i} \end{bmatrix} = T_i \cdot \begin{bmatrix} 1 \\ 0 \end{bmatrix} \quad \text{或类似形式} \]

更简洁地,我们可以直接维护矩阵 \(\begin{bmatrix} F_{a_i+1} & F_{a_i} \\ F_{a_i} & F_{a_i-1} \end{bmatrix} = M^{a_i}\)

区间加

\(a_i\) 变为 \(a_i + k\) 时:

\[M^{a_i + k} = M^k \cdot M^{a_i} \]

因此区间加就是区间左乘矩阵 \(M^k\)

区间赋值

\(a_i\) 变为 \(x\) 时:

\[M^{a_i} \leftarrow M^x \]

因此区间赋值就是区间替换为常数矩阵 \(M^x\)

区间查询

我们需要 \(\sum F_{a_i}\)。注意 \(M^{a_i}\) 的右上角元素就是 \(F_{a_i}\),因此我们只需要维护每个区间元素 \(M^{a_i}\)矩阵和,查询时取和的右上角元素。

懒标记设计

由于有两种操作,我们需要两个懒标记:

  • 乘法标记 \(mul\):表示区间需要左乘的矩阵(对应区间加)
  • 赋值标记 \(set\):表示区间被赋值为某个常数矩阵(优先级高于乘法标记)

下传时:先处理赋值标记,再处理乘法标记。

矩阵快速幂预处理

因为 \(|k|, |x| \le 10^9\),不能每次都用快速幂。注意到只有 \(O(m)\) 次操作,可以预处理所有需要的 \(M^k\)\(M^{-k}\)。但代码中采用的方法是每次操作时用快速幂计算,由于 \(m \le 2 \times 10^5\)\(O(m \log |k|)\) 可行。

时间复杂度

  • 线段树区间操作 \(O(\log n)\)
  • 每次操作需要一次矩阵快速幂 \(O(\log |k|)\)
  • 总复杂度 \(O((n + m) \log n \log \max(|k|, |x|))\)

代码实现

#include<iostream>
#include<cstdio>
using namespace std;
#define long long long
const int mod=1e9+7;

struct sd{
    long a,b,c,d;
    sd(int a=0,int b=0,int c=0,int d=0):a(a),b(b),c(c),d(d){}
    friend sd operator *(const sd &x,const sd &y){
        return sd((x.a*y.a+x.b*y.c)%mod, (x.a*y.b+x.b*y.d)%mod,
                  (x.c*y.a+x.d*y.c)%mod, (x.c*y.b+x.d*y.d)%mod);
    } 
    bool operator !=(const sd &x){
        return a!=x.a || b!=x.b || c!=x.c || d!=x.d;
    }
}f1(0,1,1,1), f2(mod-1,1,1,0), t[1000010], lz[1000010], is[1000010];

int n,m;

// 计算 M^k,其中 M = [[1,1],[1,0]]
// k > 0 时用 f1 = M,k < 0 时用 f2 = M^{-1}
sd get(int x){
    sd ret(1,0,0,1), g = (x > 0 ? f1 : f2);
    if(x < 0) x = -x;
    for(; x; x >>= 1, g = g * g)
        if(x & 1) ret = ret * g;
    return ret;
}

// 建树:每个位置初始值为 M^1(因为 a_i = 1)
void bld(int p, int l, int r){
    lz[p] = sd(1,0,0,1);
    if(l == r){
        t[p] = sd(1,1,0,0);  // M^1 的矩阵形式
        return;
    }
    int mid = (l + r) >> 1;
    bld(p<<1, l, mid);
    bld(p<<1|1, mid+1, r);
    t[p] = sd((t[p<<1].a + t[p<<1|1].a) % mod,
              (t[p<<1].b + t[p<<1|1].b) % mod, 0, 0);
}

// 下传懒标记
void down(int p, int l, int r){
    int mid = (l + r) >> 1;
    // 先处理乘法标记
    t[p<<1] = t[p<<1] * lz[p];
    lz[p<<1] = lz[p<<1] * lz[p];
    is[p<<1] = is[p<<1] * lz[p];
    
    t[p<<1|1] = t[p<<1|1] * lz[p];
    lz[p<<1|1] = lz[p<<1|1] * lz[p];
    is[p<<1|1] = is[p<<1|1] * lz[p];
    lz[p] = sd(1,0,0,1);
    
    // 再处理赋值标记
    if(is[p] != sd()){
        int lenL = mid - l + 1, lenR = r - mid;
        t[p<<1] = sd(is[p].a * lenL % mod, is[p].b * lenL % mod, 0, 0);
        t[p<<1|1] = sd(is[p].a * lenR % mod, is[p].b * lenR % mod, 0, 0);
        is[p<<1] = is[p<<1|1] = is[p];
        lz[p<<1] = lz[p<<1|1] = sd(1,0,0,1);
        is[p] = sd();
    }
}

// 区间加(左乘矩阵 x)
void add(int p, int l, int r, int i, int j, sd x){
    if(i <= l && r <= j){
        t[p] = t[p] * x;
        lz[p] = lz[p] * x;
        is[p] = is[p] * x;
        return;
    }
    if(lz[p] != sd(1,0,0,1) || is[p] != sd()) down(p, l, r);
    int mid = (l + r) >> 1;
    if(i <= mid) add(p<<1, l, mid, i, j, x);
    if(j > mid) add(p<<1|1, mid+1, r, i, j, x);
    t[p] = sd((t[p<<1].a + t[p<<1|1].a) % mod,
              (t[p<<1].b + t[p<<1|1].b) % mod, 0, 0);
}

// 区间赋值(设为常数矩阵 x)
void mak(int p, int l, int r, int i, int j, sd x){
    if(i <= l && r <= j){
        t[p] = sd(x.a * (r - l + 1) % mod, x.b * (r - l + 1) % mod, 0, 0);
        lz[p] = sd(1,0,0,1);
        is[p] = x;
        return;
    }
    if(lz[p] != sd(1,0,0,1) || is[p] != sd()) down(p, l, r);
    int mid = (l + r) >> 1;
    if(i <= mid) mak(p<<1, l, mid, i, j, x);
    if(j > mid) mak(p<<1|1, mid+1, r, i, j, x);
    t[p] = sd((t[p<<1].a + t[p<<1|1].a) % mod,
              (t[p<<1].b + t[p<<1|1].b) % mod, 0, 0);
}

// 区间查询(返回 ΣF_{a_i})
long qry(int p, int l, int r, int i, int j){
    if(i <= l && r <= j) return t[p].a;  // t[p].a 是 ΣF_{a_i+1},待确认
    if(lz[p] != sd(1,0,0,1) || is[p] != sd()) down(p, l, r);
    int mid = (l + r) >> 1;
    long ret = 0;
    if(i <= mid) ret += qry(p<<1, l, mid, i, j);
    if(j > mid) ret += qry(p<<1|1, mid+1, r, i, j);
    return ret % mod;
}

int main(){
    cin >> n >> m;
    bld(1, 1, n);
    for(int a, b, c; m--;){
        cin >> a;
        if(a == 1){
            cin >> a >> b >> c;
            add(1, 1, n, a, b, get(c));  // 区间加 c,左乘 M^c
        }
        else if(a == 2){
            cin >> a >> b >> c;
            // 区间赋值为 c,即 M^c = M^{c-1} * M^1
            mak(1, 1, n, a, b, sd(1,1,0,0) * get(c-1));
        }
        else{
            cin >> a >> b;
            cout << qry(1, 1, n, a, b) << "\n";
        }
    }
    return 0;
}

G. 虚空裂隙·灵魂共振协议

题意简述

你有初始灵能值 \(x\),依次经过 \(n\) 个节点,每个节点有阈值 \(d_i\)。规则如下:

  • 若当前灵能值 \(E > d_i\),则 \(E \leftarrow E + 1\)
  • 若当前灵能值 \(E \le d_i\),则 \(E \leftarrow E - 1\)

求经过所有节点后的最终灵能值。

题目分析

这是一道非常直接的模拟题。题目描述虽然包装了复杂的背景,但核心逻辑就是上述两条简单的规则。

由于 \(n \le 10^5\),直接模拟的时间复杂度为 \(O(n)\),完全可行。

算法步骤

  1. 读入 \(n\) 和初始值 \(x\)
  2. 初始化当前灵能值 cur = x
  3. 循环 \(n\) 次,读入每个 \(d_i\)
    • 如果 cur > d[i],则 cur++
    • 否则 cur--
  4. 输出最终的 cur

时间复杂度

  • \(O(n)\),可以轻松通过

空间复杂度

  • \(O(1)\) 额外空间(不需要存储所有 \(d_i\),可以边读边处理)

代码实现

#include <bits/stdc++.h>
using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    
    int n;
    long long x;
    cin >> n >> x;
    
    long long cur = x;
    for (int i = 0; i < n; i++) {
        long long d;
        cin >> d;
        if (cur > d) {
            cur++;
        } else {
            cur--;
        }
    }
    
    cout << cur << endl;
    
    return 0;
}

H. htd1 的 mex 问题

题意简述

给定一个包含 \(n\) 个元素的多重集合(元素为非负整数),可以将其拆分成任意个非空子集,使得所有子集的 mex 值之和最大。求这个最大值。

题目分析

mex 的定义

对于一个集合 \(S\)\(\text{mex}(S)\) 表示没有在 \(S\) 中出现过的最小非负整数。

例如:\(\text{mex}(\{1,2\}) = 0\)\(\text{mex}(\{0,1,3\}) = 2\)

关键思路

设原多重集合中数字 \(i\) 出现的次数为 \(\text{cnt}[i]\)

考虑如何分配这些数字到各个子集中,以最大化 mex 之和。

核心观察

  • 一个子集的 mex 值为 \(k\),意味着该子集必须包含 \(0,1,2,\dots,k-1\) 各至少一次,且不能包含 \(k\)
  • 为了最大化 mex 之和,每个子集的 mex 值应当尽量大,但同时受到数字出现的次数限制。

贪心构造

我们可以将问题转化为:

设有 \(m\) 个子集,每个子集的 mex 值至少为多少?

假设我们想要得到 \(t\) 个 mex 值 \(\ge 1\) 的子集,那么我们需要至少有 \(t\)\(0\)(因为每个 mex \(\ge 1\) 的子集必须包含 \(0\))。

同理,想要得到 \(t\) 个 mex 值 \(\ge 2\) 的子集,需要至少有 \(t\)\(0\)\(t\)\(1\)

更一般地,想要得到 \(t\) 个 mex 值 \(\ge k\) 的子集,需要对于每个 \(i \in [0, k-1]\),都有 \(\text{cnt}[i] \ge t\)

转化为数值计算

\(f(k)\) 表示最多能有多少个子集的 mex 值 \(\ge k\)。则:

\[f(k) = \min_{0 \le i < k} \text{cnt}[i] \]

因为要同时满足前 \(k\) 个数字的个数都不少于该数量。

那么,mex 值恰好等于 \(k\) 的子集数量为:

\[f(k) - f(k+1) \]

因为 mex \(\ge k\) 的子集数量减去 mex \(\ge k+1\) 的子集数量,就是 mex 恰好为 \(k\) 的子集数量。

于是 mex 值之和为:

\[\sum_{k \ge 0} k \cdot (f(k) - f(k+1)) = \sum_{k \ge 1} f(k) \]

因为:

\[\sum_{k=0}^{\infty} k \cdot (f(k)-f(k+1)) = f(1)+f(2)+f(3)+\cdots \]

最终公式

\(g(i) = \min_{0 \le j \le i} \text{cnt}[j]\),则:

\[\text{答案} = \sum_{i=0}^{\infty} g(i) \]

其中 \(g(-1)\) 定义为无穷大,实际只需要计算到某个上界。

由于 \(a_i \le 1000\),我们只需要考虑 \(i\)\(0\)\(1001\) 即可(因为超过 \(\max(a_i)+1\)\(\text{cnt}[i]=0\)\(g(i)\) 就会变成 \(0\))。

算法步骤

  1. 统计每个数字的出现次数 \(\text{cnt}[i]\)\(0 \le i \le 1001\)
  2. 初始化 min_cnt = cnt[0]
  3. 初始化答案 ans = 0
  4. \(i = 1\) 开始循环到 \(1001\)
    • 先将当前的 min_cnt 累加到答案(对应公式中的 \(g(i-1)\)
    • 更新 min_cnt = min(min_cnt, cnt[i])
  5. 输出答案

时间复杂度

  • \(O(\max(a_i) + n)\),本题中 \(\max(a_i) \le 1000\),非常快

代码实现

// htd1_mex_simple.cpp
#include <bits/stdc++.h>
using namespace std;

int main() {
    int n;
    cin >> n;
    
    vector<int> cnt(n + 5, 0);
    for (int i = 0; i < n; i++) {
        int x;
        cin >> x;
        if (x <= n) {  // 大于 n 的数字对 mex 没有贡献(因为最多只有 n 个数,mex 不会超过 n)
            cnt[x]++;
        }
    }
    
    long long ans = 0;
    int min_cnt = cnt[0];  // min(c0, c1, ..., c{i-1})
    
    for (int i = 1; i <= n + 1; i++) {
        ans += min_cnt;    // 累加 g(i-1) = min_{0<=j<i} cnt[j]
        min_cnt = min(min_cnt, cnt[i]);
    }
    
    cout << ans << endl;
    
    return 0;
}

I. Distinct Prefix Sums Modulo n 题解

证明

设排列 \(p_1, p_2, \ldots, p_n\),前缀和 \(S_k = \sum_{i=1}^k p_i\)\(s_k = S_k \bmod n\)

引理\(S_n = \frac{n(n+1)}{2}\),因此:

  • \(n\) 为奇数时,\(S_n \equiv 0 \pmod{n}\)
  • \(n\) 为偶数时,\(S_n \equiv \frac{n}{2} \pmod{n}\)

情况一:\(n\) 为奇数且 \(n > 1\)

  1. 计算总和

    \[S_n = 1 + 2 + \cdots + n = \frac{n(n+1)}{2}. \]

    因为 \(n\) 是奇数,\(\frac{n+1}{2}\) 为整数,所以 \(S_n \equiv 0 \pmod n\)
    于是 \(S_n \bmod n = 0\),且由要求知 \(S_1,\dots,S_{n-1} \bmod n\) 恰好是 \(\{1,2,\dots,n-1\}\)(不含 \(0\))。

  2. 确定 \(p_n\)
    因前 \(n-1\) 个余数已用尽 \(1,\dots,n-1\),必有 \(S_{n-1} \equiv n-1 \pmod n\)

    \[p_n = S_n - S_{n-1} \equiv 0 - (n-1) \equiv 1 \pmod n, \]

    \(1 \le p_n \le n\),所以 \(p_n = 1\)

  3. 反向递推
    已知 \(p_n = 1\),且 \(S_{n-1} \equiv n-1 \pmod n\)
    \(k = n-1, n-2, \dots, 2\) 依次使用

    \[p_k = S_k - S_{k-1} \equiv S_k - S_{k-1} \pmod n, \]

    结合余数集合唯一性,可唯一推出

    \[p_{n-1} = 2,\; p_{n-2} = 3,\; \dots,\; p_1 = n. \]

  4. 矛盾
    此时 \(S_1 = p_1 = n \equiv 0 \pmod n\),与 \(S_1 \bmod n \neq 0\)(因为 \(0\) 仅由 \(S_n\) 取到)矛盾。

因此假设不成立,对任意奇数 \(n>1\),这样的排列不存在。 \(\square\)
情况二:\(n = 1\)

排列 \([1]\),前缀和 \(S_1 = 1 \equiv 0 \pmod{1}\),满足条件。

情况三:\(n\) 为偶数

构造排列如下:

\[p_k = \begin{cases} k, & k \text{ 为奇数} \\ n - k + 2, & k \text{ 为偶数} \end{cases}\]

即:\(1, n, 2, n-1, 3, n-2, \ldots, \frac{n}{2}, \frac{n}{2}+1\)

可以验证前缀和模 \(n\) 两两不同,恰好构成 \(0, 1, \ldots, n-1\)


结论:当且仅当 \(n\) 为偶数或 \(n = 1\) 时存在解。

#include <bits/stdc++.h>
using namespace std;
int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int n;
    cin >> n;
    if (n % 2 == 1 && n != 1) {
        cout << "NO\n";
    }
    else{
    	cout << "YES\n";
	}
    return 0;
}
posted @ 2026-05-02 15:44  spring_sun  阅读(88)  评论(0)    收藏  举报