[题解] 2025 ICPC 网络赛 1 The 2025 ICPC Asia East Continent Online Contest (I)
2025 ICPC 网络赛 1 (9题)
The 2025 ICPC Asia East Continent Online Contest (I)
补题链接(QOJ)
A
简要题意:
模拟 ICPC 比赛,结束之后尚未解绑,此时算一算谁有可能是冠军。给出所有队伍的所有提交。
先对时间排序,压缩已知的过题数和罚时,压缩未知的过题数和罚时。
对于每个队伍,假设别人封榜后都没过题,查看一下有没有可能是冠军。
时间复杂度:\(O(\sum s\log s)\),\(s\) 为提交数。
#include <bits/stdc++.h>
using ld = long double;
using i64 = long long;
using namespace std;
void solve() {
int n;
cin >> n;
vector<tuple<int, string, char, string>> verdicts(n + 1);
for (int i = 1; i <= n; i++) {
string team, status;
char problem;
int time;
cin >> team >> problem >> time >> status;
verdicts[i] = {time, team, problem, status};
}
sort(verdicts.begin() + 1, verdicts.end(), [](const auto &a, const auto &b) {
return get<0>(a) < get<0>(b);
});
map<string, vector<int>> mp;
map<string, vector<int>> res;
for (int i = 1; i <= n; i++) {
auto [time, team, problem, status] = verdicts[i];
if (!mp.count(team)) {
mp[team] = vector<int>(26);
res[team] = vector<int>(4);
}
auto &penalty = mp[team][problem - 'A'];
if (status == "Accepted") {
if (penalty != -1) {
res[team][0]++;
penalty += time;
res[team][1] += penalty;
penalty = -1;
}
} else if (status == "Rejected") {
if (penalty != -1) {
penalty += 20;
}
} else { // Unknown
if (penalty != -1) {
res[team][2]++;
penalty += time;
res[team][3] += penalty;
penalty = -1;
}
}
}
pair<int, int> mi;
for (auto &[team, vec] : res) {
if (mi.first < vec[0] || mi.first == vec[0] && mi.second > vec[1]) {
mi = {vec[0], vec[1]};
}
}
vector<string> ans;
for (auto &[team, vec] : res) {
pair<int, int> me = {vec[0] + vec[2], vec[1] + vec[3]};
if (mi.first < me.first || mi.first == me.first && mi.second >= me.second) {
ans.push_back(team);
}
}
for (auto &s : ans) {
cout << s << ' ';
}
cout << '\n';
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int tt = 1;
cin >> tt;
while (tt--) {
solve();
}
return 0;
}
B
简要题意:
有一个排列 \(a = {1, 2, ... , n}\),从中删去 k 个数,最小化
贪心地删去贡献最大的 \(a_i\)。并删掉 与之相关联的 \(a_j\) 的贡献。
时间复杂度:\(O(n^2\log n)\)。
#include <bits/stdc++.h>
using ld = long double;
using i64 = long long;
using namespace std;
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n, k;
cin >> n >> k;
vector<int> a(n + 1);
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (i == j) continue;
a[i] += gcd(abs(i - j), n);
}
}
set<int> ans;
for (int i = 1; i <= k; i++) {
int mx = -1, mxi = 0;
for (int j = 1; j <= n; j++) {
if (a[j] != -1 && mx < a[j]) {
mx = a[j];
mxi = j;
}
}
ans.insert(mxi);
for (int j = 1; j <= n; j++) {
if (mxi == j) {
a[j] = -1;
} else {
a[j] -= gcd(abs(j - mxi), n);
}
}
}
for (int e : ans) {
cout << e << ' ';
}
cout << '\n';
return 0;
}
注意以下代码也可以通过:
原因:考虑 \(\left| a_i - a_j \right|\)可能为 1 ~ n 中的每个数,对于 1 <= m < n,m + 1 个数中至少有一对数 产生的 \(\left| a_i - a_j \right| \mod m = 0\),即 \(a_i\) 与 \(a_j\) 同余,这对数产生的贡献是 >= \(\gcd (m, n)\) 的,此时 这种对数越多,贡献越多。所以 只需 缩小 1 <= m < n 之间的所有 这种对数,只需要选凑在一起的数就行了。
#include <bits/stdc++.h>
using ld = long double;
using i64 = long long;
using namespace std;
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n, k;
cin >> n >> k;
for (int i = 1; i <= k; i++) {
cout << i << " \n"[i == k];
}
return 0;
}
C
简要题意:
1 ~ n 个位置 每个位置有一个线段(共 n 个),每个线段具有一个颜色 a[i]。起初他们的颜色都不同。
给定 m 个咒语,形式为 l, r。我们可以选择 l <= u, v <= r 令 a[u] = a[v]。咒语可以不使用,咒语可以按任意顺序使用。
最小化颜色种数。
因为染色的顺序可以改变,如果可以选择的 <i, i + 1> 有 k 对,那么答案就是 n - k。
将咒语按照 右端点排序,然后从左端点开始找位置染。试 选择 <l, l + 1>,如果不行,就选择 <l + 1, l + 2> .... 一直选到 <r - 1, r>。
先染右端点靠前的是因为,如果过了 r ,这个咒语就不能用了。最小化颜色需要尽可能的用上最多的咒语。
方法1:直接用动态开点线段树维护。\(O(m\log V)\) V 为值域。
#include <bits/stdc++.h>
using ld = long double;
using i64 = long long;
using namespace std;
const int maxn = 2e5 + 5;
// 注意 tot 必须初始值是 1,即需要手动开第一个点
struct DST {
int tot = 1;
struct Node {
int ls, rs, v;
} t[maxn * 32]; // Q(查询次数) * log V(值域大小)
// 在 [L, R] 找到 [l, r] 可以染色的点
// int rt = 1;
// add(rt, l, r);
bool add(int &p, int L, int R, int l, int r) {
if (!p) p = ++tot; // 开点
auto &me = t[p];
if (r < L || R < l) return false;
if (L == R) { // 确定了一个点
if (!me.v) { // 这个点没染
me.v = 1;
return true;
}
return false;
}
// [L, R] 都涂满了
if (me.v == R - L + 1) return false;
int M = L + R >> 1;
bool res = add(t[p].ls, L, M, l, r);
if (!res) res |= add(t[p].rs, M + 1, R, l, r);
me.v = t[me.ls].v + t[me.rs].v; // pushup
return res;
}
void clear() {
for (int i = 1; i <= tot; i++) {
t[i] = {0, 0, 0};
}
tot = 1;
}
} T;
void solve() {
int m, n;
cin >> m >> n;
vector<pair<int, int>> a(m + 1);
for (int i = 1; i <= m; i++) {
int l, r;
cin >> l >> r;
a[i] = {l, r};
}
sort(a.begin() + 1, a.end(), [](const auto &a, const auto &b) {
return a.second < b.second;
});
for (int i = 1; i <= m; i++) {
if (a[i].first == a[i].second) continue;
int rt = 1;
T.add(rt, 1, n, a[i].first, a[i].second - 1);
}
// 染了 T.t[1].v 次
cout << n - T.t[1].v << '\n';
T.clear();
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int tt = 1;
cin >> tt;
while (tt--) {
solve();
}
return 0;
}
方法2:使用优先队列维护。
这个方法也是基于 右端点越靠前的 优先选的原则。只不过 经过了左端点才能用这个咒语,这个时候再把他push进优先队列。
\(O(n\log n)\)
#include <bits/stdc++.h>
using ld = long double;
using i64 = long long;
using namespace std;
void solve() {
int m, n;
cin >> m >> n;
map<int, vector<int>> mp;
for (int i = 1; i <= m; i++) {
int l, r;
cin >> l >> r;
mp[l].push_back(r);
}
vector<pair<int, vector<int>>> spe(mp.begin(), mp.end());
priority_queue<int, vector<int>, greater<>> pq;
int L = 1;
for (int i = 0; i < spe.size(); i++) {
auto &[l, vec] = spe[i];
for (auto &r : vec) {
pq.push(r);
}
L = max(L, l);
int nxt = i + 1 == spe.size() ? 1e9 : spe[i + 1].first;
while (!pq.empty() && L < nxt) {
if (L < pq.top()) {
n--;
L++;
}
pq.pop();
}
}
cout << n << '\n';
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int tt = 1;
cin >> tt;
while (tt--) {
solve();
}
return 0;
}
D
简要题意:
给定一个无根树,有点权,可以通过删边把树分成多个联通块,联通块的值 为 联通块内点权的极差,最大化 所有联通块的值的和。
设点权数组是 a。对于一个点 i,如果作为最大值时,贡献是 a[i],最小值时贡献是 -a[i],不是最大也不是最小的时候 贡献是 0。对于一个联通块来说,让 最大值 贡献 a[i] ,让最小值贡献 -a[i] 其实是最优的,所以我们可以 枚举子树里每个点 贡献是 a[i] 和 -a[i] 的情形。得到的结果 一定是 + 大值 - 小值。
枚举的时候可以使用 二进制的最低两位,一个代表拿了 + ,一个代表拿了 - 。 \(00_2 \ 01_2 \ 10_2 \ 11_2\) (0 ~ 3),比较好写。\(11_2\) 属于已经构成了一个联通块的情况,可以转到 \(00_2\)。
\(O(n)\)
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1100000,inf=1e18;
int n,a[N];
int f[N][4];
vector<int> g[N];
void dfs(int u,int from){
f[u][1]=a[u];
f[u][2]=-a[u];
f[u][3]=-inf;
for(unsigned i=0;i<g[u].size();i++){
int v=g[u][i];
if(v==from)continue;
dfs(v,u);
int F[4]={0,-inf,-inf,-inf};
for(int x=0;x<4;x++){
for(int y=0;y<3;y++){
if(x&y)continue;
F[x|y]=max(F[x|y],f[u][x]+f[v][y]);
}
}
for(int x=0;x<4;x++)f[u][x]=F[x];
}
f[u][0]=max(f[u][0],f[u][3]);
return;
}
signed main(){
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1,u,v;i<n;i++){
cin>>u>>v;
g[u].push_back(v);
g[v].push_back(u);
}
dfs(1,0);
printf("%lld\n",f[1][0]);
return 0;
}
F
简要题意:
给定一个机器人,只能在第一象限(x > 0, y > 0)活动,玩家每一步可以布置地雷,机器人走完一步,如果在地雷上就死,玩家需要在1000步内杀死机器人。每次布置地雷之后会给出机器人的位置。地雷可以在 (0 < x <= 1000, 0 < y <= 1000) 内布置。
一种构造方法是,在外侧构造出一个直角,与 x轴和 y轴 构成一个矩形。框住机器人。
不过显然直接挨个布置地雷是不可行的,因为我们边布置,机器人也会边移动,我们总是不能框住机器人。
考虑 布置 两个地雷,然后空一个格子。形如
...xxoxxoxxoxx...
这种方式的好处就是,一旦机器人贴到这堵墙上,我们可以只用一次布置就能挡住机器人。
除此之外,因为有空格,所以速度比机器人更快,我们可以快速布置完矩形的上边界,然后领先出矩形的右边界的长度,然后在机器人到达右边界前,填满右边界。
|xxoxxoxx....xxoxxx
| x
| x
| x
|_________________x
最后我们再把 上面空的 o 填上。
注意填的过程中,我们要注意 机器人 是否碰到了上边界,必须再下一步布置上地雷。
围好矩形之后,我们可以对半把矩形分开,机器人必然会落在两半中的一半,然后我们继续分开,机器人必然会落在两半中的一半...最终用不了几个地雷就能把机器人框住。
时间复杂度:\(O(n^2)\),瓶颈在围出矩形后最后补 o。
#include <bits/stdc++.h>
using ld = long double;
using i64 = long long;
using namespace std;
const int N = 210, M = 45;
int vis[N + 10][M + 10], cnt;
struct Pt {
int x, y;
};
Pt Q() {
int x, y;
cin >> x >> y;
if (x > N || y > M) assert(0);
if (x == 0 && y == 0) exit(0);
return {x, y};
}
void A(int x, int y) {
cnt++;
vis[x][y] = 1;
cout << x << ' ' << y << endl;
}
bool B(int x) {
if (x > 1 && !vis[x - 1][M]) {
A(x - 1, M);
} else if (!vis[x][M]) {
A(x, M);
} else if (x < N && !vis[x + 1][M]) {
A(x + 1, M);
} else {
return false;
}
return true;
}
bool C() {
for (int i = 1; i <= N; i++) {
if (!vis[i][M]) return 1;
}
return 0;
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
Pt cur = Q();
A(cur.x, M);
int lx = cur.x - 1;
int rx = cur.x;
while (rx < N) {
cur = Q();
if (cur.y == M - 1 && B(cur.x)) {
;
} else if (cur.x - lx < rx - cur.x && lx >= 1) {
if (lx % 3 == 0) lx--;
A(lx, M);
lx--;
} else {
if (rx % 3 == 0) rx++;
A(rx, M);
rx++;
}
}
int dy = M;
while (dy >= 1) {
cur = Q();
if (cur.y == M - 1 && B(cur.x)) {
;
} else {
A(N, dy);
dy--;
}
}
while (C()) {
cur = Q();
if (cur.y == M - 1 && B(cur.x)) {
;
} else {
for (int i = 1; i <= N; i++) {
if (!vis[i][M]) {
A(i, M);
break;
}
}
}
}
cur = Q();
lx = 0, rx = N;
int ly = 0, ry = M;
while (rx - lx >= 1 && ry - ly >= 1) {
if (rx - lx >= ry - ly) {
int mid = lx + rx >> 1;
for (int i = ly + 1; i <= ry - 1; i++) {
A(mid, i);
cur = Q();
}
if (cur.x < mid) {
rx = mid;
} else {
lx = mid;
}
} else {
int mid = ly + ry >> 1;
for (int i = lx + 1; i <= rx - 1; i++) {
A(i, mid);
cur = Q();
}
if (cur.y < mid) {
ry = mid;
} else {
ly = mid;
}
}
}
return 0;
}
G
必须保证相邻两项都被排序。
#include <bits/stdc++.h>
using ld = long double;
using i64 = long long;
using namespace std;
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n, m;
cin >> n >> m;
set<pair<int, int>> ss;
while (m--) {
int a, b;
cin >> a >> b;
ss.insert({a, b});
}
for (int i = 2; i <= n; i++) {
if (!ss.count({i - 1, i})) {
cout << "No\n";
return 0;
}
}
cout << "Yes\n";
return 0;
}
I
简要题意:
从 1 ~ n 每个点出发,到达 T 点。走的过程中携带背包,经过一条边时,必须要将 重量为边权的物品 加入背包中,装不下就换新的,问每个点最少需要多少个背包才能到达。
初始时有一个背包。到达不了的点输出 -1。
考虑从 T 走到 1 ~ n 每个点。
对于正着走,最后一个背包可能没装满,反着走的时候 这是第一个背包,可以装更多物品,这样使用的背包数量 <= 正着走的。
对于反着走,最后一个背包可能没装满,正着走的时候 这是第一个背包,可以装更多物品,这样使用的背包数量 <= 反着走的。
所以其实正着走和反着走花费的背包数量是相同的。
直接写单源最短路即可。
时间复杂度 \(O((n+m)\log n)\)。
#include <bits/stdc++.h>
using ld = long double;
using i64 = long long;
using namespace std;
const int maxn = 1e5 + 5;
vector<pair<int, int>> g[maxn];
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n, m, V, T;
cin >> n >> m >> V >> T;
while (m--) {
int u, v, w;
cin >> u >> v >> w;
g[u].emplace_back(v, w);
g[v].emplace_back(u, w);
}
// 需要的背包数,已经占用的背包空间
const pair<int, int> inf = {(int)1e9, 0};
vector<pair<int, int>> d(n + 1, inf);
vector<int> vis(n + 1);
priority_queue<pair<pair<int, int>, int>, vector<pair<pair<int, int>, int>>, greater<>> pq;
d[T] = {1, 0};
pq.push({d[T], T});
while (pq.size()) {
auto [C, u] = pq.top();
pq.pop();
if (vis[u]) continue;
vis[u] = 1;
for (auto [v, w] : g[u]) {
pair<int, int> newC = {C.first + (C.second + w > V),
(C.second + w > V ? w : C.second + w)};
if (newC < d[v]) {
d[v] = newC;
pq.push({d[v], v});
}
}
}
for (int i = 1; i <= n; i++) {
if (d[i] == inf) {
cout << -1;
} else {
cout << d[i].first;
}
cout << ' ';
}
return 0;
}
J
简要题意:
平面上有 n 个点,必须进行 M 轮如下操作:每个点都移动 1步,可以 上/下/左/右 (四联通),最终要求 曼哈顿距离最远的两个点 距离不超过 K。
曼哈顿平面 转 切比雪夫平面,移动变成 向 左上/左下/右上/右下 4 个方向移动,所以 x 和 y 都必须动,然后我们 x 和 y 分开考虑,对于 x,最终只需要所有点的 x 坐标差距不超过 K,同理 y 也算出来 直接相乘。
具体怎么算的呢?为了防止计重,可以按照 最终聚集到的线段的 最左端点分类,枚举 [p, p + K] 为最终聚集到的区间,则 p 的位置必须要有点 才能算进答案。
如何计算 p 到 x 的方案数,如果 跟 M 的奇偶性不同 或者 距离 > M,直接就是 0,否则我们可以使用插空法,把反着走的 距离 插到正着走的距离之间。
注意预处理组合数。
\(O(M\log mod + nMK)\),组合数处理的好可以去掉 \(\log mod\),
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int Mod=998244353,N=100,M=1.01e5;
int n,m,K,x[N],y[N],fac[M],ifac[M];
int fpow(int x,int k){
int res=1;
while(k){
if(k&1)res=res*x%Mod;
x=x*x%Mod,k/=2;
}
return res;
}
int inv(int x){
return fpow(x,Mod-2);
}
int C(int x,int y){
if(x<y)return 0;
return fac[x]*ifac[y]%Mod*ifac[x-y]%Mod;
}
int calc(int x){
if(x<0)x=-x;
if((m+x)%2==1)return 0;
return C(m,(m+x)/2);
}
int sol(){
int res=0;
for(int p=-M*3;p<=M*3;p++){
int f=1,g=0;
for(int i=1;i<=n;i++){
int v1=calc(p-x[i]),v2=0;
for(int j=p+1;j<=p+K;j++){
(v2+=calc(j-x[i]))%=Mod;
}
g=(g*(v1+v2)+f*v1)%Mod;
f=f*v2%Mod;
}
(res+=g)%=Mod;
}
return res;
}
signed main(){
fac[0]=1;
for(int i=1;i<M;i++)fac[i]=fac[i-1]*i%Mod;
for(int i=0;i<M;i++)ifac[i]=inv(fac[i]);
cin>>n>>m>>K;
for(int i=1;i<=n;i++){
cin>>x[i]>>y[i];
int _x=x[i]+y[i],_y=x[i]-y[i];
x[i]=_x,y[i]=_y;
}
int ans=sol();
for(int i=1;i<=n;i++)swap(x[i],y[i]);
ans=(ans*sol())%Mod;
printf("%lld\n",ans);
return 0;
}
M
简要题意:
给定一个无向边构成的树,另外有 m 个 在节点间建立的双向 传送通道,在树上行走是需要花费时间(边权)的,使用传送通道可以瞬间在节点间传送。限制传送通道的使用次数 k,求得 从 1 号节点到达每个节点所需的最小花费 的和,对于 0 <= k <= n 每个 k 都要求答案。
考虑 让 k 从小到大求,每次使用 k - 1 得到的结果,在这个结果的基础上 把传送通道的两边的点的距离 取一下 min,然后扔进优先队列里面跑 dijkstra。这样可以用 k - 1 的结果得到 k 的结果。
时间复杂度:\(O(nm\log n)\)。给了 4s,赌一把...过了过了过了!
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=5500,inf=1e18;
int n,m,d[N],dd[N];
struct edge{
int to,w;
edge(int to=0,int w=0):to(to),w(w){}
};
vector<edge> g[N];
int U[N*2],V[N*2];
struct node{
int x,y;
node(int x=0,int y=0):x(x),y(y){}
bool operator < (const node& _)const{return y>_.y;}
};
priority_queue<node> Q;
int vis[N];
void dij(){
for(int i=1;i<=n;i++)Q.push(node(i,d[i])),vis[i]=0;
while(!Q.empty()){
int u=Q.top().x;
Q.pop();
if(vis[u]++)continue;
for(unsigned i=0;i<g[u].size();i++){
int v=g[u][i].to;
if(d[u]+g[u][i].w<d[v]){
d[v]=d[u]+g[u][i].w;
Q.push(node(v,d[v]));
}
}
}
return;
}
signed main(){
cin>>n>>m;
for(int i=1,u,v,w;i<n;i++){
cin>>u>>v>>w;
g[u].push_back(edge(v,w));
g[v].push_back(edge(u,w));
}
for(int i=1;i<=m;i++){
cin>>U[i]>>V[i];
}
for(int i=2;i<=n;i++){
d[i]=inf;
}
for(int T=0;T<=n;T++){
dij();
int ans=0;
for(int i=1;i<=n;i++)dd[i]=d[i],ans+=d[i];
for(int i=1;i<=m;i++){
dd[U[i]]=min(dd[U[i]],d[V[i]]);
dd[V[i]]=min(d[U[i]],dd[V[i]]);
}
for(int i=1;i<=n;i++)d[i]=min(d[i],dd[i]);
printf("%lld\n",ans);
}
return 0;
}
也有一些方法可以去掉这个logn,类似于换根dp的操作。
一种方法是:考虑 在对所有通道两侧 的点 都取了 min之后,接下来我们就是要最小化 1 到所有点的距离,而这个距离只能通过树上的路径转移,对于任何一条树上路径,都是可以分为 先向根走,再从根出发向外走的,所以我们可以两遍dfs ,第一遍dfs把所有的距离转移到根,第二遍dfs再从根转出去。

浙公网安备 33010602011771号