The 2025 ICPC Asia Xi'an Regional Contest
Preface
今年的西安 Regional,本来是上周三 VP 的,但因为各种原因一直拖到今天才写博客
这场的题感觉出的确实不赖,比较注重观察能力的考察而非纯粹的 adhoc 或者模板题的堆砌
感觉至少有十个题都是时间充裕的话能自然而然想出来的,只可惜打的时候因为犯病经典匆匆忙忙
这场最大的锅就是我开局把 I 题题意里的点标号读成了点权,带着全队想了半场的假题意,同时把机时和心态全部打爆了
最后时刻多开好在是过了 K,但队友的 B 分讨感觉是漏了 Case 没能通过,观察出了结论并且已经有了做法的 C 也没时间写了,最后 7 题收尾
B. Beautiful Dangos
这题的难点就是找一个好的写法来重排某个区间的颜色,队友赛时尝试分讨但可惜没过,赛后看题解发现了种高妙的构造法
对于每种颜色,钦定其权值由以下两部分构成:
- 序列中每个该颜色贡献 \(+1\),非该颜色贡献 \(-1\);
- 两端外侧每有一个该颜色贡献 \(+1\),否则贡献为 \(0\);
最后一个区间有解的充要条件为三种颜色的权值都 \(\le 1\),证明考虑归纳法即可
(值得一提的是官方题解里的证明方法是有明显漏洞的,但这个结论应该是没问题的)
要构造一组解只需要每次选出能放在该位置且权值最大的颜色即可,可以避免繁琐的分讨
#include <bits/stdc++.h>
using namespace std;
int mapch(char c) {
switch(c) {
case 'C': return 0;
case 'W': return 1;
case 'P': return 2;
default : abort();
}
}
string pat = "CWP";
int n;
std::string s;
bool isLegal(int l, int r, const std::array<int, 3> &c3) {
// std::cerr << "calling (" << l << ", " << r << ")\n";
if(r - l <= 0) return false;
int sum = 0;
for(auto c: c3) sum += c;
assert(sum == r - l + 1);
bool flag = true;
for(auto c: c3) {
if(c >= sum / 2) flag = false;
if(c > (sum + 1) / 2) return false;
}
if(flag) return true;
int lc = -1, rc = -1;
if(l - 1 >= 0) lc = mapch(s[l - 1]);
if(r + 1 <= n - 1) rc = mapch(s[r + 1]);
// std::cerr << "lc, rc = " << lc << ", " << rc << char(10);
if(sum % 2 == 1) {
for(int i = 0; i < 3; ++i) if(c3[i] == (sum + 1) / 2)
if(lc == i || rc == i) return false;
return true;
} else {
for(int i = 0; i < 3; ++i) if(c3[i] == sum / 2)
if((i == lc) && (i == rc)) return false;
return true;
}
}
// std::string construct(int l, int r) {
// static char mapint[3]{'C', 'W', 'P'};
// std::string S = s;
// std::array<int, 3> c3{0, 0, 0};
// for(int i = l; i <= r; ++i) c3[mapch(s[i])]++, S[i] = '.';
// int p[3]{0, 1, 2};
// std::sort(p, p + 3, [&](int lhs, int rhs) { return c3[lhs] > c3[rhs]; });
// int lc = -1, rc = -1;
// if(l - 1 >= 0) lc = mapch(s[l - 1]);
// if(r + 1 < n) rc = mapch(s[r + 1]);
// int i;
// int total = r - l + 1 - c3[p[0]];
// if()
// //std::cerr << S << char(10);
// for(auto c: c3) assert(c == 0);
// return S;
// }
void construct(int l, int r) {
static char mapint[3]{'C', 'W', 'P'};
std::array<int, 3> c3{0, 0, 0};
std::array<int, 3> num{0, 0, 0};
for (int c=0;c<3;++c)
{
for (int i=l;i<=r;++i)
if (s[i]==mapint[c]) ++c3[c]; else --c3[c];
if (l-1>=0&&s[l-1]==mapint[c]) ++c3[c];
if (r+1<s.size()&&s[r+1]==mapint[c]) ++c3[c];
}
for (int i=l;i<=r;++i) ++num[mapch(s[i])];
for (int i=l;i<=r;++i)
{
int mxc=-1;
for (int c=0;c<3;++c)
{
if (i-1>=0&&s[i-1]==mapint[c]) continue;
if (num[c]==0) continue;
if (mxc==-1||c3[c]>c3[mxc]) mxc=c;
}
if (i-1>=0) --c3[mapch(s[i-1])];
s[i]=mapint[mxc]; --num[mxc];
for (int c=0;c<3;++c)
if (c!=mxc) ++c3[c];
}
}
void work() {
std::cin >> n;
std::cin >> s;
int cnt[3]{0, 0, 0};
std::vector<int> colli;
for(int i = 0; i < n; ++i) cnt[mapch(s[i])] += 1;
for(int i = 1; i < n; ++i) if(s[i] == s[i - 1]) colli.emplace_back(i);
if(colli.size() == 0) {
std::cout << "Beautiful\n";
return ;
}
for(int i = 0; i < 3; ++i) if(cnt[i] > (n + 1) / 2) {
std::cout << "Impossible\n";
return ;
}
int TL = colli.front(), BR = colli.back() - 1;
// std::cerr << "TL = " << TL << ", BR = " << BR << char(10);
int l = TL, r = n - 1;
std::array<int, 3> c3{0, 0, 0};
for(int i = l; i <= r; ++i) c3[mapch(s[i])] += 1;
int ans = n + 1;
int ans_l, ans_r;
while(l >= 0) {
while(r - 1 >= BR) {
c3[mapch(s[r--])]--;
if(!isLegal(l, r, c3)) {
c3[mapch(s[++r])]++;
break;
}
}
if(isLegal(l, r, c3)) {
if(r - l + 1 < ans) {
// std::cerr << "update " << l << ", " << r << char(10);
ans = r - l + 1;
ans_l = l, ans_r = r;
}
}
l -= 1;
if(l >= 0) c3[mapch(s[l])]++;
}
if(ans == n + 1) {
std::cout << "Impossible\n";
return ;
}
std::cout << "Possible\n";
std::cout << ans_l + 1 << ' ' << ans_r + 1 << char(10);
construct(ans_l, ans_r);
std::cout << s << char(10);
return ;
}
int main() {
std::ios::sync_with_stdio(false);
int T; std::cin >> T; while(T--) work();
return 0;
}
C. Catch the Monster
祁神赛时一眼看出了结论:一个子图合法当且仅当去除了所有叶子节点后剩余部分是一条链
以上限制很容易转化为:图中不存在某个点,其邻居节点中存在 \(>2\) 个度数 \(\ge 2\) 的节点
令 \(pos_L\) 表示以 \(L\) 为左端点时,合法的右端点最大值是多少,有了这个后显然可以 \(O(1)\) 回答询问
考虑双指针移动端点,不难发现每个点只会被加入/删除一次
只需要动态维护每个点的度数,邻居节点中度数 \(\ge 2\) 的节点数量,以及全局不合法的点数目
一般的加入/删除点的影响都比较简单,需要注意的是当加入一个点 \(x\) 后,使得其某个邻居 \(y\) 的度数恰好变为 \(2\) 时,对于 \(y\) 的另一个相邻节点(非 \(x\))的合法性可能会有影响
一个巧妙的实现是动态维护每个点所有已经加入的邻居节点的编号异或和,即可快速找到上述情况对应的节点
#include<cstdio>
#include<iostream>
#include<vector>
#include<algorithm>
#include<assert.h>
#define RI register int
#define CI const int&
using namespace std;
const int N=1e6+5;
int n,m,q,live[N],deg[N],num[N],pos[N],nbr[N]; vector <int> v[N];
int main()
{
scanf("%d%d%d",&n,&m,&q);
for (RI i=1;i<=m;++i)
{
int x,y; scanf("%d%d",&x,&y);
v[x].push_back(y); v[y].push_back(x);
}
int R=1,cnt=0;
for (RI L=1;L<=n;++L)
{
auto valid=[&](CI x)
{
return num[x]>2;
};
auto add=[&](CI x)
{
if (live[x]) return;
vector <int> pnt;
pnt.push_back(x);
for (auto y:v[x])
if (live[y]) pnt.push_back(y);
for (auto y:v[x])
if (live[y]&°[y]==1) pnt.push_back(nbr[y]);
sort(pnt.begin(),pnt.end());
pnt.erase(unique(pnt.begin(),pnt.end()),pnt.end());
for (auto x:pnt) cnt-=valid(x);
live[x]=1;
for (auto y:v[x])
if (live[y])
{
++deg[x]; ++deg[y];
nbr[x]^=y; nbr[y]^=x;
}
if (deg[x]>=2)
{
for (auto y:v[x])
if (live[y]) ++num[y];
}
for (auto y:v[x])
if (live[y]&°[y]==2) ++num[nbr[y]^x];
for (auto y:v[x])
if (live[y]) num[x]+=(deg[y]>=2);
for (auto x:pnt) cnt+=valid(x);
// printf("added x = %d, cnt = %d\n",x,cnt);
return;
};
auto del=[&](CI x)
{
if (!live[x]) return;
vector <int> pnt;
pnt.push_back(x);
for (auto y:v[x])
if (live[y]) pnt.push_back(y);
for (auto y:v[x])
if (live[y]&°[y]==2) pnt.push_back(nbr[y]^x);
sort(pnt.begin(),pnt.end());
pnt.erase(unique(pnt.begin(),pnt.end()),pnt.end());
for (auto x:pnt) cnt-=valid(x);
if (deg[x]>=2)
{
for (auto y:v[x])
if (live[y]) --num[y];
}
for (auto y:v[x])
if (live[y]&°[y]==2) --num[nbr[y]^x];
num[x]=0;
live[x]=0;
for (auto y:v[x])
if (live[y])
{
--deg[x]; --deg[y];
nbr[x]^=y; nbr[y]^=x;
}
assert(deg[x]==0&&nbr[x]==0);
for (auto x:pnt) cnt+=valid(x);
return;
};
while (R<=n&&cnt==0)
{
add(R);
if (cnt>0) break;
++R;
}
pos[L]=R-1; del(L);
}
// for (RI i=1;i<=n;++i)
// printf("[%d, %d]\n",i,pos[i]);
while (q--)
{
int l,r; scanf("%d%d",&l,&r);
puts(r<=pos[l]?"Yes":"No");
}
return 0;
}
F. Follow the Penguins
对每个企鹅求出其遇到目标所需的时间,用堆维护后每次取出最小的那个然后更新影响即可
注意一个企鹅停止后只会对所有以它为目标的企鹅产生影响,因此总复杂度是可以保证的
#include<cstdio>
#include<iostream>
#include<set>
#include<vector>
#define RI register int
#define CI const int&
using namespace std;
const int N=500005,INF=2e9;
int n,a[N],p[N],d[N],ans[N],lst[N],pt[N]; vector <int> fav[N];
int main()
{
scanf("%d",&n);
for (RI i=1;i<=n;++i)
{
scanf("%d",&a[i]);
fav[a[i]].push_back(i);
}
for (RI i=1;i<=n;++i)
scanf("%d",&p[i]),p[i]*=2;
for (RI i=1;i<=n;++i)
d[i]=(p[a[i]]<p[i]?-1:1);
auto calc=[&](CI x)
{
int y=a[x];
if (d[x]==d[y]) return INF;
int v=abs(d[x]-d[y]),dis=p[y]-p[x];
if ((dis>0&&d[x]<0)||(dis<0&&d[x]>0)) return INF;
return abs(dis)/v;
};
set <pair <int,int>> hp;
for (RI i=1;i<=n;++i)
hp.insert({lst[i]=calc(i),i});
while (!hp.empty())
{
auto [t,x]=*hp.begin();
// printf("id = %d, time = %d\n",x,t);
hp.erase(*hp.begin());
ans[x]=t; p[x]+=d[x]*(t-pt[x]);
pt[x]=t; d[x]=0;
for (auto y:fav[x])
if (d[y]!=0)
{
hp.erase(hp.find({lst[y],y}));
p[y]+=d[y]*(t-pt[y]); pt[y]=t;
hp.insert({lst[y]=t+calc(y),y});
}
}
for (RI i=1;i<=n;++i)
printf("%d%c",ans[i]," \n"[i==n]);
return 0;
}
G. Grand Voting
签到,直接升序/降序取即可
#include<cstdio>
#include<iostream>
#include<algorithm>
#define RI register int
#define CI const int&
using namespace std;
const int N=100005;
int n,a[N];
int main()
{
scanf("%d",&n);
for (RI i=1;i<=n;++i)
scanf("%d",&a[i]);
sort(a+1,a+n+1);
int mn=0,mx=0;
for (RI i=1;i<=n;++i)
if (a[i]<=mx) ++mx; else --mx;
for (RI i=n;i>=1;--i)
if (a[i]<=mn) ++mn; else --mn;
return printf("%d %d",mx,mn),0;
}
I. Imagined Holly
不好好看题的后果,直接把节奏搞崩了
考虑假设树以 \(1\) 号点为根,注意到如果 \(A_{1,x}\oplus A_{1,y}\oplus A_{x,y}=x\),则说明 \(x\) 是 \(y\) 的祖先
求出所有的祖先关系后很容易从叶子节点唯一地复原整棵树
#include<cstdio>
#include<iostream>
#include<vector>
#define RI register int
#define CI const int&
using namespace std;
const int N=2005;
int n,a[N][N],son[N][N],sz[N],vis[N];
int main()
{
scanf("%d",&n);
for (RI i=1;i<=n;++i)
for (RI j=i;j<=n;++j)
scanf("%d",&a[i][j]);
vector <pair <int,int>> ans;
for (RI i=1;i<=n;++i)
for (RI j=i+1;j<=n;++j)
{
int val=a[1][i]^a[1][j]^a[i][j];
if (val==i) son[i][j]=1,++sz[i];
if (val==j) son[j][i]=1,++sz[j];
}
sz[0]=1e9;
for (RI t=1;t<n;++t)
{
int lf=-1;
for (RI i=1;i<=n;++i)
if (!vis[i]&&sz[i]==0) { lf=i ;break; }
vis[lf]=1; int fa=0;
for (RI i=1;i<=n;++i)
if (!vis[i]&&son[i][lf]&&sz[i]<sz[fa]) fa=i;
ans.push_back({fa,lf});
for (RI i=1;i<=n;++i)
if (!vis[i]&&son[i][lf]) son[i][lf]=0,--sz[i];
}
for (auto [x,y]:ans) printf("%d %d\n",x,y);
return 0;
}
J. January's Color
简单贪心,不难发现有解的充要条件为 \(x\) 在 \(y\) 的子树中,并且过程一定是一路合并上去
因此可以先 DP 求出得到每个点的最小代价,并定义一条边(\(x\) 是 \(y\) 的父亲)的边权为在已经有了点 \(y\) 的前提下得到 \(x\) 的最小代价
求边权只需要维护一个点儿子的 DP 数组的最大/次大值即可,最后询问转化为求一条路径的边权和
#include<cstdio>
#include<iostream>
#include<vector>
#define RI register int
#define CI const int&
using namespace std;
const int N=300005,INF=1e9;
int t,n,m,c[N],f[N],mn[N],smn[N],L[N],R[N],idx;
long long pfx[N]; vector <int> v[N];
inline void DFS1(CI now,CI fa)
{
L[now]=++idx; f[now]=c[now]; mn[now]=smn[now]=INF;
for (auto to:v[now])
{
if (to==fa) continue; DFS1(to,now);
if (f[to]<mn[now]) smn[now]=mn[now],mn[now]=f[to];
else if (f[to]<smn[now]) smn[now]=f[to];
}
f[now]=min(f[now],mn[now]+smn[now]);
R[now]=idx;
}
inline void DFS2(CI now,CI fa)
{
for (auto to:v[now])
{
if (to==fa) continue;
pfx[to]=pfx[now]+(mn[now]==f[to]?smn[now]:mn[now]);
DFS2(to,now);
}
}
int main()
{
for (scanf("%d",&t);t;--t)
{
scanf("%d%d",&n,&m);
for (RI i=1;i<=n;++i)
scanf("%d",&c[i]),v[i].clear();
for (RI i=1;i<n;++i)
{
int x,y; scanf("%d%d",&x,&y);
v[x].push_back(y); v[y].push_back(x);
}
idx=0; DFS1(1,0); DFS2(1,0);
while (m--)
{
int x,y; scanf("%d%d",&x,&y);
if (L[y]<=L[x]&&L[x]<=R[y])
{
printf("%lld\n",pfx[x]-pfx[y]);
} else puts("-1");
}
}
return 0;
}
K. Killing Bits
先特判掉初始全相等的情况,不难发现有解的必要条件为:
- \(\forall i\in[1,n],a_i\& b_i=b_i\)
- \(\exist \{p_i\},\forall i\in[1,n],b_i\&p_i=b_i\)
其中第二个条件是能做一次操作的充要条件,不过我们很容易发现只要存在一个这样的排列,就一定可以通过以下操作满足要求
考虑进行一次操作后得到的序列为 \(\{b'_i\}\),显然 \(b_i\) 是 \(b'_i\) 的子集;只要将 \(\{p_i\}\) 中的 \(p_i\) 与 \(p_i\oplus b_i\oplus b'_i\) 交换即可将 \(b'_i\) 变为 \(b_i\),并且不影响被交换的另一个数
因此这题只需要判断是否存在符合第二个条件的序列 \(\{p_i\}\) 即可,很容易将其转化为一个 one-one-match 的问题
朴素地跑匹配的边数是 \(O(n^2)\) 的,但由于连边性质是子集,我们可以用经典 trick
对于每个状态 \(mask\),向其每个二进制下 \(1\) 变为 \(0\) 后的状态分别连边,即可将边数控制在 \(O(n\log n)\) 级别
#include<cstdio>
#include<iostream>
#include<queue>
#include<cstring>
#define RI register int
#define CI const int&
using namespace std;
const int N=50005,INF=1e9;
int T,n,a[N],b[N];
namespace Network_Flow
{
const int NN=N,MM=N*20;
struct edge
{
int to,nxt,v;
}e[MM<<1]; int cnt=1,head[NN],cur[NN],dep[NN],s,t;
inline void addedge(CI x,CI y,CI z)
{
e[++cnt]=(edge){y,head[x],z}; head[x]=cnt;
e[++cnt]=(edge){x,head[y],0}; head[y]=cnt;
}
#define to e[i].to
inline bool BFS(void)
{
memset(dep,0,(t+1)*sizeof(int));
dep[s]=1; queue <int> q; q.push(s);
while (!q.empty())
{
int now=q.front(); q.pop();
for (RI i=head[now];i;i=e[i].nxt)
if (e[i].v&&!dep[to]) dep[to]=dep[now]+1,q.push(to);
}
return dep[t];
}
inline int DFS(CI now,CI tar,int dis)
{
if (now==tar) return dis; int ret=0;
for (RI& i=cur[now];i&&dis;i=e[i].nxt)
if (e[i].v&&dep[to]==dep[now]+1)
{
int tmp=DFS(to,tar,min(dis,e[i].v));
if (!tmp) dep[to]=0;
dis-=tmp; ret+=tmp; e[i].v-=tmp; e[i^1].v+=tmp;
if (!dis) return ret;
}
if (!ret) dep[now]=0;
return ret;
}
#undef to
inline int Dinic(int ret=0)
{
while (BFS()) memcpy(cur,head,(t+1)*sizeof(int)),ret+=DFS(s,t,INF);
return ret;
}
inline void clear(void)
{
memset(head,0,(t+1)*sizeof(int)); cnt=1;
}
}
using namespace Network_Flow;
int main()
{
for (scanf("%d",&T);T;--T)
{
scanf("%d",&n);
for (RI i=1;i<=n;++i) scanf("%d",&a[i]);
for (RI i=1;i<=n;++i) scanf("%d",&b[i]);
bool flag=1,all_same=1;
for (RI i=1;i<=n;++i)
{
if ((a[i]&b[i])!=b[i]) flag=0;
if (a[i]!=b[i]) all_same=0;
}
if (!flag) { puts("No"); continue; }
if (all_same) { puts("Yes"); continue; }
s=n; t=s+1;
for (RI mask=0;mask<n;++mask)
{
for (RI k=1;k<n;k<<=1)
if ((mask&k)==k) addedge(mask,mask^k,INF);
addedge(s,mask,1);
}
for (RI i=1;i<=n;++i) addedge(b[i],t,1);
puts(Dinic()==n?"Yes":"No");
clear();
}
return 0;
}
L. Let's Make a Convex!
签到,有解的充要条件为除了最大值之外的边之和大于最大值
#include <bits/stdc++.h>
#define int int64_t
void work() {
int n; std::cin >> n;
std::vector<int> a(n);
int64_t sum = 0, l = 0, r = n;
std::vector<int> ans(n, 0);
for(auto &a: a) std::cin >> a, sum += a;
std::sort(a.begin(), a.end());
for(int k = n; k > 0; sum -= a[l++], --k) {
while(l > 0 && a[r - 1] * 2 >= sum) {
sum += a[--l];
sum -= a[--r];
}
if(a[r - 1] * 2 < sum) ans[k - 1] = sum;
}
for(int i = 0; i < n; ++i)
std::cout << ans[i] << char(i == n - 1 ? 10 : 32);
}
int32_t main() {
std::ios::sync_with_stdio(false);
int T; std::cin >> T; while(T--) work();
return 0;
}
M. Mystique as Iris
首先观察到只要序列中含有 \([2,n-1]\) 的数则一定合法,因此只需考虑序列中只有 \(1\) 和 \(\ge n\) 的数的情形
简单手玩后发现此时合法的充要条件为:
- 序列以 \(1\) 开头或结尾;
- 序列中含有连续的两个 \(1\);
因此我们可以 DP 出不合法的序列数量,用总方案数减去之即可
注意特判整个序列全是 \(1\) 且 \(n\) 为奇数的不合法情形
#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=1e6+5,mod=1e9+7;
int n,m,a[N],f[N][2]; // 0: put 1; 1: put >=n
int main()
{
scanf("%d%d",&n,&m);
int all_one=1,has_key=0,unkn=0;
for (RI i=1;i<=n;++i)
{
scanf("%d",&a[i]);
if (2<=a[i]&&a[i]<=n-1) has_key=1;
if (a[i]!=-1&&a[i]!=1) all_one=0;
if (a[i]==-1) ++unkn;
}
int ans=1,coef=max(0,m-n+1);
for (RI i=1;i<=unkn;++i) ans=1LL*ans*m%mod;
if (has_key) return printf("%d",ans),0;
if (a[1]==1)
{
if (n%2==1&&all_one) (ans+=mod-1)%=mod;
return printf("%d",ans),0;
}
if (a[1]==-1) f[1][1]=coef; else f[1][1]=1;
for (RI i=2;i<=n;++i)
{
if (a[i]==-1)
{
f[i][0]=f[i-1][1];
f[i][1]=1LL*(f[i-1][0]+f[i-1][1])*coef%mod;
} else
if (a[i]==1)
{
f[i][0]=f[i-1][1];
} else
{
f[i][1]=(f[i-1][0]+f[i-1][1])%mod;
}
}
(ans+=mod-f[n][1])%=mod;
if (n%2==1&&all_one) (ans+=mod-1)%=mod;
return printf("%d",ans),0;
}
Postscript
感觉区域赛临近但训练还是摆的一啊,这样下去感觉又要爆炸了

浙公网安备 33010602011771号