The 2025 ICPC Asia East Continent Online Contest (I)
Preface

短暂的几场复建后又到了网络赛的时节,这个赛季的队名依然沿用了上赛季的,也许是为了一定程度上尝试去弥补去年的遗憾吧
这场总体来说题目的 GAP 有点大,9 题之外的其他题目都属于完全没有思路,感觉和我们队平时训练的状态还是很吻合的
与此同时由于今年网络赛银牌✌终于参赛了,导致我们队没有了之前几次要冲排名打名额的压力,因此打的时候心态也很放松
期间我也是一直在无视策略疯狂抢机子写题,最蘸豆爽的一集
A. Who Can Win
简单模拟题,考虑某个队伍时只需要假设它封榜后第一发就过然后其他人都没过就行
#include<cstdio>
#include<iostream>
#include<string>
#include<map>
#include<algorithm>
#include<vector>
#define RI register int
#define CI const int&
using namespace std;
typedef pair <int,int> pi;
const int N=100005;
int t,n,solved[N][30],frozen[N][30]; string names[N];
pi pre[N],add[N]; vector <int> pen[N][30];
int main()
{
ios::sync_with_stdio(0); cin.tie(0);
for (cin>>t;t;--t)
{
cin>>n; int idx=0; map <string,int> rst;
for (RI i=1;i<=n;++i)
{
add[i]={0,0};
for (RI j=0;j<26;++j)
{
solved[i][j]=frozen[i][j]=300;
pen[i][j].clear();
}
}
while (n--)
{
string name,pid,res; int tim;
cin>>name>>pid>>tim>>res;
int nid,p=pid[0]-'A';
if (rst.count(name)) nid=rst[name]; else
{
nid=rst[name]=++idx;
names[idx]=name;
}
if (res=="Accepted") solved[nid][p]=min(solved[nid][p],tim); else
if (res=="Rejected") pen[nid][p].push_back(tim); else
frozen[nid][p]=min(frozen[nid][p],tim);
}
pi mx={0,-1e9},smx={0,-1e9};
for (RI i=1;i<=idx;++i)
{
pi cur={0,0};
for (RI j=0;j<26;++j)
{
if (solved[i][j]!=300)
{
int cnt=0;
for (auto x:pen[i][j]) if (x<solved[i][j]) ++cnt;
++cur.first; cur.second-=solved[i][j]+cnt*20;
} else if (frozen[i][j]!=300)
{
++add[i].first;
add[i].second-=frozen[i][j]+(int)(pen[i][j].size())*20;
}
}
if (cur>=mx) smx=mx,mx=cur; else if (cur>=smx) smx=cur;
pre[i]=cur;
}
vector <string> ans;
for (RI i=1;i<=idx;++i)
{
pi cur=pre[i],ots;
if (cur==mx) ots=smx; else ots=mx;
cur.first+=add[i].first;
cur.second+=add[i].second;
if (cur>=ots) ans.push_back(names[i]);
}
sort(ans.begin(),ans.end());
for (auto s:ans) cout<<s<<' ';
cout<<'\n';
}
return 0;
}
B. Creating Chaos
因为数据范围不大直接构造出 \(n\times n\) 的表,每次可以删除下标相同的一行一列
显然直接贪心每次删去和最大的下标一定最优,然后跑一遍会发现其实答案就是输出 \(1\sim k\)
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<vector>
#define RI register int
#define CI const int&
using namespace std;
const int N=1005;
int n,k,g[N][N],sum[N],vis[N];
int main()
{
scanf("%d%d",&n,&k);
for (RI i=1;i<=n;++i)
for (RI j=1;j<=n;++j)
g[i][j]=__gcd(abs(i-j),n),sum[i]+=g[i][j];
vector <int> ans;
while (k--)
{
int mx=-1,id=-1;
for (RI i=1;i<=n;++i)
if (!vis[i]&&sum[i]>mx) mx=sum[i],id=i;
ans.push_back(id); vis[id]=1;
for (RI i=1;i<=n;++i)
if (!vis[i]) sum[i]-=g[i][id];
}
for (auto x:ans) printf("%d ",x);
return 0;
}
C. Canvas Painting
首先连边肯定在相邻两点之间最优,因此可以抽象出 \(n-1\) 个位置,并且把区间右端点减 \(1\),就转化为一个经典的区间和点匹配的问题
如果从点的角度考虑,从左到右考虑每个点,选择所有覆盖它的区间中右端点最小的进行匹配一定最优,但因为这题点数量很多因此不能这么做
所以我们从区间的角度考虑,把所有区间按照右端点排序再依次考虑每个区间,每次选择该区间内部最靠左且没匹配的点即可
具体实现时,可以用 set 维护所有连续的可以选的点的区间,因为一次选一个所以最后最多分裂成 \(m\) 个区间,即可保证复杂度
#include<cstdio>
#include<iostream>
#include<vector>
#include<set>
#include<algorithm>
#define RI register int
#define CI const int&
#define fi first
#define se second
using namespace std;
typedef pair <int,int> pi;
const int N=200005;
int t,m,n; pi itv[N];
int main()
{
for (scanf("%d",&t);t;--t)
{
scanf("%d%d",&m,&n);
for (RI i=1;i<=m;++i)
scanf("%d%d",&itv[i].fi,&itv[i].se);
auto cmp=[&](const pi& A,const pi& B)
{
return A.se!=B.se?A.se<B.se:A.fi<B.fi;
};
sort(itv+1,itv+m+1,cmp);
if (n==1) { puts("1"); continue; }
set <pi> s; s.insert({1,n-1});
int ans=n;
for (RI i=1;i<=m;++i)
{
auto [l,r]=itv[i]; --r;
if (l>r) continue;
auto it=s.upper_bound({l,n});
bool is_in=0;
if (it!=s.begin())
{
auto tmp=it; --tmp;
if (tmp->fi<=l&&l<=tmp->se)
{
--ans; is_in=1;
// printf("(1)%d -- [%d,%d]\n",l,l,r);
int L=tmp->fi,R=tmp->se;
s.erase(tmp);
if (L<=l-1) s.insert({L,l-1});
if (l+1<=R) s.insert({l+1,R});
}
}
if (is_in) continue;
if (it!=s.end())
{
int L=it->fi,R=it->se;
if (L>r) continue;
s.erase(it); --ans;
// printf("(2)%d -- [%d,%d]\n",L,l,r);
if (L+1<=R) s.insert({L+1,R});
}
}
printf("%d\n",ans);
}
return 0;
}
D. Min-Max Tree
刚开始告诉队友了个假结论成功带歪,不过好在后面又 fix 回来了
考虑大力 DP,把当前点是否还可以继续扩展,以及最小值和最大值是否已经选了都压到状态里面
根据经典 trick,我们并不需要真的维护哪个值是最大哪个值是最小,直接考虑所有情况,不正确的方案一定不优
#include <bits/stdc++.h>
constexpr int $n = 1000006;
using llsi = long long signed int;
constexpr llsi INF = 0x1f1f1f1f1f1f1f1f;
int n, a[$n];
std::vector<int> out[$n];
llsi dp[$n][2][4];
void chmax(llsi &a, llsi b) { if(b > a) a = b; }
void dfs(int cur, int _pa) {
for(int i = 0; i < 2; ++i)
for(int j = 0; j < 4; ++j)
dp[cur][i][j] = -INF;
dp[cur][0][0] = 0;
dp[cur][1][0] = 0;
dp[cur][1][1] = a[cur];
dp[cur][1][2] = -a[cur];
for(auto ch: out[cur]) if(ch != _pa) {
dfs(ch, cur);
llsi temp[2][4];
for(int i = 0; i < 2; ++i) for(int j = 0; j < 4; ++j) temp[i][j] = -INF;
chmax(temp[0][0], dp[cur][0][0] + dp[ch][0][0]);
for(int j = 0; j < 4; ++j)
chmax(temp[1][j], dp[cur][1][j] + dp[ch][0][0]);
for(int a = 0; a < 4; a++) for(int b = 0; b < 4; ++b) if((a & b) == 0) {
chmax(temp[1][a | b], dp[cur][1][a] + dp[ch][1][b]);
if((a | b) != 3) continue;
chmax(temp[0][0], dp[cur][1][a] + dp[ch][1][b]);
}
memcpy(dp[cur], temp, sizeof(temp));
}
// std::cerr << "debug dp[" << cur << "]:\n";
// for(int i = 0; i < 3; ++i) for(int j = 0; j < 4; ++j) std::cerr << (dp[cur][i][j] < -100 ? -100 : dp[cur][i][j]) << char(j == 3 ? 10 : 32);
return ;
}
int main() {
std::ios::sync_with_stdio(false);
std::cin >> n;
for(int i = 1; i <= n; ++i) std::cin >> a[i];
for(int i = 1, u, v; i < n; ++i) {
std::cin >> u >> v;
out[u].emplace_back(v);
out[v].emplace_back(u);
}
dfs(1, 0);
std::cout << dp[1][0][0] << char(10);
return 0;
}
F. Robot
写一半写红了,最后纯是队友遥控我在写,然后莫名其妙就过了
这题的大致思路是先搞一个正方形给里机器人堵死,然后一直二分把机器人关在更小的矩形里,直到最后将其炸死
难点在于前面部分,因为造墙的速度和机器人移动速度其实是相同的,因此我们要造墙就必须“偷工减料”
考虑以下思路,在 \(x=N,y=N\) 的位置造墙,但刚开始时造的墙之间都空一个位置
直到机器人走进某个墙附近时(可以设距离参数 \(D\)),我们再把对应区域的墙补完整即可
而由于墙有两边要修,同时进行的话是赶不上机器人逃跑的速度的,因此刚开始想的策略是根据机器人所在位置的 \(x,y\) 的大小关系决定修水平的墙还是竖直的墙(对角线上就两边补短的那边)
不过写的时候队友讨论了一种机器人在水平线附近一侧骗着修墙,一段时间后直接反向逃跑的策略卡掉了这种方法,然后我就摆烂写了一些神秘策略交上去收获各种错误
后面在我已经温度过高之时队友告诉我只要开局先在右上角 \((N,N)\) 附近造一个墙角就能 fix 这种情况,我就机械地实现了一下然后交上去就过了,最 drama 的一集
PS:最后参数选择 \(N=150,D=5\),实测可以通过
#include<cstdio>
#include<iostream>
#include<cstdlib>
#include<assert.h>
#define RI register int
#define CI const int&
using namespace std;
const int N=150,D=5,INF=1e9;
int x,y,a[N+5][N+5];
inline int dist(CI nx,CI ny)
{
return abs(nx-x)+abs(ny-y);
}
inline void set(CI sx,CI sy)
{
assert(a[sx][sy]==0);
a[sx][sy]=1;
printf("%d %d\n",sx,sy); fflush(stdout);
scanf("%d%d",&x,&y);
if (x==0&&y==0) exit(0);
}
inline void solve(CI xl,CI xr,CI yl,CI yr)
{
if (xl==xr)
{
for (RI y=yl;y<=yr;++y) set(xl,y);
return;
}
if (yl==yr)
{
for (RI x=xl;x<=xr;++x) set(x,yl);
return;
}
if (xr-xl<=yr-yl)
{
int ym=yl+yr>>1;
for (RI x=xl;x<=xr;++x) set(x,ym);
if (y<ym) solve(xl,xr,yl,ym-1);
else solve(xl,xr,ym+1,yr);
} else
{
int xm=xl+xr>>1;
for (RI y=yl;y<=yr;++y) set(xm,y);
if (x<xm) solve(xl,xm-1,yl,yr);
else solve(xm+1,xr,yl,yr);
}
}
int main()
{
scanf("%d%d",&x,&y);
set(N,N);
for (RI i=1;i<=10;++i)
set(N,N-i),set(N-i,N);
while (1)
{
int mn=INF,sx,sy;
if (min(N-x,N-y)<=D)
{
for (RI i=N;i>=1;--i)
{
if (!a[i][N]&&dist(i,N)<mn) mn=dist(i,N),sx=i,sy=N;
if (!a[N][i]&&dist(N,i)<mn) mn=dist(N,i),sx=N,sy=i;
}
if (mn!=INF)
{
set(sx,sy); continue;
} else break;
}
if (x==y)
{
bool flag=0;
for (RI i=12;N-i>0;i+=2)
{
if (!a[N-i][N]) { flag=1; set(N-i,N); break; }
if (!a[N][N-i]) { flag=1; set(N,N-i); break; }
}
if (flag) continue;
} else if (x<y)
{
bool flag=0;
for (RI i=12;N-i>0;i+=2)
{
if (!a[N-i][N]) { flag=1; set(N-i,N); break; }
}
if (flag) continue;
for (RI i=12;N-i>0;i+=2)
{
if (!a[N][N-i]) { flag=1; set(N,N-i); break; }
}
if (flag) continue;
} else
{
bool flag=0;
for (RI i=12;N-i>0;i+=2)
{
if (!a[N][N-i]) { flag=1; set(N,N-i); break; }
}
if (flag) continue;
for (RI i=12;N-i>0;i+=2)
{
if (!a[N-i][N]) { flag=1; set(N-i,N); break; }
}
if (flag) continue;
}
mn=INF;
for (RI i=N;i>=1;--i)
{
if (!a[i][N]&&dist(i,N)<mn) mn=dist(i,N),sx=i,sy=N;
if (!a[N][i]&&dist(N,i)<mn) mn=dist(N,i),sx=N,sy=i;
}
if (mn!=INF)
{
set(sx,sy); continue;
} else break;
}
for (RI i=1;i<=N;++i)
assert(a[i][N]),assert(a[N][i]);
assert(x<=N); assert(y<=N);
solve(1,N-1,1,N-1);
return 0;
}
G. Sorting
队友开局写的,我题目都没看
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5+5;
int n, m, vis[N];
signed main() {
ios::sync_with_stdio(0); cin.tie(0);
cin >> n >> m;
for (int i=1; i<=m; ++i) {
int a, b; cin >> a >> b;
if (b==a+1) vis[a] = 1;
}
bool flag = true;
for (int i=n-1; i>0; --i) {
if (!vis[i]) flag = false;
}
cout << (flag ? "Yes\n" : "No\n");
return 0;
}
I. Knapsack Problem
由于周三 VP 的那场有个题和这个几乎一样,就是走到 \(T\) 和从 \(T\) 走回来是没区别的
因此直接用二元组 \((a,b)\) 表示装了 \(a\) 个包,当前包装了 \(b\) 体积,把这个二元组当作权值跑最短路即可
#include<cstdio>
#include<iostream>
#include<vector>
#include<utility>
#include<queue>
#define RI register int
#define CI const int&
using namespace std;
typedef pair <int,int> pi;
const int N=100005,INF=1e9;
int n,m,V,T,vis[N]; pi dis[N]; vector <pi> v[N];
int main()
{
scanf("%d%d%d%d",&n,&m,&V,&T);
for (RI i=1;i<=m;++i)
{
int x,y,z; scanf("%d%d%d",&x,&y,&z);
v[x].push_back({y,z}); v[y].push_back({x,z});
}
for (RI i=1;i<=n;++i) dis[i]={INF,0}; dis[T]={0,0};
priority_queue <pair <pi,int>,vector <pair <pi,int>>,greater <pair <pi,int>>> hp;
hp.push({dis[T],T});
while (!hp.empty())
{
auto [cur,now]=hp.top(); hp.pop();
if (vis[now]) continue; vis[now]=1;
for (auto [to,w]:v[now])
{
pi nxt=cur;
if (nxt.second+w<=V) nxt.second+=w;
else ++nxt.first,nxt.second=w;
if (nxt<dis[to]) dis[to]=nxt,hp.push({dis[to],to});
}
}
for (RI i=1;i<=n;++i)
{
if (dis[i].first==INF)
{
printf("%d ",-1);
continue;
}
printf("%d ",dis[i].first+1);
}
return 0;
}
J. Moving on the Plane
首先把曼哈顿距离转为切比雪夫距离,不难发现此时移动 \(k\) 次在切式距离下等价于在 \(x,y\) 方向上都移动 \(k\) 次
因此可以将两维分开考虑,最后方案数独立直接乘起来即可,以下均以 \(x\) 方向上考虑
不妨枚举最后所有点都在 \([\overline{x},\overline{x}+k]\) 范围内,计算每个点 \(x_i\) 走到这个范围内的方案数就是简单的组合数求和
但这样计数显然会重,因此我们钦定只统计坐标最小值为 \(\overline{x}\) 的方案数,只需要用 \([\overline{x},\overline{x}+k]\) 的方案减去 \([\overline{x}+1,\overline{x}+k]\) 的方案即可
\(y\) 方向上同理,总复杂度 \(O(nmk)\)
#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=55,S=100005,mod=998244353,INF=1e9;
int n,m,k,fact[S],ifac[S],x[N],y[N];
inline void inc(int& x,CI y)
{
if ((x+=y)>=mod) x-=mod;
}
inline void dec(int& x,CI y)
{
if ((x-=y)<0) x+=mod;
}
inline int quick_pow(int x,int p=mod-2,int mul=1)
{
for (;p;p>>=1,x=1LL*x*x%mod) if (p&1) mul=1LL*mul*x%mod; return mul;
}
inline void init(CI n)
{
fact[0]=1;
for (RI i=1;i<=n;++i) fact[i]=1LL*fact[i-1]*i%mod;
ifac[n]=quick_pow(fact[n]);
for (RI i=n-1;i>=0;--i) ifac[i]=1LL*ifac[i+1]*(i+1)%mod;
}
inline int C(CI n,CI m)
{
if (n<m||n<0||m<0) return 0;
return 1LL*fact[n]*ifac[m]%mod*ifac[n-m]%mod;
}
inline int solve(CI mnx,CI mxx,int* x)
{
int res=0;
for (RI X=mnx-m;X<=mxx+m;++X)
{
auto calc=[&](CI l,CI r)
{
int res=1;
for (RI i=1;i<=n;++i)
{
int cur=0;
for (RI j=l;j<=r;++j)
{
int d=abs(x[i]-j);
if ((m+d)%2==1) continue;
inc(cur,C(m,(m+d)/2));
}
res=1LL*res*cur%mod;
if (res==0) break;
}
return res;
};
// printf("X = %d, calc = %d\n",X,calc(X,X+k));
inc(res,calc(X,X+k));
if (X+1<=X+k) dec(res,calc(X+1,X+k));
}
return res;
}
int main()
{
scanf("%d%d%d",&n,&m,&k); init(m);
int mnx=INF,mxx=-INF,mny=INF,mxy=-INF;
for (RI i=1;i<=n;++i)
{
scanf("%d%d",&x[i],&y[i]);
int tx=x[i],ty=y[i];
x[i]=tx+ty; y[i]=tx-ty;
mnx=min(mnx,x[i]); mxx=max(mxx,x[i]);
mny=min(mny,y[i]); mxy=max(mxy,y[i]);
}
return printf("%d",1LL*solve(mnx,mxx,x)*solve(mny,mxy,y)%mod),0;
}
M. Teleporter
很容易想到令 \(f_{t,x}\) 表示经过 \(t\) 次传送后,到达点 \(x\) 的最短路
不难发现按照 \(t\) 分层考虑,每层内部相当于重新跑一遍最短路,然后队友写了 DJ 发现过不了
(PS:其实是能过的,但因为数组两维开反了导致被卡 Cache 了,后面交了一次其实是能过的)
然后此时队友灵机一动想到用 SPFA 试试,然后换上去发现确实跑过了,以为是出题人没造数据卡
赛后被银牌✌指导才发现 TMD 树上最短路直接向上搜一遍向下搜一遍就行了,感觉已经没有人类了
不过换句话说这也能说明 SPFA 的复杂度放在这题上就是对的,太有智慧了闪神
PS:下面的代码是我在 DJ 做法的基础上交换数组顺序的版本,SPFA 版本没存下来,只要把中间最短路部分简单修改即可
#include<bits/stdc++.h>
using namespace std;
#define int long long
using pii = pair<int, int>;
const int INF = (int)1e18+5;
const int N = 5005;
int dp[N][N], ind[N];
vector<pii> G1[N];
vector<int> G2[N];
// int dfs(int x, int k) {
// if (dp[x][k]>-1) return dp[x][k];
// int ans = INF;
// for (auto [v, w] : G1) {
// ans = min(dfs(v, k)+w, ans);
// }
// }
void dfs1(int x, int f) {
for (auto [v, w] : G1[x]) {
if (v==f) continue;
dp[0][v] = dp[0][x] + w;
dfs1(v, x);
}
}
signed main() {
ios::sync_with_stdio(0); cin.tie(0);
int n, m; cin >> n >> m;
for (int i=1; i<n; ++i) {
int u, v, w; cin >> u >> v >> w;
G1[u].push_back({v, w}); G1[v].push_back({u, w});
}
for (int i=1; i<=m; ++i) {
int u, v; cin >> u >> v;
G2[u].push_back(v); G2[v].push_back(u);
}
for (int i=1; i<=n; ++i) G2[i].push_back(i);
dp[0][1] = 0; dfs1(1, -1);
for (int j=1; j<=n; ++j) {
for (int i=1; i<=n; ++i) dp[j][i] = INF;
for (int x=1; x<=n; ++x) {
for (auto v : G2[x]) {
dp[j][v] = min(dp[j][v], dp[j-1][x]);
}
}
priority_queue<pii> Q;
for (int i=1; i<=n; ++i) Q.push({-dp[j][i], i}), ind[i]=0;
while (!Q.empty()) {
auto [_, x] = Q.top(); Q.pop();
if (ind[x]) continue;
ind[x] = 1;
for (auto [v, w] : G1[x]) {
if (dp[j][v] > dp[j][x]+w) {
dp[j][v] = dp[j][x]+w;
Q.push({-dp[j][v], v});
}
}
}
}
for (int i=0; i<=n; ++i) {
int sum = 0;
for (int j=1; j<=n; ++j) sum += dp[i][j];
cout << sum << '\n';
}
return 0;
}
Postscript
果然有银牌✌ 参与一场就把名额打满了,再也没有去年的提心吊胆算排名了

浙公网安备 33010602011771号