AtCoder Beginner Contest 445 ABCDEF 题目解析
A - Strong Word
- 预估难度:
入门
题意
判断给定字符串的首尾字符是否相同。
代码
#include<bits/stdc++.h>
using namespace std;
int main()
{
string s;
cin >> s;
if(s[0] == s.back())
cout << "Yes";
else
cout << "No";
return 0;
}
B - Center Alignment
- 预估难度:
入门 - 标签:字符串
题意
给定 \(N\) 个长度为奇数且仅由小写英文字母组成的字符串 \(S_1,S_2,\dots,S_N\)。
设 \(m\) 是其中最长的那个字符串的长度。
对于每个字符串,请在其首尾用相同数量的字符 . 补齐,使得每个字符串的长度均变为 \(m\)。
思路
由于每个字符串长度均为奇数,因此补齐到最长的字符串这一操作一定能保证首尾 . 的数量相等。
输入时统计 \(m\) 的值,最后对于每个长度为 \(|S|\) 的字符串,在其前后分别补上 \(\frac{m-|S|}{2}\) 个字符 . 即可。
代码
#include<bits/stdc++.h>
using namespace std;
string s[105];
int main()
{
int n, m = 0;
cin >> n;
for(int i = 1; i <= n; i++)
{
cin >> s[i];
m = max(m, (int)s[i].size());
}
for(int i = 1; i <= n; i++)
{
int k = (m - s[i].size()) / 2; // 补齐的数量
for(int j = 1; j <= k; j++)
cout << '.';
cout << s[i];
for(int j = 1; j <= k; j++)
cout << '.';
cout << '\n';
}
return 0;
}
C - Sugoroku Destination
- 预估难度:
普及- - 标签:模拟
题意
有 \(N\) 个单元格排成一行,分别编号为 \(1, 2, \dots, N\)。
单元格 \(i\) 上写有一个整数 \(A_i\) \((i \le A_i \le N)\) 。
对于每个单元格 \(s = 1,2,\ldots,N\):
- 首先,在单元格 \(s\) 中放入一个棋子。
- 执行以下操作 \(10^{100}\) 次,然后输出放置棋子的单元格的编号:
- 令 \(x\) 表示当前棋子所在单元格编号,然后将棋子放到编号为 \(A_x\) 的单元格内。
思路
首先抓住 \(i \le A_i \le N\) 这一特殊条件,可以发现每次移动,当前棋子要么停在原地不动,要么只能够向编号大的单元格单向移动。
我们假设一开始棋子位于单元格 \(s\) 处,然后经过若干点,最终到达了 \(t = A_t\)(即接下来永远停在原地不动)的终点位置。很明显,如果我们将问题的起点 \(s\) 改为这一移动过程中经过的每个点,由于题目模拟的步数特别大,最终的答案一定都固定为 \(t\)。
所以我们可以借助循环或是搜索,从编号小的单元格开始依次进行模拟,当模拟到终点位置时,再返回来把过程中经过的每一个点的答案都改为本次模拟的终点即可。
而如果模拟到一个已经存在答案的起点,跳过即可。
时间复杂度 \(O(N)\)。
代码
#include<bits/stdc++.h>
using namespace std;
int n, a[500005];
int ans[500005]; // ans[i] 表示 i 点的答案
int main()
{
cin >> n;
for(int i = 1; i <= n; i++)
cin >> a[i];
for(int i = 1; i <= n; i++)
{
// 假设 i 位置为起点
if(ans[i] != 0) // 已经存在答案
continue;
int p = i; // 当前点所在位置
vector<int> G; // 记录过程中经过的每一个位置
G.push_back(p);
while(p != a[p]) // 当前点再移动一次可以到达其它点
{
p = a[p];
G.push_back(p);
}
for(int j : G) // 对于过程中经过的每一个点
ans[j] = p; // 答案均为本次模拟的终点 p
}
for(int i = 1; i <= n; i++)
cout << ans[i] << " ";
return 0;
}
D - Reconstruct Chocolate
- 预估难度:
普及/提高- - 标签:构造、排序、双指针、数据结构(可选)、递归(可选)
题意
桌子上放着 \(N\) 块巧克力。第 \(i\) 块巧克力是一个矩形,高 \(h_i\)、宽 \(w_i\)。
已知这些巧克力是通过以下方式得到的:
- 首先手里拿起一块高为 \(H\)、宽为 \(W\) 的矩形巧克力,并且桌面上没有放任何东西。
- 重复以下操作 \(N-1\) 次:
- 将手中的巧克力分割成两块,并且分割后的两块巧克力也都是矩形。
- 将其中一块放在桌面上,另一块继续拿在手中。
- 最后将手中的这块巧克力放在桌面上。
请找出一种方案,以将 \(N\) 块巧克力排列成一个高为 \(H\)、宽为 \(W\) 的矩形,并满足以下条件:
- 不同的巧克力之间不能重叠。
- 巧克力不得旋转。也就是说,在 \(h \neq w\) 时,高为 \(h\)、宽为 \(w\) 的巧克力不能被放置成高为 \(w\)、宽为 \(h\) 的巧克力。
输出你找到的方案中每块巧克力的左上角坐标位置。
思路
由于每一步都需要放一块巧克力在桌面上,那我们不妨考虑第一步分割操作所得到的小巧克力。
假设当前的目标是将剩余小巧克力拼成一个 \(H \times W\) 的矩形巧克力,那么此时一定存在至少一块小巧克力的高 \(=H\) 或宽 \(=W\)。我们便可以简单地认为这块小巧克力就是第一步操作所得到的巧克力。如果有多块小巧克力满足条件,任选一块均可。
- 如果得到的小巧克力是高 \(=H\) 的,假设其宽为 \(w\),接下来我们便可以让其占据目标矩形的右半部分,也就是把它放在目标大矩形 \(H \times W\) 的右侧,左上角起点设置为 \((1, W-w+1)\)。这样,剩余的 \(N-1\) 块巧克力就可以继续考虑如何拼成一个大小为 \(H \times (W-w)\) 的大矩形了。
- 而如果得到的小巧克力是宽 \(=W\) 的,同理假设其高为 \(h\),接下来可以让其占据目标矩形的下半部分,左上角起点设置为 \((H-h+1, 1)\)。这样,剩余的 \(N-1\) 块巧克力就可以继续考虑如何拼成一个大小为 \((H-h) \times W\) 的大矩形了。
发现每进行以上操作一次,剩余的小巧克力数量会变少,但剩余问题与原问题相似,因此可以考虑类似递归的做法来完成。
至于实现过程,做法一用了两个 multiset 来分别对每个矩形的高与宽进行排序。每次查找两个多重集合中宽/高最大的矩形,取其中满足上述两个条件之一的矩形,处理后将其从两个多重集合中均删除即可。当然也能用 priority_queue 结合计数数组标记来完成。
做法二则是存成两个数组,借助比较函数分别对宽/高排序,然后双指针倒着找两数组中当前还没用过的宽/高最大的矩形,依照上述相同的方式进行判断。删除则可以借助一个计数数组 vis 进行标记,之后双指针过程中及时跳过已经被标记的矩阵即可。
时间复杂度均为 \(O(N\log N)\)。
代码一
#include<bits/stdc++.h>
using namespace std;
struct sorth // 按 h 排序
{
int h, w, id;
bool operator < (const sorth &t) const
{
if(h != t.h)
return h < t.h;
return id < t.id;
}
bool operator == (const sorth &t) const
{
return id == t.id;
}
};
struct sortw // 按 w 排序
{
int h, w, id;
bool operator < (const sortw &t) const
{
if(w != t.w)
return w < t.w;
return id < t.id;
}
bool operator == (const sortw &t) const
{
return id == t.id;
}
};
int ansh[200005], answ[200005];
// ansh[i], answ[i] 表示第 i 个矩形的答案左上角坐标
int main()
{
int H, W, N;
cin >> H >> W >> N;
multiset<sorth> st1; // 按高排序
multiset<sortw> st2; // 按宽排序
for(int i = 1; i <= N; i++)
{
int h, w;
cin >> h >> w;
st1.insert({h, w, i});
st2.insert({h, w, i});
}
for(int i = 1; i <= N; i++)
{
sorth r1 = *prev(st1.end()); // 行最大的矩形
sortw r2 = *prev(st2.end()); // 列最大的矩形
if(r1.h == H) // r1的行与目标最大矩形相同
{
ansh[r1.id] = 1;
answ[r1.id] = W - r1.w + 1;
W -= r1.w;
st1.erase(st1.find(r1));
st2.erase(st2.find({r1.h, r1.w, r1.id}));
}
else // r2的列与目标最大矩形相同
{
ansh[r2.id] = H - r2.h + 1;
answ[r2.id] = 1;
H -= r2.h;
st1.erase(st1.find({r2.h, r2.w, r2.id}));
st2.erase(st2.find(r2));
}
}
for(int i = 1; i <= N; i++)
cout << ansh[i] << " " << answ[i] << "\n";
return 0;
}
代码二
#include<bits/stdc++.h>
using namespace std;
struct node
{
int h, w, id;
};
bool vis[200005];
// vis[i] 标记 i 这个矩形是否已经被删除
int ansh[200005], answ[200005];
// ansh[i], answ[i] 表示第 i 个矩形的答案左上角坐标
node r1[200005], r2[200005];
bool cmph(node &x, node &y)
{
return x.h > y.h;
}
bool cmpw(node &x, node &y)
{
return x.w > y.w;
}
int main()
{
int H, W, N;
cin >> H >> W >> N;
for(int i = 1; i <= N; i++)
{
cin >> r1[i].h >> r1[i].w;
r1[i].id = i;
r2[i] = r1[i];
}
sort(r1 + 1, r1 + N + 1, cmph); // r1 按 h 从大到小排序
sort(r2 + 1, r2 + N + 1, cmpw); // r2 按 w 从大到小排序
int p1 = 1, p2 = 1;
// p1 p2 表示 r1 r2 两数组中 下标最小的 还没被删除的矩形 的下标位置
for(int i = 1; i <= N; i++)
{
while(vis[r1[p1].id]) // 先跳过已经删除的矩形
p1++;
while(vis[r2[p2].id])
p2++;
// 此时 r1[p1] 即剩余的 h 最大的矩形
// r2[p2] 即剩余的 w 最大的矩形
if(r1[p1].h == H) // r1[p1]的行与目标最大矩形相同
{
ansh[r1[p1].id] = 1;
answ[r1[p1].id] = W - r1[p1].w + 1;
W -= r1[p1].w;
vis[r1[p1].id] = true;
}
else // r2[p2]的列与目标最大矩形相同
{
ansh[r2[p2].id] = H - r2[p2].h + 1;
answ[r2[p2].id] = 1;
H -= r2[p2].h;
vis[r2[p2].id] = true;
}
}
for(int i = 1; i <= N; i++)
cout << ansh[i] << " " << answ[i] << "\n";
return 0;
}
E - Many LCMs
- 预估难度:
普及+/提高 - 标签:数论、素数筛、质因数分解、快速幂、乘法逆元
题意
给定一个长度为 \(N\) 的正整数序列 \(A=(A_1,A_2,\dots,A_N)\) 。
对于每个 \(k=1,2,\dots,N\),求序列 \(A\) 中除 \(A_k\) 以外的剩余 \(N-1\) 正整数的最小公倍数除以 \(998244353\) 的余数。
思路
对于多个正整数的最小公倍数 LCM 的求法,可以考虑质因数分解,然后分别对于每个质数,在所有出现过的数字当中求其出现的最大幂次,这也就是 LCM 中该质数的幂次。
如果需要求整个序列 \(N\) 个整数的 LCM,我们可以对于 \(10^7\) 以内的每个质数 \(p\),维护一个序列 \(\text{mx}[p]\),表示该质数在整个序列所有整数当中出现的最大幂次。最后只需要对于每个质数 \(p\) 计算 \(\prod p^{\text{mx}[p]}\) 即为序列 LCM。
但本题每次会删除一个整数,再在剩余的 \(N-1\) 个整数当中取 LCM。明显“取最大”这一操作没有逆运算,而如果考虑预处理序列的前后缀 LCM,每次枚举删除的整数时,合并一段前缀与一段后缀的 LCM 答案的复杂度至少是 \(O(\sigma)\) 的(\(\sigma=\) \(\max A\) 以内所有质数的数量)。
但由于整个序列是已知的,且每次删除只会假设删其中一个数字,因此我们可以在预处理每个质数的最大幂次 \(\text{mx}[p]\) 时,再来个序列 \(\text{mx2}[p]\) 维护其次大幂次,这便可以在遇到某个质数的最大幂次所代表的原整数被删除时,以快速求得该质数对剩余整数 LCM 的贡献。
因此对于整个过程,首先处理出 \(N\) 个整数的 LCM,然后枚举要删除的数 \(A_i\),对其再进行质因数分解。如果 \(A_i\) 中出现的质数 \(p\) 的幂次不等于 \(\text{mx}[p]\),那么将其删除不会对 LCM 造成影响。而如果 \(A_i\) 中出现的质数 \(p\) 的幂次等于 \(\text{mx}[p]\),那么删除该整数后,LCM 中质数 \(p\) 的幂次将会变为 \(\text{mx2}[p]\)。幂次的降低对模意义下的整数的影响,可以借助乘法逆元来快速处理。
至于多组数据,如果用普通数组记录 \(\text{mx}\) 与 \(\text{mx2}\) 的信息,明显每次都清空一个长度为 \(10^7\) 的数组不大可能,那就需要额外再使用一个容器用于存储当前这组数据中出现过的每个质因数。如果觉得这样比较麻烦,也可以简单地借助 map 容器对其进行维护。
素数筛法预处理时间复杂度 \(O(M)\) 或 \(O(M\log\log M)\)。单组数据时间复杂度 \(O(N(\log M + \log \sigma + \log \text{MOD}) + \sigma \log M)\),其中 \(M\) 表示整数值域(质因数分解),\(\sigma\) 表示质数数量(map容器),\(\text{MOD}\) 表示模数(乘法逆元)。
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll mod = 998244353;
const int M = 10000000;
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;
}
// 计算 x / y % mod
ll divide(ll x, ll y)
{
return x * qpow(y, mod - 2) % mod;
}
bool vis[M + 5]; // 素数筛标记
int pr[1000005], pcnt; // 质数数组
int pre[M + 5]; // pre[i] 表示 i 的最小质因数
void sieve() // 筛法
{
for(int i = 2; i <= M; i++)
{
if(!vis[i])
{
pr[++pcnt] = i;
pre[i] = i; // 质数的最小质因数为自己
}
for(int j = 1; j <= pcnt; j++)
{
if(i * pr[j] > M)
break;
vis[i * pr[j]] = true;
pre[i * pr[j]] = pr[j]; // 记录最小质因数
if(i % pr[j] == 0)
break;
}
}
}
int n, a[200005];
void solve()
{
cin >> n;
map<int, int> mx; // mx[i] 记录 i 这个质因数出现的最大幂次
map<int, int> mx2; // mx2[i] 记录 i 这个质因数出现的次大幂次
for(int i = 1; i <= n; i++)
{
cin >> a[i];
int x = a[i];
while(x != 1)
{
int p = pre[x]; // 获取一个质因数
int cnt = 0; // 记录这个质因数出现的幂次
while(x % p == 0)
{
x /= p;
cnt++;
}
if(cnt > mx[p]) // 更新最大幂次和次大幂次
{
mx2[p] = mx[p]; // 此前的最大变次大
mx[p] = cnt;
}
else if(cnt > mx2[p]) // 更新次大幂次
mx2[p] = cnt;
}
}
ll lcm = 1; // 处理所有数字的 LCM
for(auto it : mx) // 对于出现过的所有质数及其最大幂次
lcm = lcm * qpow(it.first, it.second) % mod;
for(int i = 1; i <= n; i++) // 枚举要删除的数字
{
ll ans = lcm;
int x = a[i]; // 同上,对其进行质因数分解
while(x != 1)
{
int p = pre[x]; // 获取一个质因数
int cnt = 0; // 记录这个质因数出现的幂次
while(x % p == 0)
{
x /= p;
cnt++;
}
if(cnt == mx[p]) // 该数中 p 这一质因子的幂次为最大
ans = divide(ans, qpow(p, mx[p] - mx2[p])); // 将最大与次大幂次差值对应值从答案中去除
}
cout << ans << " ";
}
cout << "\n";
}
int main()
{
sieve();
int T;
cin >> T;
while(T--)
solve();
return 0;
}
F - Exactly K Steps 2
- 预估难度:
普及+/提高 - 标签:图论、矩阵快速幂
题意
有一张有向图,图中有 \(N\) 个顶点,分别编号为 \(1, 2, \dots, N\)。
对于任意一对整数 \((i,j)\) \((1 \leq i,j \leq N)\) ,从顶点 \(i\) 到顶点 \(j\) 恰好有一条有向边,代价为 \(C_{i,j}\) 。
给你一个正整数 \(K\),对于每个 \(s=1,2,\ldots,N\),求解下列问题:
- 从顶点 \(s\) 出发,严格移动 \(K\) 条边后回到顶点 \(s\),请找出所有移动方案当中,经过的边的最小总代价。
思路
一道矩阵快速幂加速图论路径统计的模板题。
先考虑动态规划,定义 \(\text{dp}[k][i][j]\) 表示从 \(i\) 出发移动到 \(j\) 并且严格移动 \(k\) 条边的最小总代价。
当 \(k=1\) 时,明显 \(\text{dp}[1][i][j] = C_{i, j}\)。
明显这个动态规划可以根据总边数 \(k\) \((k \ge 2)\) 从小到大进行,并且转移方程为:
但本题的 \(K\) 很大,明显不能通过动态规划求解。但又注意到总点数 \(N\) 很小,上述方程的后两维类似于矩阵乘法,只不过将对应数值的乘法改为了加法,将结果的累加改为了取 \(\min\)。
根据离散数学存图转移的方式,考虑直接将任意两点间的有向边代价 \(C\) 处理为邻接矩阵 \(\text{C}[i][j]\),借助矩阵快速幂的方式加速转移递推,最后对于每个点,取自己到自己的长度输出即可。
时间复杂度 \(O(N^2\log K)\)。
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n, k;
struct matrix
{
ll a[101][101];
matrix()
{
memset(a, 0x3f, sizeof a);
// 构造函数调用时清空数组
// 但本题较为特殊,矩阵计算过程采用 min 计算
// 因此初始化为极大值
}
};
// 类矩阵乘法,计算 a * b
matrix multiply(matrix a, matrix b)
{
matrix c;
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
for(int k = 1; k <= n; k++)
c.a[i][j] = min(c.a[i][j], a.a[i][k] + b.a[k][j]);
return c;
}
// 矩阵快速幂,计算 a ^ n
matrix qpow(matrix a, int n)
{
matrix r = a; // 先给 a^1
n--; // 再计算 n-1 次方的快速幂
while(n)
{
if(n & 1)
r = multiply(r, a);
a = multiply(a, a);
n >>= 1;
}
return r;
}
int main()
{
cin >> n >> k;
matrix mat;
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
cin >> mat.a[i][j];
mat = qpow(mat, k);
for(int i = 1; i <= n; i++)
cout << mat.a[i][i] << "\n";
return 0;
}

浙公网安备 33010602011771号