AtCoder Beginner Contest 472 ABCDE
A - A
- 预估难度:
入门 - 标签:字符串
题意
给定一个由大写英文字母组成的字符串 \(S\)。
请将 \(S\) 中除 A 以外的所有字符替换为 .,并输出替换后得到的字符串。
数据范围
- \(1 \le |S| \le 100\)
- \(S\) 仅由大写英文字母组成
代码
#include<bits/stdc++.h>
using namespace std;
int main()
{
string s;
cin >> s;
for(char c : s)
{
if(c == 'A')
cout << 'A';
else
cout << '.';
}
return 0;
}
B - Break a Stick
- 预估难度:
入门 - 标签:枚举
题意
有一根木棍。这根木棍上有 \(N-1\) 个凹槽,将其分成了 \(N\) 段。
从木棍的一端开始,这 \(N\) 段的长度依次为 \(L_1, L_2, \dots, L_N\)。
如果选择一个刻度将木棍折断,从而得到两根木棍,请找出折断后得到的两根木棍长度之差的绝对值的最小可能值。
注意:刻度的宽度可以忽略不计,折断后得到的每根木棍的长度等于其所包含的各段长度之和。
数据范围
- \(2 \leq N \leq 100\)
- \(1 \leq L_i \leq 10^5\)
思路
题意也就是将数组分为 \([1, i], [i+1, N]\) 两段,且让这两段总和的差值最小。
直接枚举分界点 \(i\),然后循环嵌套分别求解 \([1, i]\) 和 \([i+1, N]\) 两段各自的总和,再计算差值绝对值取最小即可,时间复杂度 \(O(N^2)\),足够通过。
也可以结合前缀和思想,从左往右枚举分界点 \(i\) 时顺便直接维护 \([1, i]\) 的总和,那么后半段 \([i + 1, N]\) 的总和可以直接借助整个数组的总和减去前半段来得到,时间复杂度 \(O(N)\)。
代码
#include<bits/stdc++.h>
using namespace std;
int n, l[105];
int main()
{
cin >> n;
int sum = 0; // 总和
for(int i = 1; i <= n; i++)
{
cin >> l[i];
sum += l[i];
}
int pre = 0; // 1 ~ i 的总和
int ans = 1e9; // 答案
for(int i = 1; i <= n; i++)
{
pre += l[i];
// 后半段总和即 (sum - pre)
ans = min(ans, abs((sum - pre) - pre));
}
cout << ans;
return 0;
}
C - On a Diet
- 预估难度:
普及- - 标签:计数思想、模拟
题意
高桥正在父母家进行为期 \(N\) 天的探亲之旅。
在父母家里,他每天都有不同的零食吃。第 \(i\) 天的零食热量为 \(A_i\)。
为了管理健康,他会保证连续 \(M\) 天内吃的零食总热量不超过 \(K\)。
具体来说,按照 \(i=1,2,\dots,N\) 的顺序,他根据以下规则决定是否吃第 \(i\) 天的零食:
- 假设当前为第 \(i\) 天,如果从第 \(\max(i-M+1,1)\) 天到第 \(i-1\) 天已经吃过的零食总热量再加上第 \(i\) 天的零食热量不会超过 \(K\) 时,那么他才会选择在第 \(i\) 天吃零食。
对于每个 \(i=1,2,\dots,N\),请判断高桥在第 \(i\) 天是否吃了零食。
数据范围
- \(1 \leq M \leq N \leq 2 \times 10^5\)
- \(1 \leq K \leq 10^{15}\)
- \(1 \leq A_i \leq 10^9\)
思路
按顺序枚举日期 \(i\),那么接下来的主要目的就是要快速求出第 \(\max(i-M+1, 1) \sim i-1\) 这 \(M-1\) 天内已经吃了多少热量的零食。
记最近 \(M\) 天(先不包含当前这一天)吃的零食总热量为 sum,然后通过判断 sum + A[i] 是否超过 \(K\) 来决定是否要在第 \(i\) 天吃零食。这是一个基础滑动窗口的变型,只需要动态维护变量 sum 的值即可。
先假设 \(i-M \ge 1\),那么当枚举到第 \(i-1\) 天时,sum 维护的就是 \(i-M \sim i-1\) 这些天所吃的零食热量总和。当枚举的日期变为第 \(i\) 天,sum 需要改为维护第 \(i-M+1 \sim i\) 这些天所吃的零食热量总和。比较前后两段区间可以发现,这里只需要将第 \(i-M\) 天对 sum 的影响去除,即可继续判断第 \(i\) 天是否吃零食。
注意如果 \(i-M \lt 1\) 或是第 \(i-M\) 天没有选择吃零食,就不要去把这天的影响去除。这步判断可以通过维护一个计数数组 eat[i] 来实现,用于表示第 \(i\) 天是否选择吃零食。
时间复杂度 \(O(N)\)。
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n, m, a[200005];
ll k;
bool eat[200005]; // 标记每一天是否吃零食
int main()
{
cin >> n >> m >> k;
for(int i = 1; i <= n; i++)
cin >> a[i];
ll sum = 0; // 最近 m 天内吃的总热量
for(int i = 1; i <= n; i++)
{
// “最近 m 天”的范围为 i-m+1 ~ i,因此需要排除第 i-m 天的影响
if(i - m >= 1 && eat[i - m]) // 如果第 i-m 天有吃零食
sum -= a[i - m];
if(sum + a[i] <= k) // 如果加上第 i 天的零食后,总热量依旧不超过 k,才会吃
{
eat[i] = true;
sum += a[i];
cout << "Yes\n";
}
else
cout << "No\n";
}
return 0;
}
D - Bomber Mad
- 预估难度:
普及 - 标签:计数思想、宽度优先搜索
题意
有一个 \(H\) 行 \(W\) 列的网格。每个格子要么是空格子,要么是炸弹格子。我们用 \((i,j)\) 表示从上往下数第 \(i\) 行、从左往右数第 \(j\) 列的格子。网格由 \(H\) 个长度为 \(W\) 的字符串 \(S_1, S_2, \dots, S_H\) 给出:如果 \(S_i\) 的第 \(j\) 个字符是 .,则 \((i,j)\) 是空格子;如果 \(S_i\) 的第 \(j\) 个字符是 #,则 \((i,j)\) 是炸弹格子。
对于一个空格子 \((i,j)\),如果第 \(i\) 行整行和第 \(j\) 列整列中都没有炸弹格子,则该格子被称为安全空格子。
在一次移动中,你可以从当前格子移动到上、下、左、右四个方向中相邻的空格子(不能移动到炸弹格子)。请找出满足以下条件的空格子 \((i,j)\) 的数量:
- 从 \((i,j)\) 出发,最多经过 \(K\) 次移动,能够到达一个安全空格子。
数据范围
- \(1 \le H,W \le 5\times 10^5\)
- \(H\times W \le 5\times 10^5\)
- \(0 \le K \le H\times W-1\)
- \(S_i\) 的长度为 \(W\),且仅由
.和#组成
思路
由于移动并不是单向的,因此问有多少个空格子能最多移动 \(K\) 步到达安全空格子,也就是问从某个安全空格子出发,最多移动 \(K\) 步能到达的空格子数量有多少个。
这明显是一个多源点且限制步数的BFS问题,只需要一次性将所有安全空格子作为搜索起点,然后宽搜求能到达的空格子数量即可。
而对于安全空格子的判断,只需对行和列分别开一个计数数组 cntX[i]、cntY[j],用于记录对应行和对应列上的炸弹格子数量。对于单元格 \((i, j)\),如果 cntX[i] == 0 && cntY[j] == 0 则说明这是一个安全空格子。
判断的时间复杂度 \(O(H \cdot W)\),BFS 过程每个格子只需看一遍,时间复杂度 \(O(H \cdot W)\)。
总时间复杂度 \(O(H \cdot W)\)。
代码
#include<bits/stdc++.h>
using namespace std;
const int dx[4] = {-1, 1, 0, 0};
const int dy[4] = {0, 0, -1, 1};
int H, W, K;
vector<vector<char>> mp;
vector<int> cntX, cntY; // 统计每行每列的炸弹格子数量
struct point
{
int x, y, step;
};
vector<vector<bool>> vis; // 标记每个点是否已搜过
int main()
{
cin >> H >> W >> K;
mp = vector<vector<char>>(H + 1, vector<char>(W + 1));
vis = vector<vector<bool>>(H + 1, vector<bool>(W + 1, false));
cntX = vector<int>(H + 1, 0);
cntY = vector<int>(W + 1, 0);
for(int i = 1; i <= H; i++)
for(int j = 1; j <= W; j++)
{
cin >> mp[i][j];
if(mp[i][j] == '#')
cntX[i]++, cntY[j]++;
}
queue<point> q;
for(int i = 1; i <= H; i++)
for(int j = 1; j <= W; j++)
if(cntX[i] == 0 && cntY[j] == 0) // (i, j) 为安全空格子
{
q.push({i, j, 0});
vis[i][j] = true;
}
while(!q.empty())
{
point u = q.front();
q.pop();
for(int i = 0; i < 4; i++)
{
point v = u;
v.x += dx[i];
v.y += dy[i];
v.step++;
if(v.x < 1 || v.y < 1 || v.x > H || v.y > W) // 限制坐标
continue;
if(v.step > K) // 限制步数
continue;
if(mp[v.x][v.y] == '#') // 限制不走到炸弹上
continue;
if(vis[v.x][v.y]) // 限制不重复搜索
continue;
q.push(v);
vis[v.x][v.y] = true;
}
}
int ans = 0; // 求被搜过的点的总数
for(int i = 1; i <= H; i++)
for(int j = 1; j <= W; j++)
if(vis[i][j])
ans++;
cout << ans;
return 0;
}
E - Odd Cycle
- 预估难度:
普及+/提高- - 标签:图论、深度优先搜索
题意
给定一个包含 \(N\) 个顶点(编号为 \(1\) 到 \(N\))和 \(M\) 条边的简单连通无向图。第 \(i\) 条边连接顶点 \(a_i\) 和 \(b_i\)。
请判断图中是否存在由奇数个顶点组成的环。如果存在,请找出其中一个这样的环。
形式化地说,请判断是否存在一个整数序列 \((v_1, v_2, \ldots, v_K)\) 满足以下所有条件。如果存在,请找出其中一个这样的序列:
- \(K\) 是一个至少为 \(3\) 的奇数。
- \(v_1, v_2, \ldots, v_K\) 互不相同。
- 对于每一个满足 \(1 \leq i \leq K\) 的整数 \(i\),顶点 \(v_i\) 和 \(v_{i+1}\) 之间存在一条边(其中规定 \(v_{K+1} = v_1\))。
本题包含 \(T\) 组测试用例,请依次求解。
数据范围
- \(1 \le T \le 2\times10^5\)
- \(1 \le N, M \le 2\times10^5\)
- \(N\) 的总和与 \(M\) 的总和均不超过 \(2 \times 10^5\)
- \(1 \le a_i,b_i \le N\)
- \(a_i\ne b_i\)
- 给定的图一定是一张简单连通无向图
思路
考虑构建无向图的深度优先搜索树,也就是按照深搜的方式,以某个点(点 \(1\))作为根结点开始搜索,将所有非父结点的邻接点均看作是子结点。
但由于图中存在环,因此可能会出现某个点 \(u\) 的子结点 \(v\) 反而是其祖先的情况,也就是遇到了返祖边。
- 这一步在代码实现过程中,如果搜索到的下一个点不是当前点的父结点,那么只要下一个点还没被搜过,就可以直接作为子结点继续搜索,否则便可以断定出现了返祖边。
但很明显,只要这样的返祖边 \(u \rightarrow v\) 出现,也就代表着从 \(v\) 到 \(u\) 的树链在拼上这条返祖边后能够形成完整的环。
因此,为了在出现环时找出树链上的所有点,我们可以在深度优先搜索的同时,借助一个栈来维护根结点到当前搜索的这个点的路径列表,这只需要每次搜索一个点时让当前点进栈,搜索完后再让当前点正常出栈即可。如此,我们便可以在发现返祖边 \(u \rightarrow v\) 时,将所有在 \(v\) 之后搜到的点全部出栈,来当作环的组成部分。
但本题还要求我们找的环必须是奇环,因此只是找到返祖边还不够。我们还可以在搜索的同时再维护一个深度数组 dep[i] 用于表示每个点在搜索树上的深度。那么在发现返祖边 \(u \rightarrow v\) 时,由于 \(u, v\) 都应在环内,因此只需要保证 dep[u] 与 dep[v] 的奇偶性相同(深度差值为偶数),即可断定环上总点数为奇数。此时再通过出栈操作找出环上的所有点作为答案即可。
单组数据时间复杂度 \(O(N + M)\)。
代码
#include<bits/stdc++.h>
using namespace std;
int N, M;
vector<int> G[200005];
stack<int> sk;
bool vis[200005];
int dep[200005]; // 假设以 1 为根,去除所有返祖边影响后,记录 i 的深度
vector<int> ans; // 找到环时存答案
void dfs(int u, int fa)
{
vis[u] = true;
dep[u] = dep[fa] + 1;
sk.push(u);
for(int v : G[u])
{
if(v == fa)
continue;
if(!ans.empty()) // 已经找到环,不用再继续搜索,写这里防止回溯时对 ans 再次修改
return;
if(!vis[v]) // v 点没搜过,当作 u 的子结点
dfs(v, u);
else // v 点已搜过,要么重复搜索,要么说明是 u 的祖先
{
// 若 u -> v 作为返祖边,则形成环
// 由于 u v 均包含在环内,因此深度差为偶数时,才说明是奇环
if(dep[v] < dep[u] && (dep[v] - dep[u]) % 2 == 0)
{
// 将栈内所有在 v 之后搜到的点全部弹出,表示环上的点
while(!sk.empty() && sk.top() != v)
{
ans.push_back(sk.top());
sk.pop();
}
ans.push_back(v);
return;
}
}
}
if(ans.empty()) // 没找到答案时可正常出栈,已经找到答案时需注意可能已被出栈
sk.pop();
}
void solve()
{
cin >> N >> M;
// 多组数据清空
for(int i = 1; i <= N; i++)
{
G[i].clear();
dep[i] = 0;
vis[i] = false;
}
ans.clear();
while(!sk.empty())
sk.pop();
for(int i = 1; i <= M; i++)
{
int a, b;
cin >> a >> b;
G[a].push_back(b);
G[b].push_back(a);
}
dfs(1, 0);
if(ans.empty())
cout << "-1\n";
else
{
cout << ans.size() << "\n";
for(int i : ans)
cout << i << " ";
cout << "\n";
}
}
int main()
{
int T;
cin >> T;
while(T--)
solve();
return 0;
}

浙公网安备 33010602011771号