Codeforces Round 977 (Div. 2, based on COMPFEST 16 - Final Round)
写在前面
补题地址:https://codeforces.com/contest/2021。
上大分失败呃呃呃呃
我不要上班呜呜
A 签到
考虑仅有三个数 时最优操作,手玩下发现最优操作顺序一定是按照升序进行操作。
进一步归纳可知对于全局按照升序操作一定是最优的。
复制复制// /* By:Luckyblock */ #include <bits/stdc++.h> #define LL long long const int kN = 110; //============================================================= int n, 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 --) { std::cin >> n; for (int i = 1; i <= n; ++ i) std::cin >> a[i]; std::sort(a + 1, a + n + 1); int ans = a[1]; for (int i = 2; i <= n; ++ i) ans = (ans + a[i]) / 2; std::cout << ans << "\n"; } return 0; }
B 贪心,枚举
显然应该先把原数列的 求出来,在此过程中对 有用的数显然之后不会被操作。然后仅需不断考虑使用剩下的数如何使 增加 1 即可。
发现每次操作一定选择最小的可行的数,且发现每次操作仅能加 ,即每个数仅能被修改成在 剩余系下相同,且大于该数的数。于是考虑将所有数 插入下标为 的 set 中,每次仅需检查 的 set 中最小值是否不大于 即可。
总时间复杂度 级别。
// /* By:Luckyblock */ #include <bits/stdc++.h> #define LL long long const int kN = 2e5 + 10; //============================================================= int n, x, mex, 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 --) { std::cin >> n >> x; for (int i = 1; i <= n; ++ i) std::cin >> a[i]; std::sort(a + 1, a + n + 1); mex = 0; std::map<int, std::multiset<int> > cnt; for (int i = 1; i <= n; ++ i) { if (a[i] != mex) cnt[a[i] % x].insert(a[i]); if (a[i] == mex) ++ mex; } while (!cnt[mex % x].empty() && *cnt[mex % x].begin() <= mex) { cnt[mex % x].erase(cnt[mex % x].begin()); ++ mex; } std::cout << mex << "\n"; } return 0; }
C1 贪心
发现这个“上台过的人随便插队”是非常灵活的,则若某个人能够成功第一次上台,则之后的上台一定可以满足。
于是仅需考虑 中每个权值第一次出现的位置,则可对 去重后得到一个排列,显然当且仅当该排列是 的一个前缀时合法。
// /* By:Luckyblock */ #include <bits/stdc++.h> #define LL long long const int kN = 2e5 + 10; //============================================================= int n, m, q, a[kN], b[kN]; bool vis[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 >> m >> q; for (int i = 0; i < n; ++ i) std::cin >> a[i], vis[a[i]] = 0; for (int i = 0; i < m; ++ i) std::cin >> b[i]; int now = 0; bool flag = 1; for (int i = 0; i < m; ++ i) { if (vis[b[i]]) continue; if (a[now] == b[i]) vis[a[now]] = 1, ++ now; else flag = 0; } std::cout << (flag ? "YA" : "TIDAK") << "\n"; } return 0; }
C2 贪心,枚举
发现 是排列,于是考虑按下标对 进行重映射到排列 上,同理对 进行重映射一下。则根据 C1 的结论,仅需检查 中出现的所有权值是不是恰好为 ,且权值 的第一次出现位置是升序的。
则此时仅需比较 中所有位置 ,其对应的权值 第一次出现位置,是否小于 第一次出现位置即可。若上述位置数量恰好为 ,则说明数列 合法。
考虑使用 set 维护每种权值的出现数量,则检查位置合法仅需检查两个 set 中最小值即可。发现每次修改仅会影响两个 set,至多 4 个位置的合法性,即可 级别维护合法性。
总时间复杂度 级别。
// /* By:Luckyblock */ #include <bits/stdc++.h> #define LL long long const int kN = 2e5 + 10; //============================================================= int n, m, q, a[kN], b[kN], map[kN]; std::set<int> s[kN]; int cnt; //============================================================= bool check(int p_) { if (p_ <= 0 || p_ >= n) return 0; return (*s[a[p_]].begin()) <= (*s[a[p_ + 1]].begin()); } bool query() { return cnt == n - 1; } void modify(int p_, int t_) { cnt -= check(map[b[p_]]) + check(map[b[p_]] - 1); s[b[p_]].erase(p_); cnt += check(map[b[p_]]) + check(map[b[p_]] - 1); cnt -= check(map[t_]) + check(map[t_] - 1); s[t_].insert(p_), b[p_] = t_; cnt += check(map[t_]) + check(map[t_] - 1); } void init() { cnt = 0; for (int i = 1; i < n; ++ i) cnt += check(i); } //============================================================= 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 >> q; for (int i = 1; i <= n; ++ i) std::cin >> a[i], map[a[i]] = i; for (int i = 1; i <= n; ++ i) s[i].clear(), s[i].insert(m + 1); for (int i = 1; i <= m; ++ i) std::cin >> b[i], s[b[i]].insert(i); init(); std::cout << (query() ? "YA" : "TIDAK") << "\n"; while (q --) { int p, t; std::cin >> p >> t; modify(p, t); std::cout << (query() ? "YA" : "TIDAK") << "\n"; } } return 0; }
D DP
我去这题在哪见过?这不今年牛客6的I?不过那题是钦定选择的区间中的某个数,而这题是钦定选择的区间的某个端点。
参考:https://blog.csdn.net/Code92007/article/details/142734368
懒得写题解了看代码吧。
// /* By:Luckyblock */ #include <bits/stdc++.h> #define LL long long const int kN = 2e5 + 10; const LL kInf = 1e18 + 2077; //============================================================= int n, m; LL sum[kN], pre[kN], suf[kN]; std::vector<LL> a[kN], f[kN], g[kN]; //============================================================= LL premax(int j) { return j < 1 ? 0 : pre[j] - (sum[m] - sum[j]); } LL sufmax(int j) { return j > m ? 0 : suf[j] - sum[j - 1]; } void init(int i) { pre[0] = suf[m + 1] = -kInf; for (int j = 1; j <= m; ++ j) sum[j] = sum[j - 1] + a[i][j]; for (int j = 1; j <= m; ++ j) pre[j] = std::max(pre[j - 1], sum[m] - sum[j - 1]); for (int j = m; j; -- j) suf[j] = std::max(suf[j + 1], sum[j]); } void trans(int i) { f[i].clear(), g[i].clear(); f[i].push_back(0), g[i].push_back(i); for (int j = 1; j <= m; ++ j) f[i].push_back(-kInf), g[i].push_back(-kInf); if (i == 1) { for (int j = 1; j <= m; ++ j) f[i][j] = sufmax(j), g[i][j] = premax(j); return ; } auto f1 = [&](int k) { return f[i - 1][k] + sum[k] + std::max(0ll, sufmax(k + 1)); }; auto f2 = [&](int k) { return f[i - 1][k] - sum[k - 2] + std::max(0ll, premax(k - 2)); }; auto g1 = [&](int k) { return g[i - 1][k] - sum[k - 1] + std::max(0ll, premax(k - 1)); }; auto g2 = [&](int k) { return g[i - 1][k] + sum[k + 1] + std::max(0ll, sufmax(k + 2)); }; int p = m, q = m - 1; for (int j = m - 1; j; -- j) { if (f1(j + 1) > f1(p)) p = j + 1; if (g2(j) > g2(q)) q = j; f[i][j] = std::max(f1(p), g2(q)) - sum[j - 1]; } p = 1, q = 2; for (int j = 2; j <= m; ++ j) { if (g1(j - 1) > g1(p)) p = j - 1; if (f2(j) > f2(q)) q = j; g[i][j] = std::max(g1(p), f2(q)) + sum[j]; } } //============================================================= 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; for (int i = 1; i <= n; ++ i) { a[i].clear(); a[i].push_back(0); for (int j = 1; j <= m; ++ j) { int x; std::cin >> x; a[i].push_back(x); } } for (int i = 1; i <= n; ++ i) init(i), trans(i); LL ans = -kInf; for (int i = 1; i <= m; ++ i) ans = std::max(ans, std::max(f[n][i], g[n][i])); std::cout << ans << "\n"; } return 0; }
E1/E2 Kruscal 重构树,树上背包
我去这题在哪见过?这不去年牛客6的A?
两点间的贡献是路径上的最长边这个性质太典了,考虑 Kruscal 重构树,原图节点变为重构树中的叶节点,两点间的最长边贡献变为两点 lca 的点权值的贡献。
考虑树上背包,枚举边时将两个子树合并,两子树代表的点集之间的最长边,即为枚举到的边的边权。则每次转移时仅需考虑两个子树中分别已经选择了多少点作为服务器即可,且当且仅当其中某个子树没有服务器选择时,需要考虑当前边权的贡献。
于是可以在 Kruscal 重构树上得到一个很显然的树形 DP,设 表示在 的子树中一共有 个点为服务器时,这棵树代表的点集对答案的贡献,初始化 。在构建 Kruscal 重构树枚举到边 合并 所在点集 得到点集 的同时,进行转移,考虑从 中分别选择了多少个服务器,有:
其中 表示点集大小, 表示点集中限定的需要连接的点的数量。
设最终得到的点集为 ,答案即:
这样实现起来比较符合重构树的思路也比较直观,但是时间复杂度上限是 级别,跑不过去。
发现新建节点 表示合并后的点集是没有必要的,可以在维护并查集时直接按秩合并到原节点上进行转移。于是修改定义 表示以 为根的点集中一共有 个点为服务器时,这棵树代表的点集对答案的贡献,其余部分均不变。
并查集合并时使用按秩合并时,总时间度复杂度变为 级别。
1A 了爽。
// /* By:Luckyblock */ #include <bits/stdc++.h> #define LL long long const int kN = 5010; const LL kInf = 1e18 + 2077; //============================================================= int n, m, p, yes[kN]; struct Edge { int u, v, w; } e[kN]; std::vector<int> edge[kN << 1]; int fa[kN], sz[kN], cnt[kN]; LL f[kN][kN], temp[kN]; //============================================================= bool cmp(const Edge& fir_, const Edge& sec_) { return fir_.w < sec_.w; } void init() { for (int i = 1; i <= n; ++ i) yes[i] = 0, fa[i] = i, sz[i] = 1, cnt[i] = 0; for (int i = 1; i <= p; ++ i) { int x; std::cin >> x; yes[x] = cnt[x] = 1; } for (int i = 1; i <= m; ++ i) { int u, v, w; std::cin >> u >> v >> w; e[i] = (Edge) {u, v, w}; } std::sort(e + 1, e + m + 1, cmp); } int find(int x_) { return x_ == fa[x_] ? x_ : (fa[x_] = find(fa[x_])); } void merge(int x_, int y_, int w_) { int fx = find(x_), fy = find(y_); int fxy = (sz[fx] > sz[fy]) ? fx : fy; for (int i = 0; i <= n; ++ i) temp[i] = kInf; for (int i = 1; i <= sz[fx]; ++ i) { for (int j = 1; j <= sz[fy]; ++ j) { temp[i + j] = std::min(temp[i + j], f[fx][i] + f[fy][j]); } } for (int i = 1; i <= sz[fx]; ++ i) { temp[i] = std::min(temp[i], f[fx][i] + 1ll * w_ * cnt[fy]); } for (int j = 1; j <= sz[fy]; ++ j) { temp[j] = std::min(temp[j], f[fy][j] + 1ll * w_ * cnt[fx]); } fa[fx] = fa[fy] = fxy; sz[fxy] = sz[fx] + sz[fy]; cnt[fxy] = cnt[fx] + cnt[fy]; for (int i = 0; i <= n; ++ i) f[fxy][i] = temp[i]; } //============================================================= 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 >> p; init(); for (int i = 1; i <= n; ++ i) { for (int j = 0; j <= n; ++ j) f[i][j] = kInf; f[i][1] = 0; } for (int i = 1; i <= m; ++ i) { auto [u, v, w] = e[i]; if (find(u) == find(v)) continue; merge(u, v, w); } for (int i = 1; i <= n; ++ i) std::cout << f[find(1)][i] << " "; std::cout << "\n"; } return 0; } /* 1 3 3 2 3 1 1 2 1 2 3 3 1 3 2 */
写在最后
参考:
学到了什么:
- C2:排列可以随便映射;
- D:限定选择的区间,转化为限定选择的区间中的某个位置;
- E:两点间的贡献是路径上的最长边这个性质太典了,考虑 Kruscal 重构树。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】