AtCoder Beginner Contest 448 ABCDEF 题目解析
A - chmin
- 预估难度:
入门 - 标签:模拟
题意
给定一个长度为 \(N\) 的整数数组 \(A\) 以及一个整数 \(X\)。
按顺序从前往后看一遍 \(A\) 数组内的每个元素 \(A_i\):
- 如果 \(A_i \lt X\),则将 \(X\) 修改为 \(A_i\),并输出
1;否则输出0。
代码
void solve()
{
int n, x;
cin >> n >> x;
for(int i = 1; i <= n; i++)
{
int a;
cin >> a;
if(a < x)
{
x = a;
cout << "1\n";
}
else
cout << "0\n";
}
}
B - Pepper Addiction
- 预估难度:
入门 - 标签:模拟
题意
一家餐厅有 \(M\) 种胡椒,分别称作第 \(1, 2, \dots, M\) 种胡椒。
该餐厅目前拥有 \(C_j\) 克第 \(j\) 种胡椒。
高桥在该餐厅点了 \(N\) 道菜。
出于搭配考虑,对于第 \(i\) 道菜,他只能往上面撒第 \(A_i\) 种胡椒,且可撒的胡椒量上限为 \(B_i\) 克。
此外,第 \(j\) 种胡椒的使用总量不能超过餐厅所拥有的总量(\(C_j\) 克)。
高桥可以自己任意决定每道菜要撒多少克的胡椒,而他希望所有菜上撒的胡椒总量最大。
请问总量的最大值是多少?
思路
由于希望撒的胡椒总量最大,很明显对于每盘菜肯定也是撒得越多越好。
两种做法:
- 一种是用 \(C_j\) 维护第 \(j\) 种胡椒的剩余重量。对于第 \(i\) 盘菜,由于菜品限制最多只能撒 \(B_i\) 克第 \(A_i\) 胡椒,因此可以通过 \(\min(B_i, C_{A_i})\) 来计算出实际能撒的重量,计入答案并从 \(C_{A_i}\) 中减去即可。
- 一种是再来个数组 \(D_j\) 维护第 \(j\) 种胡椒总共可以撒多少重量。对于第 \(i\) 盘菜,直接将最多可以撒的 \(B_i\) 克加入到 \(D_{A_i}\) 即可。最后再统计每种胡椒需要的最大数量 \(D_j\) 与可用的最大数量 \(C_j\) 之间的关系,取个较小值作为答案即可。
代码
int c[1005], d[1005];
// c[i] 表示第 i 种胡椒可用的最大数量
// d[i] 维护第 i 种胡椒需要的最大数量
void solve()
{
int n, m;
cin >> n >> m;
for(int i = 1; i <= m; i++)
cin >> c[i];
for(int i = 1; i <= n; i++)
{
int a, b;
cin >> a >> b;
d[a] += b;
}
int ans = 0;
for(int i = 1; i <= m; i++)
ans += min(c[i], d[i]);
cout << ans;
}
C - Except and Min
- 预估难度:
普及- - 标签:排序
题意
一个袋子中装有 \(N\) 个球,编号分别为 \(1\) 到 \(N\)。
\(i\) 号球上写有一个整数 \(A_i\)。
你需要处理 \(Q\) 次查询。
对于每次查询,会给出一个长度为 \(K\) \((K \le 5)\) 的序列 \(B_1, B_2, \dots, B_{K}\),你需要按顺序执行以下操作。其中 \(B_i\) 都是在 \(1 \sim N\) 之间的互不相同的整数。
- 首先,从袋子中取出编号为 \(B_1, B_2, \dots, B_K\) 的球。
- 然后,输出当前袋中剩余球上所写整数的最小值。(保证此时袋子不为空。)
- 最后,将所有取出的 \(K\) 个球放回袋子中。
思路
首先注意到 \(K \le 5\),每次删除的数字数量很少。
由于询问的又是剩余数字的最小值,因此可以进行分类讨论:
- 如果删除的这 \(K\) 个数字恰好是原数组的前 \(K\) 小值,明显此时答案就是原数组的第 \(K+1\) 小的数字。
- 否则,原数组的前 \(K\) 小的数字中一定有一个数字没有被删除。我们可以将被删除的这 \(K\) 个数字先从小到大排序,然后从前往后依次对比原数组的前 \(K\) 小值。只要找到一个位置 \(i\) 不同,则说明被删除的第 \(i\) 小的数字并不是原数组实际上的第 \(i\) 小的数字,实际第 \(i\) 小值仍然存在,那么此时的答案就是原数组的第 \(i\) 小的数字。
时间复杂度 \(O(N\log N + QK\log K)\)。
代码
int a[300005];
int b[300005];
int c[10];
void solve()
{
int n, q;
cin >> n >> q;
for(int i = 1; i <= n; i++)
{
cin >> a[i];
b[i] = a[i];
}
sort(b + 1, b + n + 1);
while(q--)
{
int k;
cin >> k;
for(int i = 1; i <= k; i++)
{
int p;
cin >> p;
c[i] = a[p]; // 取出原数组对应位置的每个数
}
sort(c + 1, c + k + 1);
bool flag = false; // 是否找到答案
for(int i = 1; i <= k; i++)
{
if(c[i] != b[i]) // 被删除的第 i 小值与实际第 i 小值不同,答案即实际的第 i 小值
{
cout << b[i] << "\n";
flag = true;
break;
}
}
if(!flag) // 删除的 k 个数与前 k 小值相同,答案即实际的第 k+1 小值
cout << b[k + 1] << "\n";
}
}
D - Integer-duplicated Path
- 预估难度:
普及/提高- - 标签:图论、搜索、数据结构/离散化
题意
给定一棵包含 \(N\) 个顶点的树,顶点编号为 \(1, 2, \dots, N\)。
每个顶点 \(i\) 上写有一个整数 \(A_i\)。
请对于每个 \(k = 1, 2, \dots, N\),回答以下问题:
- 问题:在从顶点 \(1\) 到顶点 \(k\) 的简单路径(即不重复访问任何顶点的路径)上的所有顶点之中,是否存在两个不同的顶点写有相同的整数?如果存在,输出
Yes;否则,输出No。
思路
假设令 \(1\) 号点作为根结点,那么问题便变成了:对于 \(k=1 \dots, N\),从根结点到每个点 \(k\) 的简单路径上是否存在两个整数相同的顶点。
于是便可以从根结点 \(1\) 出发进行深搜,然后在搜索过程中动态维护根结点到当前点的路径上每种整数出现的次数。
如果每个整数值较小的话,我们可以考虑开一个计数数组 cnt[] 用于统计每种整数目前的出现次数,然后对于每个点,在开始搜索其子树时,将该点写有的整数出现次数 \(+1\),而在结束对其子树的搜索时,则将该点写有的整数出现次数 \(-1\)。
由于每次修改只会让某个位置 \(\pm 1\),因此我们可以再开一个变量 tot 用于统计当前有多少种整数的出现次数 \(\ge 2\)。当一个整数的出现次数 \(+1\) 后变为了出现两次,则 tot++;而当一个整数出现次数 \(-1\) 后变为了出现一次,则 tot--。
但本题 \(A_i \le 10^9\),因此可以借助 map 映射容器来完成计数功能,或是借助离散化算法均可。
时间复杂度 \(O(N\log N)\)。
代码
int n;
int a[200005]; // 每个顶点上的整数
vector<int> G[200005];
bool ok[200005];
map<int, int> cnt; // 统计 1 到 u 路径上每个整数的出现次数
int tot = 0; // 统计 1 到 u 路径上有多少种整数的 出现次数 >= 2
void dfs(int u, int fa)
{
if(++cnt[a[u]] == 2) // 加上当前点后,a[u] 出现次数变为 2
tot++; // 表示多了一种整数的出现次数 >= 2
if(tot > 0)
ok[u] = true; // 说明 1 到 u 的路径上存在至少两个点的整数相同
for(int &v : G[u])
{
if(v == fa)
continue;
dfs(v, u);
}
if(--cnt[a[u]] == 1)// 去掉当前点后,a[u] 出现次数变为 1
tot--; // 表示少了一种整数的出现次数 >= 2
}
void solve()
{
cin >> n;
for(int i = 1; i <= n; i++)
cin >> a[i];
for(int i = 1; i < n; i++)
{
int u, v;
cin >> u >> v;
G[u].push_back(v);
G[v].push_back(u);
}
dfs(1, 0);
for(int i = 1; i <= n; i++)
cout << (ok[i] ? "Yes\n" : "No\n");
}
E - Simple Division
- 预估难度:
普及+/提高 - 标签:数论、(倍增)
题意
给定整数 \(N\) 和 \(M\)。求 \(\lfloor \dfrac N M \rfloor\) 除以 \(\color{red}{10007}\) 的余数。
在本题中,\(N\) 不是直接给出的,而是以行程长度编码的形式给出。
具体来说,\(N\) 由 \(K\) 个数对组成,每个数对包含一个数位 \(c_i\) 和一个整数 \(l_i\)。
要还原原始的 \(N\),请遵循以下步骤:
- 初始时,设字符串 \(S\) 为空字符串。
- 对于 \(i=1, 2, \dots, K\),重复以下操作:
- 将 \(l_i\) 个数位 \(c_i\) 追加到字符串 \(S\) 的末尾。
- 将最终的 \(S\) 看作一个整数,该整数即为 \(N\)。
思路
由于要求出 \(\dfrac N M\) 的商,再对其除以 \(10007\) 取余数。而每次处理一段新的行程长度编码时,往被除数后面拼上一段新的整数,商又会受到此前计算过程中余数的影响,不好处理。
难点主要还是因为 \(\dfrac N M\) 有可能并不是整除,所以我们需要先求商再除以 \(10007\) 取余数。而如果说我们能够将 \(\dfrac N M\) 的余数 \(N \bmod M\) 先从被除数 \(N\) 里去除,又由于 \(10007\) 是一个质数且 \(M \lt 10007\),根据费马小定理,除法可以直接改为乘上 \(M\) 在模意义下的逆元 \(M^{10005}\) 来简单解决。即:
再根据同余性质,上式的 \(N\) 也可以描述为 \(N \bmod 10007\),即上式:
那么问题便变成了如何求出 \(N\) 分别除以 \(10007\) 和 \(M\) 的余数。
这里的做法有很多,也比较模板,可以通过数学计算一步步计算新的数位拼接后形成的新数字的余数。
下面主要介绍借助倍增数组来进行实现。
由于除数最大为 \(10007\),拼接的数位仅为 \(0 \sim 9\) 共 \(10\) 种,因此可以开两个数组 NP[i][j][k] 与 NM[i][j][k] 分别表示在 \(i\) 后面拼上 \(2^k\) 个数位 \(j\) 所形成的整数除以 \(10007\) 或 \(M\) 所得到的余数。
由于数位拼接仅涉及乘法与加法,根据模运算分配律:
如果我们要在整数 \(i\) 后拼上 \(j+k\) 个数位 \(c\),可以先拼上 \(j\) 个数位 \(c\) 并计算余数后,再在余数后面拼上 \(k\) 个数位 \(c\) 来分两步实现。因此可以按照这个方法处理出这两个倍增数组。
倍增数组处理完成后,按照题目给定的“行程长度编码”,一步步算出 \(N \bmod 10007\) 与 \(N \bmod M\) 的值,按照上述公式计算答案即可。
时间复杂度 \(O((D \sigma + K) \log \max l_i)\),其中 \(D = 10007, \sigma = 10\)。
代码
// 快速幂求 a^n % mod
int qpow(int a, int n, int mod)
{
int r = 1;
while(n)
{
if(n & 1)
r = r * a % mod;
a = a * a % mod;
n >>= 1;
}
return r;
}
int M;
int NP[10007][10][30];
// NP[i][j][k] 表示在 i 后面拼上 2^k 个数位 j 之后所得到的数字 再 % 10007 的结果
int NM[10007][10][30];
// NM[i][j][k] 表示在 i 后面拼上 2^k 个数位 j 之后所得到的数字 再 % M 的结果
// 预处理 nxt 数组
void init()
{
for(int i = 0; i < 10007; i++)
for(int j = 0; j <= 9; j++)
{
NP[i][j][0] = (i * 10 + j) % 10007;
NM[i][j][0] = (i * 10 + j) % M;
}
// 处理倍增
for(int k = 1; k < 30; k++)
for(int i = 0; i < 10007; i++)
for(int j = 0; j <= 9; j++)
{
// 在 i 后拼上 2^k 个数位 j
// 等同于先拼上 2^(k-1) 个数位 j,求出 %10007 的余数后
// 再拼上 2^(k-1) 个数位 j 所得到的结果
NP[i][j][k] = NP[NP[i][j][k - 1]][j][k - 1];
NM[i][j][k] = NM[NM[i][j][k - 1]][j][k - 1];
}
}
void solve()
{
int k;
cin >> k >> M;
init();
int rmd_p = 0; // 求 N % 10007 的余数
int rmd_m = 0; // 求 N % M 的余数
while(k--)
{
int c, l;
cin >> c >> l;
for(int i = 29; i >= 0; i--) // 倍增处理长度 l
if(l >= (1 << i))
{
l -= 1 << i;
// 在当前余数后 拼上 2^i 个数位 c
rmd_p = NP[rmd_p][c][i];
rmd_m = NM[rmd_m][c][i];
}
}
int rmd = (rmd_p - rmd_m + 10007) % 10007;
int inv = qpow(M, 10005, 10007); // M 在 10007 下的逆元
cout << rmd * inv % 10007;
}
F - Authentic Traveling Salesman Problem
- 预估难度:
提高+/省选- - 标签:构造、莫队(分块、排序)
题意
在二维平面上有 \(N\) 个地点,编号为 \(1\) 到 \(N\)。地点 \(i\) 的坐标为 \((X_i, Y_i)\)。
你将从地点 \(1\) 出发,访问每个地点恰好一次,最后返回地点 \(1\)。
从地点 \(i\) 移动到地点 \(j\) 需要花 \(\vert X_i - X_j \vert + \vert Y_i - Y_j \vert\) 秒(即曼哈顿距离)。
请输出一条路线,使得从地点 \(1\) 出发,访问所有地点并返回地点 \(1\) 的总时间在 \(10^{10}\) 秒以内。保证答案存在。
思路
要求找出一条二维平面内的哈密顿回路,两点间距离为曼哈顿距离,且需要保证总长度 \(\le 10^{10}\)。
由于是构造题,我们并不一定要最小化总长度,只要保证长度不超过限制即可。
如果给定的点大部分都聚集在一起(存在密集的区域),那么此时我们便可以简单地任意安排顺序(比如按坐标排序)来设置这些点的访问顺序,这样的做法与最优解之间的步数差在 \(10^{10}\) 的范围内不值一提。
反之,我们可以考虑所有给定点分布得都比较稀疏的情况。这里可以采用莫队算法,将整张图借助分块思想分为多个部分,每一部分内部单独暴力解决。
设 \(W = 2 \times 10^7\) 表示坐标轴的范围,这里考虑将横坐标分为多块,每块宽度为 \(B\),即整张 \(W \times W\) 的图可以简单看作是由 \(\frac W B\) 个大小为 \(B \times W\) 的矩形区域所组成。
对于所有的 \(\frac W B\) 个区域,我们就按照从左到右的顺序分别处理每个区域内的所有点。而对于每个区域内部,由于按照目前的分块方式,每个区域都是长长的一条,所以可以按照纵坐标对所有区域内的点进行排序。
但这里并不是说排序后,每个区域内的点都会按纵坐标顺序去遍历。为了保证总长度尽可能小,我们可以按照蛇形的方式绕着去遍历每个区域。例如最左边的区域按 \(Y\) 从小到大遍历,到最后一个点时 \(Y\) 一定最大,然后第二个区域则按 \(Y\) 从大到小遍历,到最后一个点时 \(Y\) 一定最小,第三个区域重新按 \(Y\) 从小到大遍历,依次交替进行实现。
在这种方法下:
- 每个区域内部的垂直移动步数:最大为 \(W\),共 \(\dfrac W B\) 个区域,故总步数最大为 \(\dfrac {W^2} B\)。
- 每个区域内部的水平移动步数:区域内从一个点移动到另一个点在水平方向上最多移动 \(B\) 步,共 \(N\) 个点,故总步数最大为 \(NB\)。
- 跨区域移动:从一个区域移动到右侧下一个区域最坏情况下移动 \(2B\) 步,共 \(\dfrac W B\) 个区域,步数共 \(2W\) 步,再加上最后要从最右侧回到最左侧,故总步数最大为 \(4W\)。
总步数约为 \(\dfrac{W^2}B + NB + 4W\)。
根据均值不等式可得 \(\dfrac{W^2}B + NB \ge 2\sqrt{W^2N}\),为取得最小值,此时 \(\dfrac{W^2}B = NB\),即 \(B = \sqrt{\dfrac{W^2}N}\)。
此时总步数可以取到 \(2\sqrt{W^2N} + 4W \lt 9.9 \times 10^9\),可以通过。
时间复杂度 \(O(N\log N)\),主要取决于排序。
代码
struct point
{
int x, y, id;
};
point p[60005];
int B;
bool cmp(point &a, point &b)
{
if(a.x / B != b.x / B) // 不同区域
return a.x < b.x;
// 相同区域则按 Y 排序,需让相邻两个区域的排序规则相反
if((a.x / B) % 2 == 0)
return a.y < b.y;
else
return a.y > b.y;
}
void solve()
{
int n;
cin >> n;
B = 2e7 / sqrt(n);
for(int i = 1; i <= n; i++)
{
cin >> p[i].x >> p[i].y;
p[i].id = i;
}
sort(p + 1, p + n + 1, cmp);
// 输出需要从 1 号点开始,找到 1 号点的下标,再向后循环一遍输出
int j;
for(int i = 1; i <= n; i++)
if(p[i].id == 1)
{
j = i;
break;
}
for(int i = 1; i <= n; i++)
{
cout << p[j].id << " ";
if(j == n)
j = 1;
else
j++;
}
}

浙公网安备 33010602011771号