AtCoder Beginner Contest 414 ABCDEF 题目解析
A - Streamer Takahashi
题意
高桥会在 \([L, R]\) 这段时间内进行直播。
有 \(N\) 位观众,每位观众可以在 \([X_i, Y_i]\) 这段时间内观看直播。
请问总共有多少位观众可以完整地看完高桥的直播?
思路
求有多少位观众的时间区间与高桥直播的时间区间满足 \(X_i \le L \lt R \le Y_i\) 即可。
代码
void solve()
{
int n, l, r, cnt = 0;
cin >> n >> l >> r;
for(int i = 1; i <= n; i++)
{
int x, y;
cin >> x >> y;
if(x <= l && r <= y)
cnt++;
}
cout << cnt;
}
B - String Too Long
题意
给定 \(N\) 对“字符-整数”对 \((c_1, l_1), (c_2, l_2), \dots, (c_N, l_N)\)。
有一个字符串,这个字符串从前往后分别是由 \(l_1\) 个字符 \(c_1\),\(l_2\) 个字符 \(c_2\),……,\(l_N\) 个字符 \(c_N\) 拼接而成的。
如果这个字符串的长度不超过 \(100\),请输出这个字符串,否则输出 Too Long
。
思路
我们可以先求出整个字符串的总长度,如果总长度不超过 \(100\) 再执行输出。
但注意题目的数据范围 \(1 \le l_i \le 10^{18}\),如果等到所有数字全部加完之后再判断,这个总和可能会超出 \(64\) 位长整型 long long
的范围。
所以建议是每次输入 \(l_i\) 之后马上先判断单个 \(l_i\) 是否超过 \(100\),或者在过程中每加一个 \(l_i\) 就判断总和是否超过 \(100\),不要放到最后再判断。
代码
int n;
char c[105];
long long l[105];
void solve()
{
cin >> n;
long long len = 0;
for(int i = 1; i <= n; i++)
{
cin >> c[i] >> l[i];
len += l[i];
if(len > 100) // 过程中判断
{
cout << "Too Long";
return;
}
}
for(int i = 1; i <= n; i++)
{
for(int j = 1; j <= l[i]; j++) // 输出 l[i] 个 c[i]
cout << c[i];
}
}
C - Palindromic in Both Bases
题意
给定一个正整数 \(N\) \((N \le 10^{12})\),问 \(N\) 以内有多少个正整数符合以下两个条件,并输出所有符合条件的正整数之和:
- \(N\) 在十进制下是一个回文。
- \(N\) 在 \(A\) 进制下是一个回文。
思路
因为 \(N\) 范围很大,直接循环找“哪些数字的十进制是回文”这个操作肯定就已经超时了。
所以这题我们采用构造回文的方法。
因为回文正着读和反着读都一样,所以我们可以 for 循环枚举回文数的其中一半。
对于 \(N = 10^{12}\) 的情况,可以发现 \(N\) 以内最大的回文数,其左半边数值不超过 \(10^6\),因此考虑枚举 \(i = 1 \sim 10^6\),以 \(i\) 当作要枚举的回文数的左半边。
接下来分两种情况讨论:
- 回文数的长度是一个偶数,这需要对 \(i\) 进行前后翻转,然后再将其拼在原本的 \(i\) 后面。
- 例如 \(10 \rightarrow 1001, 123 \rightarrow 123321, 1024 \rightarrow 10244201\)。
- 回文数的长度是一个奇数,此时我们可以把 \(i\) 的个位当作回文数的正中间数位,此时就需要先对 \(i\) 去掉个位再进行前后翻转,最后再将其拼在原本的 \(i\) 后面。
- 例如 \(10 \rightarrow 101, 123 \rightarrow 12321, 1024 \rightarrow 1024201\)。
上面的翻转与拼接操作可以借助进行数位分解以及数位拼接完成。
在我们构造出回文数之后,记得先判断这个回文数是否在 \(N\) 以内。如果是,再将该数字转为 \(A\) 进制,暴力判断 \(A\) 进制下是否是一个回文即可。
数位分解及转进制后判断回文的时间复杂度均为 \(\log\) 级别,并且根据上述方法可以发现 \(N\) 以内的回文数量不会超过 \(N\) 数位的一半,即数量级为 \(\sqrt N\)。
时间复杂度 \(O(\sqrt N \log \sqrt N)\)。
代码
typedef long long ll;
int a;
ll n;
// 通过 n 来构造出一个完整的回文数
// flag = false 表示回文数长度为偶数
// flag = true 表示回文数长度为奇数
ll f(ll n, bool flag)
{
ll t = n; // 复制一份 n,然后将其首尾翻转后拼在 n 后面
if(flag)
t = t / 10; // 先去掉个位
while(t > 0) // 对 t 进行数位分解
{
n = n * 10 + t % 10; // 把当前 t 的个位拼在 n 的最后面,以此实现 t 的翻转
t /= 10;
}
return n;
}
int s[70];
// 检查 x 在 a 进制下是否是回文
bool check(ll x)
{
int p = 0;
while(x > 0) // 先转为 a 进制,存储在 s[0 ~ p-1]
{
s[p++] = x % a;
x /= a;
}
int i = 0, j = p - 1; // 判断回文
while(i < j)
{
if(s[i] != s[j])
return false;
i++;
j--;
}
return true;
}
void solve()
{
cin >> a >> n;
ll sum = 0;
for(int i = 1; i <= 1000000; i++)
{
ll x = f(i, true);
ll y = f(i, false);
if(x <= n && check(x))
sum += x;
if(y <= n && check(y))
sum += y;
}
cout << sum;
}
D - Transmission Mission
题意
在一个数轴上有 \(N\) 个房子,第 \(i\) 个房子位于坐标 \(X_i\) 处。可能存在某些房子位于相同的坐标。
你可以在数轴上任意选择 \(M\) 个位置放置基站,并且为每个基站都设置一个非负的信号强度。
如果一个基站被放在坐标 \(p\) 处,并且信号强度设置为 \(x\),那么区间 \([p - \dfrac x 2, p + \dfrac x 2]\) 内的所有房子就都能够收到这个基站的信号。(也就是说信号范围是以 \(p\) 为中心点的长度为 \(x\) 的一段区间。)
你的任务是让每个房子都能够至少收到一个基站的信号。问在满足这个条件的前提下,所有基站的信号强度总和的最小值是多少?
思路
我们假设一开始只能放一个基站,为了让信号强度最小,这个基站的信号范围区间的左端点一定是恰好位于坐标最小的房子所在位置,右端点一定是恰好位于坐标最大的房子所在位置,信号强度就是这两座房子的距离。
也就是说我们可以先对房子的坐标进行排序,记 \(X_i\) 表示排序后坐标第 \(i\) 小的房子所在位置,那么在只能放一个基站的情况下,答案即 \(X_N - X_1\)。
然后我们考虑多放一个基站,也就是可以选择两段区间。为了让信号强度总和最小,我们肯定会让两段区间不产生交叉。但由于又需要让两段区间覆盖每个房子的所在位置,所以此时可以选择去掉某两个相邻房子之间的一段位置,让这两段区间不去覆盖这两个房子之间的这些位置。
也就是说,如果我们选择不覆盖 \(X_p\) 和 \(X_{p+1}\) 这两个房子之间的位置,那么此时两个基站覆盖的区间应该是 \([X_1, X_p]\) 和 \([X_{p+1}, X_N]\) 这两段。为了让这两段区间覆盖的总长度最小,所以我们肯定会选择隔得最开的两个相邻的房子,也就是需要在 \(i \in [1, N-1]\) 中找出最大的 \(X_{i+1} - X_i\),然后将这个最大距离从答案中减去。
如果再考虑多放一个基站,明显我们又可以选择一段最大且还没被选择过的两个相邻房子之间的间隔,。
因为我们总共可以放 \(M\) 个基站,所以也就是可以在 \(X_N-X_1\) 的基础上,再找出前 \(M-1\) 大的 \(X_{i+1} - X_i\) 将其从答案中减去。
所以接下来只需要再求出 \(D_i = X_{i+1} - X_i\) 所对应的 \(D\) 数组,排序后取前 \(M-1\) 大的总和即可。
时间复杂度 \(O(N \log N)\)。
代码
long long x[500005];
long long d[500005];
void solve()
{
int n, m;
cin >> n >> m;
for(int i = 1; i <= n; i++)
cin >> x[i];
sort(x + 1, x + n + 1);
long long ans = x[n] - x[1]; // 只能设置一个基站时
for(int i = 1; i < n; i++)
d[i] = x[i + 1] - x[i];
sort(d + 1, d + n);
// 在答案基础上可以多放置 m-1 个基站
for(int i = n - 1; i >= n - m + 1; i--) // 取前 m-1 大的数
ans -= d[i];
cout << ans;
}
E - Count A%B=C
题意
给定一个正整数 \(N\) \((N \le 10^{12})\),问有多少对三元组 \((a, b, c)\) 满足以下条件,输出数量并对 \(998\,244\,353\) 取模:
- \(1 \le a, b, c \le N\)
- \(a, b, c\) 互不相同
- \(a \bmod b = c\)
思路
考虑 \(a \bmod b = c\) 这个等式。
我们先假设固定除数 \(b\),那么对于每个被除数 \(a \in [1, n]\) 一定都只有唯一的整数 \(c\) 满足上述等式,所以可以得到 \(N\) 个不同的等式。
当除数为 \(b\) 时,余数的范围一定是在 \([0, c-1]\) 之内,所以可以发现除数 \(b\) 一定不等于余数 \(c\)。
接下来讨论 \(a\) 与 \(b\) 的关系:
- 当 \(a \lt b\) 时,此时被除数 \(a\) 和余数 \(c\) 一定是相同的,不符合条件,这部分共有 \(b-1\) 种情况。
- 当 \(a = b\) 时,此时 \(a = b\) 且 \(c = 0\),不符合条件。
- 当 \(a \gt b\) 时,此时三个整数一定是互不相同的,但可能会在 \(a\) 是 \(b\) 的倍数时出现 \(c = 0\) 的情况,这是不符合条件的。这部分可以和上面的 \(a = b\) 放在一起,情况数即 \([1, N]\) 以内 \(b\) 的倍数数量,即 \(\lfloor \dfrac N b \rfloor\) 种。
综上,答案即:
但 \(N\) 非常大,直接 for 循环枚举 \(b\) 不现实,所以我们把上式分为三部分:
- \(\sum\limits_{b=1}^N N\),该部分答案即 \(N^2\)。
- \(\sum\limits_{b=1}^N (b-1)\),该部分答案即 \(\sum\limits_{b=0}^{N-1} b = \dfrac{N(N-1)}{2}\)。
- \(\sum\limits_{b=1}^N \lfloor \dfrac N b \rfloor\),此时被除数固定且涉及向下取整操作,可以借助数论分块算法在 \(O(\sqrt N)\) 范围内完成求解。
时间复杂度 \(O(\sqrt N)\)。
代码
typedef long long ll;
const ll mod = 998244353;
const ll inv2 = (mod + 1) / 2; // 2 的逆元
void solve()
{
ll n;
cin >> n;
// 务必注意不要让 n 直接乘进式子里
ll ans = (n % mod) * (n % mod) % mod; // 第一部分
ans -= ((n - 1) % mod) * (n % mod) % mod *inv2 % mod; // 第二部分
ans = (ans + mod) % mod; // 减法注意负数情况
for(ll i = 1; i <= n; )
{
ll t = n / i;
ll j = n / t; // 满足 n / j = n / i 的最大整数 j
// 当题意内的整数 b 在区间 [i, j] 内,此时 N/b 的答案固定
ans -= ((j - i + 1) % mod) * (t % mod) % mod; // 第三部分
ans = (ans + mod) % mod; // 减法注意负数情况
i = j + 1; // 跳过 [i, j] 这一段
}
cout << ans;
}
F - Jump Traveling
题意
给定一棵包含 \(N\) 个点的树以及一个正整数 \(K\)。树上每条边的长度均为 \(1\)。
一开始你站在点 \(1\) 上,每次可以选择移动到与当前点的距离恰好为 \(K\) 的任意一个点上。
对于 \(2 \sim N\) 中的每个点,请判断到达每个点的最少移动次数,或者判断不可能到达。
思路
考虑借助 SPFA 等最短路算法对图进行搜索以求解到达每个点的最短路。
搜索过程记录从起点出发走到当前点的总步数。只有当总步数恰好为 \(K\) 的倍数时,才可以说明当前搜索到的这个点是可到达的。
首先要注意一个问题:在实际移动的过程中,某些点是可以被重复经过的。所以我们不能够直接限制每个点只能被走过一次。但这样可能又会出现一个问题,就是从某个点出发,在搜索下一个点的过程中又往回走的情况。例如 \(K = 3\) 时按 \(1 \rightarrow 2 \rightarrow 1 \rightarrow 2\) 这样移动,明显这样的移动的起点与终点的距离不为 \(K\)。
所以在实际搜索的过程中,我们需要再记录一个状态,表示走到当前点时上一个点的编号,以此来控制在单次搜 \(K\) 步的过程中不往回走。只有当总步数恰好为 \(K\) 的倍数时,我们才可以认为某次移动结束了,这时候可以考虑往回走,不用受到上一个点的限制。
然后从起点 \(1\) 出发,开始搜索。每当走到一个点上且总步数恰好为 \(K\) 的倍数时,如果这个点是第一次出现这种情况,直接记录当前总步数 \(\div K\) 作为该点答案。
但如果只是这样搜索的话是会超时的,接下来考虑优化。
第一,对于每个点来说,虽然每个点并不是只会被经过一次,但只要结合上到达该点的总步数 \(\bmod K\) 的值,就可以发现每个点其实最多只会被经过两次,其中第二次是因为要考虑到上一次以相同状态到达该点时,不能走回头路的情况。
第二,对于每条边来说,同样也要考虑经过这条边时总步数 \(\bmod K\) 的值。与上一步讨论点最多经过次数的原理相同,因为可能第一次以相同状态经过一条边时,某次移动还没进行完,不能够走回头路,因此之后可能还会倒回来以相同状态重新经过一次。因此每条边在结合上到达该边的总步数 \(\bmod K\) 的值之后,可以发现其最多被经过两次,且正反两个方向分别只能最多经过一次。
对于上面的两步优化方法,第一步优化可以直接开一个计数数组 cntNode[i][j]
统计以总步数 \(\bmod K = j\) 的状态访问 \(i\) 这个点的次数,只要超过 \(2\) 次就直接跳过即可;第二步优化可以考虑为每条边分配两个编号,正反各一个编号,然后也用一个计数数组 visEdge[i]
来标记每条边是否已经经过即可。
时间复杂度为 \(O((N + M)\cdot K)\)。
代码
struct point
{
int id; // 当前点
int pre; // 当前点的上一个点 如果不需要判断回头则记作 0
int step; // 从起点开始的总步数
};
struct edge
{
int to; // 下一个点
int id; // 这条边的编号
};
int N, K;
vector<edge> G[200005];
int ans[200005]; // 答案
int cntNode[200005][20];
// cntNode[i][j] 记录到达 i 点且 总步数%K=j 的次数
bool visEdge[400005][20];
// visEdge[i][j] 表示经过 i 边且 总步数%K=j 的情况是否已经出现过
void solve()
{
cin >> N >> K;
for(int i = 1; i <= N; i++) // 多组数据清空
{
G[i].clear();
for(int j = 0; j < K; j++)
{
cntNode[i][j] = 0;
visEdge[i * 2 - 1][j] = 0;
visEdge[i * 2][j] = 0;
}
}
for(int i = 1; i < N; i++)
{
int u, v;
cin >> u >> v;
G[u].push_back(edge{v, i * 2 - 1}); // 给每条边分配编号
G[v].push_back(edge{u, i * 2});
}
memset(ans, -1, sizeof ans);
queue<point> q;
q.push(point{1, 0, 0});
while(!q.empty())
{
point p = q.front();
q.pop();
if(p.step % K == 0 && ans[p.id] == -1)
ans[p.id] = p.step / K; // 第一次在总步数为 K 的倍数时访问到该点,记录答案
if(cntNode[p.id][p.step % K] == 2)
continue; // 每个点当 步数%K 值相同时,只会被访问至多两次
cntNode[p.id][p.step % K]++;
for(edge &e : G[p.id])
{
if(p.step % K != 0 && e.to == p.pre)
continue; // 还没恰好到 K 步,只能单向行走,不能回头
if(visEdge[e.id][(p.step + 1) % K] == true)
continue; // 每条边当 步数%K 值相同时 只会被经过一次
visEdge[e.id][(p.step + 1) % K] = true;
q.push(point{e.to, p.id, p.step + 1});
}
}
for(int i = 2; i <= N; i++)
cout << ans[i] << " ";
cout << "\n";
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
int T;
cin >> T;
while(T--)
solve();
return 0;
}