AtCoder ABC 472 题解
A - A
题目描述
给定一个仅由大写英文字母组成的字符串 SSS。
把 SSS 中所有不是 A 的字符都替换成 .,输出得到的新字符串。
约束条件
- SSS 是长度在 111 到 100100100 之间(含两端)的大写英文字母串。
输入格式
输入从标准输入按以下格式给出:
S
输出格式
输出答案。
解题思路
按题意直接遍历字符串:遇到 A 保留,其余字符改成 .。没什么坑。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
int main()
{
string s;
cin >> s;
// 遍历每个字符,注意用 char& 引用才能直接修改原字符串
for (char &c : s)
{
if (c != 'A')
c = '.';
}
cout << s;
return 0;
}
B - Break a Stick
题目描述
有一根棍子,上面打了 N−1N-1N−1 个切口,把棍子分成 NNN 段。
从左到右每段的长度依次是 L1,L2,...,LNL_1,L_2,\dots,L_NL1,L2,...,LN。
选择一个切口把棍子折成两根,求两根棍子长度差的绝对值的最小值。
(切口宽度忽略不计,每根棍子的长度是它所含各段长度之和。)
约束条件
- 2≤N≤1002 \le N \le 1002≤N≤100
- 1≤Li≤1051 \le L_i \le 10^51≤Li≤105
- 输入全部为整数。
输入格式
N
L1 L2 ... LN
输出格式
输出答案。
解题思路
在第 iii 个切口处折断,左半段是前缀和 ∑k=1iLk\sum_{k=1}^{i} L_k∑k=1iLk,右半段是 sum−cur\text{sum}-\text{cur}sum−cur。
对每个位置 iii,枚举所有切口,取 ∣sum−2×cur∣\left|\text{sum}-2\times\text{cur}\right|∣sum−2×cur∣ 的最小值即可。
(实际上不把最后一个位置也算作切口,因为只有 N−1N-1N−1 个切口;但多算一次对最小值没有影响。本题 N≤100N\le100N≤100,直接暴力也足够。)
代码
cpp
#include <bits/stdc++.h>
using namespace std;
int main()
{
int n;
cin >> n;
vector<int> a;
int sum = 0; // 所有段长度之和
for (int i = 1; i <= n; i++)
{
int x;
cin >> x;
sum += x;
a.push_back(x);
}
// cur 为当前前缀(左半段)长度,右半段为 sum - cur
// 差值 = |(sum - cur) - cur| = |sum - 2*cur|
int ans = 0x3f3f3f3f, cur = 0;
for (int i = 0; i < a.size(); i++)
{
cur += a[i];
ans = min(ans, abs(sum - cur - cur));
}
cout << ans;
return 0;
}
C - On a Diet
题目描述
高桥君回到老家住了 NNN 天,第 iii 天的点心卡路里为 AiA_iAi。
为了养生,他重复下面的规则:如果最近 MMM 天 内吃掉的点心卡路里总和不超过 KKK,就吃掉这天的点心。
具体地,依次对 i=1,2,...,Ni=1,2,\dots,Ni=1,2,...,N 决定:
- 假设第 iii 天吃了点心,如果从第 max(i−M+1,1)\max(i-M+1,1)max(i−M+1,1) 天到第 iii 天之间吃掉的点心卡路里总和不超过 KKK,就真的吃;否则不吃。
对每个 iii 判断这天是否吃点心。
约束条件
- 1≤M≤N≤2×1051 \le M \le N \le 2\times10^51≤M≤N≤2×105
- 1≤K≤10151 \le K \le 10^{15}1≤K≤1015
- 1≤Ai≤1091 \le A_i \le 10^91≤Ai≤109
输入格式
N M K
A1 A2 ... AN
输出格式
输出 NNN 行,第 iii 行吃则 Yes,不吃则 No。
解题思路
维护一个滑动窗口:只统计最近 MMM 天里实际吃掉的点心的卡路里总和。
- 用队列
q记录最近 MMM 天窗口内吃掉的日期下标,用sum记录这些点心的卡路里总和。 - 处理第 iii 天时,先把队头里已经滑出窗口(下标 <i−M+1< i-M+1<i−M+1)的点心弹出并减掉。
- 若
sum + a[i]不超过 KKK,则吃,把 iii 入队、累加sum;否则不吃(不入队,不影响窗口)。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
const int maxn = 2e5 + 10;
queue<int> q; // 记录窗口内"吃掉的"那些天的下标
int n, m, a[maxn];
long long k, sum = 0; // sum 是窗口内已吃点心卡路里总和,会很大,用 long long
int main()
{
cin >> n >> m >> k;
for (int i = 1; i <= n; i++)
{
cin >> a[i];
// 先弹出已经滑出最近 m 天窗口的已吃点心
while (!q.empty())
{
int u = q.front();
if (u < i - m + 1)
q.pop(), sum -= a[u];
else
break;
}
// 看看加上当天是否超过 K,决定吃或不吃
if (sum + a[i] > k)
{
cout << "No\n";
continue; // 不吃:不入队,不影响 sum
}
else
{
cout << "Yes\n";
q.push(i);
sum += a[i];
}
}
return 0;
}
D - Bomber Mad
题目描述
有一个 HHH 行 WWW 列的网格,每个格子要么是空地 .,要么是炸弹 #。
对于空地 (i,j)(i,j)(i,j),如果 iii 行和 jjj 列上都不存在 炸弹,则称它是安全空地。
每次可以移动到上下左右相邻的空地(不能走到炸弹上)。求满足以下条件的空地 (i,j)(i,j)(i,j) 的个数:
- 从 (i,j)(i,j)(i,j) 出发,可以在不超过 KKK 次移动内到达某个安全空地。
约束条件
- 1≤H,W≤5×1051 \le H,W \le 5\times10^51≤H,W≤5×105,H×W≤5×105H\times W \le 5\times10^5H×W≤5×105
- 0≤K≤H×W−10 \le K \le H\times W-10≤K≤H×W−1
输入格式
H W K
S1
S2
...
SH
输出格式
输出满足条件的空地个数。
解题思路
多源 BFS。
- 预处理每行、每列是否含炸弹:
rb[i]、cb[j]。 - 把所有"安全空地"(
S[i][j]=='.'且!rb[i]且!cb[j])作为 BFS 起点,初始距离记为 1,全部入队。 - BFS 在空地
.上四方向扩展,距离 +1+1+1;遇到#或已访问则跳过。当dist[u] - 1 == K时不再往外扩展(已经用掉 KKK 步)。 - 最后统计
dist != 0的格子数。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
const int maxn = 5e5 + 10;
int dist[maxn]; // dist[idx]:到最近安全空地的最少移动次数 + 1(0 表示未到达)
queue<int> q;
int main()
{
int h, w;
long long k;
cin >> h >> w >> k;
vector<string> g(h);
for (int i = 0; i < h; i++)
cin >> g[i];
// 预计算每行、每列是否有炸弹,用来找安全空地
vector<int> rb(h, 0), cb(w, 0);
for (int i = 0; i < h; i++)
for (int j = 0; j < w; j++)
if (g[i][j] == '#')
rb[i] = cb[j] = 1;
// 所有安全空地作为多源 BFS 的起点
for (int i = 0; i < h; i++)
for (int j = 0; j < w; j++)
if (g[i][j] == '.' && !rb[i] && !cb[j])
{
int idx = i * w + j;
dist[idx] = 1;
q.push(idx);
}
int dx[] = {0, 0, 1, -1}, dy[] = {1, -1, 0, 0};
while (!q.empty())
{
int u = q.front();
q.pop();
int x = u / w, y = u % w;
if (dist[u] - 1 == k) // 已经用掉 k 步,不再扩展
continue;
for (int d = 0; d < 4; d++)
{
int nx = x + dx[d], ny = y + dy[d];
if (nx < 0 || nx >= h || ny < 0 || ny >= w)
continue;
int v = nx * w + ny;
if (g[nx][ny] == '#' || dist[v]) // 炸弹或已访问
continue;
dist[v] = dist[u] + 1;
q.push(v);
}
}
int ans = 0;
for (int i = 0; i < h; i++)
for (int j = 0; j < w; j++)
if (dist[i * w + j])
ans++;
cout << ans;
return 0;
}
E - Odd Cycle
题目描述
给定一个 NNN 个点、MMM 条边的简单连通无向图,判断是否存在由奇数个顶点构成的环,如果存在就输出一个。
形式化地说,判断是否存在整数序列 (v1,...,vK)(v_1,\dots,v_K)(v1,...,vK) 满足:
- K≥3K\ge 3K≥3 且为奇数;
- v1,...,vKv_1,\dots,v_Kv1,...,vK 互不相同;
- 对所有 1≤i≤K1\le i\le K1≤i≤K,viv_ivi 与 vi+1v_{i+1}vi+1 之间有边(vK+1=v1v_{K+1}=v_1vK+1=v1)。
有 TTT 个测试用例。
约束条件
- 1≤T≤2×1051\le T\le 2\times10^51≤T≤2×105
- 1≤N,M≤2×1051\le N,M\le 2\times10^51≤N,M≤2×105,所有用例的 NNN、MMM 总和均 ≤2×105\le 2\times10^5≤2×105
输入格式
T
case1
case2
...
caseT
每个用例:
N M
a1 b1
...
aM bM
输出格式
对每个用例,不存在则输出 -1;存在则输出:
K
v1 v2 ... vK
解题思路
一个图是二分图当且仅当它不存在奇环。所以用 1,21,21,2 两种颜色做 BFS 染色:
- 用
col[v] = 3 - col[u]给未访问的邻点染色,同时记录父节点par[v]=u和深度dep[v]。 - 一旦发现某条边两端颜色相同(
col[v]==col[u]),就找到了一个奇环,记下这条边的两端fu, fv。
重建奇环:从 fu、fv 分别沿 par 往上爬,先对齐深度,再一起往上爬到公共祖先(a==b),把两条路径拼起来,加上原来 fu-fv 的那条边,就得到一条奇环。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
const int maxn = 2e5 + 10;
int col[maxn], dep[maxn], par[maxn];
queue<int> q;
int main()
{
int T;
cin >> T;
while (T--)
{
int n, m;
cin >> n >> m;
vector<vector<int>> e(n + 1);
for (int i = 0; i < m; i++)
{
int u, v;
cin >> u >> v;
e[u].push_back(v);
e[v].push_back(u);
}
// 重置每个用例的状态
for (int i = 1; i <= n; i++) col[i] = dep[i] = par[i] = 0;
while (!q.empty()) q.pop();
q.push(1);
col[1] = 1;
int fu = 0, fv = 0; // 两端同色的那条边
while (!q.empty() && !fu)
{
int u = q.front();
q.pop();
for (int v : e[u])
{
if (!col[v]) // 未染色:染色 + 记录父节点和深度
{
col[v] = 3 - col[u];
dep[v] = dep[u] + 1;
par[v] = u;
q.push(v);
}
else if (col[v] == col[u]) // 发现同色边 -> 存在奇环
{
fu = u, fv = v;
break;
}
}
}
if (!fu) // 染色成功,是二分图,无奇环
{
cout << "-1\n";
continue;
}
// 从 fu、fv 分别爬父指针,对齐深度后一起爬到公共祖先
vector<int> pu, pv;
int a = fu, b = fv;
while (dep[a] > dep[b]) { pu.push_back(a); a = par[a]; }
while (dep[b] > dep[a]) { pv.push_back(b); b = par[b]; }
while (a != b) { pu.push_back(a); pv.push_back(b); a = par[a]; b = par[b]; }
pu.push_back(a); // 公共祖先
// 环 = fu 向上的路径 + 公共祖先向下(fv 向上)的路径反向拼回
vector<int> cyc = pu;
for (int i = (int)pv.size() - 1; i >= 0; i--) cyc.push_back(pv[i]);
cout << cyc.size() << "\n";
for (int x : cyc) cout << x << " ";
cout << "\n";
}
return 0;
}
F - Centroid of a Slice
题目描述
平面上有一个凸 NNN 边形 PPP,顶点按逆时针编号 1,...,N1,\dots,N1,...,N,顶点 iii 的坐标为 (xi,yi)(x_i,y_i)(xi,yi)。
回答 QQQ 个询问。第 jjj 个询问给定两个不相邻的顶点 uj,vju_j,v_juj,vj,用线段把 PPP 分成两个多边形,取从 uju_juj 指向 vjv_jvj 的直线右侧 的那个为 P′P'P′,求 P′P'P′ 的重心(把多边形内部看作均匀薄板时的质心)。
约束条件
- 4≤N≤3×1044\le N\le 3\times10^44≤N≤3×104,1≤Q≤2×1051\le Q\le 2\times10^51≤Q≤2×105
- ∣xi∣,∣yi∣≤5×105|x_i|,|y_i|\le 5\times10^5∣xi∣,∣yi∣≤5×105
输入格式
N Q
x1 y1
...
xN yN
u1 v1
...
uQ vQ
输出格式
输出 QQQ 行,每行两个数 pj qjp_j\ q_jpj qj,误差不超过 10−610^{-6}10−6 即可。
解题思路
多边形的重心公式:把多边形三角剖分为以原点为参考的三角形,每条有向边 (i,i+1)(i,i+1)(i,i+1) 贡献
cri=xiyi+1−xi+1yicr_i = x_i y_{i+1} - x_{i+1} y_icri=xiyi+1−xi+1yi
然后
Cx=∑(xi+xi+1) cri3∑cri,Cy=∑(yi+yi+1) cri3∑criC_x = \frac{\sum (x_i+x_{i+1})\,cr_i}{3\sum cr_i},\quad C_y = \frac{\sum (y_i+y_{i+1})\,cr_i}{3\sum cr_i}Cx=3∑cri∑(xi+xi+1)cri,Cy=3∑cri∑(yi+yi+1)cri
(逆时针多边形 ∑cri>0\sum cr_i>0∑cri>0,即面积的两倍。)
对于询问 (u,v)(u,v)(u,v),裁剪出的多边形 P′P'P′ 是 PPP 顶点序列中 uuu 到 vvv(沿逆时针的一段)加上弦 v→uv\to uv→u。做法:
- 用某个邻点 w=(u mod N)+1w=(u\bmod N)+1w=(umodN)+1 判断弦的方向:根据叉积 cxcxcx 的正负确定 P′P'P′ 是 u→vu\to vu→v 这段还是 v→uv\to uv→u 这段(题目要求的是直线 u→vu\to vu→v 右侧的部分)。
- 把顶点复制一遍(
x[n+i]=x[i]),用前缀和预处理 ∑cr\sum cr∑cr、∑(xi+xi+1)cr\sum (x_i+x_{i+1})cr∑(xi+xi+1)cr、∑(yi+yi+1)cr\sum (y_i+y_{i+1})cr∑(yi+yi+1)cr,从而 O(1)O(1)O(1) 得到弧上所有边的贡献。 - 最后补上弦边 b→ab\to ab→a 的贡献,再用公式求重心。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
const int maxn = 4e5 + 10;
int n, q;
long long x[maxn], y[maxn];
__int128 P_cr[maxn], P_sx[maxn], P_sy[maxn]; // 前缀和:叉积、x 一阶矩、y 一阶矩
int main()
{
cin >> n >> q;
for (int i = 1; i <= n; i++) cin >> x[i] >> y[i];
// 复制一遍顶点方便跨越 n 的区间取前缀
for (int i = n + 1; i <= 2 * n + 1; i++) { x[i] = x[i - n]; y[i] = y[i - n]; }
P_cr[0] = P_sx[0] = P_sy[0] = 0;
for (int i = 1; i <= 2 * n; i++)
{
__int128 cr = (__int128)x[i] * y[i + 1] - (__int128)x[i + 1] * y[i];
__int128 sx = (__int128)(x[i] + x[i + 1]) * cr;
__int128 sy = (__int128)(y[i] + y[i + 1]) * cr;
P_cr[i] = P_cr[i - 1] + cr;
P_sx[i] = P_sx[i - 1] + sx;
P_sy[i] = P_sy[i - 1] + sy;
}
while (q--)
{
int u, v;
cin >> u >> v;
int w = (u % n) + 1;
// 用 u 的下一个顶点判断直线 u->v 的哪一侧,决定取哪段弧
long long cx = (x[v] - x[u]) * (y[w] - y[u]) - (y[v] - y[u]) * (x[w] - x[u]);
int a, b;
if (cx < 0) { a = u; b = v; }
else { a = v; b = u; }
int bb = b;
if (bb <= a) bb += n; // 保证区间 [a, bb-1] 合法
__int128 tcr = P_cr[bb - 1] - P_cr[a - 1];
__int128 tsx = P_sx[bb - 1] - P_sx[a - 1];
__int128 tsy = P_sy[bb - 1] - P_sy[a - 1];
// 补上弦边 b->a 的贡献
__int128 cr_c = (__int128)x[b] * y[a] - (__int128)x[a] * y[b];
tsx += (__int128)(x[b] + x[a]) * cr_c;
tsy += (__int128)(y[b] + y[a]) * cr_c;
tcr += cr_c;
long double Cx = (long double)tsx / (3.0L * (long double)tcr);
long double Cy = (long double)tsy / (3.0L * (long double)tcr);
cout << fixed << setprecision(15) << Cx << " " << Cy << "\n";
}
return 0;
}
G - Cascading Grid
题目描述
有一个 HHH 行 WWW 列的网格,每个格子上写着 +、-、# 之一。
你可以进行任意次(含 0 次)操作:
- 选择一个不是
#的格子。把从该格子出发,只通过左、右、下 方向的相邻移动、且不经过#格子所能到达的所有格子(含自身)都变成#。
操作结束后,求网格中 + 的个数减去 - 的个数所能达到的最大值。
约束条件
- 1≤H,W≤301\le H,W\le 301≤H,W≤30
输入格式
H W
S1
S2
...
SH
输出格式
输出答案。
解题思路
先观察这个"淹没"操作的性质:只能往左、右、下移动,不能向上。因此:
- 每一行里由
#分割出的连续非#段是一个整体------选其中任意一个格子,整段会被一起变成#。 - 若某一行的某个段被变成
#,那么它上、下相邻(同列)的段,只要连着的格子是非#,也会被一起淹没。
于是把每一段的"整段删除/保留"看作一个决策:
- 把每个水平段看作一个节点,权重 w=w=w=(段内
+个数)−-−(段内-个数)。 - 如果段 AAA 在某个列上位于段 BBB 的正上方且两格都非
#,连一条 B→AB\to AB→A 的有向边------表示"如果段 BBB 被淹没,段 AAA 也无法保留"。 - 我们保留的段集合 SSS 必须满足封闭性:若 x∈Sx\in Sx∈S 且存在边 x→yx\to yx→y,则 y∈Sy\in Sy∈S(即保留一个段,其下方的段也必须保留)。目标是最大化 ∑c∈Swc\sum_{c\in S} w_c∑c∈Swc。
这就是经典的最大权闭合子图问题,用最大流求解:
- 源点 SSS 向所有 wc>0w_c>0wc>0 的段连容量 wcw_cwc 的边;
- 所有 wc<0w_c<0wc<0 的段向汇点 TTT 连容量 −wc-w_c−wc 的边;
- 对每条依赖边 x→yx\to yx→y 连容量 +∞+\infty+∞ 的边;
- 答案 === 正权总和 - 最小割(最大流)。
因为 H,W≤30H,W\le30H,W≤30,段数最多 H×W=900H\times W=900H×W=900,跑 Dinic 足够快。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
const int maxn = 910;
int H, W;
char g[35][35];
int id[35][35], w[maxn], sccCnt; // 每个水平段一个节点,w 为其净收益(+ 数 - - 数)
struct Edge { int to, nxt; long long cap; };
struct Dinic
{
Edge e[maxn * 20];
int head[maxn], cur[maxn], dep[maxn], cnt = 0, S, T, n;
void init(int n_, int s, int t)
{
n = n_; S = s; T = t;
for (int i = 0; i <= n; i++) head[i] = -1;
cnt = 0;
}
void add(int u, int v, long long c)
{
e[cnt] = {v, head[u], c}; head[u] = cnt++;
e[cnt] = {u, head[v], 0}; head[v] = cnt++;
}
bool bfs()
{
for (int i = 0; i <= n; i++) dep[i] = 0;
queue<int> q; q.push(S); dep[S] = 1;
while (!q.empty())
{
int u = q.front(); q.pop();
for (int i = head[u]; ~i; i = e[i].nxt)
{
int v = e[i].to;
if (e[i].cap && !dep[v]) { dep[v] = dep[u] + 1; q.push(v); }
}
}
return dep[T];
}
long long dfs(int u, long long f)
{
if (u == T || !f) return f;
long long s = 0;
for (int &i = cur[u]; ~i; i = e[i].nxt)
{
int v = e[i].to;
if (e[i].cap && dep[v] == dep[u] + 1)
{
long long d = dfs(v, min(f, e[i].cap));
e[i].cap -= d; e[i ^ 1].cap += d; s += d; f -= d;
if (!f) break;
}
}
return s;
}
long long flow()
{
long long r = 0;
while (bfs()) { for (int i = 0; i <= n; i++) cur[i] = head[i]; r += dfs(S, 1e18); }
return r;
}
} din;
int main()
{
cin >> H >> W;
for (int i = 1; i <= H; i++) { string s; cin >> s; for (int j = 1; j <= W; j++) g[i][j] = s[j - 1]; }
// 划分水平段:同一行内由 # 分隔的连续非 # 格子视为一个节点
for (int i = 1; i <= H; i++)
{
int j = 1;
while (j <= W)
{
if (g[i][j] == '#') { j++; continue; }
sccCnt++;
while (j <= W && g[i][j] != '#')
{
id[i][j] = sccCnt;
w[sccCnt] += (g[i][j] == '+' ? 1 : -1);
j++;
}
}
}
int S = sccCnt + 1, T = sccCnt + 2;
din.init(T, S, T);
// 最大权闭合子图:源 -> 正权,负权 -> 汇
long long pos = 0;
for (int c = 1; c <= sccCnt; c++)
{
if (w[c] > 0) { din.add(S, c, w[c]); pos += w[c]; }
else if (w[c] < 0) din.add(c, T, -w[c]);
}
// 依赖边:下段 -> 上段(保留下段必须保留上段)
set<pair<int, int>> es;
for (int i = 1; i < H; i++)
for (int j = 1; j <= W; j++)
if (g[i][j] != '#' && g[i + 1][j] != '#')
{
int u = id[i][j], v = id[i + 1][j];
if (u != v) es.insert({v, u}); // v 在下,u 在上:v -> u
}
for (auto &p : es) din.add(p.first, p.second, 1e18);
cout << pos - din.flow(); // 最大权 = 正权总和 - 最小割
return 0;
}