国庆集训模拟赛记录
2025.9.26
A 序列
OI 赛制收益者,挂了 70 分。
先考虑构造一个相邻逆序对最大的序列。
最佳的序列一定是从最大数扫到最小数,每个出现次数不为 \(0\) 的数依次放入数组末尾,并将出现次数减一,扫完最小数后重新扫最大数,一直重复即可。
构造出来后,若 \([1,j]\) 的相邻逆序对个数为 \(m\),将 \([j+1,m]\) 的部分从小到大排序,消去贡献。
具体实现用 vector,可以参考代码,复杂度为 \(O(Tn\log n)\)。
点击查看代码
#include <iostream>
#include <cstdio>
#include <vector>
#include <map>
#include <algorithm>
using namespace std;
const int N=1e5+10;
int n,m,ans[N];
vector<int>t[N];
map<int,int>H;
bool cmp(int a,int b){
return a>b;
}
void work(){
scanf("%d %d",&n,&m);
int mx=0;
for(int i=1,x;i<=n;i++){
scanf("%d",&x);
H[x]++,mx=max(mx,H[x]);
t[H[x]].push_back(x);
}
int idx=0;
for(int i=1;i<=mx;i++){
sort(t[i].begin(),t[i].end(),cmp);
for(auto v:t[i]){
ans[++idx]=v;
}
}
int now=0;
bool flag=0;
for(int i=1;i<=n;i++){
if(now==m){
sort(ans+i,ans+n+1);
flag=1;break;
}
if(ans[i]>ans[i+1])now++;
}
if(!flag){
printf("-1\n");
}else{
for(int i=1;i<=n;i++)printf("%d ",ans[i]);
printf("\n");
}
H.clear();
for(int i=1;i<=mx;i++)t[i].clear();
for(int i=1;i<=n;i++)ans[i]=0;
return;
}
int main(){
freopen("seq.in","r",stdin);
freopen("seq.out","w",stdout);
int T;
scanf("%d",&T);
while(T--)work();
return 0;
}
B 平衡数列
容易发现若所有的 \(A_i>1\),则一个平衡数列不超过 \(20\)。因为 \(2^{20}>2\times 20\),所以一定不行。
这启发我们平衡数列个数很少,记录每个位置 \(i\) 前上一个 \(A_i>1\) 的位置。枚举区间右端点 \(r\),然后往左跳 \(nxt_l\),跳的次数一定不超过 \(20\),时间复杂度为 \(O(n\log n)\)。
点击查看代码
#include <iostream>
#include <cstdio>
using namespace std;
typedef long long ll;
const int N=2e5+10;
const ll V=5e11;
int n,a[N],pre[N],ans;
ll s[N];
int main(){
freopen("bal.in","r",stdin);
freopen("bal.out","w",stdout);
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d",a+i);
s[i]=s[i-1]+a[i];
}
int x=0;
for(int i=1;i<=n;i++){
pre[i]=x;
if(a[i]>1)x=i;
}
for(int i=1,l;i<=n;i++){
ll prod=1,sum=0;
int lst=i;
for(int j=i;j&&prod<=V;j=pre[j]){
prod*=a[j],sum+=s[lst]-s[j-1],l=pre[j];
if(prod-sum>=0&&prod-sum<=j-1-l)ans++;
lst=j-1;
}
}
printf("%d\n",ans);
return 0;
}
C 间谍部署
设 \(f_{i,j}\) 表示 \(i\) 子树内,距离 \(i\) 最近的选择的点距离 \(i\) 为 \(j\),做一个树上背包状物即可,因为可以子树选空,所以要注意继承的情况。
时间复杂度为 \(O(n)\)。
点击查看代码
#include <iostream>
#include <cstdio>
#include <vector>
using namespace std;
typedef long long ll;
const int N=2e5+10;
const int mod=998244353;
int n,mk[N];
vector<int>G[N];
ll f[N][4],ans[4],sum;
void add(int x,int y){
G[x].push_back(y);
}
void dfs(int x,int fa){
for(auto y:G[x]){
if(y==fa)continue;
dfs(y,x);
for(int i=1;i<=3;i++)ans[i]=f[x][i];
for(int i=0,s;i<=3;i++){
s=min(3,i+1);
ans[s]=(ans[s]+f[y][i])%mod;
}
for(int i=1,s;i<=3;i++){
for(int j=0;j<=3;j++){
if(i+j<2)continue;
s=min(3,min(i,j+1));
ans[s]+=f[x][i]*f[y][j]%mod;
ans[s]%=mod;
}
}
for(int i=1;i<=3;i++)f[x][i]=ans[i];
}
if(mk[x])f[x][0]=f[x][3]+1;
return;
}
int main(){
freopen("spy.in","r",stdin);
freopen("spy.out","w",stdout);
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",mk+i);
for(int i=2,f;i<=n;i++){
scanf("%d",&f);
add(f,i),add(i,f);
}
dfs(1,0);
for(int i=0;i<=3;i++)sum=(sum+f[1][i])%mod;
printf("%d\n",sum);
return 0;
}
D 小A与字符串
不难发现,字符串的循环节一定是最小循环节的倍数,所以可以求出区间最小循环节。
若 \(p\) 为 \([l,r]\) 的循环节,需要满足 \([l+p,r]=[l,r-p]\),具体和 KMP 相关。可以预处理字符串 Hash 值,方便 \(O(1)\) 比较。
从小到大试倍数,直到试出第一个循环节为答案,复杂度 \(O(n\sqrt{n})\),慢。
反过来!若 \(p\) 是循环节,则一定是最小循环节的倍数,考虑逐个剔除其中质因子。
最后预处理每个数因数个数,质因子的东西,时间复杂度为 \(O(n\log n)\)。
点击查看代码
#include <iostream>
#include <cstdio>
#include <vector>
using namespace std;
typedef long long ull;
const int N=5e5+10;
int n,m,ys[N],mk[N];
char s[N];
vector<int>pr[N];
struct Hash{
ull p,pw[N],hs[N];
void prework(ull Ciallo,char *s,int n){
p=Ciallo,pw[0]=1,hs[0]=0;
for(int i=1;i<=n;i++){
pw[i]=pw[i-1]*p;
hs[i]=hs[i-1]*p+s[i]-'a';
}
return;
}
ull qry(int l,int r){
return hs[r]-hs[l-1]*pw[r-l+1];
}
}T1,T2;
bool check(int l1,int r1,int l2,int r2){
if(l1>r1)return 1;
if(T1.qry(l1,r1)!=T1.qry(l2,r2))return 0;
if(T2.qry(l1,r1)!=T2.qry(l2,r2))return 0;
return 1;
}
void init(){
T1.prework(131,s,n);
T2.prework(13331,s,n);
for(int i=1;i<=n;i++)for(int j=i;j<=n;j+=i)ys[j]++;
for(int i=2;i<=n;i++){
if(mk[i])continue;
for(int j=i;j<=n;j+=i){
pr[j].push_back(i);
mk[j]=1;
}
}
return;
}
int main(){
freopen("str.in","r",stdin);
freopen("str.out","w",stdout);
scanf("%d %d",&n,&m);
scanf("%s",s+1);
init();
for(int i=1,l,r;i<=m;i++){
scanf("%d %d",&l,&r);
int x=1,y=r-l+1;
for(auto v:pr[r-l+1]){
while(1){
if(y%v!=0)break;
if(!check(l,r-y/v,l+y/v,r))break;
x*=v,y/=v;
}
}
printf("%d\n",ys[x]);
}
return 0;
}
2025.10.1
有 n 种物品
注意到 \(a_i=b_i\) 对分差没有影响,直接扔掉。两边都想最大化自己得分,得分总和一定,等价与 \(A\) 想最大化分差,\(B\) 想最小化分差。
注意到 \(A\) 先手,则分类讨论:
若 \(a_i>b_i\):此时 \(A\) 已经选了 \(i\),就算不选最后也是小 \(B\) 选,因此小 \(B\) 会寻找此时的次优。
若 \(a_i<b_i\):若此时 \(A\) 已经选了 \(i\),则 \(B\) 跟选 \(i\) 一定更优。
前者按照分差从大到小奇偶选,后者对分差贡献一定是 \(a_i-b_i\)。时间复杂度为 \(O(n\log n)\)。
点击查看代码
#include <iostream>
#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
typedef long long ll;
int n;
ll ans;
vector<ll>a;
bool cmp(ll a,ll b){
return a>b;
}
int main(){
freopen("nit.in","r",stdin);
freopen("nit.out","w",stdout);
scanf("%d",&n);
for(int i=1;i<=n;i++){
ll x,y;
scanf("%lld %lld",&x,&y);
if(x==y)continue;
if(x<y)ans+=x-y;
else a.push_back(x-y);
}
sort(a.begin(),a.end(),cmp);
for(int i=0;i<a.size();i++){
if(i&1)ans-=a[i];
else ans+=a[i];
}
printf("%lld\n",ans);
return 0;
}
火柴排队
用排名替换具体数值,得到两个排列,然后让通过交换操作让两个排列相同,一定是最优,感性理解即可。
这个问题是可以转化为排序的最小交换此时,其实就是逆序对,树状数组维护即可,复杂度 \(O(n\log n)\)。
点击查看代码
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N=1e5+10,mod=1e8-3;
int n,a[N],b[N],c[N],book1[N],book2[N],t[N],tot1,tot2,ans,tree[N];
int lowbit(int x){return x&(-x);}
void add(int x,int t){
for(;x<=n;x+=lowbit(x))tree[x]+=t;
return;
}
int sum(int x){
int cnt=0;
for(;x>0;x-=lowbit(x))cnt=(cnt+tree[x])%mod;
return cnt;
}
int main(){
freopen("MatchNOIP2013.in","r",stdin);
freopen("MatchNOIP2013.out","w",stdout);
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",a+i),book1[++tot1]=a[i];
for(int i=1;i<=n;i++)scanf("%d",b+i),book2[++tot2]=b[i];
sort(book1+1,book1+1+tot1);
tot1=unique(book1+1,book1+1+tot1)-(book1+1);
sort(book2+1,book2+1+tot2);
tot2=unique(book2+1,book2+1+tot2)-(book2+1);
for(int i=1;i<=n;i++){
a[i]=lower_bound(book1+1,book1+1+tot1,a[i])-book1;
b[i]=lower_bound(book2+1,book2+1+tot2,b[i])-book2;
t[a[i]]=i;
}
for(int i=1;i<=n;i++)c[i]=t[b[i]];
for(int i=n;i>0;i--){
ans=(ans+sum(c[i]-1)%mod)%mod;
add(c[i],1);
}
cout<<ans<<endl;
return 0;
}
国旗计划
断环成链后,问题转化为从每个区间出发走 \(L\) 的最小人数。
贪心不难发现,若选择某个区间 \(i\),则下一个选择的区间 \(j\) 唯一确定。
转移过程一定,但次数较多,考虑倍增,然后就没了,找后继区间可以利用题目给定的单调性双指针,时间复杂度为 \(O(n\log n)\)。
点击查看代码
#include <iostream>
#include <cstdio>
#include <algorithm>
#define int long long
using namespace std;
const int N=1e6+10;
int n,m,f[26][N],cnt,pos[N];
struct seg{
int l,r,id;
void init(int a,int b,int c){
l=a,r=b,id=c;
}
}a[N];
bool cmp(seg a,seg b){
return a.l<b.l;
}
signed main(){
freopen("flagplan.in","r",stdin);
freopen("flagplan.out","w",stdout);
scanf("%lld %lld",&n,&m);
for(int i=1,c,d;i<=n;i++){
scanf("%lld %lld",&c,&d);
if(c>d)d+=m;
a[++cnt].init(c,d,i);
a[++cnt].init(c+m,d+m,i);
}
sort(a+1,a+1+cnt,cmp);
for(int i=1,j=1;i<=cnt;i++){
while(j<cnt&&a[j+1].l<=a[i].r)j++;
if(a[i].l<=m)pos[a[i].id]=i;
f[0][i]=j;
}
for(int i=1;i<=25;i++){
for(int j=1;j<=cnt;j++){
f[i][j]=f[i-1][f[i-1][j]];
}
}
for(int i=1;i<=n;i++){
int ans=1,x=pos[i],R=a[x].l+m;
for(int i=25;i>=0;i--){
int nxt=f[i][x];
if(a[nxt].r<R){
x=nxt;
ans+=(1<<i);
}
}
ans++;
printf("%lld ",ans);
}
return 0;
}
信使
sol。
2025.10.2
ICPC 欢乐赛,神人队友。
A
暴力枚举或打表,轻松做到 \(O(T)\) 或 \(O(Tk)\)。
B
注意到 \(2n\) 是一个周期,处理一下后累加即可。
C
不会
D
每个人按照当前最优策略就是全局最优策略
E
考虑走到 \(x\):
若能走到 \(x+1\):走
若不能走到 \(x+1\) 且有其他节点:必须连边
其他:回溯
不难体会其正确性。
F
费用流,小于等于 \(a_i\) 的贡献为 \(0\),大于 \(a_i\) 的贡献为 \(1\)。最大流强制选择,最小费用优化答案。
G
二分 \(k\),看序列每一段不超过 \(k\) 最大划分几段,离散化树状数组优化 dp。
H
模拟即可,推荐 string.substr
I
不会
J
神秘题目队友做了 3h。
碰撞后是否交换方向不重要,先用周期然后模拟。
K
同余分类后从小到大输出。
L
跑 \(100\) BFS 即可,用前缀和优化。
M
注意到时间只会延迟,所以一个人开始行动后一定会到终点。
按照 \(s_i\) 排序后一个一个处理。
2025.10.4
神仙题,狗屎部分分。
A 毛一琛
不难发现比较难搞。
转化题意,当前你有一个数 \(x\),可以选择一些位置 \(i\) 进行 \(x\gets x,x\gets x+a_i,x\gets x-a_i\),最后使得 \(x=0\) 的位置选择方案数。
直接搜索是 \(O(3^n)\) 的,注意操作可能不一样,但选择操作的位置的集合是一样的,要注意去重。
考虑折半搜索,分成前一半和后一遍,前一半搜出所有可能集合,后一半去和前一半对应,注意去重。时间复杂度为 \(O(6^{\frac{n}{2}})\)
点击查看代码
#include <iostream>
#include <cstdio>
#include <vector>
#include <map>
using namespace std;
const int N=25,M=(1<<20);
int n,k,ans,a[N],vis[M];
map<int,vector<int>>H;
void dfs1(int x,int now,int sum){
if(x==k+1){
H[sum].push_back(now);
}else{
dfs1(x+1,now,sum);
dfs1(x+1,now|(1<<(x-1)),sum+a[x]);
dfs1(x+1,now|(1<<(x-1)),sum-a[x]);
}
return;
}
void dfs2(int x,int now,int sum){
if(x==n+1){
for(auto v:H[-sum]){
vis[(now<<(n/2))|v]=1;
}
}else{
dfs2(x+1,now,sum);
dfs2(x+1,now|(1<<(x-k-1)),sum+a[x]);
dfs2(x+1,now|(1<<(x-k-1)),sum-a[x]);
}
return;
}
int main(){
freopen("subsets.in","r",stdin);
freopen("subsets.out","w",stdout);
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d",a+i);
}
k=n/2;
dfs1(1,0,0);
dfs2(k+1,0,0);
for(int i=1;i<(1<<n);i++){
if(vis[i])ans++;
}
printf("%d\n",ans);
return 0;
}
B 魔法卡片
神题!!
不难发现结论一:若 \(r-l+1\ge 20\),则答案一定为 \(\sum_{i=1}^m i^2\)。
如何证明:题目的一个特殊条件是每张卡牌正反面正好凑齐 \(1\sim m\),假设我们第一张牌选择数字多的一面,则此时我们没获得的数字个数 \(x\le \frac{n}{2}\)。考虑第二张牌,显然上一步遗留的 \(x\) 张牌中要么出现在第二张牌的正面,要么在反面。我们优先考虑选择那个面会让 \(x\) 更小,显然,选择这个面后缺少的牌数 \(x'\le \dfrac{x}{2}\)。就这样,每选一次,\(x\) 缩小一倍,不超过 \(\log n\) 次就一定为 \(0\),此时可以取到 \(1\sim m\) 的所有数字。
处理掉 \(r-l+1\ge 20\) 的部分,顺利成章的考虑对于每个 \(l\),预处理出 \([l,r](r-l+1<20)\) 的答案,这部分直接爆搜的时间复杂度为 \(O(nm^2\log m)\),比较垃圾,考虑优化。我们长期以来一直被局限到正向思维,即做出决策后会拥有那些数字,考虑倒着维护我们做出决策后那些数字还未拥有,若当前未拥有的数字集合为 \(X\),要对第 \(i\) 张牌进行决策,如果第 \(i\) 张牌选正面,则未拥有的数字集合为 \(S\),若选负面,集合为 \(T\),有 \(S\cup T=X,S\cap T=\varnothing\)。这个性质非常重要。
考虑最开始决策第 \(1\) 张牌之前,集合 \(X=\{1,2,\dots,m\}\)。决策后会分成 \(S,T\),第二张牌决策后\(S\) 决策后分成 \(SS,ST\) 两个集合,\(T\) 分成 \(TS,TT\) 两个集合。不难发现 \(SS,ST,TS,TT\) 的并为 \(X\),两两交集为空。换句话说,进过 \(k\) 次决策后,不同状态所代表的集合其实是最开始的 \(X\) 分裂而成的若干小集合,其之间两两不重合。而我们要计算这个集合的权值,只需要扫一遍,也就是说,经过 \(k\) 次决策后,把计算所有状态的权值的复杂度为 \(O(m)\)。
说到这里,这个题已经做完了,枚举左端点 \(O(n)\),枚举区间长度 \(O(\log m)\),搜索计算决策 \(O(m)\),总的复杂度为 \(O(nm\log m)\)。代码实现比较重要:如果用 vector 暴力维护集合,频繁的 push_back 和 clear 操作会炸。所以用静态数组,做一个类似归并排序自上而下划分的过程。加了一些剪枝,拿到了 COGS 最优解。
点击查看代码
#include <iostream>
#include <cstdio>
#include <vector>
using namespace std;
const int N=1e6+10;
int n,m,q,p[N],g[N],lim;
long long sum,val[N][21];
vector<int>H[N];
const int MAXSIZE=(1<<20);
char buf[1<<20],*p1,*p2;
#define gc() (p1==p2&&(p2=(p1=buf)+fread(buf,1,MAXSIZE,stdin),p1==p2)?EOF:*p1++)
inline void read(int &a){
int x=0,f=1;char ch=gc();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=gc();}
while(isdigit(ch))x=(x<<3)+(x<<1)+ch-48,ch=gc();
return a=x*f,void();
}
void dfs(int st,int dep,int x,int l,int r){
if(dep>=lim)return;
if(l>r){
long long res=sum;lim=min(lim,dep);
for(int k=dep;k<=20;k++)val[st][k]=sum;
return;
}
long long res=sum;
for(int k=l;k<=r;k++)res-=1ll*p[k]*p[k];
val[st][dep]=max(val[st][dep],res);
if(x==n+1)return;
int i=l,j=r;
for(int k=l;k<=r;k++){
if(H[x][p[k]])g[i++]=p[k];
else g[j--]=p[k];
}
for(int k=l;k<=i-1;k++)p[k]=g[k];
for(int k=j-1;k<=r;k++)p[k]=g[k];
dfs(st,dep+1,x+1,l,i-1);
dfs(st,dep+1,x+1,j+1,r);
return;
}
int main(){
freopen("magic.in","r",stdin);
freopen("magic.out","w",stdout);
read(n),read(m),read(q);
for(int i=1;i<=n;i++){
int x;read(x);
H[i].resize(m+2);
for(int j=1,p;j<=x;j++){
read(p);
H[i][p]=1;
}
}
for(int i=1;i<=m;i++)sum+=1ll*i*i;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++)p[j]=j;
lim=20;dfs(i,0,i,1,m);
}
int l,r;
while(q--){
read(l),read(r);
if(r-l+1>=20){
printf("%lld\n",sum);
}else{
printf("%lld\n",val[l][r-l+1]);
}
}
return 0;
}
C 排列
神仙题!!!
考虑枚举一个排练 \(b\),判断 \(a\) 能否到达 \(b\)。
容易发现序列应该变的更加有序,关键在于如何体现有序。考虑并非排列,而是 \(01\) 序列的情况,这种情况需要 \(b\) 中的每一个 \(1\) 都在 \(a\) 的 \(1\) 之前。
那对于排列的情况,只需要对于任意 \(k\in [1,n]\),将序列按照大于小于 \(k\) 将每个位置分成 \(01\),都满足上述限制即可。
设 \(dp_{i,S}\) 表示满足大于等于 \(i\) 的位置为 \(1\),排列对应的 \(01\) 串为 \(S\) 的方案数,容易发现若 \(S\) 是原串可达,则去掉 \(S\) 中任意一个元素依旧合法,直接枚举一个 \(1\) 即可,注意到 \(i\) 为 \(S\) 中 \(1\) 的个数,所以不必要存在。
做状压 dp 即可,时间复杂度 \(O(n2^n+n^2)\)。
点击查看代码
#include <iostream>
#include <cstdio>
using namespace std;
const int M=(1<<20);
const int mod=1e9+7;
int n,cnt[M],dp[M],a[21],S,s1,s2;
bool check(int p){
s1=0,s2=0;
for(int k=0;k<n;k++){
if((p>>k)&1)s1++;
if((S>>k)&1)s2++;
if(s2>s1)return 1;
}
return 0;
}
int main(){
freopen("changgao_perm.in","r",stdin);
freopen("changgao_perm.out","w",stdout);
scanf("%d",&n);
for(int i=0;i<n;i++)scanf("%d",a+i);
for(int i=0;i<(1<<n);i++)cnt[i]=cnt[i>>1]+(i&1);
dp[0]=1;
for(int i=1;i<=n;i++){
for(int j=0;j<n;j++)if(a[j]==i)S|=(1<<j);
for(int j=0;j<(1<<n);j++){
if(cnt[j]!=i||check(j))continue;
for(int k=0;k<n;k++){
if((j>>k)&1){
dp[j]+=dp[j^(1<<k)];
dp[j]%=mod;
}
}
}
}
printf("%d\n",dp[(1<<n)-1]);
return 0;
}
D 追忆
神人题!!!
rerererererererererererererererererererecall!!!
先做 DAG 可达性统计,得出每个点的可达点集。
维护一个后缀/前缀(我写的是后缀)集合 \(A_i\),表示集合 \(\{x|a_x\in [i,n]\}\),则满足 \(a_x\in [l,r]\) 的点集即为 \(A_{l} \backslash A_{r+1}\)。注意到你对 \(a\) 有修改操作,考虑分块,设块长为 \(T=\sqrt{n}\),\(A_i=\{x|a_x\in [iT,n]\}\),修改时只用修改 \(\sqrt{n}\) 次,单次查询维护 \(a\) 的逆排列,将散块部分暴力求即可。
将可达性点集合上述点集进行与与运算,就得到了所有满足限制条件的点的点集 \(C\),考虑这个点集中最大的 \(b\) 值。维护 \(B_i=\{x|b_x\in [iT,n]\}\),先找到最大 \(b\) 所在的块,即一个最靠后的编号 \(p\),满足 \(B_p\cap C\ne \varnothing\),在这个块里找答案即可。
二分带 \(\log\),所以不写,考虑一些很牛的东西。我们维护集合均使用 bitset,本质是一堆 unsigned long long 变量,分别编号这些变量为 \(w_{0\sim \frac{n}{w}}\)。容易发现,\(B_0\cap C\ne \varnothing\),单独考虑 \(B_0,C\) 的每一个 \(w_i\),若当 \(x\le q\) 时,\(B_{x_{w_i}}\cap C_{w_i}\ne \varnothing\),则说明答案一定大于 \(q\),且各个位的贡献独立。所以枚举 \(C\) 的每一个 \(w\),并维护一个指针 \(p\),若满足 \(B_{{p+1}_{w_i}}\cap C_{w_i}=\varnothing\),则 \(p\gets p+1\),最终走完一遍就是 \(O(\frac{n}{w}+\sqrt{n})\)。
总时间复杂度为 \(O(\frac{nm}{w}+q(\sqrt{n}+\dfrac{w}{n}))\)。比较狗屎。record。

浙公网安备 33010602011771号