AtCoder Beginner Contest 226 解题报告
A - Round decimals
题目链接
题意简述
给你一个实数 \(x\) ,请你输出它四舍五入后的值。
保证输入的 \(x\) 有效位数 [不确定] 不超过 3 位。
解题思路
少年,你是否听说过 C++ 有一个叫 round 的函数?
顺便,如果你使用 printf("%.0lf", x); 恭喜你挂了这道题。
因为 printf 不是四舍五入,而是四舍六入五成双。
什么?你问我怎么知道?你不妨猜猜看
B - Counting Arrays
题目链接
题意简述
给你 \(N\) 个序列,第 \(i\) 个序列长度为 \(L_i\),第 \(i\) 个序列中的第 \(j\) 个数记为 \(a_{i, j}\),求这 \(N\) 个序列中有多少个是不一样的。
- \(1 \leq N \leq 2 \times 10^5\)
- $ 1 \leq L_i \leq 2 \times 10^5 $
- \(0 \leq a_{i, j} \leq 10^9\)
解题思路
少年,你是否听说过 C++ 中有个叫 set 的玩意?
直接把所有序列塞到一个 set 里,最后输出 set 的 size() 即可。
C - Martial artist
题目链接
题意简述
某人要点科技树,点某个科技需要一些前置科技。
科技树上一共有 \(N\) 个点。
你知道每个科技被点出来所需要花费的时间和其需要的前置科技。
请问想把第 \(N\) 个科技点出来最少要花多少时间。
- \(1 \leq N \leq 2\times 10^5\)
- \(1 \leq T_i \leq 10^9\)
- $ 0 \leq K_i < i$
- \(1 \leq A_{i,j} < i\)
- \(\sum_{i = 1}^{N}K_i \leq 2 \times 10^5\)
解体思路
比上面两道水题稍微难度大些。
很容易想到只要把第 \(N\) 个科技所有的前置科技点出来即可。
而把前置科技点出来需要点出前置科技的前置科技。
我们建反向边 dfs 把所有一定需要点出来的前置科技点完即可。
容易证明正确性:因为所有 dfs 到的科技一定要点,并且我们每分钟都在学科技(没有造成时间浪费),必然最优。
参考代码
#include <vector>
#include <cctype>
#include <cstdio>
using namespace std;
using LL = long long;
const int N = 2e5 + 10;
int n;
int w[N];
bool vis[N];
vector<int> G[N];
LL dfs(int u, int fa)
{
LL res = 0;
vis[u] = 1;
for (int v : G[u]) {
if (v == fa || vis[v]) continue;
res += dfs(v, u);
}
return res + w[u];
}
int main(void)
{
scanf("%d", &n);
for (int u = 1; u <= n; ++u) {
scanf("%d", &w[u]);
int x; io >> x;
for (int j = 0; j < x; ++j) {
int v; io >> v;
G[u].emplace_back(v);
}
}
printf("%lld\n", dfs(n, n));
return 0;
}
D - Teleportation
题目链接
题意简述
有 \(N\) 个小镇,第 \(i\) 个小镇坐标为 \((x_i, y_i)\)。
你可以学瞬移魔法,每个瞬移魔法可以用一个二元组 \((a, b)\) 描述。
当你在 \((x, y)\) 使用瞬移魔法 \((a, b)\) 时,你会被传送到 \((x + a, y + b)\)。
你希望在任意两个小镇之间通行时,只要反复用同一个瞬移魔法就能到达目的地。
问你最少学几个瞬移魔法。
- \(2 \leq N \leq 500\)
- $ 0 \leq x_i \leq 10^9$
- \(0 \leq y_i \leq 10^9\)
解题思路
也挺简单。
你从某个小镇到另一个小镇,假设位移为 \((dx, dy)\)。
如果要想能够仅通过反复使用同一个魔法到达目的地,你需要有一个魔法 \((x', y')\) 满足 \(kx' = dx,ky' = dy,k \in N_+\)。
显然能贪心,让 \(x'\) 与 \(y'\) 互质(约分直到最简)的时候最优。
贪心正确性很容易证明。
参考代码
#include <set>
#include <cctype>
#include <cstdio>
#include <cstdlib>
#include <algorithm>
using namespace std;
using LL = long long;
const int N = 510;
using PII = pair<int, int>;
int n;
PII pos[N];
set<PII> S;
int gcd(int a, int b) { return b ? gcd(b, a % b) : a; }
int main(void)
{
scanf("%d", &n);
for (int i = 1; i <= n; ++i) {
scanf("%d%d", &pos[i].first, &pos[i].second);
for (int j = 1; j < i; ++j) {
int dx = pos[i].first - pos[j].first;
int dy = pos[i].second - pos[j].second;
int d = gcd(dx, dy);
dx /= d;
dy /= d;
S.insert(make_pair(dx, dy));
S.insert(make_pair(-dx, -dy));
}
}
printf("%ld\n", S.size());
return 0;
}
E - Just one
题目链接
题意简述
给你一张简单无向图。
你需要遍历整张图,并且每个点都能被走出一次。
求遍历这张图的方案数,对 \(998244353\) 取模。
- \(2 \leq N \leq 2 \times 10^5\)
- \(1 \leq M \leq 2 \times 10 ^ 5\)
- \(1 \leq U_i,V_i \leq N\)
- \(U_i \neq V_i\)
解题思路
并不难,每个点只能被走出一次,就是只对应一条出边,然后这玩意就是一棵基环树。
显然每棵基环树都只有两种遍历方式。
需要注意的是连通块不止一个,你需要判断每个连通块是否都是一棵基环树,用并查集判断这个连通块的点数和边数是否一样即可。
如果是,假设有 \(S\) 个连通块,输出 \(2^S\) 即可,否则输出 \(0\)。
参考代码
#include <cctype>
#include <cstdio>
using namespace std;
using LL = long long;
const int N = 2e5 + 10, mod = 998244353;
int n, m;
bool vis[N];
int fa[N], sz[N], esz[N];
int find(int x) { return x == fa[x] ? x : fa[x] = find(fa[x]); }
int main(void)
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i) {
fa[i] = i;
sz[i] = 1;
}
for (int i = 1; i <= m; ++i) {
int u, v; scanf("%d%d", &u, &v);
u = find(u), v = find(v);
if (u != v) {
fa[u] = v;
sz[v] += sz[u];
esz[v] += esz[u] + 1;
}
else ++esz[u];
}
int ans = 1;
for (int i = 1; i <= n; ++i) {
int u = find(i);
if (!vis[u]) {
vis[u] = 1;
if (esz[u] != sz[u]) {
puts("0");
return 0;
}
ans = 2ll * ans % mod;
}
}
printf("%d\n", ans);
return 0;
}
F - Score of Permutations
题目链接
题意简述
对于一个长度为 \(N\) 的排列 \(P\),我们通过一种方式定义它的权值:
有 \(N\) 个人,初始的时候第 \(i\) 个人手中有 \(i\) 个球。
Snuke 每嚎叫一声,第 \(i\) 个人就会把手中所有的球传递给第 \(P_i\) 个人。
Snuke 嚎叫 \(x\ (x>0)\) 次时,第 \(i\) 个人手中的球又变成了 \(i\) 个,我们把 \(x\) 的最小值称为 \(P\) 的权值,即 \(S(P)\) 。
求 \(\sum S(P)^K\),对 \(998244353\) 取模。
- \(2 \leq N \leq 50\)
- \(1 \leq K \leq 10^4\)
解题思路
首先可以想到,当确定了这个排列的时候,我们可以很容易算出这个排列的权值。
我们拿出 \(N\) 个点,然后从每个 \(i\) 到 \(P_i\) 连边。这张图一定是由许多环组成的,所有环的大小的最小公倍数就是这个排列的权值。
我们考虑强行枚举这张图是由哪几个环组成的,通过 dfs 可以知道,当 \(N = 50\) 时,方案数最多是 \(204226\) 。
接着我们用组合数学计算有多少种方案可以组成这几个环。
首先想到方案数是
这个式子的意思很简单,先求从 \(n\) 个点里面挑 \(cnt[1]\) 个的方案数,乘上再从剩下的 \(n - cnt[1]\) 个点里面挑 \(cnt[2]\) 的的方案数,再乘上从剩下的 \(n - cnt[1] - cnt[2]\) 个点中挑 \(cnt[3]\) 个的方案数……如此反复到挑完。
但是其实如果有几个环大小相同会算重,所以这个方案数要除去重复的方案。
然后发现同样的 \(x\) 个点组成环会有 \((x - 1)!\) 种方案,需要乘到方案数里。
最后给排列的权值做一遍快速幂,乘上方案数,累加进答案即可。
我这题考场上挂了,没写出来。究其原因,居然是我求最小公倍数写错了。
下面是我的写法
int d = a[1];
for (int i = 1; i <= n; ++i) d = gcd(d, a[i]);
int M = 1;
for (int i = 1; i <= n; ++) M = M * a[i] / d;
我真是个大nt,居然觉得 \(lcm(a, b) = \cfrac{a \times b}{\gcd(a,b)}\) 可以扩展到 \(lcm(a, b, c,\cdots) = \cfrac{a \times b \times c \times \cdots}{\gcd(a, b, c,\cdots)}\),我真是弱智啊。。。
实际上应当这么求
int M = a[1];
for (int i = 2; i <= n; ++i) M = lcm(M, a[i]);
下次考试不能再因为这个挂分了。。。
参考代码
#include <cctype>
#include <cstdio>
#include <algorithm>
using namespace std;
using LL = long long;
const int mod = 998244353;
int n;
int k;
int ans;
int cnt[100];
int stk[100];
int fac[100], inv[100];
int tinv[100];
inline int qpow(int base, int kk)
{
int res = 1;
while (kk) {
if (kk & 1) res = 1ll * res * base % mod;
kk >>= 1;
base = 1ll * base * base % mod;
}
return res;
}
int gcd(int a, int b) { return b ? gcd(b, a % b) : a; }
int lcm(int a, int b) { return 1ll * a * b / gcd(a, b); }
inline int C(int n, int m)
{
if (n > m) return 0;
return 1ll * fac[m] * inv[n] % mod * inv[m - n] % mod;
}
void dfs(int now, int mintot, int dep)
{
if (now > n) {
int res = 1;
for (int i = 1; i < dep; ++i)
res = 1ll * res * fac[stk[i] - 1] % mod;
int tmp = n;
for (int i = 1; i < dep; ++i) {
res = 1ll * res * C(stk[i], tmp) % mod;
tmp -= stk[i];
}
tmp = 1;
for (int i = 2; i < dep; ++i) {
if (stk[i] == stk[i - 1]) ++tmp;
else {
res = 1ll * res * inv[tmp] % mod;
tmp = 1;
}
}
res = 1ll * res * inv[tmp] % mod;
int M = stk[1];
for (int i = 2; i < dep; ++i) M = lcm(M, stk[i]);
M = qpow(M, k);
ans = (ans + 1ll * M * res % mod) % mod;
return;
}
for (int i = min(n - now + 1, mintot); i >= 1; --i) {
stk[dep] = i;
dfs(now + i, i, dep + 1);
}
}
int main(void)
{
fac[0] = 1;
for (int i = 1; i <= 60; ++i) fac[i] = 1ll * fac[i - 1] * i % mod;
for (int i = 0; i <= 60; ++i) inv[i] = qpow(fac[i], mod - 2);
for (int i = 0; i <= 60; ++i) tinv[i] = qpow(i, mod - 2);
scanf("%d%d", &n, &k);
dfs(1, n, 1);
printf("%d\n", ans);
return 0;
}
G - The baggage
题目链接
题意简述
有 \(5\) 种包裹,第 \(i\) 种包裹的重量为 \(i\)。
有 \(5\) 种人,第 \(i\) 种人的力量为 \(i\)。
给你这 \(5\) 种包裹的数量,第 \(i\) 种的数量为 \(A_i\)。
给你这 \(5\) 种人的数量,第 \(i\) 种的数量为 \(B_i\)。
请问你是否能安排一种方案,使所有的包裹被这些人拿起?
一个人可以不拿包裹,也可以拿多个包裹,但是他拿的包裹的总重量不能超过自己的力量。
题目包含 \(T\) 组数据。
- \(1 \leq T \leq 5 \times 10^4\)
- \(0 \leq A_i,B_i \leq 10^{16}\)
- \(1 \leq A_1 + A_2 + A_3 + A_4 + A_5\)
- \(1 \leq B_1 + B_2 + B_3 + B_4 + B_5\)
解题思路
一看就觉得是贪心。
首先可以发现,一个人不能同时拿起两个重量大于等于 \(3\) 的包裹。
这是一个很重要的性质,不要觉得我侮辱了你的智商。
于是在安排重量大于 \(3\) 的包裹时只能搭配上重量小于 \(3\) 的包裹。我们贪心地尽可能把重量为 \(2\) 的包裹搭配上去,因为重量为 \(1\) 的包裹哪里都可以凑。
首先重量为 \(5\) 的包裹只能让力量为 \(5\) 的人拿。
接着重量为 \(4\) 的包裹先让力量为 \(4\) 的人拿,再让力量为 \(5\) 的拿,因为力量为 \(5\) 的拿会凑出 \(1\)。
重量为 \(3\) 的包裹优先让 \(5\) 拿,因为可以凑出 \(2\),接着让力量为 \(3\) 的拿,因为力量为 \(4\) 的拿会凑出 \(1\),最后再让力量为 \(4\) 的拿。
最后先让所有人去拿 \(2\),拿完 \(2\) 再拿 \(1\) 即可。如果能拿完输出 Yes,否则输出 No。
其中有的顺序其实可以交换,就比如重量为 \(3\) 的包裹可以先让力量为 \(3\) 的拿,也可以先让力量为 \(5\) 的拿,两者都符合贪心原则。
参考代码
#include <cctype>
#include <cstdio>
#include <algorithm>
using namespace std;
using LL = long long;
int T;
LL a[10], b[10];
void carry(int y, int x)
{
LL c = min(a[x], b[y]);
a[x] -= c;
b[y] -= c;
b[y - x] += c;
}
int main(void)
{
scanf("%d", &T);
while (T--) {
for (int i = 1; i <= 5; ++i) scanf("%d", &a[i]);
for (int i = 1; i <= 5; ++i) scanf("%d", &b[i]);
carry(5, 5);
carry(4, 4);
carry(5, 4);
carry(3, 3);
carry(5, 3);
carry(4, 3);
carry(5, 2);
carry(4, 2);
carry(3, 2);
carry(2, 2);
carry(5, 1);
carry(4, 1);
carry(3, 1);
carry(2, 1);
carry(1, 1);
bool fail = 0;
for (int i = 1; i <= 5; ++i)
if (a[i] > 0) {
puts("No");
fail = 1; break;
}
if (!fail) puts("Yes");
}
return 0;
}

浙公网安备 33010602011771号