当前位置: 首页 > news >正文

AtCoder Beginner Contest 427 ABCDEF 题目解析

A - ABC -> AC

题意

给定一个长度为奇数且仅由大写字母组成的字符串 \(S\)

请删除该字符串正中间的字符,再输出该字符串。

代码

void solve()
{string s;cin >> s;for(int i = 0; i < s.size(); i++)if(i != s.size() / 2)cout << s[i];
}

B - Sum of Digits Sequence

题意

定义 \(f(x)\) 表示 \(x\) 在十进制下的数位和。

通过以下方法定义一个无限长度的序列 \(A=(A_0, A_1, A_2, \dots)\)

  • \(A_0 = 1\)
  • \(A_i = \sum\limits_{j=0}^{i-1} f(A_j), \quad (i \ge 1)\)

给定一个正整数 \(N\),请求出 \(A_N\) 的值。

思路

\(N\) 较小,直接模拟公式的计算方法即可。

代码

int f(int n)
{int res = 0;while(n > 0){res += n % 10;n /= 10;}return res;
}void solve()
{int n;cin >> n;int a[105];a[0] = 1;for(int i = 1; i <= n; i++){a[i] = 0;for(int j = 0; j < i; j++)a[i] += f(a[j]);}cout << a[n];
}

C - Bipartize

题意

给定一张 \(N\) 个点 \(M\) 条边的简单无向图,每个点分别编号为 \(1, 2, \dots, N\),第 \(i\) 条边连接 \(u_i, v_i\) 这两个点。

你会执行以下操作零次或多次:

  • 选择一条尚未删除的边,然后将其删除。

你的目标是让这张图最终成为一张二分图,并求出满足目标的最少操作数。

思路

注意到本题的 \(N\) 较小,可以尝试借助深度优先搜索/二进制枚举来找出最终每种可能的二分图分组方案。

\(N\) 个点分为两组后,求出所有边中连接同一组内的点的边数,取个最小值即为答案。

时间复杂度 \(O((N+M)\cdot 2^N)\)

代码一 深搜

int n, m;
int u[50], v[50];
int ans = 1e9;
int gp[15]; // gp[i] 表示 i 点的组别void dfs(int p)
{if(p > n){int cnt = 0; // 连接同组点的边数for(int i = 1; i <= m; i++)if(gp[u[i]] == gp[v[i]])cnt++;ans = min(ans, cnt);return;}// 尝试把 p 点分到 1 组内gp[p] = 1;dfs(p + 1);// 尝试把 p 点分到 2 组内gp[p] = 2;dfs(p + 1);
}void solve()
{cin >> n >> m;for(int i = 1; i <= m; i++)cin >> u[i] >> v[i];dfs(1);cout << ans << "\n";
}

代码二 二进制枚举

int n, m;
int u[50], v[50];
int gp[15];void solve()
{cin >> n >> m;for(int i = 1; i <= m; i++)cin >> u[i] >> v[i];int ans = m;for(int sta = 0; sta < (1 << n); sta++){// sta 的二进制表示分组状态for(int i = 1; i <= n; i++)gp[i] = (sta >> (i - 1) & 1); // 把分组情况提出来int cnt = 0; // 连接同组点的边数for(int i = 1; i <= m; i++){if(gp[u[i]] == gp[v[i]]) // u[i] 和 v[i] 在同一组内cnt++;}ans = min(ans, cnt);}cout << ans << "\n";
}

D - The Simple Game

题意

给定一张 \(N\) 个点 \(M\) 条边的有向图,每个点编号分别为 \(1, 2, \dots, N\),第 \(i\) 条边是从点 \(U_i\) 连向点 \(V_i\) 的有向边。保证每个点的出度至少为 \(1\)

每个点上都有一个字符,第 \(i\) 个点上的字符为 \(S_i\)

Alice 和 Bob 正使用一枚棋子在这张图中玩游戏:

  • 最初,该棋子被放在点 \(1\) 上,然后他们两人会交替进行 \(K\) 轮游戏,每轮 Alice 先进行,Bob 后进行。

    • \(u\) 表示当前棋子所在顶点的编号。每次每人可以选择一个点 \(v\) 并将棋子移动到该点处,但必须要保证 \(u\)\(v\) 之间存在一条有向边。
  • \(v\) 表示最终棋子所在顶点的编号。如果 \(S_v = \texttt{A}\),则 Alice 获胜;如果 \(S_v = \texttt{B}\),则 Bob 获胜。

若两人均以最优策略进行游戏,请问最终的获胜者是谁。

思路

因为最终获胜者一定是确定的,必胜的状态在游戏过程中不会改变,因此可以考虑借助动态规划倒推必胜态。

由于题意中的 \(K\) 轮表示 \(K\) 个来回,实际上两人总共执行了 \(2K\) 次操作,下文以 \(2K\) 来表示操作数。

定义 dp[i][j] 表示第 \(j\) 次操作结束后,如果棋子来到了 \(i\) 这个点,接下来谁会赢。(\(1/2\) 分别表示 Alice/Bob 的必胜态。)

一开始能够确定的必胜态一定是在游戏结束的时候,也就是当第 \(2K\) 次操作结束后,如果棋子来到 \(i\) 这个点,那么根据该点上的字符 \(S_i\) 可以直接确定这个状态是谁赢。因此动态规划的初始状态 dp[i][2*k] 可以直接求出。

考虑倒推每一次操作 \(j = 2K-1 \sim 0\),然后对于图中的每一个点 \(i\),状态 dp[i][j] 表示的是谁的必胜态,取决于下一步的操作者能否把棋子移动到他自己的必胜态上。

下一步的操作者可以根据当前操作次数 \(j\) 的奇偶性判断出来,这里我们假设下一步的操作者是 Bob,那么他的任务就是对于所有从 \(i\) 出发能够到达的下一个点 \(v\),然后看看在所有 dp[v][j+1] 中,是否存在至少一个状态能够保证他必赢。如果有,那么当前的 dp[i][j] 就表示他必赢,这里我们记 dp[i][j] = 2 即可;如果没有,说明下一步的必胜态全都是 Alice 的,记 dp[i][j] = 1。如果下一步操作者是 Alice 同理。

最后只需要检查在一次操作也没有进行,且棋子位于点 \(1\) 时的必胜态指向谁即可,也就是 dp[1][0] 的值。

单组数据时间复杂度 \(O(K\cdot (N+M))\)

代码

int dp[200005][21];
// dp[i][j] 表示第 j 次操作结束后如果来到 i 这个点,谁会赢 1-Alice 2-Bob
vector<int> G[200005];void solve()
{int n, m, k;cin >> n >> m >> k;string s;cin >> s;for(int i = 1; i <= n; i++){G[i].clear();for(int j = 0; j <= 2 * k; j++)dp[i][j] = 0;if(s[i - 1] == 'A')dp[i][2 * k] = 1; // 如果最后一次操作站在 i 点上,则 Alice 必赢elsedp[i][2 * k] = 2; // 如果最后一次操作站在 i 点上,则 Bob 必赢}for(int i = 1; i <= m; i++){int u, v;cin >> u >> v;G[u].push_back(v);}for(int j = 2 * k - 1; j >= 0; j--) // 倒推每次操作{for(int i = 1; i <= n; i++) // 如果第 j 次操作结束时站在 i 点上{bool alice = false, bob = false; // 判断下一步是否能够移动到 alice/bob 必赢的状态上for(int &v : G[i]){if(dp[v][j + 1] == 1)alice = true;elsebob = true;}if((j + 1) % 2 == 0) // 如果下一步是 Bob 移动{if(bob) // 并且下一步能够移动到 Bob 必赢的点上dp[i][j] = 2;else // 如果不能,则当前状态表示 Alice 赢dp[i][j] = 1;}else // 同上,如果下一步是 Alice 移动{if(alice)dp[i][j] = 1;elsedp[i][j] = 2;}}}if(dp[1][0] == 1) // 一开始站在 1 号点上,且未进行操作的必胜态cout << "Alice\n";elsecout << "Bob\n";
}signed main()
{int t;cin >> t;while(t--)solve();return 0;
}

E - Wind Cleaning

题意

有一个 \(H \times W\) 的网格图。定义 \((i, j)\) 表示上到下第 \(i\) 行,左到右第 \(j\) 列的单元格。

高桥在这个网格图中的某个单元格上,并且某些单元格上存在一些垃圾。

单元格的状态由 \(H\) 个长度为 \(W\) 的字符串给出,第 \(i\) 个字符串的第 \(j\) 个字符记作 \(S_{i, j}\)

  • 如果 \(S_{i, j} =\) T,则单元格 \((i, j)\) 表示高桥一开始的位置。
  • 如果 \(S_{i, j} =\) #,则单元格 \((i, j)\) 存在垃圾。
  • 如果 \(S_{i, j} =\) .,则单元格 \((i, j)\) 是一个空单元格。

高桥一开始的位置没有垃圾。

每次高桥可以重复执行以下操作:

  • 选择上、下、左、右四个方向之一,然后将所有垃圾同时朝着该方向移动一个单元格。如果垃圾在本次操作中被移出了这个网格图,那么这个垃圾就会永远消失;而如果垃圾移动到高桥所在的位置,那么他就会变脏。

请帮助高桥判断是否有可能在不弄脏他自己的前提下让所有垃圾都消失。如果可能,求最少的操作次数。

思路

题目中提到,如果垃圾被移出网格图,就相当于永远消失了。又因为每次我们会移动所有的垃圾,所以垃圾的消除肯定是整行或者整列一起消除的。

因为消除后相当于原本的这一整行或整列直接没有垃圾了,所以我们可以简单地当作这一整行或整列成为了网格图外的一部分,而我们的网格图每次移动后都会缩小

例如:让所有垃圾往左移动一格,相当于是垃圾不动,而高桥以及地图的左边界全部往右移动了一格。

于是我们便可以在地图不移动的情况下,通过维护当前地图可行部分的上、下、左、右边界以及高桥的位置,来借助宽搜对问题进行求解。

在这种思路下,高桥的坐标可能会超出地图边界,并且在离开地图范围时可能地图内还存在垃圾,需要继续移动。发现高桥在 \(x, y\) 方向的坐标变化量只会在 \([-H, H], [-W, W]\) 以内,如果超出此范围,那么地图内的可行部分便全部消失了,此时所有垃圾肯定都已经被移除。

方便起见,我们可以新定义一个坐标系,把高桥一开始的坐标定作 \((H, W)\),让其移动范围不超出 \(0 \sim 2H\) 以及 \(0 \sim 2W\) 来防止越界,在过程中根据其移动的位置顺便更新实际可行边界即可。

最后,只要可行边界内不存在任何垃圾,搜索就可以结束,说明找到了终点。这一步可以借助二位前缀和等方法快速求解范围内的垃圾数量。

时间复杂度 \(O(H^3\cdot W^3)\)

代码

int n, m;
char mp[14][14];int s[14][14]; // 二位前缀和,表示左上角有多少垃圾struct point
{int t, b, l, r; // 上、下、左、右 边界相对于原地图的位置int x, y, step; // 高桥当前位置 及 移动次数
};
queue<point> q;
bool vis[14][14][14][14][28][28];void solve()
{cin >> n >> m;int sx, sy;for(int i = 1; i <= n; i++)for(int j = 1; j <= m; j++){cin >> mp[i][j];s[i][j] = s[i-1][j] + s[i][j-1] - s[i-1][j-1] + (mp[i][j] == '#' ? 1 : 0);if(mp[i][j] == 'T')sx = i, sy = j;}q.push(point{1, n, 1, m, n, m}); // 高桥的起始位置假设是 (n, m),平移防止越界vis[1][n][1][m][n][m] = true;while(!q.empty()){point u = q.front();q.pop();for(int i = 0; i < 4; i++){point v = u;v.step++;v.x += dx[i];v.y += dy[i];v.t = max(v.t, v.x - n + 1);v.b = min(v.b, v.x);v.l = max(v.l, v.y - m + 1);v.r = min(v.r, v.y);int X = v.x - n + sx, Y = v.y - m + sy; // 高桥当前位置对应原地图的实际坐标// 排除非法情况if(v.x < 0 || v.x > 2 * n || v.y < 0 || v.y > 2 * m) continue;if(X >= v.t && X <= v.b && Y >= v.l && Y <= v.r && mp[X][Y] == '#') // 还在地图内且与垃圾位置重合continue;if(vis[v.t][v.b][v.l][v.r][v.x][v.y] == true)continue;if(s[v.b][v.r] - s[v.b][v.l - 1] - s[v.t - 1][v.r] + s[v.t - 1][v.l - 1] == 0) // 剩余位置无垃圾{cout << v.step;return;}q.push(v);vis[v.t][v.b][v.l][v.r][v.x][v.y] = true;}}cout << -1;
}

F - Not Adjacent

题意

给定一个长度为 \(N\) 的整数序列 \(A=(A _ 1,A _ 2,\ldots,A _ N)\)

明显 \(A\) 存在 \(2 ^ N\) 个子序列(即元素不一定相邻)。

问有多少个子序列满足以下两个条件:

  • 选出来的元素(保留的数字)在原序列 \(A\) 中不相邻。
  • 选出来的数字总和是 \(M\) 的倍数。

只要元素被取出的位置不同,就可以被当作不同的子序列。

思路

考虑爆搜,但 \(N = 60\) 显然行不通。

考虑折半搜索,将序列分为前后两部分,因为本题存在“不能选择相邻元素”的限制,因此在 \(\frac N 2 = 30\) 个数字组成的序列中,最多只能取出 \(15\) 个数字求和。这样每部分能够得到的总和个数实测最多只会到达 \(2 \times 10^6\) 级别。

对于 \([1, \frac N 2]\) 部分,借助深搜求出所有可能的总和(\(\bmod M\) 的结果),然后借助哈希表(unordered_map)存储每种总和出现的次数。

对于 \([\frac N 2+1, N]\) 部分,同样借助深搜求出所有可能的总和(\(\bmod M\) 的结果),然后考虑每个总和与左半部分的值相加,能够得到多少个不同子序列的总和恰好是 \(M\) 的倍数。假设当前得到的总和为 \(S\),那么再左半部分需要再找到总和为 \((M-S)\bmod S\) 的子序列,才可以保证两者相加的值 \(\bmod M = 0\)

但这样还需要注意一个问题,我们需要保证 \(\frac N 2\)\(\frac N 2 + 1\) 这两个数字不能够同时取到。所以在搜索的时候需要额外再处理一个条件,即两部分交界处的这个数字在当前总和当中是否有用到。

对于左半部分,根据该条件把总和分别存在两个不同的哈希表中。

而对于右半部分,如果当前搜索得到的总和未使用到 \(\frac N 2 + 1\) 位置的数,那么就可以把两个哈希表中总和为 \((M-S)\bmod S\) 的子序列都找出来加入答案;而如果当前搜索得到的总和使用到了 \(\frac N 2 + 1\) 位置的数,那么就只能取未使用 \(\frac N 2\) 位置的哈希表中的总和为 \((M-S)\bmod S\) 的子序列。

由于读取的次数较多,因此本题更偏向于使用哈希表 unordered_map 而非红黑树 map

时间复杂度 \(O(D\log D)\),其中 \(D\) 表示从 \(\frac N 2\) 个数中不取相邻元素所能得到的总和情况数。

代码

typedef long long ll;int n, m;
int a[65];
unordered_map<int, int> mp[2]; // mp[0/1] 分别存储 未使用/使用了 a[n/2] 时,在左半部分能够获得的子序列总和以及数量
ll ans = 0;void dfs1(int p, int maxx, int sum, bool useMax)
{if(p > maxx){mp[useMax][sum]++;return;}// 如果选 a[p]dfs1(p + 2, maxx, (sum + a[p]) % m, p == maxx);// 如果不选 a[p]dfs1(p + 1, maxx, sum, 0);
}void dfs2(int p, int minn, int sum, bool useMin)
{if(p < minn){sum = (m - sum) % m; // 需要在左半部分找的数值,使其加上当前 sum 后为 m 的倍数if(useMin) // 当前使用了 a[n/2+1],那么左半部分就不能使用 a[n/2]ans += mp[0][sum];elseans += mp[0][sum] + mp[1][sum];return;}// 如果选 a[p]dfs2(p - 2, minn, (sum + a[p]) % m, p == minn);// 如果不选 a[p]dfs2(p - 1, minn, sum, 0);
}void solve()
{cin >> n >> m;for(int i = 1; i <= n; i++)cin >> a[i];if(n == 1){if(a[1] % m == 0)cout << 2;elsecout << 1;return;}dfs1(1, n / 2, 0, false);dfs2(n, n / 2 + 1, 0, false);cout << ans;
}
http://www.hskmm.com/?act=detail&tid=29017

相关文章:

  • zju博士资格考试考前复习(微分方程方向)ode 部分
  • 测试一下博客功能
  • AI如何改变芯片设计
  • NOIP 2024
  • 2025/10/11
  • 好玩热门的switch游戏推荐【PC+安卓】塞尔达传说:王国之泪|v1.4.2整合版|官方中文| 附switch模拟器
  • 十年运维工程师总结
  • 运动控制教学——5分钟学会Dijkstra与A*搜索算法!(附仿真视频及代码) - 教程
  • ffplay数据结构解析
  • CNN 发展历程
  • FileX和ThreadX精简版
  • ue4素材场景 - MKT
  • 阅读《构建之法》的思考与问题
  • 实验报告5(链栈基本操作,数制转换,匹配算法,伴舞问题)
  • 阅读和提问作业1:《构建之法》提问
  • 企业推行OKR中层领导关注的10个关键问题及解决方案
  • 初四夜间/休息日安排
  • AWS自然语言处理技术实战指南
  • 多线程
  • 三级模式
  • abc427
  • 20232320 2025-2026-1 《网络与系统攻防技术》实验一实验报告
  • [HarekazeCTF2019]baby_rop2
  • 开个视频网站很烧钱吧
  • 13. Canvas画布
  • 预训练相关的一些概念
  • 2025/10/11 模拟赛总结 - sb
  • 分布式训练的一些知识
  • Visual Studio 2013 Update 4 中文版安装步骤(带TFS拥护)附安装包​
  • 排列