Codeforces Round 965 (Div. 2)
写在前面
比赛地址:https://codeforces.com/contest/1998
为了保证队长当前是 1k9 这个事实不变方便劝诱新大神,于是上小号。
比较手速场呃呃,小号大概也能上紫了爽,要是手快点还能更爽。
置顶广告:中南大学 ACM 集训队绝赞招新中!
有信息奥赛基础,获得 NOIP 省一等奖并达到 Codeforces rating 1900+ 或同等水平及以上者,可以直接私聊我与校队队长联系,免选拔直接进校集训队参加区域赛!
没有达到该水平但有志于 XPCX 赛事请关注每学年开始的 ACM 校队招新喵!
到这个时候了还缺队友实在不妙!求求求求快来个大神带我呜呜呜呜
A
签到。
草怎么那么多 fst 的不懂。
复制复制// /* 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 T; std::cin >> T; while (T --) { int xc, yc, k; std::cin >> xc >> yc >> k; for (int i = 1; i <= k - k % 2; i += 2) { std::cout << xc + (i / 2 + 1) << " " << yc << "\n"; std::cout << xc - (i / 2 + 1) << " " << yc << "\n"; } if (k % 2) std::cout << xc << " " << yc << "\n"; } return 0; }
B
思维,结论。
本地 check 了一下发现样例中给定排列与答案里,有且仅有 的和是相等的,于是猜想一定存在一种构造方案,使得两个排列仅有 的和相等。
赛时猜了个结论,直接把所有位置循环向后位移一位即可,本地跑了几组数据 check 了一下发现可行于是交上去过了。
其正确性是显然的,考虑给定的排列 与循环向后位移一位得到的排列 ,对于任意一个长度小于 的区间 ,一定有:
// /* By:Luckyblock */ #include <bits/stdc++.h> #define LL long long const int kN = 2e5 + 10; //=============================================================4 int n, a[kN], ans[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; for (int i = 1; i <= n; ++ i) std::cin >> a[i]; for (int i = 2; i <= n; ++ i) ans[i] = a[i - 1]; ans[1] = a[n]; for (int i = 1; i <= n; ++ i) std::cout << ans[i] << " "; // LL cnt = 0; // for (int i = 1; i <= n; ++ i) { // for (int j = i; j <= n; ++ j) { // LL s1 = 0, s2 = 0; // for (int k = i; k <= j; ++ k) { // s1 += a[k], s2 += ans[k]; // } // if (s1 == s2) ++ cnt; // } // } // std::cout << cnt << "\n"; std::cout << "\n"; } return 0; } /* 3 2 1 2 2 1 5 1 2 3 4 5 3 5 4 2 1 7 4 7 5 1 2 6 3 6 2 1 4 7 3 5 */
C
枚举,数据结构。
考虑枚举最终答案里的 ,再考虑通过修改增大 和增大中位数的代价:
若有 ,显然一次修改至少使 而不一定使中位数 ,显然此时应仅修改 ,对答案的贡献即 ,直接对顶堆动态维护删去 的中位数即可。单次检查的时间复杂度为 级别。
对顶堆维护中位数可见:https://www.cnblogs.com/luckyblock/p/18159496。
若有 ,则此时仅能修改其他位置使中位数增大,一个显然的想法是二分答案枚举增大后的中位数的值 ,则仅需检查能否通过修改使 个数不小于 。
考虑先查询原数列中不小于 的数的个数 ,再查询原数列中小于 且 的数,则最优的操作是选择其中最大的 个数使它们变为 ,查询他们的和与 的差值是否不大于 即可。
上述原数列中不小于 的数的个数、以及原数列中小于 且 的数,均可以通过维护排序后的原数列,再在上面二分得到,在此基础上最大的 个数可通过维护前缀和得到,则单次检查的时间复杂度为 级别。
总时间复杂度 级别。
// /* By:Luckyblock */ #include <bits/stdc++.h> #define LL long long #define pr std::pair #define mp std::make_pair const int kN = 2e5 + 10; const LL kMaxa = 2e9; //============================================================= int n, k, a[kN], b[kN]; int a1num, sorta1[kN], sorta[kN]; LL sum[kN]; //============================================================= namespace Set { const int kInf = 1e9 + 2077; std::multiset<int> less, greater; void init() { less.clear(), greater.clear(); less.insert(-kInf), greater.insert(kInf); } void adjust() { while (less.size() > greater.size() + 1) { std::multiset<int>::iterator it = (--less.end()); greater.insert(*it); less.erase(it); } while (greater.size() > less.size()) { std::multiset<int>::iterator it = greater.begin(); less.insert(*it); greater.erase(it); } } void add(int val_) { if (val_ <= *greater.begin()) less.insert(val_); else greater.insert(val_); adjust(); } void del(int val_) { std::multiset<int>::iterator it = less.lower_bound(val_); if (it != less.end()) { less.erase(it); } else { it = greater.lower_bound(val_); greater.erase(it); } adjust(); } int get_middle() { return *less.rbegin(); } } void init() { std::cin >> n >> k; Set::init(); for (int i = 1; i <= n; ++ i) { std::cin >> a[i]; Set::add(a[i]); } a1num = 0; for (int i = 1; i <= n; ++ i) { std::cin >> b[i]; sorta[i] = a[i]; if (b[i] == 1) sorta1[++ a1num] = a[i]; } std::sort(sorta + 1, sorta + n + 1); std::sort(sorta1 + 1, sorta1 + a1num + 1); for (int i = 1; i <= a1num; ++ i) sum[i] = sorta1[i] + sum[i - 1]; } LL solveb1(int pos_) { Set::del(a[pos_]); LL ret = Set::get_middle(); Set::add(a[pos_]); return ret; } LL solveb0(int pos_) { LL ret = 0; int middle = (n - 1) / 2 + 1; for (LL l = 1, r = kMaxa; l <= r; ) { LL mid = (l + r) >> 1ll; int p1 = std::lower_bound(sorta1 + 1, sorta1 + a1num + 1, mid) - sorta1; int p2 = std::lower_bound(sorta + 1, sorta + n + 1, mid) - sorta; int c1 = p1 - 1, c2 = n - (p2 - 1) - (a[pos_] >= mid); if (c2 >= middle) { ret = mid; l = mid + 1; continue; } int need = middle - c2; if (need > c1) { r = mid - 1; continue; } if (1ll * sum[c1] - sum[c1 - need] + k < 1ll * need * mid) { r = mid - 1; } else { ret = mid; l = mid + 1; } } return ret; } //============================================================= 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(); LL ans = 0; for (int i = 1; i <= n; ++ i) { if (b[i] == 1) { ans = std::max(ans, 1ll * a[i] + k + solveb1(i)); } else { ans = std::max(ans, 1ll * a[i] + solveb0(i)); } } std::cout << ans << "\n"; } return 0; }
D
DP。
游戏题照例先先手玩下。以下称先手移动的仅能走边 的牛牛为先手,另一牛牛为后手。边 称为杂鱼边,其他边称为牛逼边。
发现考虑先手必胜需要大力枚举起点比较麻烦,于是取个反考虑起点固定为 1 的后手必胜。
发现对于某个先手的起点 ,后手必胜的充要条件是存在一条牛逼边 ,满足:
- ,使得先手不会把岛屿 干掉。
- 记 为后手从 1 到 的最短路的长度,有 ,使得后手可以比先手更早地到达 ,将先手到达终点的必经之路干掉。
于是考虑拓扑排序 DP 求得从 1 到每个节点的最短路径,在此过程中对于每个节点 枚举所有出边 ,则对于先手的起点 均为后手必胜的,可通过差分维护。
最后还原答案序列输出即可,总时间复杂度 级别。
虽然赛时懒得想了真的写了拓扑排序但实际上并无必要,拓扑序实际上即 ,直接大力枚举即可。
// /* By:Luckyblock */ #include <bits/stdc++.h> #define LL long long const int kN = 2e5 + 10; //============================================================= int n, m; int edgenum, into[kN], head[kN], v[kN << 1], ne[kN << 1]; int ans[kN], f[kN]; //============================================================= void addedge(int u_, int v_) { v[++ edgenum] = v_; ne[edgenum] = head[u_]; head[u_] = edgenum; ++ into[v_]; } void topsort() { std::queue<int> q; for (int i = 1; i <= n; ++ i) f[i] = kN; f[1] = 0; q.push(1); while (!q.empty()) { int u_ = q.front(); q.pop(); for (int i = head[u_]; i; i = ne[i]) { int v_ = v[i]; f[v_] = std::min(f[v_], f[u_] + 1); int l = u_ + 1, r = v_ - (f[u_] + 1) - 1; if (l <= r) ++ ans[l], -- ans[r + 1]; if (!(-- into[v_])) q.push(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 >> m; edgenum = 0; for (int i = 1; i <= n; ++ i) head[i] = into[i] = ans[i] = 0; for (int i = 1; i < n; ++ i) addedge(i, i + 1); for (int i = 1; i <= m; ++ i) { int u_, v_; std::cin >> u_ >> v_; addedge(u_, v_); } topsort(); for (int i = 1; i < n; ++ i) { ans[i] += ans[i - 1]; std::cout << (ans[i] <= 0); } std::cout << "\n"; } return 0; } /* 1 15 3 2 8 4 9 8 15 11000111000111 12345678901234 */
E1
枚举,分治,笛卡尔树
妈的什么东西怎么开局十分钟就有光速过的呃呃,赛后一看我草这直接上笛卡尔树就做完了还是线性的,写完 D 时间不够了就直接下班了没看亏炸呃呃
题意实际上即不断地选择相邻的两个数,并将他们合并到较大的数上,直到只剩一个位置,并检查有哪些位置可作为最终剩下的位置。
显然原数列中最大值的位置一定合法;又发现若位置 合法且位置 满足 且可以通过合并使得合并后 且此时 与 相邻,则对位置 的操作可以同样地对 进行,从而将 完全合并到 中,则最终也可只剩下 一个元素推导出位置 也合法。
则一个显然的想法是按照权值递减的顺序枚举位置 ,检查该位置能否通过仅与不大于 的 合并,使其变为最大的 ,使得存在 ,且合并后 与 相邻,即可通过位置 是否合法递推位置 是否合法。
考虑对于某个位置 应当如何操作变为最大的 。由于仅能相邻的两个数合并,记位置 左右两侧第一个大于 的位置分别为 ,则其最大合并范围为 。于是仅需检查 和 是否成立,若成立则若 合法 也一定合法即可进行递推。
发现按上述递减枚举元素,并求左右两侧第一个大于该元素的过程,可以看做不断地取区间 的最值 ,并将区间以 为界分治为两部分 ,则两部分内权值均不大于 ,且有 。于是仅需递归地分治判断子区间之和是否不小于 ,若不小于则子区间内的最值的位置 即满足上述可以递推的条件,则可通过 是否合法递推。
这个过程显然可以直接放到笛卡尔树上进行。笛卡尔树上节点与区间最值位置一一对应,按照权值递减建立笛卡尔树后,初始化全局最大值位置合法,dfs 维护对应区间 ,比较 之和与父节点的最值 的权值大小关系,即可不断地递推当前节点最值位置 是否合法。
代码非常好写,复杂度 级别跑得飞快:https://codeforces.com/contest/1998/submission/275679413。
// /* By:Luckyblock */ #include <bits/stdc++.h> #define LL long long const int kN = 2e5 + 10; const LL kInf = 1e18 + 2077; //============================================================= int n, x, yes[kN]; LL a[kN], sum[kN]; int rt, son[kN][2]; int top, st[kN]; //============================================================= void dfs(int u_, int fa_, int l_, int r_) { LL s = sum[r_ - 1] - sum[l_]; if (s >= a[fa_]) yes[u_] |= yes[fa_]; if (son[u_][0]) dfs(son[u_][0], u_, l_, u_); if (son[u_][1]) dfs(son[u_][1], u_, u_, r_); } //============================================================= 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 >> x; for (int i = 1; i <= n; ++ i) { std::cin >> a[i]; sum[i] = sum[i - 1] + a[i]; } a[0] = a[n + 1] = kInf; for (int i = 0; i <= n; ++ i) son[i][0] = son[i][1] = yes[i] = 0; st[top = 0] = 0; for (int i = 1; i <= n; ++ i) { while (top && a[st[top]] < a[i]) -- top; son[i][0] = son[st[top]][1], son[st[top]][1] = i; st[++ top] = i; } rt = st[1]; yes[rt] = 1; dfs(rt, 0, 0, n + 1); int ans = 0; for (int i = 1; i <= n; ++ i) if (yes[i]) ++ ans; std::cout << ans << "\n"; } return 0; }
E2
枚举,分治,笛卡尔树。
我有一个极其优美的笛卡尔树做法!而且跑得飞快!
同样考虑笛卡尔树。一个显然的想法是在线性建笛卡尔树过程中,对于每个前缀对应的笛卡尔树套用上述做法。显然不行呃呃这就变 了铁过不去。于是考虑细致地观察 E1 中做法得到的合法位置,在笛卡尔树中呈现什么形态:
- 一定是以笛卡尔树根为根的,连通的树形的连通块;
- 每对连通的父子节点间满足:子节点对应区间之和,不小于父节点对应的最值。
- 答案即为该连通块的大小。
考虑在线性建立笛卡尔树过程中,维护上述以笛卡尔树根为根的,连通的树形的连通块。线性建立笛卡尔树过程中实际上是在维护笛卡尔树的右链,则容易发现,每次新增一个节点时,仅会影响右链上连通块的形态,已遍历过的左侧部分完全不会受影响,于是仅需考虑每次加点对右链上的连通性的影响。
连通性实际上即区间之和与父节点对应最值的大小关系,又区间之和仅会单调递增,加点过程中不会出现失去连通性的情况,于是考虑使用并查集维护连通性,当某个子节点区间之和大于父节点对应最值时,则将它们 merge 起来,并记 merge 后祖先为子节点,也即对应右链上深度较深的节点。
然后基于上述分析,考虑在建树过程中加入 时如何维护右链:
- 为保证右链的性质,先将右链上小于 的点全部弹出。
- 检查是否有 的左儿子 对应区间和不小于 ,若是则将 与 merge 起来。
- 此时右链上所有节点对应区间之和均会增大 ,考虑直接从根暴力下跳右链上的连通块,在此过程中检查相邻连通块之间,是否有子节点对应区间之和不小于父节点对应的最值,若是则可以建立它们之间的连通性,将他们 merge 起来即可。
可以证明:并查集合并后,右链上相邻的连通块 之间一定有: 的顶部节点对应区间和小于 的底部节点最值 。则可知右链上连通块的个数一定不会超过 级别,当且仅当右链上最值形如: 时达到上界。则每次暴跳更新右链信息的复杂度上界仅为 级别,
此时直接查询根节点所在连通块大小即为答案。
总复杂度 级别,但是常数超级小跑得飞快:https://codeforces.com/contest/1998/submission/275684758。
// /* By:Luckyblock */ #include <bits/stdc++.h> #define LL long long const int kN = 2e5 + 10; const LL kInf = 1e18 + 2077; //============================================================= int n, x, ans[kN]; LL a[kN], sum[kN]; int fa[kN], sz[kN], son[kN][2]; int top, st[kN]; //============================================================= int find(int x_) { return (fa[x_] == x_) ? (x_) : (fa[x_] = find(fa[x_])); } void merge(int u_, int v_) { int fu = find(u_), fv = find(v_); if (fu == fv) return ; fa[fu] = fv; sz[fv] += sz[fu]; } //============================================================= 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 >> x; for (int i = 1; i <= n; ++ i) { std::cin >> a[i]; sum[i] = sum[i - 1] + a[i]; } a[0] = a[n + 1] = kInf; for (int i = 0; i <= n; ++ i) { son[i][0] = son[i][1] = ans[i] = 0; fa[i] = i, sz[i] = 1; } st[top = 0] = 0; for (int i = 1; i <= n; ++ i) { while (top && a[st[top]] < a[i]) -- top; son[i][0] = son[st[top]][1], son[st[top]][1] = i; if (son[i][0] && sum[i - 1] - sum[st[top]] >= a[i]) merge(son[i][0], i); st[++ top] = i; int f = find(st[1]), u = son[f][1]; while (u) { if (sum[i] - sum[f] >= a[f]) merge(f, u); f = find(u), u = son[f][1]; } ans[i] = sz[find(st[1])]; } for (int i = 1; i <= n; ++ i) std::cout << ans[i] << " "; std::cout << "\n"; } return 0; }
写在最后
学到了什么:
- C:想好在写!别看到什么求什么数量啊 k 大值啊就高潮了光速拉个板子过来改了改发现还歹删了唐氏得一批
- E1/E2:发现维护过程与某些经典数据结构类似,考虑能否直接扔到上面跑。
你说的对按照常理来说现在又是夹带私货环节,为师已经迫不及待地要看 C104 的【中国翻訳】了口牙
结尾广告:中南大学 ACM 集训队绝赞招新中!
有信息奥赛基础,获得 NOIP 省一等奖并达到 Codeforces rating 1900+ 或同等水平及以上者,可以直接私聊我与校队队长联系,免选拔直接进校集训队参加区域赛!
没有达到该水平但有志于 XPCX 赛事请关注每学年开始的 ACM 校队招新喵!
到这个时候了还缺队友实在不妙!求求求求快来个大神带我呜呜呜呜
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】