第一次打网络赛,真是惨败收场呀,一定要好好补题了
下面将由我认为的难度顺序讲一下网络赛的几道题
目录
[F 50 Years Of Excellent](#F 50 Years Of Excellent)
[M Check In](#M Check In)
[C Permutation Inversions](#C Permutation Inversions)
[D Sequence](#D Sequence)
[A Recall](#A Recall)
[核心思路:过期时间标记 + 惰性弹出](#核心思路:过期时间标记 + 惰性弹出)
[L Longest Common Prefix](#L Longest Common Prefix)
F 50 Years Of Excellent

签到题
cpp
#include<bits/stdc++.h>
using namespace std;
const int N=55;
int main()
{
int n,m;
cin>>n>>m;
int last=0;
int res=0;
for(int i=1;i<=n;i++)
{
int sum=0;
for(int j=1;j<=m;j++)
{
int x;
cin>>x;
sum+=x;
}
if(sum<last)res++;
last=sum;
}
cout<<res<<endl;
return 0;
}
M Check In


map
这个题也比较简单,用map存一下字符串就好了
cpp
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
unordered_map<string,int>mp;
unordered_map<string,int>a;
int n,m;
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
string s;
cin>>s;
mp[s]++;
}
for(int i=1;i<=m;i++)
{
string s;
cin>>s;
if(mp[s]==0)
{
cout<<"WRONG"<<endl;
}
else if(a[s]==0)
{
a[s]++;
cout<<"OK"<<endl;
}
else cout<<"REPEAT"<<endl;
}
return 0;
}
C Permutation Inversions

拓扑排序+单调队列
这个题当时真是应该想到的
我们可以根据题目给出的大小关系构建一个拓扑图,小的指向大的,p1->p2->p3.....
最后遍历整个图,按照拓扑序赋值,但是由于题目要求最后的答案逆序对尽可能小,也就是要字典序最小的答案,我们就不能只是简单的BFS,而是要用单调队列
如果图中存在环,即最后遍历的点数<n,那么就输出-1
cpp
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int t,n,m;
int e[N],ne[N],h[N],idx;
int q[N];
int d[N];
void add(int a,int b)
{
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
int res[N];
int main()
{
cin>>t;
while(t--)
{
cin>>n>>m;
vector<pair<int,int>>edges;
for(int i=1;i<=m;i++)
{
int l,r;
cin>>l>>r;
int k=r-l+1;
for(int j=0;j<k;j++)
{
cin>>q[j];
}
for(int j=0;j<k-1;j++)
{
edges.push_back({q[j],q[j+1]});
}
}
sort(edges.begin(),edges.end());
edges.erase(unique(edges.begin(),edges.end()),edges.end());
for(int i=1;i<=n;i++)h[i]=-1;
for(int i=1;i<=n;i++)d[i]=0;
idx=0;
for(int i=0;i<edges.size();i++)
{
int a=edges[i].first,b=edges[i].second;
add(a,b);
d[b]++;
}
priority_queue<int,vector<int>,greater<int>>heap;
for(int i=1;i<=n;i++)
{
if(!d[i])heap.push(i);
}
int cnt=0;
while(heap.size())
{
int t=heap.top();
heap.pop();
res[t]=++cnt;
for(int i=h[t];i!=-1;i=ne[i])
{
int j=e[i];
if(--d[j]==0)
{
heap.push(j);
}
}
}
if(cnt!=n)cout<<-1<<endl;
else
{
for(int i=1;i<=n;i++)cout<<res[i]<<' ';
cout<<endl;
}
}
return 0;
}
D Sequence

贪心+构造
这个题当时做的时候真是吃了英语不好的亏了,不知道multiset是什么意思,不然是能做出来的
很显然,这个结果一定是2的x次方,因为每个位置只能有两个取值,0或1
那么,什么时候*2呢?
当第i位取0或者取1值都一样时,res就该*=2,即pi-1==(i-1)-pi-1时,和前一位有关系
当pi-1!=(i-1)-pi-1时,该取什么值呢?
这里有一个关键的结论:先取较小的一个
为什么呢?根据感觉,较小的值,肯定时更稀缺,因为p肯定时有一个越来越大的趋势,但是"稀缺"这个概念比较感性,于是我让AI给了我一个比较严谨的数学证明
贪心证明
一、先回到最本质的对应关系
我们先把 p 值和 01 串的对应关系扒透: 设前 (i-1) 位里,有 (c_0) 个 0,(c_1) 个 1,显然 (c_0 + c_1 = i-1)。
- 如果第 i 位选0:和它不同的字符是 1,共 (c_1) 个 → (p_i = c_1)
- 如果第 i 位选1:和它不同的字符是 0,共 (c_0) 个 → (p_i = c_0)
所以每一步的两个候选值,本质就是当前的 (c_0) 和 (c_1)。 选哪个候选值,等价于选当前位放哪个字符。
二、核心定理:切换字符会让小值永久消失
假设当前 (c_0 < c_1)(也就是两个候选值 (a=c_0),(b=c_1),(a<b))。
我们来看两种选择的后果:
- 选小的 a(放 1) :1 的数量加 1 → 新的 (c_1' = c_1+1),(c_0) 不变。 下一步的两个候选值还是 (c_0) 和 (c_1+1),a 这个值还在。
- 选大的 b(放 0) :0 的数量加 1 → 新的 (c_0' = c_0+1),(c_1) 不变。 下一步的两个候选值变成 (c_0+1) 和 (c_1),(a = c_0) 这个值永久消失了。 因为之后 (c_0) 只会越来越大(放 0 就加 1,放 1 就不变),再也不会回到 (c_0) 这个数值。
三、贪心策略的严格证明
命题:如果当前小候选值 a 还有库存,就必须优先选 a,否则必然无解。
反证法: 假设当前 a 还有库存,但我们选了大的 b(切换字符)。 根据上面的结论,a 这个值会永久消失,后续所有步骤的候选值都不会再出现 a。 而库存里还剩有 a,意味着这些 a 再也没有机会被用掉了,最终库存无法清零,和 "题目有解" 矛盾。
因此,只要小值还有库存,就绝对不能切换字符,必须一直选小值,直到把它用完 。 这就是 swap(a,b) 然后优先选小的的严格依据,不是凭感觉,是不选就一定会无解。
cpp
#include <iostream>
#include <cstdio>
#include <map>
using namespace std;
const int N = 100010;
const int MOD = 998244353;
int p[N];
int main()
{
int n;
scanf("%d", &n);
map<int, int> cnt;
for (int i = 0; i < n; i ++ )
{
int x;
scanf("%d", &x);
cnt[x] ++ ;
}
long long res = 1;
for (int i = 1; i <= n; i ++ )
{
int a = p[i - 1];
int b = (i - 1) - p[i - 1];
if (a > b) swap(a, b);
if (cnt[a] > 0)
{
p[i] = a;
cnt[a] -- ;
}
else
{
p[i] = b;
cnt[b] -- ;
}
if (a == b)
res = res * 2 % MOD;
}
printf("%lld\n", res);
return 0;
}
A Recall


离散化/map+模拟+贪心
题目大意
我们有一个初始为空的栈,支持三种操作:
+ x:将元素 x 压入栈中? x:查询元素 x 是否存在于栈中,查询结果分为T x(存在)和F x(不存在)-:弹出栈顶元素(执行时保证栈非空)
现在给定一段操作的记录,但记录中只保留了所有的 + x 和 T x / F x,丢失了全部的 - 操作。请你补全所有的 - 操作,重构出完整的操作序列,满足:
- 任意时刻栈中的元素都互不重复
- 所有查询的结果都和记录完全一致 输出任意一个合法的序列即可。
为什么直接模拟不可行?
很多同学会尝试按顺序直接模拟栈的操作:遇到 T 就保证元素在栈里,遇到 F 就保证元素不在栈里。但这种思路存在本质缺陷,最终一定会出错:
- 栈的操作限制:栈只能在栈顶进行压入和弹出,无法直接操作中间的元素。如果为了满足某个查询,需要弹出不在栈顶的元素,这本身就是非法操作,违反栈的基本规则。
- 保留时机无法判断:一个元素压入后,你无法提前知道它需要保留到什么时候。提前弹出会导致后面的 T 查询失败,一直保留又会导致后面的 F 查询失败,还可能引发重复元素的冲突。
- 边界情况复杂:重复压入同一元素、T 和 F 查询交替出现的场景,直接模拟需要大量回溯和状态调整,逻辑极易出错,几乎不可能覆盖所有边界。
核心思路:过期时间标记 + 惰性弹出
这道题的标准最优解是贪心策略,核心思想是「给每个压入操作标记存活期限,只在栈顶惰性清理过期元素」,完全贴合栈的操作特性。
关键观察
每个 + x 操作的核心意义,都是为了满足它之后某个 T x 查询的要求。如果一个压入的元素,后面已经没有需要它的 T 查询了,那它就失去了存在的价值,可以安全弹出。 反过来,只要还没到最后一个需要它的查询,它就必须留在栈里。
步骤一:预处理每个压入操作的过期时间
我们先从前往后遍历一遍所有操作:
- 遇到
+ x时,记录下 x 最新一次压入的位置下标 - 遇到
T x时,找到对应的最新一次+ x,把这个压入操作的「过期时间」设为当前查询的下标
含义:这个压入的元素,必须在栈里存活到本次查询执行完毕;过了这个位置,它就没有用了,可以弹出。
步骤二:正序遍历,惰性弹出栈顶
再次从前往后遍历所有操作:
- 遇到
+操作:将该操作的下标压入栈,答案字符串追加+ - 遇到
T或F操作:答案字符串追加? - 每一步操作执行完后,循环检查栈顶:如果栈顶元素的过期时间 ≤ 当前下标(说明它已经没用了),就弹出栈顶,答案字符串追加
-
为什么这个逻辑是严谨的?
- 完全符合栈的规则:永远只弹出栈顶元素,没有任何非法操作,完全满足栈的后进先出特性。
- 自动满足所有 T 查询:对应的压入操作在查询位置还没过期,所以一定还在栈里,查询结果必然是 T。
- 自动满足所有 F 查询:如果 x 应该不存在,那它对应的压入操作早就过期弹出了,自然不在栈里,查询结果必然是 F。
- 自动保证元素不重复:同一个 x 的新压入出现时,旧的压入一定已经过期被弹出,栈里不会同时存在两个相同的 x。
cpp
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
vector<int> alls;
struct Op
{
char c;
int x;
};
vector<Op> a;
int find(int x)
{
int l = 0, r = alls.size() - 1;
int ans = -1;
while(l <= r)
{
int mid = l + r >> 1;
if(alls[mid] >= x)
{
ans = mid;
r = mid - 1;
}
else l = mid + 1;
}
return ans + 1;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while(t--)
{
int n;
cin >> n;
alls.clear();
a.clear();
a.resize(n);
for(int i = 0; i < n; i++)
{
char c;
int x;
cin >> c >> x;
alls.push_back(x);
a[i] = {c, x};
}
sort(alls.begin(), alls.end());
alls.erase(unique(alls.begin(), alls.end()), alls.end());
int m = alls.size();
vector<int> last_push(m + 2, -1);
vector<int> expire(n, -1);
for(int i = 0; i < n; i++)
{
int fx = find(a[i].x);
if(a[i].c == '+')
{
last_push[fx] = i;
}
else if(a[i].c == 'T')
{
expire[last_push[fx]] = i;
}
}
stack<int> sk;
string res;
for(int i = 0; i < n; i++)
{
if(a[i].c == '+')
{
sk.push(i);
res += '+';
}
else
{
res += '?';
}
while(!sk.empty() && expire[sk.top()] <= i)
{
res += '-';
sk.pop();
}
}
cout << res << '\n';
}
return 0;
}
L Longest Common Prefix
本来想着这个题就不补了,但是好心学长主动把AC代码发给我,怎么说也要补了呀


字典树
字典树(Trie)的每个节点对应一个公共前缀:
- 节点的深度 = 该前缀的长度
- 节点的 cnt 值 = 有多少个字符串经过该节点(即有多少个串拥有这个前缀)
由此可以得到关键结论:对于任意 j,f ij 等于所有 cnt ≥ j 的节点中,最大的深度值。也就是说,能选出 j 个串共享的最长前缀,就是出现次数不少于 j 次的前缀里最长的那个。
基于这个结论,我们可以在逐个插入字符串的过程中,增量维护答案: 每插入一个字符串,它经过的所有节点的 cnt 都会加 1。对于每个节点,当 cnt 变为 k 时,说明现在存在 k 个串共享长度为 depth 的前缀。如果 depth 比当前 ans k(选 k 个串的最大 LCP)更大,就更新 ans k,同时同步更新总和 sum。
cpp
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+10;
int son[N][26];
int cnt[N],idx;
int ans[N];
long long sum;
void insert(string str,int id)
{
int p=0;
int fg=-1;
for(int i=0;i<str.size();i++)
{
int u=str[i]-'a';
if(!son[p][u])son[p][u]=++idx;
p=son[p][u];
cnt[p]++;
if(i+1>ans[cnt[p]])
{
sum+=(i+1)^cnt[p];
if(cnt[p]!=id||ans[cnt[p]])
sum-=ans[cnt[p]]^cnt[p];
else
fg=1;
ans[cnt[p]]=i+1;
}
}
if(fg==-1)sum+=id;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin>>n;
for(int i=1;i<=n;i++)
{
string s;
cin>>s;
insert(s,i);
cout<<sum<<'\n';
}
return 0;
}
我看了一下外榜,只要是能做对这六道题,就能给学校争取到区域赛的正式名额了