The 2022 ICPC Asia Xian Regional Contest
写在前面
比赛地址:https://codeforces.com/gym/104077。
以下按个人向难度排序。
vp 8 题 900+ 罚时差 100 罚时金。
唉唉现在题数够了还是很爽的,然而唐氏太多白白吃好多发不该。
而且感觉队友在线时长不够啊呃呃必须要 push 一下、、、
F 签到
祝大秦酒店越办越好。
不对我草今年估计 EC 西安应该能拿个名额怎么轮到我去住大秦酒店了我草
Code by wenqizhi:
复制复制#include<bits/stdc++.h> using namespace std; #define ll long long #define ull unsigned long long 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 = 1000; int n, c1, c2; char s[100]; int main() { n = read(), c1 = read(), c2 = read(), c1 = min(c1, c2); if(c1 * 2 <= c2){ printf("%lld\n", 3ll * n * c1); return 0; } ll ans = 0; for(int i = 1; i <= n; ++i) { scanf("%s", s + 1); int flag = 0; if(s[1] == s[2] || s[1] == s[3] || s[2] == s[3]) flag = 1; if(flag) ans += c1 + c2; else ans += c1 * 3; } printf("%lld\n", ans); return 0; }
J 签到
考虑枚举选的数中下标 的最大值,发现之后仅能在 中再选一个数,优先队列搞一下即可。
// /* By:Luckyblock */ #include <bits/stdc++.h> #define LL long long //============================================================= //============================================================= //============================================================= int main() { //freopen("1.txt", "r", stdin); std::ios::sync_with_stdio(0), std::cin.tie(0); int n; std::cin >> n; std::priority_queue <int> q; LL ans = 0; for (int i = 1; i <= n; ++ i) { int a; std::cin >> a; LL sum = a; if (!q.empty()) sum += std::max(0, q.top()); ans = std::max(ans, sum); q.push(a); } std::cout << ans << "\n"; return 0; }
C 贪心,模拟
发现所有人先进行复制再进行造题一定是最优的,若某轮中既有造题的又有复制的,将这轮都改成复制,则最终结束时刻一定不会更靠后。
复制时数量是倍增的,于是直接枚举复制几轮即可。
Code by dztlb:
#include<bits/stdc++.h> using namespace std; #define int long long const int N=1e5+5; const int inf=1e9; int T; int a,b,c; signed main(){ cin>>T; while(T--){ cin>>a>>b>>c; int ans=inf*inf; for(int i=0;i<=30;++i){ int cnt=1; int tmp=0; for(int j=1;j<=i;++j){ cnt*=2; tmp+=a; } int now=(c/cnt); if(c%cnt!=0) ++now; tmp+=now*b; ans=min(ans,tmp); } cout<<ans<<'\n'; } return 0; }
G 字符串,哈希
显然答案一定是给定的 个字符串中的某个。
答案 的所有子串均在给定的 个字符串中出现过,发现这等价于: 与 均出现过。
于是考虑把所有字符串哈希一下用于判重,然后按照长度枚举所有字符串,通过上述结论判断当前字符串是否合法并标记即可。
被标记的最长的字符串即为答案。
有唐氏写哈希忘驱魔了呃呃
// /* By:Luckyblock */ #include <bits/stdc++.h> #define LL long long const int kN = 1e5 + 10; const LL p = 1e9 + 7; const LL c = 1331; //============================================================= int n; std::string s[kN]; bool yes[kN]; std::set<LL> hash; //============================================================= bool cmp(const std::string &fir_, const std::string &sec_) { return fir_.length() < sec_.length(); } //============================================================= int main() { // freopen("1.txt", "r", stdin); std::ios::sync_with_stdio(0), std::cin.tie(0); std::cin >> n; for (int i = 1; i <= n; ++ i) std::cin >> s[i]; std::sort(s + 1, s + n + 1, cmp); int ans = 0; for (int i = 1; i <= n; ++ i) { int len = s[i].length(); LL h = 0, h1 = 0, h2 = 0; if (len == 1) { yes[i] = 1; } else { for (int j = 0; j < len - 1; ++ j) h1 = (c * h1 + s[i][j]) % p; for (int j = 1; j < len; ++ j) h2 = (c * h2 + s[i][j]) % p; yes[i] = (hash.count(h1) && hash.count(h2)); } h = (c * h1 + s[i][len - 1]) % p; if (yes[i]) ans = std::max(ans, len), hash.insert(h); } std::cout << ans << "\n"; return 0; } /* 5 a aa aaa aaaa aaaaa */
L 贪心,结论
有唐氏写的一坨嗯吃两发被我狠狠批判然后五分钟重构一发过了。
题意等价于使用极大的链或反链完全覆盖一棵有根树的最小覆盖次数。
发现若使用反链进行覆盖,最优的情况是每次选择当前有根树的所有叶节点。若不覆盖叶节点则既不能减少链的数量,从而便于使用链覆盖,且之后不可避免地还需要覆盖叶节点。
叶节点至多仅有 层,于是考虑模拟地不断使用反链覆盖,枚举删去叶节点的层数,则之后需要的链覆盖的次数即为当前叶节点数量。
// /* By:Luckyblock */ #include <bits/stdc++.h> #define LL long long const int kN = 2e6 + 10; //============================================================= std::vector<int> leaves[2]; int n, fa[kN], son[kN]; //============================================================= //============================================================= 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; leaves[0].clear(), leaves[1].clear(); for (int i = 1; i <= n; ++ i) son[i] = fa[i] = 0; for (int i = 2; i <= n; ++ i) { std::cin >> fa[i]; ++ son[fa[i]]; } for (int i = 1; i <= n; ++ i) { if (son[i] == 0) leaves[0].push_back(i); } int ans = leaves[0].size(), now = 0; for (int i = 1; i <= n; ++ i, now ^= 1) { if (leaves[now].empty()) break; leaves[now ^ 1].clear(); for (auto u: leaves[now]) { -- son[fa[u]]; if (son[fa[u]] == 0) leaves[now ^ 1].push_back(fa[u]); } ans = std::min(ans, (int) (i + leaves[now ^ 1].size())); } std::cout << ans << "\n"; } return 0; } /* 3 7 1 1 2 2 2 3 5 1 2 3 4 11 1 2 3 4 5 2 3 4 5 6 */
E 数学,模拟
发现给定式子相当于考虑 的三进制表示, 即三进制下的位数+三进制各位之和。
于是大力讨论即可。最优情况是各位全部取 2,需要讨论被上下界限制时取不到最优情况。
Code by wenqizhi:
#include<bits/stdc++.h> using namespace std; #define ll long long #define ull unsigned long long ll read() { ll 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 = 105; ll sum[N]; void init() { sum[0] = 1; for(int i = 1; i; ++i) { sum[i] = sum[i - 1] * 3; if(sum[i] > 1e18) break; } } int get(int len, ll r) { ll L = sum[len - 1]; int cnt = 1; for(int i = 1; i <= len; ++i) { for(int j = 1; j <= 2 - (i == len); ++j) if(sum[i - 1] + L <= r) ++cnt, L += sum[i - 1]; } return cnt; } void solve() { ll l = read(), r = read(), L = l; int ans = 0; int len = 0, cnt = 0; while(L) { cnt += L % 3; L /= 3; ++len; } L = l; len = max(len, 1); ans = max(ans, cnt); for(int i = 1; i <= len; ++i) { int res = 3 - l / sum[i - 1] % 3 - 1; for(int j = 1; j <= res; ++j) if(sum[i - 1] + L <= r) ++cnt, L += sum[i - 1]; } ans = max(ans, cnt + len); for(int i = len + 1; i; ++i) { if(sum[i - 1] > r || sum[i - 1] == 0) break; ans = max(ans, get(i, r) + i); } printf("%d\n", ans); } int main() { init(); int T = read(); while(T--) solve(); return 0; }
B 结论,网络流
我去这题给 dztlb 大神开出来了真是我叠吧
根据给定的变量的含义,颜色数量 与填 0 数量 显然成反比关系,则容易看出 是关于 的单谷函数。于是考虑三分,检查在使用 种颜色情况下,最少填多少 0(最多可以填多少非 0)可以合法,即可求得对应的 的值。
发现问题可以看做是在给定颜色种类数限制下,保证每行/每列同种非 0 颜色数量不大于 1,最大化填的非 0 的数量,又数据范围很小,一个显然的想法是通过网络流进行非 0 的填色最大化流量,跑最大流即为所求。
对于上述问题,记三分量为 ,dztlb 大神的建图策略如下:
- 先统计每行/每列的空格数量,记为 ;
- 对每行/每列建点,编号分别为 ,;
- 对于第 行,若有 ,连边 ,然后枚举列 ,若有 ,连边 ;
- 对于第 列,若有 ,连边 ;
- 则最小化填 0 数量即:。
建边数量为 级别,但是这图只有 4 层根本跑不满,实际运行效率非常高。
然后我当代码黑奴给 dztlb 大神写了最大流写了三分就过了哈哈
//知识点:网络最大流,Dinic /* By:Luckyblock */ #include <bits/stdc++.h> #define LL long long const int kN = 1e5 + 10; const int kM = 2e6 + 10; const LL kInf = 1e18 + 2077; //============================================================= int n, m, c, d, S, T; int nodenum, maxnodenum, edgenum = 1, v[kM], ne[kM], head[kN]; int cur[kN], dep[kN]; std::string map[kN]; int cn[kN],cm[kN]; LL w[kM]; //============================================================= void addedge(int u_, int v_, LL w_) { v[++ edgenum] = v_; w[edgenum] = w_; ne[edgenum] = head[u_]; head[u_] = edgenum; v[++ edgenum] = u_; w[edgenum] = 0; ne[edgenum] = head[v_]; head[v_] = edgenum; } void init() { edgenum = 1; nodenum = n + m; maxnodenum = 3 * n + 2; S = ++ nodenum, T = ++ nodenum; for (int i = 1; i <= maxnodenum; ++ i) { head[i] = 0; } } bool bfs() { std::queue <int> q; memset(dep, 0, (nodenum + 1) * sizeof (int)); dep[S] = 1; //注意初始化 q.push(S); while (!q.empty()) { int u_ = q.front(); q.pop(); for (int i = head[u_]; i > 1; i = ne[i]) { int v_ = v[i], w_ = w[i]; if (w_ > 0 && !dep[v_]) { dep[v_] = dep[u_] + 1; q.push(v_); } } } return dep[T]; } LL dfs1(int u_, LL into_) { if (u_ == T) return into_; LL ret = 0; for (int i = cur[u_]; i > 1 && into_; i = ne[i]) { int v_ = v[i]; LL w_ = w[i]; if (w_ && dep[v_] == dep[u_] + 1) { LL dist = dfs1(v_, std::min(into_, w_)); if (!dist) dep[v_] = kN; into_ -= dist; ret += dist; w[i] -= dist, w[i ^ 1] += dist; if (!into_) return ret; } } if (!ret) dep[u_] = 0; return ret; } LL dinic() { LL ret = 0; while (bfs()) { memcpy(cur, head, (nodenum + 1) * sizeof (int)); ret += dfs1(S, kInf); } return ret; } LL check(LL mid_) { init(); LL tmp=0; for(int i=1;i<=n;++i){ if(cn[i]>mid_){ addedge(S,i,cn[i]-mid_); tmp+=cn[i]-mid_; for(int j=1;j<=m;++j){ if(cm[j]>mid_){ addedge(i,n+j,1); } } } } for(int i=1;i<=m;++i){ if(cm[i]>mid_){ addedge(n+i,T,cm[i]-mid_); tmp+=cm[i]-mid_; } } return 1ll * mid_ * c + d * (tmp - dinic()); } //============================================================= int main() { // freopen("1.txt", "r", stdin); std::ios::sync_with_stdio(0), std::cin.tie(0); std::cin >> n >> m >> c >> d; for (int i = 1; i <= n; ++ i) { std::cin >> map[i]; map[i] = "$" + map[i]; } for(int i=1;i<=n;++i){ for(int j=1;j<=m;++j){ if(map[i][j]=='.'){ cn[i]++,cm[j]++; } } } LL lmid, rmid, lans = kInf, rans = kInf; for (LL l = 0, r = std::max(n, m); l <= r; ) { lmid = l + (r - l) / 3; rmid = r - (r - l) / 3; lans = check(lmid), rans = check(rmid); if (rans <= lans) l = lmid + 1; else r = rmid - 1; } std::cout << std::min(lans, rans) << "\n"; return 0; } /* 5 5 3 1 *..*. ...** .*... ..*.. ..*.. */
A LCT or 根号预处理 or 线段树分治维护连通性
我去这题给我开出来了最后十分钟一发过了我真是叠吧
保证一个点至多连一条桥,则从每个起点出发,到达的终点一定是不同且唯一的。
发现在 连边,对移动路径的影响,等价于断开边:,然后连边:。
发现断边连边后,给定的图仍是 条长度为 的链,一个显然的想法是使用 LCT 直接维护,考虑仅令每条链结尾的点权值为其链号,则查询仅需查询给定起点对应链的树的权值和即可。
然而有 个点并不能直接做,但是发现至多只有 次操作,则有影响的点仅有 级别,于是考虑将每行的点按是否连了桥进行分段,则每段都可以直接缩成一个点考虑,就能直接上 LCT 了。
我的分段缩点方法是先将每条链看做一个点,然后模拟连桥后将原来的点分裂得到新点,比较唐呃呃,优美做法可见 HolyK 的这发提交:https://codeforces.com/gym/104077/submission/183684508。
std 是根号预处理;此外还有大神的线段树分治维护连通性,可见这发提交:https://codeforces.com/gym/104077/submission/272665032。
// /* By:Luckyblock */ #include <bits/stdc++.h> #define LL long long #define pr std::pair #define mp std::make_pair const int kN = 5e5 + 10; //============================================================= int n, m, q, a[kN], ans[kN]; struct O { int opt, a, b; } op[kN]; struct Node { int id, a, l, r, val; bool operator <(const Node& sec_) const { if (a != sec_.a) return a < sec_.a; if (r != sec_.r) return r < sec_.r; if (l != sec_.l) return l < sec_.l; if (id != sec_.id) return id < sec_.id; return val < sec_.val; } } node[kN]; int nodenum, next[kN]; std::set<Node> st; //============================================================= inline int read() { int f = 1, w = 0; char ch = getchar(); for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1; for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0'); return f * w; } namespace LCT { #define f fa[now_] #define ls son[now_][0] #define rs son[now_][1] int fa[kN], son[kN][2]; int val[kN], sum[kN], sum1[kN]; bool rev[kN]; void clear(int now_) { ls = rs = f = val[now_] = 0; } void Pushup(int now_) { sum[now_] = (sum[ls] + sum[rs] + val[now_] + sum1[now_]); } void Rev(int now_) { if (!now_) return ; std::swap(ls, rs); rev[now_] ^= 1; } void Pushdown(int now_) { bool rev_ = rev[now_]; if (rev_) Rev(ls), Rev(rs); rev[now_] = 0; } bool Isroot(int now_) { return son[f][0] != now_ && son[f][1] != now_; } bool Whichson(int now_) { return son[f][1] == now_; } void Rotate(int now_) { int fa_ = f, ffa = fa[f], w = Whichson(now_); if (!Isroot(f)) son[fa[f]][Whichson(f)] = now_; f = fa[f]; son[fa_][w] = son[now_][w ^ 1]; fa[son[fa_][w]] = fa_; son[now_][w ^ 1] = fa_; fa[fa_] = now_; Pushup(fa_), Pushup(now_), Pushup(ffa); } void Update(int now_) { if (!Isroot(now_)) Update(f); Pushdown(now_); } void Splay(int now_) { Update(now_); for (; !Isroot(now_); Rotate(now_)) { if (!Isroot(f)) Rotate(Whichson(f) == Whichson(now_) ? f : now_); } } void Access(int now_) { for (int last = 0; now_; last = now_, now_ = f) { Splay(now_); sum1[now_] += sum[rs] - sum[last]; rs = last; Pushup(now_); } } void Makeroot(int now_) { Access(now_); Splay(now_); Rev(now_); } int Find(int now_) { Access(now_); Splay(now_); while (ls) now_ = ls; Splay(now_); return now_; } void Split(int x_, int y_) { Makeroot(x_); Access(y_); Splay(y_); } void Link(int x_, int y_) { Makeroot(x_); // fa[x_] = y_; if (Find(y_) != x_) fa[x_] = y_; } void Cut(int x_, int y_) { // Split(x_, y_); // fa[x_] = son[y_][0] = 0; // Pushup(y_); Makeroot(x_); if (Find(y_) != x_ || fa[y_] != x_ || son[y_][0]) return ; fa[y_] = son[x_][1] = 0; Pushup(x_); } int query(int x_) { Makeroot(x_); return sum[x_]; } void Init() { for (int i = 1; i <= nodenum; ++ i) { val[i] = sum[i] = node[i].val; } for (int i = 1; i <= nodenum; ++ i) { if (next[i]) Link(i, next[i]); } } } void Init() { n = read(), m = read(), q = read(); nodenum = n; for (int i = 1; i <= n; ++ i) { node[i] = (Node) {i, i, 1, m + 1, i}; st.insert(node[i]); } for (int i = 1; i <= q; ++ i) { int opt = read(); if (opt == 1) { int a = read(), b = read(); Node u1 = *st.lower_bound((Node) {0, a, 0, b, 0}); Node u2 = *st.lower_bound((Node) {0, a + 1, 0, b, 0}); node[++ nodenum] = (Node) {nodenum, a, b + 1, u1.r, u1.val}; node[++ nodenum] = (Node) {nodenum, a + 1, b + 1, u2.r, u2.val}; node[u1.id].r = node[u2.id].r = b; node[u1.id].val = node[u2.id].val = 0; st.erase(st.lower_bound(u1)), st.erase(st.lower_bound(u2)); st.insert(node[u1.id]), st.insert(node[u2.id]); st.insert(node[nodenum - 1]), st.insert(node[nodenum]); /* struct Node { int id, a, l, r, val; */ op[i] = (O) {1, a, b}; } else { int a = read(); op[i] = (O) {2, a, 0}; } } for (std::set<Node>::iterator it = st.begin(); it != st.end(); ++ it) { std::set<Node>::iterator it1 = it; ++ it1; if (it1 == st.end()) break; if (it->a != it1->a) continue; next[it->id] = it1->id; } LCT::Init(); } void modify(int x_, int y_) { int u1 = st.lower_bound((Node) {0, x_, 0, y_, 0})->id; int u2 = st.lower_bound((Node) {0, x_ + 1, 0, y_, 0})->id; LCT::Cut(u1, next[u1]), LCT::Cut(u2, next[u2]); std::swap(next[u1], next[u2]); LCT::Link(u1, next[u1]), LCT::Link(u2, next[u2]); } int query(int x_) { return LCT::query(st.lower_bound((Node) {0, x_, 0, 0, 0})->id); } //============================================================= int main() { // freopen("1.txt", "r", stdin); Init(); for (int i = 1; i <= q; ++ i) { if (op[i].opt == 2) { printf("%d\n", query(op[i].a)); } else { modify(op[i].a, op[i].b); } } return 0; }
D 倍增,DP
感觉比 A 的 std 好想,但是鉴于 A 大力 LCT 就草过去了所以把 D 放在 A 后面。
考虑对于一次询问 ,可以看做从 开始每次向某个排列中 更低的位置移动一位,直至移动到 ,求最少的移动次数。
显然这个过程和大力跳链非常类似,考虑将每个排列看成一条链,考虑能否倍增优化这个过程。然后容易发现,若最终可以移动到某条链上的 ,其必要条件是可以移动到这条链上 低于 的位置,即仅需考虑通过若干步可移动到某条链上 最低的位置即可。
于是考虑倍增,记 表示从 开始,移动 步,最终可以跳到第 条链上的最低的 ,大力 预处理即可。
查询时同样考虑倍增,考虑倒序枚举步数 ,并检查 是否一定需要出现在答案中。若在当前记录的答案基础上再移动 步仍然不能移动到某条链上 低于 的位置,则答案中一定有 。单次询问与预处理类似,时间复杂度 级别。
总时间复杂度 级别。
// /* By:Luckyblock */ #include <bits/stdc++.h> #define LL long long const int kN = 1e5 + 10; //============================================================= int n, m, a[6][kN], rk[6][kN], f[21][kN][6]; //============================================================= void init() { std::cin >> n >> m; for (int i = 1; i <= m; ++ i) { for (int j = 1; j <= n; ++ j) { std::cin >> a[i][j]; rk[i][a[i][j]] = j; } } for (int i = 1; i <= m; ++ i) { for (int j = 1; j <= n; ++ j) f[0][j][i] = rk[i][j]; for (int j = 1; j <= m; ++ j) { int minrk = n + 1; for (int k = n; k; -- k) { minrk = std::min(minrk, rk[i][a[j][k]]); f[0][a[j][k]][i] = std::min(f[0][a[j][k]][i], minrk); } } } for (int i = 1; i <= 20; ++ i) { for (int j = 1; j <= n; ++ j) { for (int k = 1; k <= m; ++ k) { f[i][j][k] = f[i - 1][j][k]; for (int l = 1; l <= m; ++ l) { int p = a[l][f[i - 1][j][l]]; f[i][j][k] = std::min(f[i][j][k], f[i - 1][p][k]); } } } } } int query(int s_, int t_) { int ret = 2, minrk[6], newrk[6]; for (int i = 1; i <= m; ++ i) { if (rk[i][s_] <= rk[i][t_]) return 1; minrk[i] = rk[i][s_]; } for (int i = 20; ~i; -- i) { int flag = 1; for (int j = 1; j <= m; ++ j) newrk[j] = minrk[j]; for (int j = 1; j <= m; ++ j) { for (int k = 1; k <= m; ++ k) { newrk[j] = std::min(newrk[j], f[i][a[k][minrk[k]]][j]); } if (newrk[j] <= rk[j][t_]) flag = 0; } if (flag) { ret += (1 << i); for (int k = 1; k <= m; ++ k) minrk[k] = newrk[k]; } } return (ret <= n * m) ? ret : -1; } //============================================================= int main() { // freopen("1.txt", "r", stdin); std::ios::sync_with_stdio(0), std::cin.tie(0); init(); int q; std::cin >> q; while (q --) { int x, y; std::cin >> x >> y; std::cout << query(x, y) << "\n"; } return 0; }
写在最后
学到了什么:
- A:转化为经典数据结构问题;LCT 通过维护虚子树,维护可加性的子树信息。
- B:双变量函数,但是两个变量有线性关系——则双变量函数对于单变量为单峰/单谷。
然后是日常的夹带私货环节:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!