The 2024 ICPC Asia East Continent Online Contest (I)
写在前面
补题地址:https://codeforces.com/contest/2005。
以下按个人难度向排序。
复刻 CCPC 网赛开头超顺利但是三个人坐牢同一个题四个小时没出哈哈太唐乐
题解 by dls:https://qoj.ac/blog/bulijiojiodibuliduo/blog/994
M 签到
模拟即可。
复制复制#include <bits/stdc++.h> using namespace std; #define ll long long #define ull unsigned long long const ll p = 998244353; const int kN = 1e6 + 10; int num, cnt[26], solved[kN][26]; std::map <std::string, int> id; int main() { ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int T; std::cin >> T; while (T --) { for (int i = 0; i < 26; ++ i) cnt[i] = 0; num = 0; id.clear(); int n; std::cin >> n; while (n --) { std::string s, t; char name; std::cin >> s >> name >> t; if (t[0] != 'a') continue; if (!id.count(s)) { id[s] = ++ num; for (int i = 0; i < 26; ++ i) solved[num][i] = 0; } int d = id[s], p = name - 'A'; if (solved[d][p]) continue; solved[d][p] = 1; ++ cnt[p]; } int ans = 0, c = 0; for (int i = 0; i < 26; ++ i) if (cnt[i] > c) ans = i, c = cnt[i]; cout << (char) ('A' + ans) << "\n"; } return 0; }
F 笛卡尔树 or 单调栈,dfs or ST 表,排序
场上 wenqizhi 直接高呼笛卡尔树秒了,我一听笛卡尔树就嗯了一看题真就傻逼题直接秒了。
发现最优情况下,每次操作一定仅会操作两个数,且合并的过程一定是每次找到极大的全局最小值的区间,并依次将每个全局最小值与相邻的第一个大于它的值操作,直至全部变成这个值。
发现这个过程直接放到笛卡尔树上,自底向下地根据区间长度统计贡献即可。建树后直接 dfs,总时间复杂度 级别。
当然用单调栈+排序 / ST 表 + dfs 实现也可以,复杂度多一个 。
#include <bits/stdc++.h> using namespace std; #define ll long long #define ull unsigned long long const ll p = 998244353; const int kN = 2e5 + 10; int n, rt, top, a[kN], st[kN]; int lson[kN], rson[kN]; ll ans; void dfs(int u_, int L_, int R_) { if (lson[u_]) { dfs(lson[u_], L_, u_ - 1); if (a[lson[u_]] < a[u_]) ans += u_ - 1 - L_ + 1; } if (rson[u_]) { dfs(rson[u_], u_ + 1, R_); if (a[rson[u_]] < a[u_]) ans += R_ - (u_ + 1) + 1; } } int main() { ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int T; std::cin >> T; while (T --) { std::cin >> n; for (int i = 1; i <= n; ++ i) std::cin >> a[i], lson[i] = rson[i] = 0; st[top = 0] = rt = 0; for (int i = 1; i <= n; ++ i) { int k = top; while (k > 0 && a[st[k]] < a[i]) -- k; if (k) rson[st[k]] = i; if (k < top) lson[i] = st[k + 1]; st[++ k] = i; top = k; } rt = st[1]; ans = 0; dfs(rt, 1, n); cout << ans << "\n"; } return 0; }
A 大力讨论,结论
显然实际的实力值是无用的,仅需考虑有多少队伍比中国队弱即可,称他们为弱弱队。
然后场上和 dztlb 大力模拟讨论下达到每个阶段所需的弱弱队数量就做完了。
一个很天才的地方是发现 8 强进 4 强,和 4 强进 2 强规则是一致的,然后发现 8 强进 4 强所需的弱弱队数变化为 ,于是大胆猜测 4 强进 2 强的变化也类似地有:。
Code by dztlb:
#include <bits/stdc++.h> using namespace std; #define ll long long #define ull unsigned long long const ll p = 998244353; int read() { int x = 0; bool f = false; char c = getchar(); while(c < '0' || c > '9') f |= (c == '-'), c = getchar(); while(c >= '0' && c <= '9') x = (x << 1) + (x << 3) + (c & 15), c = getchar(); return f ? -x : x; } ll qpow(ll x_, ll y_, ll mod_ = p) { ll ret = 1; while (y_) { if (y_ & 1) ret = ret * x_ % mod_; x_ = x_ * x_ % mod_, y_ >>= 1ll; } return ret; } int T; int a[50]; int main() { ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); cin>>T; while(T--){ for(int i=1;i<=32;++i){ cin>>a[i]; } int cnt=0; for(int i=2;i<=32;++i){ if(a[1]>a[i]) ++cnt; } if(cnt>=31){ puts("1"); continue; } if(cnt>=27){ puts("2"); continue; } if(cnt>=13){ puts("4"); continue; } if(cnt>=6){ puts("8"); continue; } if(cnt>=2){ puts("16"); continue; } puts("32"); } return 0; }
G 二分答案,前缀和
对于最优化中位数,一个众所周知的套路是考虑二分答案 ,仅需检查数列中不小于 的数的数量,是否不小于 即可,然后枚举 的所有区间 即可求得 是否不小于 。
然后考虑如何求得 是否不小于 ,仅需检查所有子区间 对应的 中不小于 的数量,是否不少于子区间数量的一半即可。同理可求得 是否不小于 。
使用前缀和维护即可,总时间复杂度 级别。
Code by wenqizhi:
#include <bits/stdc++.h> using namespace std; #define int long long #define ll long long #define ull unsigned long long const ll p = 998244353; int read() { int x = 0; bool f = false; char c = getchar(); while(c < '0' || c > '9') f |= (c == '-'), c = getchar(); while(c >= '0' && c <= '9') x = (x << 1) + (x << 3) + (c & 15), c = getchar(); return f ? -x : x; } const int N = 2005; int n, A[N], a[N], d[N], b[N][N]; ll sum1[N][N], sum2[N][N], c[N][N]; bool check(int mid) { for(int i = 1; i <= n; ++i) a[i] = (A[i] >= mid); for(int i = 1; i <= n; ++i) sum1[i][i] = a[i]; for(int i = 1; i <= n; ++i) sum2[i][i] = b[i][i] = a[i]; for(int len = 2; len <= n; ++len) for(int l = 1, r = l + len - 1; r <= n; ++l, ++r) { sum1[l][r] = sum1[l][r - 1] + a[r]; sum2[l][r] = b[l][r] = (len / 2 + 1 <= sum1[l][r]); } for(int r = 1; r <= n; ++r) for(int l = 1; l <= r; ++l) sum2[l][r] += sum2[l - 1][r]; for(int i = 1; i <= n; ++i) c[i][i] = b[i][i]; for(int len = 2; len <= n; ++len) for(int l = 1, r = l + len - 1; r <= n; ++l, ++r) { c[l][r] = c[l][r - 1] + sum2[r][r] - sum2[l - 1][r]; } int ans = 0; for(int i = 1; i <= n; ++i) for(int j = i; j <= n; ++j) ans += (c[i][j] >= (j - i + 1) * (j - i + 2) / 2 / 2 + 1); return ans >= (n * (n + 1) / 2) / 2 + 1; } signed main() { n = read(); for(int i = 1; i <= n; ++i) A[i] = d[i] = a[i] = read(); sort(d + 1, d + n + 1); int l = 1, r = n; while(l < r) { int mid = (l + r + 1) >> 1; if(check(d[mid])) l = mid; else r = mid - 1; } printf("%lld\n", d[l]); return 0; }
C 结论,图论,剩余系,线性代数
牛逼提!让我想起 ICPC2021 Jinan 的 J(补题在 PTA 上),赛后一看也是北大出的题那可以理解了,感觉这两题肯定是一块出出来的。
考虑将 个限制 看做 条线段,大力手玩发现结论:答案为 0 当且仅当存在一条线段,使得这条线段可以被其他若干条线段完美地拼出来。于是考虑并查集实现,顺序枚举线段每次检查 是否在一个集合中,若在则满足上述情况答案为 0,否则将 merge 起来。
但是为什么会有这个结论?感觉和题目完全没关系啊?证明需要考虑线性代数:
考虑构造一个 的矩阵 用于表示每个位置可选的权值的范围。对于第 行,位置 为 1,其他位置为 0。则对于任意一个排列(不一定符合题意),都对应矩阵上一种选择 个位置的方案,使得每一行、每一列至多选择一个位置;若为符合题意的有贡献的排列,则选择的所有位置上均为 1。
发现上述选择位置的方案,与矩阵行列式的逆序定义相符,当且仅当选择的位置里均为 1 才有贡献 1,否则为 0。且发现 ,即在 剩余系下,合法排列的方案数为偶数当且仅当行列式的值为 0,这又等价于在 剩余系下矩阵的秩小于 ,即各分量线性相关。
于是仅需判断各分量是否线性相关即可,又观察到每行上的 1 都是连续的,因此不必要真的通过高斯消元求秩,仅需通过并查集转化为图论问题实现即可,此时的并查集实际上也可以看做一种消元法的模拟。
// /* By:Luckyblock */ #include <bits/stdc++.h> #define LL long long const int kN = 1e6 + 10; //============================================================= int n, fa[kN]; //============================================================= int find(int x_) { return fa[x_] == x_ ? x_ : fa[x_] = find(fa[x_]); } void merge(int x_, int y_) { int fx = find(x_), fy = find(y_); if (fx == fy) return ; fa[fx] = fy; } //============================================================= int main() { //freopen("1.txt", "r", stdin); std::ios::sync_with_stdio(0), std::cin.tie(0); int T; std::cin >> T; while (T --) { std::cin >> n; for (int i = 0; i <= n; ++ i) fa[i] = i; int ans = 1; for (int i = 1; i <= n; ++ i) { int l, r; std::cin >> l >> r; if (find(l - 1) == find(r)) ans = 0; merge(l - 1, r); } std::cout << ans << "\n"; } return 0; }
L 图论转化,建图技巧,最短路
照例大力手玩。发现对于每种操作,考虑看做一张每个节点出度均为 1 的基环内向森林,则有如下结论:
- 若图中均为环,则该操作在任何情况下均不会出现碰撞,且可令空位在其所在的环中任意移动;
- 若图中有点入度不小于 3,则该操作一定会出现碰撞;
- 若图中有不少于两个点入度为 2,则该操作一定会出现碰撞;
- 若图中有且仅有一个点 入度为 2,则当前仅当空位出现在指向 的两个点上时,该操作才不会出现碰撞,且操作后空位一定会移动到入度为 0 的点,且可证明入度为 0 的点仅有一个。
于是合法的操作可以看做:
- 在图中加若干个环,保证每个点出现且仅出现在一个环中;
- 在图中加两条有向边。
询问即动态查询在当前有向图上的连通性。
动态加边、维护有向图连通性做不到呃呃,于是套路地考虑离线,先把图全部建出来,并令每条边的边权值为加入该边的时间,则查询仅需路径 上边权最大值是否不大于 即可。
发现需要建图后枚举所有点作为起点,都跑一遍最短路。则直接按照上述定义建图不行,第一种操作边的数量会是 级别的会挂掉,于是考虑优化第一种操作加环的建边。发现加环 ,等价于加数量级相同的双向边:。考虑加这种双向边时顺便使用并查集维护连通性,若已连通则不连边。易证此时第一种操作的连边数量不多于 级别,总连边数量为 级别。
然后枚举所有点作为起点都跑一遍最短路即可 回答所有询问。
则总时间复杂度为 级别。
// /* By:Luckyblock */ #include <bits/stdc++.h> #define LL long long #define pr std::pair #define mp std::make_pair const int kN = 2010; //============================================================= int n, l, q; int fa[kN][2], into[kN]; std::vector<pr<int, int> > edge[kN]; int dis[kN][kN]; bool vis[kN]; //============================================================= int get(char a_, char b_) { int ret = ((int) a_ - 48) * 50 + ((int) b_ - 48); return ret; } int find(int id_, int x_) { return fa[x_][id_] == x_ ? x_ : fa[x_][id_] = find(id_, fa[x_][id_]); } void merge(int id_, int x_, int y_) { int fx = find(id_, x_), fy = find(id_, y_); if (fx == fy) return ; fa[fx][id_] = fy; } void addedge(int u_, int v_, int w_) { edge[u_].push_back(mp(v_, w_)); } void init() { for (int i = 1; i <= n; ++ i) edge[i].clear(), fa[i][0] = fa[i][1] = i; for (int time = 1; time <= l; ++ time) { std::string s; std::cin >> s; int flag = 0; for (int i = 1; i <= n; ++ i) { vis[i] = 0, into[i] = 0, fa[i][1] = i; } for (int i = 0; i < 2 * n; i += 2) { int x = get(s[i], s[i + 1]); ++ into[x]; if (into[x] == 2) ++ flag; if (into[x] == 3) flag = kN; merge(1, i / 2 + 1, x); } if (flag >= 2) continue; if (flag) { int u = 0, v1 = 0, v2 = 0; for (int i = 0; i < 2 * n; i += 2) { int p = i / 2 + 1, x = get(s[i], s[i + 1]); if (into[p] == 0) u = p; if (into[x] == 2 && v1 != 0 && v2 == 0) v2 = p; if (into[x] == 2 && v1 == 0) v1 = p; } addedge(v1, u, time), addedge(v2, u, time); } else { for (int i = 1; i <= n; ++ i) { if (find(0, find(1, i)) == find(0, i)) continue; addedge(i, find(1, i), time), addedge(find(1, i), i, time); merge(0, find(1, i), i); } } } } int query(int a_, int b_, int c_) { return dis[a_][b_] <= c_; } void dijkstra(int s_) { std::priority_queue <pr <int, int> > q; for (int i = 1; i <= n; ++ i) vis[i] = 0, dis[s_][i] = kN; dis[s_][s_] = 0; q.push(mp(0, s_)); while (!q.empty()) { int u = q.top().second; q.pop(); if (vis[u]) continue; vis[u] = 1; for (auto [v, w]: edge[u]) { if (dis[s_][v] > std::max(dis[s_][u], w)) { dis[s_][v] = std::max(dis[s_][u], w); q.push(mp(-dis[s_][v], v)); } } } } //============================================================= int main() { // freopen("1.txt", "r", stdin); std::ios::sync_with_stdio(0), std::cin.tie(0); int T; std::cin >> T; while (T --) { std::cin >> n >> l >> q; init(); for (int i = 1; i <= n; ++ i) dijkstra(i); while (q --) { std::string s; std::cin >> s; int a = get(s[0], s[1]), b = get(s[2], s[3]), c = get(s[4], s[5]); std::cout << query(a, b, c); } std::cout << "\n"; } return 0; }
H 括号序列,网络流
见过括号序列转换成差分约束的,这下又见到转换成网络流的了,括号序列真是牛逼。
写在最后
参考:
学到了什么:
- C:有特殊的数学限制,考虑转化成数学模型,并考虑数学模型下限制的等价形式。
- G:最优化中位数,套路二分答案;
- H:小范围下,有数量的约束关系,考虑跑网络流确定方案。
然后日常夹带私货:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!