NUIST2026个人赛题解
河童重工
当 k 小于 n 则输出 k , 当 k 大于n , 如果 ( n - k ) 为偶数则输出 n , 否则输出n - 1
时间复杂度为O (1)。
查看代码
#include<bits/stdc++.h>
using namespace std;
int main()
{
int n,k;
cin>>n>>k;
if(n>k)
{
cout<<k<<endl;
}
else
{
if((n-k)%2==0) cout<<n<<endl;
else cout<<n-1<<endl;
}
return 0;
}
这个勇者明明超强却过分慎重
由于可以选择至少一名队员 ,因此可以只选择属性值最大的队员组成小队。
对于任意一个小队 ,其属性值的平均值都不可能超过小队中最大的属性值。
所以只需要找到所有队员中属性值的最大值并输出即可。
时间复杂度为 O(n)。
查看代码
#include<bits/stdc++.h>
using namespace std;
int main()
{
int n;
cin>>n;
int ans=INT_MIN;
for(int i=1;i<=n;i++)
{
int x;
cin>>x;
ans=max(ans,x);
}
cout<<ans<<endl;
return 0;
}
琪露诺的完美算术教室
每次选择两个数中较大的数 ,将它除以 9 并向下取整。
同时 ,由于可以通过加 1 让较小的数变成较大的数 ,因此每次操作后 ,都可以尝试更新答案。假设已经进行了 cnt 次操作 ,那么此时直接将两个数变得相等 ,还需要 abs(a-b) 次操作。
因此每次更新:
|----------------------------|
| ans=min(ans,cnt+abs(a-b)); |
当两个数相等时停止。
注意 ,不能只考虑不断除以 9,例如 a=1,b=2,直接将 a 加 1 只需要一次操作。时间复杂度为 O (log9 (max(a , b)))。
查看代码
#include<bits/stdc++.h>
using namespace std;
void solve()
{
int a,b;
cin>>a>>b;
int ans=abs(a-b),cnt=0;
while(a!=b)
{
if(a>b) a/=9;
else b/=9;
cnt++;
ans=min(ans,cnt+abs(a-b));
}
cout<<ans<<endl;
return;
}
int main()
{
int T;
cin>>T;
while(T--)
{
solve();
}
return 0;
}
正确性说明: 如果某个数需要先加 1 再除以 9,可以改为先除以 9,再根据需要加至多 1,操作次数不会增加。因此可以把加法操作放到所有除法操作之后。
而在除法阶段 ,优先除以较大的数即可 ,因为除以较小的数只会扩大当前差距;如果之后还要除以较大的数,可以交换这两次操作的顺序。
因此枚举每次除以较大数后的状态 ,并计算直接补齐差值的代价 ,就能得到最少操作次数。
熊大快跑
由于每次到达位置 0 后 ,都会重新从第一个指令开始执行 ,因此只需要考虑两种情况。
首先 ,从初始位置 x 开始执行指令 ,找到第一次到达位置 0 所需要的时间 t1。
如果无法在 k 秒内到达位置 0,则答案为 0。
否则 ,答案至少为 1,并且剩余时间为 k-t1。
接下来 ,由于熊大每次到达位置 0 都会重新执行指令 ,因此只需要计算从位置 0 出发 ,再次回到位置 0 所需要的最短时间 t2。
如果不存在这样的时间 ,则答案为 1。
否则 ,每经过 t2 秒 ,熊大就能再次到达位置 0,因此答案为:

只需要遍历一次字符串 ,维护当前位置的相对位移 ,就可以找到 t1 和 t2。
时间复杂度为 O (n)。
查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
void solve()
{
int n,x,k;
cin>>n>>x>>k;
string s;
cin>>s;
int p=0,t1=-1,t2=-1;
for(int i=0;i<n;i++)
{
if(s[i]=='L') p--;
else p++;
if(t1==-1&&x+p==0) t1=i+1;
if(t2==-1&&p==0) t2=i+1;
}
if(t1==-1||t1>k)
{
cout<<0<<endl;
return;
}
int ans=1;
k-=t1;
if(t2!=-1) ans+=k/t2;
cout<<ans<<endl;
return;
}
signed main()
{
ios::sync_with_stdio(false); cin.tie(nullptr);
int T;
cin>>T;
while(T--)
{
solve();
}
return 0;
}
芙兰达
由于每张优惠券可以让所选炸弹中价格最低的一个免费 ,因此我们需要尽可能让价格较高的炸弹免费 ,从而使总花费最小。
可以采用贪心策略:
首先 ,将所有炸弹的价格按照从大到小排序 ,将所有优惠券的 k 按照从小到大排序。
然后 ,依次使用优惠券 ,每次从尚未分配的炸弹中选择价格最高的 k 个炸弹。
由于这 k 个炸弹中价格最低的一个可以免费 ,因此只需要将这 k 个炸弹中最后一个的价格从总花费中减去。如果剩余炸弹数量不足以使用当前优惠券 ,则直接结束 ,因为后面的优惠券需要选择的炸弹数量只会更多。最后输出剩余的总花费即可。
注意 ,所有炸弹的价格之和可能超过 int 范围 ,需要使用 long long。
时间复杂度为 O( n log n + m log m)。
查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
void solve()
{
int n,m;
cin>>n>>m;
vector<int> a(n),k(m);
int ans=0;
for(int i=0;i<n;i++)
{
cin>>a[i];
ans+=a[i];
}
for(int i=0;i<m;i++) cin>>k[i];
sort(a.begin(),a.end(),greater<int>());
sort(k.begin(),k.end());
int pos=0;
for(int i=0;i<m;i++)
{
if(pos+k[i]>n) break;
pos+=k[i];
ans-=a[pos-1];
}
cout<<ans<<endl;
return;
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
solve();
return 0;
}
鲤鱼鉴人心
这是一道模拟题。
我们按照题目给出的 m 次操作依次模拟 ,维护每名玩家的生命值上限、 当前生命值、阵营以及是否存活。需要注意以下几点:
- 玩家生命值小于等于 0时立即出局,但 1 号玩家永远不会出局。
- 每次行动结束后,轮到顺时针方向上最近的存活玩家行动。
- 除 1号玩家外 ,所有玩家都将 1 号玩家视为敌人。
- 如果与 1号玩家同阵营的玩家攻击或削弱了 1号玩家 ,那么 1 号玩家也会永久将其视为敌人。
为了快速找到下一名行动者和攻击目标 ,可以使用 set 维护存活玩家。
其中:
- alive:所有存活玩家的编号。
- col[0](#0)、col[1](#1):两个阵营中除 1号外的存活玩家。
- enemy: 当前被 1 号玩家视为敌人的存活玩家。
对于普通玩家 ,优先寻找顺时针方向上编号更大的敌对阵营玩家。如果不存在 ,则攻击 1 号玩家。
对于 1 号玩家 ,直接寻找 enemy 中编号最小的玩家即可。
每次操作结束后 ,如果有玩家出局 ,就从对应的集合中删除。使用 set 可以在 O (log n) 的时间内完成查找和删除。
总时间复杂度为 O ((n + m )log n)。
查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=10005;
int n,m;
int a[N],b[N],c[N];
set<int> alive,col[2],enemy;
void del(int x)
{
if(x==1||b[x]>0) return;
alive.erase(x);
col[c[x]].erase(x);
enemy.erase(x);
}
int find_enemy(int x)
{
if(x==1)
{
if(enemy.empty()) return 0;
return *enemy.begin();
}
auto it=col[c[x]^1].upper_bound(x);
if(it!=col[c[x]^1].end()) return *it;
return 1;
}
void solve()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>a[i]>>c[i];
b[i]=a[i];
alive.insert(i);
if(i!=1) col[c[i]].insert(i);
}
for(int i=2;i<=n;i++)
{
if(c[i]!=c[1]) enemy.insert(i);
}
int now=1;
while(m--)
{
int op;
cin>>op;
if(op==2){b[now]=min(a[now],b[now]+(int)alive.size()); }
else
{
int to=find_enemy(now);
if(to!=0)
{
if(op==1)
{
b[to]-=a[now]-b[now]+1;
}
else
{
a[to]--;
b[to]=min(b[to],a[to]);
}
if(to==1&&now!=1&&c[now]==c[1]){enemy.insert(now);}
del(to);
}
}
auto it=alive.upper_bound(now);
if(it==alive.end()) now=*alive.begin();
else now=*it;
}
for(int i=1;i<=n;i++)
{
cout<<(alive.count(i)?1:0);
if(i<n) cout<<" ";
}
cout<<endl;
return;
}
signed main()
{
ios::sync_with_stdio(false); cin.tie(nullptr);
solve();
return 0;
}
注意: 普通玩家寻找敌人时 ,如果顺时针方向上没有编号更大的敌人 ,那么下一个合法目标一定是 1 号玩家,因为所有其他玩家都将鲤鱼视为敌人。 因此不需要继续遍历整个圆环。
比利时小子 正在和籽山民先生 闹矛盾,,,
U736579 H - 比利吋小子 正在和 籽山民先生 闹矛盾,,,
首先 ,长度为 n 的排列 ,其所有元素之和一定为:

如果 S 是完全平方数 ,那么无论如何排列 ,最后一个前缀和都一定是完全平方数 ,因此无解 ,直接输出 -1。
否则 ,可以采用贪心构造。
首先构造一个从 1 到 n 的升序排列:
|-------------------|
| 1 2 3 4 5 6 7 ... |
对于这个排列 ,前 i 个元素的和为:

从左到右枚举每个位置 i,如果当前前缀和是完全平方数 ,就交换第 i 个元素和第 i+1 个元素。
这样当前前缀和就会增加 1,不再是完全平方数。
同时 ,交换相邻两个元素不会影响后续位置的前缀和。并且不存在连续两个位置的上述前缀和都是完全平方数的情况 ,因此交换操作不会冲突。
最终得到的排列就是一个合法答案。
时间复杂度为 O(n)。
查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
void solve()
{
int n;
cin>>n;
int sum=n*(n+1)/2;
int x=sqrtl(sum);
if(x*x==sum)
{
cout<<-1<<endl;
return;
}
vector<int> a(n+1);
for(int i=1;i<=n;i++) a[i]=i;
for(int i=1;i<n;i++)
{
int s=i*(i+1)/2;
int t=sqrtl(s);
if(t*t==s)
{
swap(a[i],a[i+1]);
}
}
for(int i=1;i<=n;i++)
{
cout<<a[i]<<" ";
}
cout<<endl;
return;
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin>>T;
while(T--)
{
solve();
}
return 0;
}
鬼人正邪Ⅱ
由于每次操作只会翻转区间 (l,r) 内的字符 ,而不会改变两个端点 ,因此整个字符串的第一个字符和最后一个字符始终不会改变。
我们分类讨论。
当 n≤2 时 ,由于不存在可以翻转的字符 ,因此答案为 1。
当 n=3 时 ,只有当第一个字符和最后一个字符相同时 ,才能翻转中间的字符。
因此 ,如果 a[1](#1)=a[3](#3),答案为 2,否则答案为 1。
当 n=4 时 ,可以分类讨论:
- 如果 a[1](#1)=a[4](#4),那么中间两个字符的 4种状态均可以到达,答案为 4。
- 如果字符串为 0011或 1100,那么无法进行有效的翻转操作,答案为 1。
- 其他情况下,答案为3。
当 n≥5 时 ,可以证明 ,对于任意长度为 5 的子串 ,在保持两端字符不变的情况下 ,中间三个字符的 8 种状态可以互相转化。
因此 ,我们可以通过选择长度为 5 的区间 ,单独翻转任意一个非端点字符 ,而不改变其他字符。
也就是说 ,除了第一个和最后一个字符 ,其余 n-2 个字符都可以任意取值。
所以答案为:
ans = 2n-2 mod 998244353
直接分类讨论即可。
时间复杂度为 O(n)。
查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int mod=998244353;
void solve()
{
int n;
cin>>n;
string s;
cin>>s;
if(n<=2)
{
cout<<1<<endl;
return;
}
if(n==3)
{
cout<<(s[0]==s[2]?2:1)<<endl;
return;
}
if(n==4)
{
if(s[0]==s[3]) cout<<4<<endl;
else if(s=="0011"||s=="1100") cout<<1<<endl;
else cout<<3<<endl;
return;
}
int ans=1;
for(int i=1;i<=n-2;i++)
{
ans=ans*2%mod;
}
cout<<ans<<endl;
return;
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin>>T;
while(T--)
{
solve();
}
return 0;
}
怜梦的棋子游戏
这是一道博弈论题 ,可以使用 SG 函数结合线段树解决。
首先 ,由于不同棋子之间互不影响 ,因此可以将整个游戏看作多个独立游戏的组合。
定义 sgi 表⽰只有一个棋子位于位置 i 时的 SG 值 ,并规定: sg [0](#0) = 0
对于位置 i,如果选择位置 j,棋子就会移动到 j,或者分裂到 j 和 pj。
根据 SG 定理 ,这次操作得到的状态对应的 SG 值为:
sg *j* ⊕ sg *p* *j*
其中
⊕
表示按位异或。因此有:
sg *i* = mex {sg *j* ⊕ sg *p* *~j~* ∣ 1 ≤ j < i , a j + a *p*j < a *i*}
这里 mex 表⽰集合中最小的没有出现的非负整数。
接下来考虑如何快速计算所有 sgi。
首先 ,pi 表⽰ i 左侧最近的、满足 aj<ai 的位置 ,因此可以通过单调栈在 O (n) 时间内求出。
对于每个位置 j,定义:
v *j*= sg *j* ⊕ sg *p* ~*j*~
w *j*= a *j*+ a ~p j~那么计算 sgi 时 ,只需要考虑所有 j<i 且 wj<ai 的 vj。
我们从左到右依次计算 SG 值 ,使用线段树维护:

如果不存在对应的位置 ,则令 mnv=INF。
这样 ,对于当前的 i:
- 如果 mnv<ai,说明 v可以通过一次合法操作得到。
- 如果 mnv>=ai,说明 v无法通过一次合法操作得到。
因此 ,sgi 就是最小的满足 mnv>=ai 的非负整数 v。
使用线段树维护每个区间内 mn 的最大值 ,就能通过二分式下降 ,在 O (log n) 时间内找到这个位置。
求出 sgi 后 ,将:
v = sg *i* ⊕ sg *p* *i*
对应的 mnv 更新为:
min(mn *v* , a *i*+ a ~p i~)
随后继续处理下一个位置。
由于每个位置至多有 i-1 种操作 ,因此 sgi≤i-1,线段树只需要维护 0 到 n 的范围即可。
最后 ,对于每次询问 ,假设棋子分别位于:

那么整个游戏的 SG 值为:

如果 ans 不为 0,则先手必胜 ,输出 First;否则输出 Second。
时间复杂度为 O (n log n + Σ k)。
查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int INF=1LL<<60;
struct SegTree
{
int n;
vector<int> mx;
SegTree(int n):n(n)
{
mx.assign(4*(n+1),INF);
}
void update(int p,int l,int r,int x,int v)
{
if(l==r)
{
mx[p]=min(mx[p],v);
return;
}
int mid=(l+r)>>1;
if(x<=mid) update(p<<1,l,mid,x,v);
else update(p<<1|1,mid+1,r,x,v);
mx[p]=max(mx[p<<1],mx[p<<1|1]);
}
int query(int p,int l,int r,int x)
{
if(l==r) return l;
int mid=(l+r)>>1;
if(mx[p<<1]>=x)return query(p<<1,l,mid,x);
else return query(p<<1|1,mid+1,r,x);
}
};
void solve()
{
int n,q;
cin>>n>>q;
vector<int> a(n+1),p(n+1),sg(n+1);
for(int i=1;i<=n;i++) cin>>a[i];
vector<int> stk;
SegTree tr(n);
for(int i=1;i<=n;i++)
{
while(!stk.empty()&&a[stk.back()]>=a[i])
stk.pop_back();
if(!stk.empty()) p[i]=stk.back();
sg[i]=tr.query(1,0,n,a[i]);
int v=sg[i]^sg[p[i]];
if(v<=n)tr.update(1,0,n,v,a[i]+a[p[i]]);
stk.push_back(i);
}
while(q--)
{
int k;
cin>>k;
int ans=0;
while(k--)
{
int x;
cin>>x;
ans^=sg[x];
}
if(ans) cout<<"First"<<endl;
else cout<<"Second"<<endl;
}
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
solve();
return 0;
}
御坂网络
这是一道最小费用最大流题 ,可以使用 CDQ 分治优化建图 + 最小费用最大流 解决。
由于每个御坂妹妹可以选择直接连接网络 ,或者连接到前面的某个御坂妹妹 ,并且每个御坂妹妹最多只能被后面的一个人连接 ,因此可以考虑拆点建图。
将每个御坂妹妹 i 拆成两个点 i 和 i+n,分别表⽰主动连接别人和被别人连接。
建立超级源点 S 和超级汇点 T,按照以下方式建边:
- S→i,容量为 1,费用为0。
- i→T,容量为 1,费用为 W,表⽰直接连接网络。
- i+n→T,容量为 1,费用为 0,限制每个人最多被连接一次。
- 对于所有 j<i,建立i→j+n,容量为 1,费用为 |ai-aj|。
这样 ,从源点到汇点发送 n 单位流量的最小费用 ,就是答案。
但是直接建图需要 O (n 2) 条边 ,运行费用流可能超时 ,因此考虑使用 CDQ 分治优化建图。
对于区间 l,r,设中点为 mid。
由于每个御坂妹妹只能连接前面的人 ,因此只需要考虑右半区间的点向左半区间的点连边。
将当前区间的所有频段排序去重 ,为每个不同的频段建立一个虚点。
对于相邻的两个频段 x,y,建立双向边 ,容量为 INF,费用为 |x-y|。
这样 ,从频段 x 对应的虚点走到频段 y 对应的虚点 ,最小费用就是 |x-y|。
随后:
- 对于左半区间的每个 j,建立对应频段虚点到 j+n的边,容量为 1,费用为 0。
- 对于右半区间的每个 i,建立i到对应频段虚点的边 ,容量为 1,费用为 0。
递归处理左右两个区间 ,就可以表⽰所有合法的连接关系。
这样建图的点数和边数均为 O (n log n) ,再运行最小费用最大流即可得到答案。
由于总代价可能超过 int 范围 ,需要使用 long long。
建图时间复杂度为 O (n log2 n ) ,图中点数和边数均为 O (n log n)。
费用流部分采用 ZKW 多路增广优化。
查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int INF=1LL<<60;
struct MCMF
{
struct Edge
{
int to,nxt,cap,cost;
};
vector<Edge> e;
vector<int> head,dis,vis,inq;
int S,T;
MCMF(int n,int s,int t):S(s),T(t)
{
head.assign(n,-1);
}
int newnode()
{
head.push_back(-1);
return head.size()-1;
}
void add(int u,int v,int cap,int cost)
{
e.push_back({v,head[u],cap,cost});
head[u]=e.size()-1;
e.push_back({u,head[v],0,-cost});
head[v]=e.size()-1;
}
bool spfa()
{
int n=head.size();
dis.assign(n,INF);
inq.assign(n,0);
deque<int> q;
q.push_back(T);
dis[T]=0;
inq[T]=1;
while(!q.empty())
{
int u=q.front();
q.pop_front();
inq[u]=0;
for(int i=head[u];i!=-1;i=e[i].nxt)
{
int v=e[i].to;
if(e[i^1].cap>0&&dis[v]>dis[u]-e[i].cost)
{
dis[v]=dis[u]-e[i].cost;
if(!inq[v])
{
if(!q.empty()&&dis[v]<dis[q.front()]) q.push_front(v);
else q.push_back(v);
inq[v]=1;
}
}
}
}
return dis[S]!=INF;
}
int dfs(int u,int f)
{
if(u==T) return f;
vis[u]=1;
int used=0;
for(int i=head[u];i!=-1&&used<f;i=e[i].nxt)
{
int v=e[i].to;
if(e[i].cap>0&&!vis[v]&&dis[u]==dis[v]+e[i].cost)
{
int d=dfs(v,min(f-used,e[i].cap));
e[i].cap-=d;
e[i^1].cap+=d;
used+=d;
}
}
vis[u]=0;
return used;
}
int run(int n)
{
int ans=0,flow=0;
while(flow<n&&spfa())
{
vis.assign(head.size(),0);
int f=dfs(S,n-flow);
if(!f) break;
flow+=f;
ans+=dis[S]*f;
}
return ans;
}
};
int n,W;
vector<int> a;
void build(int l,int r,MCMF &g)
{
if(l==r) return;
int mid=(l+r)>>1;
vector<int> b;
for(int i=l;i<=r;i++)
b.push_back(a[i]);
sort(b.begin(),b.end());
b.erase(unique(b.begin(),b.end()),b.end());
int m=b.size();
vector<int> p(m);
for(int i=0;i<m;i++)
p[i]=g.newnode();
for(int i=1;i<m;i++)
{
int w=b[i]-b[i-1];
g.add(p[i-1],p[i],n,w);
g.add(p[i],p[i-1],n,w);
}
for(int i=l;i<=r;i++)
{
int k=lower_bound(b.begin(),b.end(),a[i])-b.begin();
if(i<=mid) g.add(p[k],n+i,1,0);
else g.add(i,p[k],1,0);
}
build(l,mid,g);
build(mid+1,r,g);
}
void solve()
{
cin>>n>>W;
a.resize(n+1);
for(int i=1;i<=n;i++)
cin>>a[i];
int S=0,T=2*n+1;
MCMF g(2*n+2,S,T);
for(int i=1;i<=n;i++)
{
g.add(S,i,1,0);
g.add(i,T,1,W);
g.add(n+i,T,1,0);
}
build(1,n,g);
cout<<g.run(n)<<endl;
return;
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
solve();
return 0;
}