Educational Codeforces Round 162 (Rated for Div. 2)
写在前面
比赛地址:https://codeforces.com/contest/1923。
为唐氏儿的寒假带来了一个构式的结局,飞舞一个。
天使骚骚不太行啊妈的,推了三条线了感觉剧情太白开水了,咖啡馆也是这个熊样子、、、
A
签到。
显然最优的策略是不断地选择最右侧的 1 进行操作,每次操作等价于将最右侧的连续一段左移一位。
操作次数即为第一个 1 与最后一个 1 之间 0 的个数。
复制复制// /* By:Luckyblock */ #include <bits/stdc++.h> #define LL long long const int kN = 5e5 + 10; //============================================================= int a[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 --) { int n; std::cin >> n; int l = 0, r = 0, cnt = 0; for (int i = 1; i <= n; ++ i) { std::cin >> a[i]; if (l == 0 && a[i] == 1) l = i; if (a[i] == 1) r = i, ++ cnt; } std::cout << r - l + 1 - cnt << "\n"; } return 0; }
B
贪心,模拟。
显然最优策略是从近往远攻击。
于是记录距离原点各距离的怪物的总血量,模拟求击败各距离的怪物所需最短时间,若该时间大于距离则输。
// /* By:Luckyblock */ #include <bits/stdc++.h> #define LL long long const int kN = 3e5 + 10; //============================================================= int n, a[kN], x[kN]; LL k, suma[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 >> k; for (int i = 1; i <= n; ++ i) { std::cin >> a[i]; suma[i] = 0; } for (int i = 1; i <= n; ++ i) { std::cin >> x[i]; suma[abs(x[i])] += a[i]; } LL nowt = 0, r = 0, flag = 1; for (int i = 1; i <= n; ++ i) { if (suma[i] > r) suma[i] -= r, r = 0; else r -= suma[i], suma[i] = 0; LL need = 1ll * ceil(1.0 * suma[i] / k); if (suma[i] % k) r = k - (suma[i] % k); nowt += need; if (nowt > i) flag = 0; } std::cout << (flag ? "YES\n" : "NO\n"); } return 0; } /* 1 2 1 1 2 1 2 */
C
构造。
首先 可以随意构造,则数列 中元素的顺序是不重要的,仅需关注某种元素的数量即可。
手玩样例发现,若某数列 不合法,则该数列一定只能被表示为:1 1 1 1 ....
(很多 1)的形式,使得无论怎么调整 中元素的顺序都会有某位置同为 1,否则总能通过调整元素的值或是顺序使其合法。考虑将数列 升序排序,令构造的数列 降序排序,则若合法, 可以构造类似下列形式:
设 中原有 个 1,则 合法等价于存在一种 的构造方案使其中 1 的数量至多为 ,即有:
即:
前缀和维护区间和与区间 1 的数量即可 查询某区间是否合法。
注意特判区间长度为 1 时必定不合法。
// /* By:Luckyblock */ #include <bits/stdc++.h> #define LL long long const int kN = 5e5 + 10; //============================================================= int n, q, a[kN]; LL sum[kN], sum1[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 >> q; for (int i = 1; i <= n; ++ i) { std::cin >> a[i]; sum[i] = sum[i - 1] + a[i]; sum1[i] = sum1[i - 1] + (a[i] == 1); } while (q --) { int l, r; std::cin >> l >> r; LL s = sum[r] - sum[l - 1]; LL s1 = sum1[r] - sum1[l - 1] + (r - l + 1); std::cout << (l != r && s >= s1 ? "YES\n" : "NO\n"); } } return 0; } /* 1 8 1 1 1 1 1 2 2 2 2 1 8 1 7 1 1 1 1 1 1 1 5 1 7 */
D
枚举,二分。
首先是非常重要的结论:对于所有不完全相等的区间,都存在一种操作方案使它们可以合并为一个史莱姆,所需时间为区间长度。正确性显然,不断操作其中的最大者即可。
考虑史莱姆 的答案,分吃掉 的史莱姆来自左侧还是右侧讨论。由上述结论,若来自左侧,则用于合成吃掉 的区间 满足:
- 。
- 。
- 不完全相等。
对于所有满足上述条件的区间合成并吃掉 所需时间为 ,则其中最大的 是最优的。 为正数,则满足上述条件 2 的位置 可二分求得,为了满足条件 3 仅需再预处理每个数左侧第一个与其不同的数的位置即可。史莱姆来自右侧的情况同理,预处理每个数右侧第一个与其不同的数的位置+二分答案即可求得,上述两种情况取最小值即为答案。
总时间复杂度 级别。
// /* By:Luckyblock */ #include <bits/stdc++.h> #define LL long long const int kN = 3e5 + 10; //============================================================= int n, a[kN], fl[kN], fr[kN]; LL sum[kN]; //============================================================= void Init() { std::cin >> n; sum[0] = 0; a[0] = a[n + 1] = -1; for (int i = 1; i <= n; ++ i) { std::cin >> a[i]; sum[i] = sum[i - 1] + a[i]; if (a[i] == a[i - 1]) fl[i] = fl[i - 1]; else fl[i] = i - 1; } for (int i = n; i; -- i) { if (a[i] == a[i + 1]) fr[i] = fr[i + 1]; else fr[i] = i + 1; } } bool Check(int l_, int r_, LL val_) { return sum[r_] - sum[l_ - 1] > val_; } void Solve(int pos_) { int posl = 0, posr = n + 1; for (int l = 1, r = pos_ - 1; l <= r; ) { int mid = (l + r) >> 1; if (Check(mid, pos_ - 1, a[pos_])) { posl = mid; l = mid + 1; } else { r = mid - 1; } } if (posl != 0 && pos_ - posl > 1 && fr[posl] >= pos_) posl = fl[posl]; for (int l = pos_ + 1, r = n; l <= r; ) { int mid = (l + r) >> 1; if (Check(pos_ + 1, mid, a[pos_])) { posr = mid; r = mid - 1; } else { l = mid + 1; } } if (posr != n + 1 && posr - pos_ > 1 && fl[posr] <= pos_) posr = fr[posr]; if (posl == 0 && posr == n + 1) std::cout << -1 << " "; else if (posl == 0) std::cout << posr - pos_ << " "; else if (posr == n + 1) std::cout << pos_ - posl << " "; else std::cout << std::min(pos_ - posl, posr - pos_) << " "; } //============================================================= int main() { // freopen("1.txt", "r", stdin); std::ios::sync_with_stdio(0), std::cin.tie(0); int T; std::cin >> T; while (T --) { Init(); for (int i = 1; i <= n; ++ i) Solve(i); std::cout << "\n"; } return 0; }
E
dsu on tree。
考虑对于所有节点 维护函数 表示满足下列条件的以 为一端点的路径 的数量:
- 。
- 。
- 除 外,路径 上无其他颜色为 的节点。
对于所有 ,函数 显然可以 dfs 求得。考虑枚举节点 并求得以 为 的所有合法路径的数量。考虑按顺序枚举 的儿子 进行 dfs 并在求 的过程中求得贡献,若当前枚举到子树 且 的增量为 ,则在更新 前可求得答案的增量为:
发现仅需令子节点 的 即可将 变为子树 对 的贡献,存在可继承性,一眼感觉很 dsu on tree 的样子,考虑在 dsu 过程中维护 并在 dfs 求子树贡献时求答案即可。
注意删除子树对 的贡献时也应使用 dfs。
总时间复杂度 级别。
更加显然的做法是基于上述函数 考虑枚举路径两端点的颜色,并在以此颜色为关键点建成的虚树上进行 DP 维护 并计算贡献,复杂度同样为 级别。
// /* By:Luckyblock */ #include <bits/stdc++.h> #define LL long long const int kN = 2e5 + 10; const int kM = kN << 1; //============================================================= int n, a[kN]; int edgenum, head[kN], v[kM], ne[kM]; int dfnnum, sz[kN], son[kN]; int f[kN], vis[kN]; LL ans; //============================================================= void Add(int u_, int v_) { v[++ edgenum] = v_; ne[edgenum] = head[u_]; head[u_] = edgenum; } void AddDfs(int u_, int fa_, int top_) { if (!vis[a[u_]]) { if (a[u_] != a[top_]) ans += 1ll * f[a[u_]]; } ++ vis[a[u_]]; for (int i = head[u_]; i; i = ne[i]) { int v_ = v[i]; if (v_ == fa_) continue; AddDfs(v_, u_, top_); } -- vis[a[u_]]; } void AddDfs1(int u_, int fa_, int top_) { if (!vis[a[u_]]) ++ f[a[u_]]; ++ vis[a[u_]] ; for (int i = head[u_]; i; i = ne[i]) { int v_ = v[i]; if (v_ == fa_) continue; AddDfs1(v_, u_, top_); } -- vis[a[u_]]; } void DelDfs(int u_, int fa_, int top_) { if (!vis[a[u_]]) -- f[a[u_]]; ++ vis[a[u_]]; for (int i = head[u_]; i; i = ne[i]) { int v_ = v[i]; if (v_ == fa_) continue; DelDfs(v_, u_, top_); } -- vis[a[u_]]; } void Dfs1(int u_, int fa_) { sz[u_] = 1; for (int i = head[u_]; i; i = ne[i]) { int v_ = v[i]; if (v_ == fa_) continue; Dfs1(v_, u_); sz[u_] += sz[v_]; if (sz[v_] > sz[son[u_]]) son[u_] = v_; } } void Dfs2(int u_, int fa_, bool son_) { for (int i = head[u_]; i; i = ne[i]) { int v_ = v[i]; if (v_ == fa_ || v_ == son[u_]) continue; Dfs2(v_, u_, 0); } if (son[u_]) { Dfs2(son[u_], u_, 1); } for (int i = head[u_]; i; i = ne[i]) { int v_ = v[i]; if (v_ == fa_ || v_ == son[u_]) continue; AddDfs(v_, u_, u_); AddDfs1(v_, u_, u_); } ans += f[a[u_]]; if (!son_) { for (int i = head[u_]; i; i = ne[i]) { int v_ = v[i]; if (v_ == fa_) continue; DelDfs(v_, u_, u_); } } else { f[a[u_]] = 1; } } //============================================================= 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; edgenum = dfnnum = 0; ans = 0; for (int i = 1; i <= n; ++ i) { son[i] = f[i] = sz[i] = head[i] = 0; } for (int i = 1; i <= n; ++ i) std::cin >> a[i]; for (int i = 1; i < n; ++ i) { int u_, v_; std::cin >> u_ >> v_; Add(u_, v_), Add(v_, u_); } Dfs1(1, 1), Dfs2(1, 0, 0); std::cout << ans << "\n"; } return 0; }
F
结论+后缀排序
为什么是后缀排序而不是后缀数组?因为真的只是用来比较后缀的字典序的哈哈,这题时限很宽甚至直接 sort
都能过。
大力手玩结论题。
- 收缩翻转操作的本质是在不考虑后导零的影响的同时取翻转后的贡献,即取最靠左的 1 与最靠右的 1之间的贡献,下文称最靠左的 1 与最靠右的 1 之间部分为贡献段。
- 若不进行收缩翻转操作,则每次进行交换操作时一定会将当前最靠左的 1 与最靠右的 0 交换,即每次删去最高位的 1。
- 若进行收缩翻转操作,则交换操作的作用是首先尽可能缩小贡献段的长度(即二进制的位数),其次最小化贡献段代表的二进制数的大小。第二种作用与收缩翻转操作没有关系,第一种作用若更改了贡献段的右端点(即将最靠右的 1 交换到了贡献段内部),则之后有收缩翻转操作可能使答案更优,否则与收缩翻转操作无关——可知将收缩翻转操作放在最后进行不会使答案变劣。
- 由上述交换与收缩翻转操作的本质,可知收缩翻转操作不会超过 1 次。将超过 1 次部分替换为交换操作,使贡献段长度至少减 1 更优。
于是仅需考虑进行 0/1 次收缩翻转操作即可。
若不进行收缩翻转操作,则不断地将最靠左的 1 与最靠右的 0 交换,双指针实现即可。
若进行 1 次收缩翻转操作,记字符串中 1 的数量为 则需要找到一个代表进行收缩翻转操作前的贡献段的区间 。该区间需要满足:
- :至少能够包含下所有的 1。
- 区间 中的 1 的数量不大于 :至多进行 次交换操作。
在满足上述两条件的区间中,最优的区间应当是首先是长度最短的。于是考虑枚举上述区间的左端点,双指针即可求得所有满足条件的且最短的区间。之后将这些最短的区间转化为答案的过程,等价于将其中的 个 0 替换为 1,手玩下发现区间代表的二进制数越小,则替换后代表的二进制数越小,由于区间等长,则比较二进制数大小等价于比较字典序,等价于比较后缀数组中对应后缀的 ,选择字典序最小的即为最优区间。
将上述两种情况的答案比较取最小值即可。
复杂度瓶颈在于后缀排序。用了倍增法,总时间复杂度 级别。
//结论+后缀排序 /* By:Luckyblock */ #include <bits/stdc++.h> #define LL long long const int kN = 5e5 + 10; const LL mod = 1e9 + 7; //============================================================= int n, k; char s[kN]; int m, sa[kN], rk[kN << 1], oldrk[kN << 1], cnt[kN], id[kN], rkid[kN]; int sum1[kN]; //============================================================= 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 = 10 * w + (ch ^ '0'); return f * w; } bool cmp(int x_, int y_, int w_) { return oldrk[x_] == oldrk[y_] && oldrk[x_ + w_] == oldrk[y_ + w_]; } void SA() { m = 200; for (int i = 1; i <= n; ++ i) cnt[rk[i] = s[i]] ++; for (int i = 1; i <= m; ++ i) cnt[i] += cnt[i - 1]; for (int i = n; i >= 1; -- i) sa[cnt[rk[i]] --] = i; for (int p, w = 1; w < n; w <<= 1) { p = 0; for (int i = n; i > n - w; -- i) id[++ p] = i; for (int i = 1; i <= n; ++ i) { if (sa[i] > w) id[++ p] = sa[i] - w; } for (int i = 1; i <= m; ++ i) cnt[i] = 0; for (int i = 1; i <= n; ++ i) cnt[rkid[i] = rk[id[i]]] ++; for (int i = 1; i <= m; ++ i) cnt[i] += cnt[i - 1]; for (int i = n; i >= 1; -- i) sa[cnt[rkid[i]] --] = id[i]; m = 0; for (int i = 1; i <= n; ++ i) std::swap(rk[i], oldrk[i]); for (int i = 1; i <= n; ++ i) { rk[sa[i]] = (m += cmp(sa[i], sa[i - 1], w) ^ 1); } } } std::string Solve0() { std::string ret(s + 1); int temp = k; for (int i = 1, j = n; i < j; ++ i) { if (s[i] != '1') continue; while (i < j && s[j] != '0') -- j; std::swap(ret[i - 1], ret[j - 1]); -- j; if (!(-- temp)) break; } return ret.substr(ret.find_first_of('1')); } bool cmp1(int fir_, int sec_) { return rk[fir_] < rk[sec_]; } std::string Solve1() { std::reverse(s + 1, s + n + 1); for (int i = 1; i <= n; ++ i) sum1[i] = sum1[i - 1] + (s[i] == '1'); SA(); int minlen = n + 1, lpos = 0; for (int i = 1, j = 1; i <= n; ++ i) { while (j < n && (sum1[n] - sum1[j] + sum1[i - 1] > k - 1 || j - i + 1 < sum1[n])) { ++ j; } if ((sum1[n] - sum1[j] + sum1[i - 1] > k - 1 || j - i + 1 < sum1[n])) break; if (j - i + 1 > minlen) continue; if (j - i + 1 == minlen) lpos = (rk[lpos] > rk[i] ? i : lpos); if (j - i + 1 < minlen) minlen = j - i + 1, lpos = i; } for (int i = lpos + minlen - 1, temp = k - 1; temp; -- i) { if (s[i] == '1') continue; s[i] = '1', -- temp; } std::string ret(s + 1); return ret.substr(lpos - 1, minlen); } LL Getans(std::string s1_, std::string s2_) { if (s1_.length() > s2_.length() || (s1_.length() == s2_.length() && s1_ > s2_)) { std::swap(s1_, s2_); } LL ret = 0; for (auto c: s1_) ret = (2ll * ret + c - '0') % mod; return ret; } //============================================================= int main() { // freopen("1.txt", "r", stdin); n = read(), k = read(); scanf("%s", s + 1); std::string s1 = Solve0(), s2 = Solve1(); printf("%lld\n", Getans(s1, s2)); return 0; } /* 10110001111100 00110001111101 10010010 00010011 00000111 */
写在最后
学到了什么:
- D:区间最值。
- E:发现树上问题中用于统计答案的某函数可由子节点在适当复杂度内转化为父节点的贡献,可考虑 dsu on tree 加速该过程。
- F:大力手玩,操作数量上限,等价操作序列。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】