AtCoder Beginner Contest 401 ABCDEFG 题目解析
A - Status Code
题意
给定一个正整数 \(S\),判断是否在 \(200 \sim 299\) 之间,是则输出 Success,否则输出 Failure。
代码
int s;
cin >> s;
if(s >= 200 && s <= 299)
cout << "Success";
else
cout << "Failure";
B - Unauthorized
题意
模拟网站的登入登出以及访问权限判断。
有 \(N\) 次操作,每次操作分为以下四种:
login,表示用户登入logout,表示用户登出public,表示用户要访问一个公开的网页private,表示用户要访问一个私有的网页
如果用户已经登入,再次登入也不会报错;如果用户已经登出,再次登出也不会报错。
只有在“没有登入”的情况下,出现了访问“私有的网页”的情况时,系统才会报错。
问如果按顺序执行这 \(N\) 次操作,总共会报错多少次?(如果过程中出现了报错,操作也会继续执行)
思路
模拟即可,用一个 bool 类型的变量来记当前是否已经登录,之后直接判断
代码
int n;
cin >> n;
int ans = 0;
bool login = false; // 是否已经登入
while(n--)
{
string s;
cin >> s;
if(s == "login")
login = true;
else if(s == "logout")
login = false;
else if(s == "private")
{
if(!login)
ans++;
}
}
cout << ans;
C - K-bonacci
题意
给定两个正整数 \(N\) 和 \(K\)。
已知有一个数组 \(A\),其第 \(0\) 项至第 \(K-1\) 项均为 \(1\)。
从第 \(K\) 项开始,每一项等于前面 \(K\) 项之和,即 \(A_i = A_{i-K} + A_{i-K+1} + \dots + A_{i-1}\)
问第 \(N\) 项的值对 \(10^9\) 取模后的结果。
思路
发现可以借助一个变量 sum 来动态维护前 \(K\) 项总和。
假如对于 \(i\) 位置而言,sum 表示 \(A_{i-K} + A_{i-K+1} + \dots + A_{i-1}\)
那么对于 \(i+1\) 位置,sum 需要变成 \(A_{i-K+1} + A_{i-K+2} + \dots + A_{i-1} + A_i\)
发现只是前面少了项 \(A_{i-K}\),后面多了项 \(A_i\)
每次向后求答案时,只需要加一个新数字,再删一个最前面的数字来维护 sum 变量即可。
注意取余运算,在减去一个数字后如果结果为负整数,需要加上一倍模数重新变回非负整数。
代码
#include<bits/stdc++.h>
using namespace std;
const int mod = 1e9;
int a[1000005];
int main()
{
int n, k;
cin >> n >> k;
for(int i = 0; i < k; i++)
a[i] = 1;
int sum = k; // 维护当前的前 k 项总和
for(int i = k; i <= n; i++)
{
a[i] = sum;
sum = (sum + a[i]) % mod;
sum = (sum - a[i - k] + mod) % mod;
}
cout << a[n];
return 0;
}
D - Logical Filling
题意
给定一个长度为 \(N\) 且仅由 .,o 以及 ? 组成的字符串。
现在你可以将 ? 任意改为 . 或 o 这两种字符的其中一种,但需要保证:
- 整个字符串中
o的个数恰好为 \(K\) - 不能让两个字符
o相邻
我们假设 \(X\) 表示满足上述条件的前提下,所有能够构造出来的字符串所组成的集合。保证至少能够构造出来一种字符串。
请你再输出一个长度为 \(N\) 的字符串 \(T\),其中第 \(i\) 个字符 \(T_i\) 需要满足:
- 如果在集合 \(X\) 中,所有字符串的第 \(i\) 位均是
.,那么 \(T_i\) 也是. - 如果在集合 \(X\) 中,所有字符串的第 \(i\) 位均是
o,那么 \(T_i\) 也是o - 否则,\(T_i\) 是
?
思路
首先,对于输入字符串中的每个 ?,如果其相邻位置有确定的字符 o,那么这个 ? 肯定是 .,可以直接修改。
同时,对于每一个确定的字符 o,我们可以将其从数量中减去,之后让 \(K\) 来表示还需要让多少个字符 ? 改成字符 o
然后思考在什么情况下,能够让剩下的某些字符 ? 改成确定的字符
明显在 \(K\) 比较小,? 数量比较多时,我们可以随意安排 o 字符的位置,此时所有 ? 位置均不能确定,当前字符串就是最终答案。
而只有当现存的每一段 ? 都应该放满字符 o,也就是没法让 o 随意调换位置时,我们才能够确定 ? 的最终字符。
接下来我们可以反向思考:如果现在不考虑 \(K\) 的限制,剩余的每一段 ? 最多能够再放多少个字符 o?
注意这里不能直接拿 \(\dfrac K 2\) 来当作最多可以放的数量,因为对于每一段连续的 ?,假设数量为 \(t\),那么放满的状态可以分为以下两种情况:
?的个数为偶数,此时第一个字符o可以放在第一个位置,也可以放在第二个位置,答案可能是o.o.o.o.或者.o.o.o.o两种形式,每个位置的字符依然不能确定,此时能放的o的最大数量为 \(\dfrac t 2\)?的个数为奇数,此时第一个和最后一个字符一定是o,每个位置的字符均可以确定,此时能放的o的最大数量为 \(\lceil\dfrac t 2\rceil = \lfloor\dfrac t 2\rfloor + 1\)
因此我们还是要循环一整遍字符串,找出最多还能放的字符数量。如果数量等于 \(K\),我们就按照上述规则,把每一段数量为奇数的 ? 字符改为确定的字符即可。
特别需要注意一点:当我们发现 \(K = 0\) 时,此时所有 ? 都可以直接确定为 .。
代码
#include<bits/stdc++.h>
using namespace std;
char s[200005];
int main()
{
int n, k;
cin >> n >> k;
cin >> (s + 1);
for(int i = 1; i <= n; i++)
{
if(i + 1 <= n && s[i + 1] == 'o' || i - 1 >= 1 && s[i - 1] == 'o')
s[i] = '.'; // 如果相邻是 o,那么该位置一定是 .
if(s[i] == 'o')
k--;
}
// 接下来 k 表示还可以把多少个 ? 改成 .
if(k == 0) // 特判,可以直接确定每个 ? 都是 .
{
for(int i = 1; i <= n; i++)
if(s[i] == '?')
s[i] = '.';
}
else
{
int cnt = 0; // 求出目前能够放的 o 的最大数量
for(int i = 1; i <= n; i++)
{
if(s[i] != '?')
continue;
int j = i;
while(j + 1 <= n && s[j + 1] == '?')
j++;
// 此时 [i, j] 区间内全是字符 ?
// 区间长度为 j-i+1
cnt += (j - i + 1 + 1) / 2; // 除以 2 向上取整
i = j;
}
if(cnt == k) // 如果确实需要放满
{
for(int i = 1; i <= n; i++)
{
if(s[i] != '?')
continue;
int j = i;
while(j + 1 <= n && s[j + 1] == '?')
j++;
// 做法同上,但只有当区间长度为奇数时才能够确定答案
if((j - i + 1) % 2 == 1)
{
for(int u = i; u <= j; u += 2) // i 位置开始隔着放 o
s[u] = 'o';
for(int u = i + 1; u <= j; u += 2) // i+1 位置开始隔着放 .
s[u] = '.';
}
i = j;
}
}
}
cout << (s + 1);
return 0;
}
E - Reachable Set
题意
给定一张由 \(N\) 个点 \(M\) 条边组成的无向图。
对于 \(k = 1, 2, 3, \dots, N\) 的每一个 \(k\):
- 你可以做任意次删除操作,每次选择图中的一个点,删除这个点以及它的所有连边
- 问至少需要进行多少次删除操作,才能够使得最终点 \(1\) 所在的连通块内有且只有 \(k\) 个顶点,且每个顶点编号分别为 \(1, 2, 3, \dots, k\)
思路
首先对于单个 \(k\),为了让所有编号为 \(1 \sim k\) 的顶点能够独立在一个连通块中,首先我们需要把两端点分别在 \(1 \sim k\) 和 \(k+1\sim N\) 范围内的边全部删除
换句话说,我们需要求出有多少个编号在 \(k+1\sim N\) 范围内的顶点,他们存在至少一条边连向编号在 \(1 \sim k\) 范围内的顶点。这些顶点全部都需要删除,顶点数也就是我们的最少删除次数。
但除此之外,我们还需要保证 \(1\sim k\) 这些顶点是连通的。
由于删除操作比较麻烦,我们可以反向思考,也就是每次采取加点的方式考虑问题。由于还要判断连通,因此考虑使用并查集。
按照 \(1 \sim n\) 的顺序,我们把每个点一个一个看过去,并考虑加入到连通块当中。对于点 \(i\) 而言,假设存在一条边 \(i \leftrightarrow j\):
- 如果 \(j\) 的编号比 \(i\) 小,说明这条边需要保留,可以合并 \(i\) 和 \(j\) 两个集合。
- 如果 \(j\) 的编号比 \(i\) 大,说明这条边需要被删除,但反过来说,为了让 \(1 \sim i\) 能够在一个连通块中,说明 \(j\) 这个点需要被删除。我们可以开一个
set集合用来存所有与 \(1 \sim i\) 存在连边且比 \(i\) 大的结点编号,并把 \(j\) 加入这个set当中。
那么很明显,每次要删除的结点数量就是 set 集合内的点数。
至于动态判断 \(1 \sim i\) 的方法,可以在维护并查集的关系时顺便维护另一个数值 cnt,用来表示当前集合内的结点个数。当考虑完所有与 \(i\) 有关的连边后,检查 \(1\) 号点的集合内部是否恰好存在 \(i\) 个结点即可。
代码
#include<bits/stdc++.h>
using namespace std;
int n, m;
vector<int> G1[200005]; // 向编号大的点的所有连边
vector<int> G2[200005]; // 向编号小的点的所有连边
int fa[200005];
int cnt[200005]; // 集合内结点个数
int find(int p)
{
return p == fa[p] ? p : (fa[p] = find(fa[p]));
}
void merge(int a, int b)
{
a = find(a), b = find(b);
if(a == b)
return;
// 考虑让 b 连向 a
cnt[a] += cnt[b]; // b 集合的点数加到 a 集合内
cnt[b] = 0;
fa[b] = a;
}
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i++)
{
fa[i] = i;
cnt[i] = 1;
}
for(int i = 1; i <= m; i++)
{
int u, v;
cin >> u >> v;
G1[u].push_back(v);
G2[v].push_back(u);
}
set<int> st; // 存所有编号比 i 大且与 1~i 存在连边的结点
for(int i = 1; i <= n; i++)
{
st.erase(i); // 如果已经处理到 i 点,那么 i 就不需要在集合中出现了
for(int &v : G1[i])
st.insert(v); // 对于编号大的点,加入 set
for(int &v : G2[i])
merge(v, i); // 对于编号小的点,合并并查集
if(cnt[find(1)] != i) // 判断 1 ~ i 是否连通
cout << -1 << "\n";
else
cout << st.size() << "\n";
}
return 0;
}
F - Add One Edge 3
题意
给定一棵共 \(N_1\) 个点的树以及一棵共 \(N_2\) 个点的树。
现在可以在两棵树之间再加一条双向边 \((i, j)\),其中 \(i\) 是第一棵树上的结点,\(j\) 是第二棵树上的结点,加上这条边后两棵树会被合并成一棵树。此时我们记 \(f(i, j)\) 表示合并后的这棵树的直径。
请对于每一对 \((i, j)\) \((1 \le i \le N_1, 1 \le j \le N_2)\),求出所有的 \(f(i, j)\) 之和,即 \(\displaystyle \sum_{i=1}^{N_1}\sum_{j=1}^{N_2} f(i,j)\)。
思路
首先,我们可以求出原来两棵树的直径,分别记作 \(d_1, d_2\)。
将两棵树加一条边 \((i, j)\) 合并成一棵树,直径只会出现两种情况:
- 加的这条边不影响直径,此时树的直径还是原来两棵树的直径最大值,即 \(\max(d_1, d_2)\)
- 加的这条边会影响直径,此时树的直径会经过加上的这条边 \((i, j)\),那么我们需要求出在第一棵树上距离 \(i\) 点最远的那个点与 \(i\) 的距离,记作 \(f_{1, i}\),以及在第二棵树上距离 \(j\) 点最远的那个点与 \(j\) 的距离,记作 \(f_{2, j}\),此时树的直径就是 \(f_{1, i} + f_{2, j} + 1\)
上面哪种情况会成为答案,实际上只要关注 \(\max(d_1, d_2)\) 和 \(f_{1, i} + f_{2, j} + 1\) 谁更大就行。
至于 \(f_{1/2, x}\) 这个数组怎么求,我们只需要找出树的直径上的两端点 \(u, v\),根据树的直径的定义,此时距离 \(x\) 最远的点一定在 \(u, v\) 两点当中。因此我们可以在求树的直径的过程中,把从直径两端点出发到树上每个点的距离分别记作两个数组 dis1 和 dis2,那么最后只需要取两者最大值即可。
但两棵树的点数均为 \(2\times 10^5\),直接对于每对顶点求一遍一定会超时。
可以考虑只枚举其中一棵树的每个顶点,例如 \(f_{1, i}\),我们只要能够找出在 \(f_{2, j}\) 整个数组当中,有多少个 \(f_{2, j}\) 能够使得 \(f_{1, i} + f_{2, j} + 1 \gt \max(d_1, d_2)\),将此数量记作 \(t\),那么答案明显就是 \(t \times (f_{1, i} +\alpha + 1) + (N_2 - t) \times \max(d_1, d_2)\),其中 \(t \times \alpha\) 应当是所有满足条件的 \(f_{2, j}\) 总和。
我们可以对 \(f_{1, i}\) 和 \(f_{2, j}\) 两个数组进行排序,那么在枚举过程中,随着 \(f_{1, i}\) 慢慢变大,\(f_{2, j}\) 一定是单调变小的,因此这个数量可以采用双指针或者二分查找轻松求出。
最后对于公式中的 \(t \times \alpha\) 这一项,由于此时 \(f_{2, j}\) 数组已经排序,因此可以借助前缀和思想直接求出 \(f_{2, j}\) 数组的后 \(t\) 项之和。
代码
写得较为繁琐,希望大家自己练习的时候能有优美的码风
#include<bits/stdc++.h>
using namespace std;
int n[2];
// 表示两棵树的点数
vector<int> G[2][200005];
// G[t] 表示 t 这棵树的邻接表
int dis1[2][200005];
int dis2[2][200005];
// dis1[t][i] 和 dis[2][t][i] 分别表示 t 这个树直径上的两端点到树上任意一个点的距离
int d[2];
// d[t] 表示 t 这棵树的直径
int f[2][200005];
// f[t][i] 表示 i 到直径两端点的最远距离
long long sum[200005];
void dfs(int u, int fa, vector<int> G[], int dis[])
{
for(int &v : G[u])
{
if(v == fa)
continue;
dis[v] = dis[u] + 1;
dfs(v, u, G, dis);
}
}
int main()
{
for(int t = 0; t <= 1; t++)
{
cin >> n[t];
for(int i = 1; i < n[t]; i++)
{
int u, v;
cin >> u >> v;
G[t][u].push_back(v);
G[t][v].push_back(u);
}
dfs(1, 0, G[t], dis1[t]); // 假设从 1 出发,把每个点与 1 的距离求出来
int p = max_element(dis1[t] + 1, dis1[t] + n[t] + 1) - dis1[t]; // 找出最远点,作为直径的一端
dis1[t][p] = 0;
dfs(p, 0, G[t], dis1[t]); // 从直径端点 p 出发,把每个点与 p 的距离存放在 dis1[t] 数组内
p = max_element(dis1[t] + 1, dis1[t] + n[t] + 1) - dis1[t]; // 此时 p 为直径的另一端
d[t] = dis1[t][p]; // 这棵树的直径长度
dfs(p, 0, G[t], dis2[t]); // 从另一直径端点 p 出发,把每个点与 p 的距离存放在 dis2[t] 数组内
for(int i = 1; i <= n[t]; i++)
f[t][i] = max(dis1[t][i], dis2[t][i]); // 求到直径两端点的最远距离
sort(f[t] + 1, f[t] + n[t] + 1);
}
for(int i = 1; i <= n[1]; i++)
sum[i] = sum[i - 1] + f[1][i]; // 求出第二棵树 f[1][i] 的前缀和
long long ans = 0;
for(int i = 1, j = n[1]; i <= n[0]; i++) // 双指针
{
// 找有多少个 f[1][j] 满足 f[0][i] + f[1][j] + 1 > max(d[0], d[1])
while(j >= 1 && f[0][i] + f[1][j] + 1 > max(d[0], d[1]))
j--;
// 满足条件的数量就是 n[1] - j
ans += 1LL * (n[1] - j) * (f[0][i] + 1) + (sum[n[1]] - sum[j]);
// 不满足条件的数量就是 j
ans += 1LL * j * max(d[0], d[1]);
}
cout << ans;
return 0;
}
G - Push Simultaneously
题意
在一个平面直角坐标系中有 \(N\) 个人和 \(N\) 个按钮,每个人和按钮都有具体的 \((x, y)\) 坐标。
请你为每个人分配一个按钮,然后每个人会以 \(1 \text{m/s}\) 的速度朝着所分配到的按钮走去。
当所有人都走到所分配的按钮位置时,记当前时间为 \(t\)。
请在所有的分配方案当中,找出 \(t\) 的最小值。
思路
明显是一道二分图匹配问题,但要求的是所有匹配边中最大权值的最小值,比较难以处理。
我们可以先预处理出每一对“人 - 按钮”的关系,求出每个人到每个按钮的距离,存在一个大小为 \(N^2\) 的数组中,并按照距离升序排序。
考虑二分答案 \(t\),在确定最大时间的前提下,我们可以将所有距离不超过 \(t\) 的“人 - 按钮”关系从数组中取出,建立一张二分图。这样,就只需要判断是否能够让二分图中的每个点均完成匹配,不用在意权值了。
至于二分图匹配部分,可以采用匈牙利算法,结合二分的时间复杂度是 \(O(N^3\log N^2)\);或者采用网络流中的最大流判断,但因为建立的是二分图,因此边数对时间复杂度的影响可以开方,总时间复杂度是 \(O(N^2\log N^2)\)。均可通过本题。
最后对于精度问题,虽然在 \(10^{18}\) 范围内算平方后开方,精度已经超出 double 类型所能接受的范围了,但题目要求与标准答案的相对误差不超过 \(10^{-6}\) 即可,因此无需在意。
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n;
ll sx[305], sy[305];
ll gx[305], gy[305];
struct edge
{
int a, b;
double dis;
bool operator < (const edge &e) const
{
return dis < e.dis;
}
};
edge e[90005];
int cnt = 0;
double dist(int a, int b) // 求 a 这个人到 b 这个按钮的距离
{
double dx = sx[a] - gx[b];
double dy = sy[a] - gy[b];
return sqrt(dx * dx + dy * dy);
}
vector<int> G[305];
int match[305];
bool vis[305];
bool hungary(int u) // 匈牙利算法
{
for(int &v : G[u])
{
if(!vis[v])
{
vis[v] = true;
if(!match[v] || hungary(match[v]))
{
match[v] = u;
return true;
}
}
}
return false;
}
bool check(int maxIndex)
{
for(int i = 1; i <= n; i++) // 清空
{
G[i].clear();
match[i] = 0;
}
for(int i = 1; i <= maxIndex; i++) // 把下标在 [1, maxIndex] 内的所有关系取出,建立二分图关系
G[e[i].a].push_back(e[i].b);
for(int i = 1; i <= n; i++)
{
memset(vis, false, sizeof vis);
if(!hungary(i))
return false;
}
return true;
}
int main()
{
cin >> n;
for(int i = 1; i <= n; i++)
cin >> sx[i] >> sy[i];
for(int i = 1; i <= n; i++)
cin >> gx[i] >> gy[i];
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
e[++cnt] = {i, j, dist(i, j)};
sort(e + 1, e + cnt + 1); // 按距离升序排序
int l = n, r = n * n; // 直接二分 e 数组内可以使用的最大下标,明显至少需要 n 条关系
while(l <= r)
{
int mid = (l + r) / 2;
if(check(mid))
r = mid - 1;
else
l = mid + 1;
}
printf("%.10f", e[l].dis);
return 0;
}

浙公网安备 33010602011771号