AtCoder Beginner Contest 411 ABCDEF 题目解析
A - Required Length
题意
给定一个字符串 \(P\) 以及一个正整数 \(L\),问 \(P\) 的长度是否至少为 \(L\)?
思路
直接输入为字符数组/字符串,取长度判断大小即可。
代码
#include<bits/stdc++.h>
using namespace std;
char s[105];
int main()
{
int l;
cin >> s >> l;
if(strlen(s) >= l)
cout << "Yes";
else
cout << "No";
return 0;
}
B - Distance Table
题意
在一条直线上有 \(N\) 个车站,分别编号为 \(1, 2, \dots, N\)。对于 \(i\) 和 \(i+1\) 这两个车站,他们之间的距离为 \(D_i\)。
现在请你求出任意两个车站之间的距离,并输出成一个三角形:
- 对于每一对 \((i, j)\) \((1 \le i \lt j \le N)\),请在第 \(i\) 行的第 \(j-i\) 列输出车站 \(i\) 和 \(j\) 之间的距离。
思路
\(N\) 很小,先循环枚举 \((i, j)\),再暴力求解两车站之间的距离即可。时间复杂度 \(O(N^3)\)。
或者可以借助前缀和优化为 \(O(N^2)\)。
代码一
#include<bits/stdc++.h>
using namespace std;
int d[55];
// d[i] 表示 i ~ i+1 的距离
int main()
{
int n;
cin >> n;
for(int i = 1; i < n; i++)
cin >> d[i];
for(int i = 1; i < n; i++)
{
for(int j = i + 1; j <= n; j++)
{
int sum = 0; // 求 (i, j) 的距离
for(int k = i; k < j; k++)
sum += d[k];
cout << sum << " ";
}
cout << "\n";
}
return 0;
}
代码二
#include<bits/stdc++.h>
using namespace std;
int d[55];
// d[i] 表示 i ~ i+1 的距离
int main()
{
int n;
cin >> n;
for(int i = 1; i < n; i++)
cin >> d[i];
for(int i = 1; i < n; i++)
{
int sum = 0; // 车站 i 开始的前缀和
for(int j = i + 1; j <= n; j++)
{
sum += d[j - 1]; // 每次加上 j-1 ~ j 的距离
cout << sum << " ";
}
cout << "\n";
}
return 0;
}
代码三
#include<bits/stdc++.h>
using namespace std;
int d[55];
// d[i] 表示 i ~ i+1 的距离,前缀和处理后表示 1 ~ i+1 的距离
int main()
{
int n;
cin >> n;
for(int i = 1; i < n; i++)
cin >> d[i];
for(int i = 1; i < n; i++)
d[i] += d[i - 1];
for(int i = 1; i < n; i++)
{
for(int j = i + 1; j <= n; j++)
{
// i ~ j 的距离即 d[i] + d[i+1] + ... + d[j-1]
cout << d[j-1] - d[i-1] << " ";
}
cout << "\n";
}
return 0;
}
C - Black Intervals
题意
有 \(N\) 个格子自左向右排成一列,一开始每个格子都是白色的。
有 \(Q\) 次操作,每次操作给定一个正整数 \(A_i\),表示选择从左开始的第 \(A_i\) 个格子,将其颜色反转(白变黑,黑变白)。
在每次操作完成之后,问整列的格子中总共有多少段连续的黑色格子?
思路
每次只会改变其中一个格子,分类讨论左右两边格子的颜色即可。
我们以把某个位置的格子从白色变成黑色为例:
- 如果该位置的左右两边原本都不是黑色格子(要么是首尾两个位置,要么旁边的是白色格子),明显当前这个格子变成黑色后,就成为了单独的一段黑色格子区间,这会使得答案 \(+1\)。
- 如果该位置的左右两边原本都是黑色格子,在当前这个格子变成黑色后,原本左右两边的两段黑色格子区间被合并成了一段,这会使得答案 \(-1\)。
- 如果该位置的左右两边原本只有一边是黑色格子,在当前这个格子变成黑色后,相当于把黑色格子的区间扩大了一格,但并不会对答案产生影响。
反之亦然,黑色变白色的过程对答案的影响和上面三个情况是反着来的。
因此过程中我们只需要有一个数字能够辅助判断反转的格子两边的颜色分别是什么,以及当前的黑色格子区间数量即可。
代码
#include<bits/stdc++.h>
using namespace std;
int clr[500005];
int main()
{
int n, q;
cin >> n >> q;
int cnt = 0; // 当前答案
for(int i = 1; i <= q; i++)
{
int x;
cin >> x;
clr[x] ^= 1; // 反转 0->1 1->0
if((x == 1 || clr[x - 1] != 1) && (x == n || clr[x + 1] != 1))
{
// 如果左右两边没有任何一个格子是黑色
if(clr[x] == 1) // 当前格子白变黑
cnt++;
else // 当前格子黑变白
cnt--;
}
else if((x > 1 && clr[x - 1] == 1) && (x < n && clr[x + 1] == 1))
{
// 如果左右两边的格子都是黑色
if(clr[x] == 1)
cnt--;
else
cnt++;
}
cout << cnt << "\n";
}
return 0;
}
D - Conflict 2
题目
现有一个服务器与 \(N\) 台电脑,服务器以及每台电脑都持有着一个字符串。一开始每个字符串都是空的。
有 \(Q\) 次操作,每次操作为以下三种之一:
1 p:表示把服务器的字符串复制给第 \(p\) 台电脑。2 p s:表示往第 \(p\) 台电脑的字符串后面拼接上一段新字符串 \(s\)。3 p:表示把第 \(p\) 台电脑的字符串复制给服务器。
思路
直接模拟肯定是不行的,字符串过长时一定会超时。
我们可以把服务器当作是第 \(N+1\) 台电脑。
可以发现,每个字符串多出来的一部分都是在之前某个出现过的字符串的最后拼上一段得到的,因此我们可以根据拼接关系构造出一棵树,树上每个结点存有一个字符串(也就是每次新拼接上的一段字符串)。类似于字典树。
假如说现在需要在之前某个字符串的基础上拼上一段 \(S\) 来得到一个新字符串,我们可以先找到之前那个字符串在树上所表示的点 \(i\),因为这会得到一个新字符串,所以我们可以创建一个新结点 \(j\),让结点 \(j\) 仅存储新出现的 \(S\) 这一段字符串,然后构建 \(i \rightarrow j\) 这一条树上父子结点关系即可。但因为每个结点可能有多个子结点,因此我们只需要记录每个结点的父结点即可(就是我这个字符串是以哪个字符串作为基础,拼上一段得到的)。
所以本题我们需要有数组 str[i] 表示树上每个结点存储的字符串,数组 pos[i] 表示每台电脑当前存储的字符串在树上的哪个结点,还要有一个数组 pre[i] 记录每个结点的父结点。
对于三种操作:
1 p:让第 \(p\) 台电脑暂时与服务器共享同一个结点,复制一下pos[]数组的信息即可。3 p:让服务器暂时与第 \(p\) 台电脑共享同一个结点,同样也是复制一下pos[]数组的信息即可。2 p s:即上文所述,构建一个新结点并建立树上关系,最后把pos[p]更改为这个新结点编号即可。
时间复杂度 \(O(N)\)。
代码
#include<bits/stdc++.h>
using namespace std;
string str[200005]; // 树上每个结点存储的字符串
int len = 0; // 当前树的大小(结点总数)
int pre[200005]; // 每个结点的父结点编号
int pos[200005]; // 每台PC代表的字符串对应的树上结点编号
int server = 0; // 服务器代表的字符串对应的树上结点编号
int main()
{
int n, q;
cin >> n >> q;
for(int i = 1; i <= q; i++)
{
int op, p;
cin >> op >> p;
if(op == 1)
{
pos[p] = server; // 共享编号
}
else if(op == 2)
{
len++; // 新建一个结点
cin >> str[len];
// 因为新字符串是在之前第 p 台电脑表示的字符串的基础上拼上一段所得到的
// 所以新建的结点的父结点就是之前第 p 台电脑所表示的字符串对应结点
pre[len] = pos[p];
// 同时更新 当前第 p 台电脑表示的字符串对应结点为新建的结点
pos[p] = len;
}
else
{
server = pos[p]; // 共享编号
}
}
// 最后我们找到服务器对应结点,找出其前往根结点的整条路径上的所有字符串,倒序输出即可
// 这里借助栈进行实现,也可以借助深搜等方式
stack<int> sk;
while(server != 0)
{
sk.push(server);
server = pre[server];
}
while(!sk.empty())
{
cout << str[sk.top()];
sk.pop();
}
return 0;
}
E - E [max]
题意
有 \(N\) 个骰子,分别编号为 \(1, 2, \dots, N\),每个骰子都有 \(6\) 个面,第 \(i\) 个骰子的每个面上的数字分别为 \(A_{i,1}, A_{i,2}, A_{i,3}, A_{i,4}, A_{i,5}, A_{i,6}\)。
现在,所有骰子被随机投掷,每个骰子每一面朝上的概率都是相同的。问在投掷完成之后,对于所有骰子,朝上这一面的数字的最大值的期望值是多少?
思路
根据“期望 = 每种情况的结果贡献 * 发生概率”,我们可以针对本题中出现的每一种的数字,分别计算当该数字作为最大值这一事件的发生概率,最后将点数与概率相乘后全部相加即可。
本题共有最多 \(10^5\) 个骰子,因此不同点数个数最多可能有 \(6 \times 10^5\) 种。
对于每种出现过的点数 \(x\),如果投掷所有骰子后出现的最大数字就是该点数 \(x\),记该点数出现在第 \(y\) 个骰子上,将该事件概率记作 \(P(x, y)\),则该事件发生的概率应当是:
最终答案即 \(\sum\limits_{y=1}^N\sum\limits_{x \in {\text{骰子 }y \text{ 的点数集}}} P(x, y)\)
我们肯定需要枚举一遍每一种出现过的点数,但又因为我们需要能够在枚举的过程中快速统计出上面式子的后半段的值(即除我以外其他骰子上不大于 \(x\) 的数字数量除以 \(6\) 的累乘),因此可以考虑排序后从大到小看一遍每个出现过的数字,按顺序依次把大的数字的影响去除,线性维护概率。
记 cnt[i] 表示第 \(i\) 个骰子上还有多少个面的点数不大于当前正在处理的数字,记 totalProd 表示当前所有骰子的 \(\dfrac{cnt[i]}{6}\) 的累乘。
排序后,对于每种处理到的数字,可能我们还需要考虑一种情况:可能同一种点数在多个不同骰子上出现过。但其实这种情况不需要担心,虽然同一种点数出现在了多个位置,可能会在并列最大的情况下对答案产生影响,但我们算概率是在排序后一个个数字看过去的,因此同种数字在出现并列最大时,并列一个、两个、三个……一直到全部都出现的每一种情况其实都能在线性维护过程中考虑到(也就是我们不管上一个数字是否与我相同,只要处理过了就当作大于我的数字即可)。
最后就是代码细节。注意现在我们的 totalProd 表示当前所有骰子的 \(\dfrac{cnt[i]}{6}\) 的累乘,所以为了获得除当前骰子外的所有骰子概率累乘的话,记得先把当前骰子 \(y\) 的 \(\dfrac{cnt[y]}{6}\) 借助逆元从概率中除去。然后根据上一段的说法,一个数字只要处理过就当作是一个大于下一个处理的数字的数即可,因此可以让 \(cnt[y] := cnt[y] - 1\),但在减一的前后记得同样先把当前格子的贡献从 totalProd 中除去,减完后再乘回来一个新贡献。
时间复杂度 \(O(N\log N)\)。
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll mod = 998244353;
ll qpow(ll a, ll n)
{
ll r = 1;
while(n)
{
if(n & 1)
r = r * a % mod;
a = a * a % mod;
n >>= 1;
}
return r;
}
int a[100005][7];
struct node
{
int val, i, j;
bool operator < (const node &nd) const
{
return val > nd.val;
}
};
vector<node> vec;
int cnt[100005]; // cnt[i] 表示 i 这个骰子还有几个面的点数小于当前处理的数字
int main()
{
int n;
cin >> n;
for(int i = 1; i <= n; i++)
{
cnt[i] = 6;
for(int j = 1; j <= 6; j++)
{
cin >> a[i][j];
vec.push_back({a[i][j], i, j});
}
}
sort(vec.begin(), vec.end());
ll ans = 0, totalProd = 1, div6 = qpow(6, mod - 2); // div6 表示 六分之一
// totalProd = product i in [1, n] : cnt[i] / 6
for(int i = 0; i < vec.size(); i++)
{
int p = vec[i].i;
ll prop = totalProd * qpow(cnt[p] * div6 % mod, mod - 2) % mod; // 除当前骰子外的所有骰子概率累乘
ans = (ans + prop * div6 % mod * vec[i].val % mod) % mod; // 六分之一 乘 其它概率 乘 值
totalProd = totalProd * qpow(cnt[p] * div6 % mod, mod - 2); // 先把当前骰子的概率去除
cnt[p]--;
totalProd = totalProd * cnt[p] % mod * div6 % mod; // 再把新概率乘回来
}
cout << ans;
return 0;
}
F - Contraction
题意
有一个无向图 \(G_0\),包含 \(N\) 个点与 \(M\) 条边,第 \(i\) 条边连接 \(U_i, V_i\) 两个点。
高桥有一个图 \(G\) 以及 \(N\) 颗棋子,分别编号为 \(1, 2, \dots, N\)。一开始,图 \(G\) 与 \(G_0\) 相同,且第 \(i\) 颗棋子被放置在第 \(i\) 个点上。
现在他会按顺序做 \(Q\) 次操作,第 \(i\) 次操作给定一个 \([1, M]\) 范围内的整数 \(X_i\),并做以下操作:
- 如果在当前的图 \(G\) 中,棋子 \(U_{X_i}\) 和 \(V_{X_i}\) 被放在不同的顶点上,并且这两个顶点之间存在一条直接连接的边,那么则需要收缩这条边(去掉这条边并将两个点合并成一个点),创建出一个图 \(G'\)。如果因此图 \(G'\) 出现了自环或重边,请去除。此时你可以当作原本的 \(U_{X_i}\) 和 \(V_{X_i}\) 被放在了收缩边之后合并出来的这个新的点上,并且图 \(G\) 成为 \(G'\)。
- 但如果一开始棋子 \(U_{X_i}\) 和 \(V_{X_i}\) 被放在同一个顶点上,或者两点间不存在直接相连的边,则不做处理。
对于每一次操作,请输出操作完成后 \(G\) 的总边数。
思路
对于题目中是否两个棋子已经被放在同一个顶点上这一条件,可以想到借助并查集来帮我们进行点的合并以及连通性判断。
但本题中合并两点这一操作,可能会同时造成大量的原图中的边被合并。因此这里可以采用启发式合并操作。
启发式合并其实就是个贪心,每次合并两个集合时,我们根据两个集合的大小,固定将小的集合往大的集合中合并,依次减少合并次数。
但如果采用了启发式合并,可以思考最坏情况下合并次数。对于每个元素,假设他在每次启发式合并中都是被合并的那个元素(也就是总是处于 size 较小的集合中),那么每次合并完成后他所在的集合 size 一定会变成原来的至少两倍,因此可以得出单个元素的合并次数不会超过 \(\log N\) 次,时间复杂度即 \(O(N\log N)\)。
至于合并过程中的去重操作,借助 STL 的 set 完成即可,但这会多出一个 \(O(\log N)\)。
最终时间复杂度约为 \(O(N\log^2N)\)。
代码
#include<bits/stdc++.h>
using namespace std;
int U[300005], V[300005];
set<int> G[300005];
int fa[300005];
int find(int p)
{
return p == fa[p] ? p : fa[p] = find(fa[p]);
}
int main()
{
int n, m;
cin >> n >> m;
for(int i = 1; i <= n; i++)
fa[i] = i;
for(int i = 1; i <= m; i++)
{
cin >> U[i] >> V[i];
G[U[i]].insert(V[i]);
G[V[i]].insert(U[i]);
}
int ans = m;
int q;
cin >> q;
while(q--)
{
int x;
cin >> x;
int u = find(U[x]), v = find(V[x]);
if(u != v) // 不在同一个集合里
{
if(G[u].size() < G[v].size())
swap(u, v); // 固定 u 的集合大小不小于 v
fa[v] = u; // 合并两集合
G[u].erase(v);
G[v].erase(u); // 防止合并后出现自环
ans--; // 要合并,边数先 -1
for(int i : G[v])
{
// 对于与 v 连接的每一个点 i
G[i].erase(v);
ans--; // 这条边因为合并而被去除
if(G[i].find(u) == G[i].end()) // 如果 i 与 u 原本不存在边
{
// 则会因为合并新建一条边
G[i].insert(u);
G[u].insert(i);
ans++;
}
}
}
cout << ans << "\n";
}
return 0;
}

浙公网安备 33010602011771号