AtCoder Beginner Contest 374
省流版
- A. 判断末三位即可
- B. 逐位判断即可
- C. 枚举所有分组情况即可
- D. 枚举线段顺序、端点顺序即可
- E. 二分答案,发现贵的机器数量不超过,枚举求最小花费看是否可行即可
- F. 朴素DP,复杂度分析得到有效时刻不超过而非,直接即可
- G. 最小路径覆盖问题,建有向图、缩点、求传递闭包、二分图最大匹配即可
稍微完善了E题的证明
更新了G
A - Takahashi san 2 (abc374 A)
题目大意
给定一个字符串,问结尾是不是san
解题思路
直接判断最后三个字母即可,python
可以一行。
神奇的代码
print("Yes" if input().strip().endswith('san') else "No")
B - Unvarnished Report (abc374 B)
题目大意
给定两个字符串,问第一个字母不相同的次数。
解题思路
逐位判断即可。
python
的想法,即先找出不同的位置,然后取最小值。
神奇的代码
a = input().strip() b = input().strip() if len(a) > len(b): a, b = b, a if len(a) < len(b): a += ' ' * (len(b) - len(a)) pos = [i for i in range(len(a)) if a[i] != b[i]] if not pos: print(0) else: print(pos[0] + 1)
C - Separated Lunch (abc374 C)
题目大意
给定个数字,分成两组,使得和最大值最小。
解题思路
,直接花 枚举分组情况,每种情况花 统计和,所有情况取最小值即可。总的时间复杂度为。
神奇的代码
#include <bits/stdc++.h> using namespace std; using LL = long long; int main(void) { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); int n; cin >> n; vector<int> a(n); for (auto& x : a) cin >> x; int tot = accumulate(a.begin(), a.end(), 0); int up = (1 << n); int ans = 2e9 + 7; for (int i = 0; i < up; ++i) { int cnt = 0; for (int j = 0; j < n; ++j) { cnt += ((i >> j) & 1) * a[j]; } ans = min(ans, max(cnt, tot - cnt)); } cout << ans << '\n'; return 0; }
D - Laser Marking (abc374 D)
题目大意
二维平面,给定个线段,用激光打印机打印。
激光移动速率为,打印时的速率为 。
规定打印顺序,使得耗时最短。初始激光位于。
解题思路
打印顺序,即规定打印线段的顺序,以及每个线段从哪个端点开始打印。
由于,因此花 枚举顺序,花 枚举线段的打印端点,然后花 计算时间即可。
总的时间复杂度为。
神奇的代码
#include <bits/stdc++.h> using namespace std; using LL = long long; int main(void) { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); int n, s, t; cin >> n >> s >> t; vector<array<int, 4>> a(n); for (auto& x : a) { cin >> x[0] >> x[1] >> x[2] >> x[3]; } vector<int> id(n); iota(id.begin(), id.end(), 0); double ans = 1e9 + 7; int up = (1 << n); auto dist = [](int x, int y, int sx, int sy) -> double { return sqrt((x - sx) * (x - sx) + (y - sy) * (y - sy)); }; auto solve = [&](vector<int>& id, int dir) -> double { int x = 0, y = 0; double res = 0; for (auto& i : id) { auto [sx, sy, ex, ey] = a[i]; if ((dir >> i) & 1) { swap(sx, ex); swap(sy, ey); } res += dist(x, y, sx, sy) / s; res += dist(sx, sy, ex, ey) / t; x = ex; y = ey; } return res; }; do { for (int i = 0; i < up; i++) { ans = min(ans, solve(id, i)); } } while (next_permutation(id.begin(), id.end())); cout << fixed << setprecision(10) << ans << '\n'; return 0; }
E - Sensor Optimization Dilemma 2 (abc374 E)
题目大意
制作产品,有道工序。
每道工序有两种设备,单价 和 ,一天可做 和 的产品。
最终的生产效率是所有工序的产品数量的最小值。
现有 元,问生产效率的最大值。
解题思路
首先枚举这个最大值,然后看可不可行。容易发现生产效率越小越容易满足,越大越难满足,因此这个答案可以二分(典型的最小值最大)。
二分了生产效率后,剩下的问题就是让每一个工序的产能都,且让花费最小,然后看所有的工序的花费是否。
这里只有两种机器,一个朴素的想法就是计算单位产能的价格,即 和 看看那个小,那我们肯定买小的那个。
但是会有个问题,如果我们只买小的,然后产能刚好 ,此时肯定是最优策略。但如果产能 ,此时满足了,但花费不见得是最小的:比如可以少买几个,多买另外的,使得产能刚好 ,且花费比前面的还小(样例一就是个反例)。
假设 的单价更低,上述考虑的是全买的,但不一定是花费最小的,退而求其次,买一些 来替换 ,那我应该买多少个呢?
由于,比赛时就直接猜的 的范围就是 ,再大的话完全可以由等价的替换之类的。然后就过了。
现在细细想来,这里的问题即为,找到一个最好的满足该不等式,且 最小。的范围都高达 ,直接遍历不现实,但我们知道 尽可能大是最好的,因此这里的 的范围不会很大,但有多小呢?
假设的情况,此时全买 ,最坏情况就是,产量超了太多,现在想通过买一些使得产量超标少一点,比如多一台,少几台,就能让超标量少 ,这样最多多买台,就能调整产能刚刚好之类的。
或者从另一个角度来看,因为是尽可能多买,因此注意到最终的产量范围是,这里只有 个数,如果多买一台,少买几台,产量要么不变,要么变成另外的一个数(比如产量了)。这里的另外的一个数
最坏情况下只有种情况,因此的范围最多就到 ,就能遍历到可行的中的所有情况了。注意这里可行的情况不一定是种,有可能一种(比如多买一台 ,少买几台 ,最终产量不变),也可能其他种(产量 就有 种,如果 就可能只有一半的数能取到),具体多少种呢?其实就是,但它们一定是的因子,因此 直接遍历 就一定能遍历到所有能取到的情况了(虽然可能有一些情况被多次考虑了),当然遍历也是正确的,这样所有的情况都只考虑一次了。
神奇的代码
#include <bits/stdc++.h> using namespace std; using LL = long long; int main(void) { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); int n, x; cin >> n >> x; vector<array<int, 4>> a(n); for (auto& i : a) { cin >> i[0] >> i[1] >> i[2] >> i[3]; } auto check = [&](int t) { LL sum = 0; for (auto& i : a) { auto [a, p, b, q] = i; if (1ll * p * b > 1ll * q * a) swap(a, b), swap(p, q); LL tmp = 1e9 + 7; for (int j = 0; j < a; ++j) { // j < a / gcd(a,b) 也可以 LL cost = 1ll * max(0, (t - j * b + a - 1) / a) * p + j * q; tmp = min(tmp, cost); } sum += tmp; } return sum <= x; }; int l = 0, r = 1e9 + 8; while (l + 1 < r) { // [l,r) int mid = (l + r) / 2; if (check(mid)) l = mid; else r = mid; } cout << l << '\n'; return 0; }
F - Shipping (abc374 F)
题目大意
个单,第 个单从 时刻可以接。
一次最多接 个单,接了后 时刻之后才能再接。
一个单的不满意度为接单时刻与可接时刻的差,即。
求最小的不满意度,
解题思路
这题难在复杂度分析。
朴素即 表示前 时刻,完成了前 个单的最小不满意度。但这里时刻数高达,不大行。
时刻数不能作为状态。但考虑上述状态,有非常多的显然不优的状态:我接单的时刻,只有两类:
- 我现在刚刚可以接单,就立刻接还在囤积的单。
- 或者我等等下一个单,然后一起接。
考虑这样的时刻数有多少:
- 第一类的时刻数,就是形如,但注意到每 ,必定有一个囤积的单,如果没有囤积的单,那下一个时刻就是第二类的(某个)。因此第一类的时刻数,对于每个 来说只有个,即最多有个 。因此总的时刻数就个。
- 第二类的时刻数,显然就是个。
所以,上述中的 ,抛去显然不优的状态,剩下的只有 个需要考虑的状态,加之 有状态,其状态数就是 ,加之转移复杂度是,总的时间复杂度就是。
转移考虑往后转移的方式,代码里,则为表示完成前 个单,此时时刻为 的最小不满意度,因为是离散的所以是个 。然后枚举接下来完成的单的数量,计算不满意度转移即可。计算不满意度即,可以用前缀和优化,或者枚举时维护 。
标准写法500ms
#include <bits/stdc++.h> using namespace std; using LL = long long; int main(void) { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); int n, k, x; cin >> n >> k >> x; vector<LL> t(n); for (auto& i : t) cin >> i; vector<LL> sum(n); partial_sum(t.begin(), t.end(), sum.begin()); vector<map<LL, LL>> dp(n + 1); dp[0][0] = 0; auto get_sum = [&](int l, int r) { if (l > r) return 0ll; return sum[r] - (l ? sum[l - 1] : 0); }; for (int i = 0; i < n; ++i) { for (auto& [now, val] : dp[i]) { int st = i; for (int j = 1; j <= k && i + j <= n; ++j) { int ed = st + j; LL cur = max(now, t[ed - 1]); LL nxt = val + j * cur - get_sum(st, ed - 1); if (dp[i + j].count(cur + x)) { dp[i + j][cur + x] = min(dp[i + j][cur + x], nxt); } else { dp[i + j][cur + x] = nxt; } } } } LL ans = 1e18 + 7; for (auto& [_, val] : dp[n]) { ans = min(ans, val); } cout << ans << '\n'; return 0; }
下面的是比赛时写的,加了点小小的转移优化,省去了一些不必要的转移(即囤积的单肯定全部处理)。
赛场时写的稍加优化的1ms
#include <bits/stdc++.h> using namespace std; using LL = long long; int main(void) { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); int n, k, x; cin >> n >> k >> x; vector<LL> t(n); for (auto& i : t) cin >> i; vector<LL> sum(n); partial_sum(t.begin(), t.end(), sum.begin()); vector<map<LL, LL>> dp(n + 1); dp[0][0] = 0; auto get_sum = [&](int l, int r) { if (l > r) return 0ll; return sum[r] - (l ? sum[l - 1] : 0); }; for (int i = 0; i < n; ++i) { for (auto& [now, val] : dp[i]) { int st = i; int ed = upper_bound(t.begin(), t.end(), now) - t.begin(); // 囤积的单 int cnt = max(1, ed - st); for (int j = min(k, cnt); j <= k && i + j <= n; ++j) { // 囤积的单肯定全部处理 ed = st + j; LL cur = max(now, t[ed - 1]); LL nxt = val + j * cur - get_sum(st, ed - 1); if (dp[i + j].count(cur + x)) { dp[i + j][cur + x] = min(dp[i + j][cur + x], nxt); } else { dp[i + j][cur + x] = nxt; } } } } LL ans = 1e18 + 7; for (auto& [_, val] : dp[n]) { ans = min(ans, val); } cout << ans << '\n'; return 0; }
G - Only One Product Name (abc374 G)
题目大意
给定个长度为 的大写字符串,构造一个字符串列表,满足每个大写字符都作为子串出现在这列表里,且列表里每个字符串的 字母子串都是这 个字符串里的,即出现的都在子串里,子串里的都出现了。
问这个列表的字符串数量的最小值。
解题思路
显然答案的下界就是,即每个字符串都在这个列表里。
如何让答案更小呢?那就是可以将其拼接,比如两个字符串AB
和BC
拼接起来,得到ABC
,这样仍能满足题意条件,且该字符串列表的字符串数量减少了一个。
这启发我们考虑如何拼接,将每个字符串看作图的点,两个字符串可以拼接,则一条有向边。
每一条有向边就是一次拼接,一条路径就是拼接多次,也就对应字符串列表里的一个字符串。
因此问题转换成,用最少的路径,覆盖所有顶点。注意这里的路径的点可以重复,同时也可以相互相交。
著名的最小路径覆盖是适用于的,而这里有环,但因为一条路径的点可以重复,因此可以先用Tarjan
将环缩成一个点 ,得到一张DAG
,剩下的问题就是一个最小可相交路径覆盖问题
,就是套路了。
关于套路的理解,首先得明白最小不相交路径覆盖
与二分图最大匹配
的关系:因为路基不能相交,可以理解成每个点只能有一个入度和出度,因此每个点拆成两个点,代表出度
和入度
分居两边,一个匹配就是一个点的出度和一个点的入度匹配,即选了一条有向边,即拼接了一次,答案减一。而最大匹配就对应了最小答案。
理解了匹配的意义,来看可相交的情况下,此时每个点不一定只有一个入度和出度,但有多少个无所谓,我们只关心一条路径的起点和终点,中间的无所谓覆盖过与否,因此如果两点可通过若干个中间点可达,无所谓,我们直接连一条 的边即可,一旦匹配到这条边,意味着有一条路径 ,中间的点是什么无所谓,因为可以重复覆盖。而这是一个传递闭包(可达性)。
总体而言,
- 根据拼接关系建立有向图
- 用
Tarjan
将环收缩成点后重建新图(转成) - 新图跑一遍
floyd
得到可达性的闭包,根据可达性建立新图(两点可达则有边) - 将新图转换成二分图,求最大匹配,答案即为
点数-匹配数
神奇的代码
#include <bits/stdc++.h> using namespace std; using LL = long long; int main(void) { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); int n; cin >> n; vector<string> s(n); for (auto& i : s) cin >> i; vector<vector<int>> G(n); for (int i = 0; i < n; ++i) { for (int j = 0; j < n; ++j) { if (s[i].back() == s[j].front()) G[i].push_back(j); } } // SCC vector<int> low(n), dfn(n), belong(n), in(n); vector<vector<int>> bcc; int clk = 0; auto tarjan = [&](auto&& tarjan, int u) -> void { static stack<int> st; dfn[u] = low[u] = ++clk; st.push(u); in[u] = true; for (int& v : G[u]) { if (!dfn[v]) { tarjan(tarjan, v); low[u] = min(low[u], low[v]); } else if (in[v]) low[u] = min(low[u], dfn[v]); } if (dfn[u] == low[u]) { vector<int> tmp; while (1) { int x = st.top(); st.pop(); in[x] = false; belong[x] = bcc.size(); tmp.push_back(x); if (x == u) break; } bcc.push_back(tmp); } }; for (int i = 0; i < n; ++i) { if (!dfn[i]) tarjan(tarjan, i); } // reconstract int m = bcc.size(); vector<vector<int>> edge(m, vector<int>(m)); for (int i = 0; i < n; ++i) { for (auto j : G[i]) { if (belong[i] != belong[j]) edge[belong[i]][belong[j]] = 1; } } // floyd for (int k = 0; k < m; ++k) { for (int i = 0; i < m; ++i) { for (int j = 0; j < m; ++j) { if (i == j) continue; if (edge[i][k] && edge[k][j]) edge[i][j] = 1; } } } // augmenting path vector<int> vis(m), pa(m, -1); int tt = 0; function<bool(int)> dfs = [&](int u) { vis[u] = tt; for (int v = 0; v < m; ++v) { if (edge[u][v] && (pa[v] == -1 || (vis[pa[v]] != tt && dfs(pa[v])))) { pa[v] = u; return true; } } return false; }; int ans = 0; for (int i = 0; i < m; ++i) { ++tt; if (dfs(i)) ++ans; } cout << m - ans << '\n'; return 0; }
本文作者:~Lanly~
本文链接:https://www.cnblogs.com/Lanly/p/18448640
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步