2024牛客暑期多校训练营6
写在前面
比赛地址:https://ac.nowcoder.com/acm/contest/81601#question
以下按个人难度向排序。
纯纯战犯场呃呃呃呃做题不看题小保底当成 100 抽一发我草太唐了开局吃五发呃呃呃呃中期口了三题出来写出来两道最后好歹没太烂呃呃
置顶广告:中南大学 ACM 集训队绝赞招新中!
有信息奥赛基础,获得 NOIP 省一等奖并达到 Codeforces rating 1900+ 或同等水平及以上者,可以直接私聊我与校队队长联系,免选拔直接进校集训队参加区域赛!
没有达到该水平但有志于 XPCX 赛事请关注每学年开始的 ACM 校队招新喵!
到这个时候了还缺队友实在不妙!求求求求快来个大神带我呜呜呜呜
H
签到。
模拟即可。
小保底是 90 发非常不行啊,不如我们马娘和 BA 直接吃井妈的
复制复制// /* By:Luckyblock */ #include <bits/stdc++.h> #define LL long long const int kN = 2e6 + 10; //============================================================= int n, cnt[310]; std::string s; std::vector<char> 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 --) { std::cin >> s; n = s.length(); s = "$" + s; pos.clear(); int flag = 1; if (n >= 10) { for (int i = 0; i <= 'U'; ++ i) cnt[i] = 0; for (int i = 1; i <= 9; ++ i) ++ cnt[(int)s[i]]; for (int l = 1, r = 10; r <= n; ) { ++ cnt[(int)s[r]]; if (cnt[(int)'3'] == 10) flag = 0; -- cnt[(int)s[l]]; ++ l, ++ r; } } if (n >= 90) { for (int i = 0; i <= 'U'; ++ i) cnt[i] = 0; for (int i = 1; i <= 89; ++ i) ++ cnt[(int)s[i]]; for (int l = 1, r = 90; r <= n; ) { ++ cnt[(int)s[r]]; if (cnt[(int)'5'] == 0 && cnt[(int)'U'] == 0) flag = 0; -- cnt[(int)s[l]]; ++ l, ++ r; } } for (auto c: s) if (c == '5' || c == 'U') pos.push_back(c); for (int i = 1, sz = pos.size(); i < sz; ++ i) { if (pos[i] != 'U' && pos[i - 1] != 'U') flag = 0; } std::cout << (flag ? "valid\n" : "invalid\n"); } return 0; }
B
数学,模拟。
考虑按照给定顺序进行切分。发现第一刀下去会变成 2 块,在此之后每刀经过 i 条线,答案便增加 i+1。然后手玩了下发现每刀经过的线数变化规律为:
赛时懒了就直接模拟了,实际上可以 O(1) 算,有关平面图的证明详见官方题解。
#include <bits/stdc++.h> using namespace std; #define int long long int n, k, ans; signed main(){ cin >> n >> k; if(n == k * 2) { cout << n << endl; return 0; } k = min(k, n - k); ans = 2; for(int i = 1; i <= k - 1; ++ i) ans += i + 1; for(int i = k; i <= n - k; ++ i) ans += k - 1 + 1; for(int i = n - k + 1, j = 0; i <= n - 1; ++ i, ++ j) ans += k + j + 1; cout << ans << endl; return 0; }
D
连通性问题,Tarjan
所有 Lun 都要在环中,所有 Qie 都不在环中,容易想到当删完边后剩下的子图使用边双连通分量缩完点后一定会很好看——所有 Lun 均在边双中,而所有 Qie 将缩完点后的子图连成树状结构。
发现可以分别处理 Lun 和 Qie。考虑仅对所有 Lun 运行边双连通分量算法,保留其中所有边双中的 Lun,并将他们使用并查集缩成一个点。然后再枚举所有 Qie 进行加边,在此过程中使用并查集维护连通性即可。
若最后可以得到一张连通图则 YES,否则 NO。
// /* By:Luckyblock https://www.luogu.com.cn/problem/P8436 */ #include <bits/stdc++.h> #define LL long long const int kN = 1e5 + 10; const int kM = 1e6 + 10; //============================================================= struct Edge {int u, v;}; std::vector <Edge> qie, ans; int n, m; int edgenum = 1, head[kN], v[kM], ne[kM]; int dfnnum, dfn[kN], low[kN]; bool bridge[kM], inans[kM]; int dccnum, indcc[kN], bel[kN]; std::vector <int> dcc[kN]; int fa[kN]; //============================================================= void Add(int u_, int v_) { v[++ edgenum] = v_; ne[edgenum] = head[u_]; head[u_] = edgenum; } void Tarjan(int u_, int from_) { dfn[u_] = low[u_] = ++ dfnnum; for (int i = head[u_]; i > 1; i = ne[i]) { int v_ = v[i]; if (!dfn[v_]) { Tarjan(v_, i); if (low[v_] > dfn[u_]) bridge[i] = bridge[i ^ 1] = 1; low[u_] = std::min(low[u_], low[v_]); } else if (i != (from_ ^ 1)) { low[u_] = std::min(low[u_], dfn[v_]); } } } void Dfs(int u_, int id_) { indcc[u_] = true; bel[u_] = id_; dcc[id_].push_back(u_); for (int i = head[u_]; i > 1; i = ne[i]) { int v_ = v[i]; if (indcc[v_] || bridge[i]) continue; Dfs(v_, id_); } } 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); std::cin >> n >> m; for (int i = 1; i <= m; ++ i) { int u_, v_; std::string w_; std::cin >> u_ >> v_ >> w_; if (w_ == "Lun") { Add(u_, v_), Add(v_, u_); } else { qie.push_back((Edge) {u_, v_}); } } for (int i = 1; i <= n; ++ i) if (!dfn[i]) Tarjan(i, 0); for (int i = 1; i <= n; ++ i) if (!indcc[i]) Dfs(i, ++ dccnum); int flag = 1; for (int i = 1; i <= n; ++ i) fa[i] = i; for (int id = 1; id <= dccnum; ++ id) { for (auto u_: dcc[id]) { for (int i = head[u_]; i > 1; i = ne[i]) { if (inans[i] || inans[i ^ 1] || bel[v[i]] != id) continue; inans[i] = inans[i ^ 1] = 1; ans.push_back((Edge) {u_, v[i]}); merge(u_, v[i]); } } } for (auto e: qie) { if (find(e.u) == find(e.v)) continue; ans.push_back(e); merge(e.u, e.v); } for (int i = 1; i <= n; ++ i) if (find(i) != find(1)) flag = 0; if (!flag) { std::cout << "NO\n"; } else { std::cout << "YES\n"; std::cout << ans.size() << "\n"; for (auto e: ans) std::cout << e.u << " " << e.v << "\n"; } // printf("%d\n", dccnum); // for (int i = 1; i <= dccnum; ++ i) { // printf("%d ", dcc[i].size()); // for (int j = 0, sz = dcc[i].size(); j < sz; ++ j) { // printf("%d ", dcc[i][j]); // } // printf("\n"); // } return 0; } /* 5 6 1 2 Lun 1 3 Lun 2 3 Lun 3 4 Lun 4 5 Lun 5 3 Lun */
A
树形 DP。
赛时 dztlb 大神单刷的,我看不懂他在写啥。
#include <bits/stdc++.h> using namespace std; #define int long long #define double long double const double eps=1e-10; const int N=510000; int T,n; int head[N],tot; struct node{ int to,nxt,w; }e[N<<1]; void add(int u,int v,int w){ e[++tot].to=v,e[tot].nxt=head[u],head[u]=tot; e[tot].w=w; } double ans[N]; void dfs(int u,int fa,int dep,int x,int y){ int cnt=0; for(int i=head[u];i;i=e[i].nxt){ int v=e[i].to; if(v==fa) continue; ++cnt; } if(cnt!=0)ans[u]=0; else {ans[u]=(double)x/(double)(x+y); return;} if(dep%2==1){ for(int i=head[u];i;i=e[i].nxt){ int v=e[i].to; if(v==fa) continue; int dx=x,dy=y; if(e[i].w==1) ++dx; else ++dy; dfs(v,u,dep+1,dx,dy); if(ans[u]<=ans[v]+eps) ans[u]=ans[v]; } if((x+y)!=0) { if(ans[u]>=(double)x/(double)(x+y)) ans[u]=(double)x/(double)(x+y); } }else{ ans[u]=(double)x/(double)(x+y); for(int i=head[u];i;i=e[i].nxt){ int v=e[i].to; if(v==fa) continue; int dx=x,dy=y; if(e[i].w==1) ++dx; else ++dy; dfs(v,u,dep+1,dx,dy); if(ans[u]>=ans[v]+eps) ans[u]=ans[v]; } } } signed main(){ cin>>T; while(T--){ cin>>n; for(int i=1;i<=n;++i){ head[i]=0; ans[i]=0; } tot=0; for(int i=1,u,v,w;i<n;++i){ cin>>u>>v>>w; add(u,v,w); add(v,u,w); } dfs(1,0,1,0,0); printf("%.12Lf\n",ans[1]); } return 0; } /* 1 9 1 2 1 2 3 1 3 4 0 4 5 0 5 6 0 6 7 0 7 8 1 8 9 0 4 3 1 2 1 1 3 0 4 1 2 0 1 3 1 3 4 0 5 1 2 0 1 3 0 3 4 1 4 5 1 9 1 2 1 2 3 1 3 4 0 4 5 0 5 6 0 6 7 0 7 8 1 8 9 0 */
F
图论,构造
想了下完全图的情况就直接秒了呃呃,然而一直被卡最后 5min 被 dztlb 大神叉掉了过了,实在刺激!!!
首先考虑 m=0 的情况,此时没有任何限制可以随便走;然后考虑有多棵树的情况,发现此时对于不同树间的节点没有任何限制,也可以随便走,于是仅需考虑单棵树的情况,并将所有单棵树构造的通路连起来即可。
对于单棵树,发现仅有菊花图的情况是无解的,此时仅可以将所有叶子构造成哈密尔顿路径但会剩下一个根,除此之外一定可以构造出完整的哈密尔顿路径;然后发现若有多棵树,无论有多少菊花图都有解。若其中某棵树是菊花图,则可先将其中所有叶子构造成一条通路,然后将剩下的根塞到到其他树构造出的通路里,再把所有树的通路连起来即可。
然后考虑对于某棵树应当如何构造哈密尔顿通路:
- 若点数为 1,此时相当于没有限制,不看做菊花图,通路即为自身。
- 若为点数不小于 2 的菊花图,则将其中所有叶子构造成一条通路,并额外记录一下根。
- 否则直径长度一定不小于 4,发现此时可以以直径端点为根对树进行分层,每层间可以随便连,且非相邻层也可以随便连。于是想到奇偶分层,分别将奇数偶数层按照层数连成一条通路,并将奇数层接到偶数层后面即可。
此时所有树都变成了一条通路,若菊花图则外加一个根。考虑将所有菊花图的根插入到下一棵树的通路的最后,再将所有通路首尾相连即可。注意若最后一棵树是菊花图,则需要特判将最后一棵树的菊花插到整个通路的开头。
特判仅有一棵树且为菊花图时无解。
// /* By:Luckyblock */ #include <bits/stdc++.h> #define LL long long const int kN = 5e5 + 10; //============================================================= int n, m, treenum, fa[kN], du[kN], bel[kN]; int edgenum, head[kN], v[kN << 1], ne[kN << 1]; bool vis[kN]; int from[kN], dis[kN]; int asshole[kN]; std::vector<int> tree[kN], seq[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; } void addedge(int u_, int v_) { v[++ edgenum] = v_; ne[edgenum] = head[u_]; head[u_] = edgenum; } void dfs(int u_, int fa_, bool flag) { if (flag) from[u_] = fa_; dis[u_] = dis[fa_] + 1; for (int i = head[u_]; i; i = ne[i]) { int v_ = v[i]; if (v_ == fa_) continue; dfs(v_, u_, flag); } } void solve(int id_) { int road[2] = {tree[id_][0], tree[id_][0]}, maxdis = 0; dfs(road[0], 0, 0); for (auto x: tree[id_]) if (dis[x] > maxdis) road[0] = x, maxdis = dis[x]; dfs(road[0], 0, 1); maxdis = 0; for (auto x: tree[id_]) if (dis[x] > maxdis) road[1] = x, maxdis = dis[x]; if (maxdis == 1) { seq[id_].push_back(road[0]); return ; } else if (maxdis == 2) { asshole[id_] = road[0]; seq[id_].push_back(road[1]); return ; } else if (maxdis == 3) { for (auto x: tree[id_]) { if (du[x] == (int) tree[id_].size() - 1) asshole[id_] = x; else seq[id_].push_back(x); } return ; } std::vector<int> temp; std::queue<int> q; q.push(road[0]); while (!q.empty()) { int u_ = q.front(); q.pop(); vis[u_] = 1; if (dis[u_] % 2 == 1) temp.push_back(u_); else seq[id_].push_back(u_); for (int i = head[u_]; i; i = ne[i]) { int v_ = v[i]; if (vis[v_]) continue; q.push(v_); } } for (auto x: temp) seq[id_].push_back(x); } //============================================================= 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 = treenum = 0; for (int i = 1; i <= n; ++ i) { fa[i] = i; du[i] = head[i] = bel[i] = asshole[i] = 0; dis[i] = from[i] = vis[i] = 0; tree[i].clear(), seq[i].clear(); } for (int i = 1; i <= m; ++ i) { int u_, v_; std::cin >> u_ >> v_; addedge(u_, v_), addedge(v_, u_); ++ du[u_], ++ du[v_]; merge(u_, v_); } for (int i = 1; i <= n; ++ i) { if (!bel[find(i)]) bel[find(i)] = ++ treenum; tree[bel[find(i)]].push_back(i); } for (int i = 1; i <= treenum; ++ i) solve(i); if (treenum == 1 && asshole[1]) { std::cout << -1 << "\n"; continue; } if (asshole[treenum]) std::cout << asshole[treenum] << " "; for (int i = 1; i < treenum; ++ i) if (asshole[i]) seq[i + 1].push_back(asshole[i]); for (int i = 1; i <= treenum; ++ i) { for (auto x: seq[i]) std::cout << x << " "; } std::cout << "\n"; } return 0; } /* 1 3 1 2 3 */
I
DP。
实际上 2h 就秒了这个状态太显然了呃呃,然而直到结束都在调代码红温于是没时间写呃呃呃呃,要是有三个人铁拿下妈的
比较显然的 DP,考虑记状态 fi,j 表示当前选择到第 i 行,第 i 行必选位置 j 时可获得的最大价值。初始化 f0,j=0,转移时考虑上一行必选的位置 k,则该行选择的区间中一定包含区间 [k,j](或 [j,k]),除此之外为了尽可能获得最大价值,应在必选的区间 [k,j](或 [j,k])两端再补上以 k−1 为右端点的和以 j+1 为左端点的最大子段和(可以为空)。
考虑预处理 Li,j 表示第 i 行中以 j 为左端点的最大子段和,Ri,j 表示第 i 行中以 j 为右端点的最大子段和(注意可以为空)。按照经典的最大子段和的 DP 方程即可 O(nm) 地全部预处理。则有转移:
然而这个转移数直接做是 O(nm^2) 的铁过不去。但是发现上面的转移方程很有意思,其中很多项是仅与 j 或 k 中的一方有关的。于是套路地考虑拆贡献,记前缀和数组 \operatorname{sum}_{i, j} = \sum_{1\le l\le j} a_{i, l},则上述转移方程可以改写为:
发现上述转移方程前半部分仅与 j 有关,后半部分仅与 k 有关,且后半部分是一个前/后缀最大值的形式。于是先枚举 k 并 O(m) 地预处理上述转移方程的后半部分,然后再在枚举 j 时直接查询预处理的前/后缀最大值即可 O(m) 地转移。
最终答案即为:
转移数被优化到了 O(nm) 级别,总时间复杂度 O(nm) 级别。
// /* By:Luckyblock */ #include <bits/stdc++.h> #define LL long long const int kN = 1e6 + 10; const LL kInf = 1e18 + 2077; //============================================================= int n, m; std::vector<LL> a[kN], sum[kN], f[kN]; //============================================================= void init() { std::cin >> n >> m; f[0].clear(); for (int i = 0; i <= m + 1; ++ i) f[0].push_back(0), sum[0].push_back(0); for (int i = 1; i <= n; ++ i) { a[i].clear(), f[i].clear(), sum[i].clear(); a[i].push_back(0), sum[i].push_back(0), f[i].push_back(-kInf); for (int j = 1; j <= m; ++ j) { int x; std::cin >> x; a[i].push_back(x); sum[i].push_back(sum[i][j - 1] + x); f[i].push_back(-kInf); } f[i].push_back(-kInf); } } void DP() { for (int i = 1; i <= n; ++ i) { std::vector<LL> left, right, hl, hr; left.push_back(-kInf), right.push_back(-kInf), hl.push_back(-kInf), hr.push_back(-kInf); for (int j = 1; j <= m; ++ j) { left.push_back(f[i - 1][j] - sum[i][j - 1]); right.push_back(f[i - 1][j] + sum[i][j]); hl.push_back(-kInf), hr.push_back(-kInf); } left.push_back(-kInf), right.push_back(-kInf), hl.push_back(-kInf), hr.push_back(-kInf); for (int r = 1; r <= m; ++ r) { hr[r] = std::max(hr[r - 1] + a[i][r], a[i][r]); left[r] += std::max(0ll, hr[r - 1]); left[r] = std::max(left[r], left[r - 1]); } for (int l = m; l; -- l) { hl[l] = std::max(hl[l + 1] + a[i][l], a[i][l]); right[l] += std::max(0ll, hl[l + 1]); right[l] = std::max(right[l], right[l + 1]); } for (int j = 1; j <= m; ++ j) { f[i][j] = std::max(f[i][j], left[j] + sum[i][j] + std::max(0ll, hl[j + 1])); f[i][j] = std::max(f[i][j], right[j] - sum[i][j - 1] + std::max(0ll, hr[j - 1])); } left.clear(), right.clear(), hl.clear(), hr.clear(); } } //============================================================= 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(); DP(); LL ans = -kInf; for (int i = 1; i <= m; ++ i) ans = std::max(ans, f[n][i]); std::cout << ans << "\n"; } return 0; }
写在最后
学到了什么:
- F:对情况讨论分类时,应当尽可能考虑全面每种情况的具体限制,以防多加限制导致需要特判一堆不必要的东西。
结尾广告:中南大学 ACM 集训队绝赞招新中!
有信息奥赛基础,获得 NOIP 省一等奖并达到 Codeforces rating 1900+ 或同等水平及以上者,可以直接私聊我与校队队长联系,免选拔直接进校集训队参加区域赛!
没有达到该水平但有志于 XPCX 赛事请关注每学年开始的 ACM 校队招新喵!
到这个时候了还缺队友实在不妙!求求求求快来个大神带我呜呜呜呜
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Deepseek官网太卡,教你白嫖阿里云的Deepseek-R1满血版
· 2分钟学会 DeepSeek API,竟然比官方更好用!
· .NET 使用 DeepSeek R1 开发智能 AI 客户端
· DeepSeek本地性能调优
· 一文掌握DeepSeek本地部署+Page Assist浏览器插件+C#接口调用+局域网访问!全攻略