Loading

[题解] 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 个数,最小化

\[\sum_{i = 1}^{n - k}\sum_{j = i + 1}^{n - k} \gcd(\left| a_i - a_j \right|, n) \]

贪心地删去贡献最大的 \(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再从根转出去。

posted @ 2025-09-09 15:18  Music163  阅读(2260)  评论(2)    收藏  举报