AtCoder Beginner Contest 309
感觉F写了个乱搞做法
A - Nine (abc309 A)
题目大意
给定一个\(3 \times 3\)的网格,以及两个数字。
问这两个数字是否水平相邻。
解题思路
求出两个数字的横纵坐标,看是否横坐标相同,纵坐标差一即可。
读题不仔细,开题就WA了。
神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
int a, b;
cin >> a >> b;
-- a, -- b;
if (abs(a % 3 - b % 3) == 1 && abs(a / 3 - b / 3) == 0)
cout << "Yes" << '\n';
else
cout << "No" << '\n';
return 0;
}
B - Rotate (abc309 B)
题目大意
给定一个矩形。将最外围的数字顺时针旋转一格。
解题思路
可以模拟一个指针跑,也可以分别考虑四个边界的转移公式。
神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
int n;
cin >> n;
vector<string> a(n);
for(auto &i : a)
cin >> i;
auto ans = a;
for(int i = 0; i < n; ++ i){
ans[0][i] = i ? a[0][i - 1]: a[1][0];
ans[i][0] = i == n - 1 ? a[n - 1][1] : a[i + 1][0];
ans[n - 1][i] = i == n - 1 ? a[n - 2][n - 1] : a[n - 1][i + 1];
ans[i][n - 1] = i ? a[i - 1][n - 1] : a[0][n - 2];
}
for(auto &i : ans){
cout << i;
cout << '\n';
}
return 0;
}
C - Medicine (abc309 C)
题目大意
给定\(n\)种药,第 \(i\)种药吃 \(a_i\)天,每天 \(b_i\)粒。
问最早到第几天,吃的药的粒数 \(\leq K\)。
解题思路
我们可以从小到大枚举天数,直到某天吃的药的粒数\(\leq K\),但天数高达\(O(10^9)\),直接枚举会超时。
但是我们会发现,每天的吃的药的粒数会越来越少,因此每天吃的药的粒数和天数之间有单调性。
于是我们可以二分天数,判断这一天吃的药的粒数是否\(\leq K\),进而排除一半的答案,搜索另一片。
神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
int n, k;
cin >> n >> k;
vector<array<int, 2>> a(n);
for(auto &i : a)
cin >> i[0] >> i[1];
int l = 0, r = 1e9 + 7;
auto check = [&](int x){
LL tot = 0;
for(auto &i : a){
tot += i[1] * (i[0] >= x);
}
return tot > k;
};
while(l + 1 < r){
int mid = (l + r) >> 1;
if (check(mid))
l = mid;
else
r = mid;
}
cout << r << '\n';
return 0;
}
D - Add One Edge (abc309 D)
题目大意
给定两个连通块,现在从这两个连通块中各选一个点,连一条边,问\(1\)号点和 \(n_1+n_2\)号点的最小距离的最大值。
解题思路
很显然,从\(1\)号点到 \(n_1+n_2\)号点,其路径是 \(1 \to l \to r \to n_1 + n_2\)。其中\(l\)号点在第一个连通块,\(r\)号点在第二个联通块,是我们选择连边的两个点,其中 \(l \to r\)距离是 \(1\),最大化总距离,那就是最大化 \(1 \to l\)和 \(r \to n_1 + n_2\)。
那就分别从\(1\)号点和 \(n_1 + n_2\)号点分别 \(BFS\),找到最远的距离的点,连边即是答案。
神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const int inf = 1e9 + 7;
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
int n1, n2, m;
cin >> n1 >> n2 >> m;
vector<array<int, 2>> edges(m);
vector<vector<int>> e(n1 + n2);
for(int i = 0; i < m; ++ i){
int u, v;
cin >> u >> v;
-- u, -- v;
edges[i] = {u, v};
e[u].push_back(i);
e[v].push_back(i);
}
vector<int> dis(n1 + n2, inf);
auto BFS = [&](int st){
dis[st] = 0;
queue<int> team;
team.push(st);
while(!team.empty()){
int u = team.front();
team.pop();
for(auto &i : e[u]){
int v = u ^ edges[i][0] ^ edges[i][1];
if (dis[v] > dis[u] + 1){
dis[v] = dis[u] + 1;
team.push(v);
}
}
}
};
BFS(0);
BFS(n1 + n2 - 1);
cout << *max_element(dis.begin(), dis.begin() + n1) + *max_element(dis.begin() + n1, dis.end()) + 1 << '\n';
return 0;
}
E - Family and Insurance (abc309 E)
题目大意
给定一棵(?或者若干棵)树,以及\(m\)次操作。
每次操作将以\(x_i\)号点的子树,到根距离不超过\(y_i\) 的点都加一。
问有多少个点至少加了一。
解题思路
如果将问题简化成一条链,那很显然就是个区间加问题,差分一下,每次操作就变成一个点加一,一个点减一,最后再求一遍前缀和还原数组就解决了。
但本题是树,与链的区别是,每次操作是一个点加一,很多个点减一(并且减一的点不能很快找到)。
但考虑对这棵树\(DFS\),在某一刻,我们维护的信息其实就是当前点到根的这条链,于是问题貌似就解决了?
在\(DFS\)时,实时维护上述的差分数组
和该差分数组的和
(即在搜索到该点时才考虑对该点操作的影响,影响都是对后续的影响),回溯的时候消除对该点操作影响到的差分数组
和和
即可。
神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
int n, m;
cin >> n >> m;
vector<vector<int>> edge(n);
for(int i = 1; i < n; ++ i){
int v;
cin >> v;
-- v;
edge[v].push_back(i);
}
int ans = 0;
vector<int> visit(n, false);
vector<int> diff(n + 1, 0);
int sum = 0;
vector<int> ins(n, 0);
for(int i = 0; i < m; ++ i){
int x, y;
cin >> x >> y;
-- x;
ins[x] = max(ins[x], y);
}
function<void(int, int)> dfs = [&](int u, int deep){
visit[u] = 1;
if (ins[u]){
++ sum;
diff[min(n, deep + ins[u] + 1)] -= 1;
}
sum += diff[deep];
if (sum)
++ ans;
for(auto &v : edge[u]){
dfs(v, deep + 1);
}
sum -= diff[deep];
if (ins[u]){
-- sum;
diff[min(n, deep + ins[u] + 1)] += 1;
}
};
for(int i = 0; i < n; ++ i){
if (!visit[i])
dfs(i, 0);
}
cout << ans << '\n';
return 0;
}
也可以用\(dp\),设 \(dp[i]\)表示从该节点往下,保险还能有效多少代子孙,很显然从父亲转移过来即可。因此从根直接 \(DFS\)求该 \(dp\)值即可。
F - Box in Box (abc309 F)
题目大意
给定\(n\)个三元组,可以交换三元组的元素的顺序。
问是否存在两个三元组,其一个三元组的三个元素都严格大于另一个三元组的三个元素。
解题思路
考虑二元组的做法。
如果不能交换,这就是一个典型的二维偏序问题。
如果能交换,枚举一下交换情况,可以发现在比较的时候,希望让最大值之间比较,最小值之间比较,而不是让一个二元组的最大值和一个二元组的最小值比较。
因此我们将二元组的元素排个序,小的在左,大的在右,然后再按第一位排序,此时可以发现用双指针法就解决了,当然也可以用二维偏序的解法比如树状数组解决。
从左到右枚举枚举一个二元组\(a_i\),然后找到第一个 \(a_j[0] > a_i[0]\),此时如果 \(\max_{k \geq j} a_k[1] > a_i[1]\),那就存在两个二元组满足上述条件了。而\(\max_{k \geq j} a_k[1]\)就是一个后缀最大值,事先预处理一下就可以 \(O(1)\)查询。
二维的解决了,考虑三维。
二维偏序转到三维偏序的一个做法是\(CDQ\)分治(然后花了几分钟学了下\(CDQ\)分治)。注意二维偏序的做法是保证一维有序的情况下处理另一维,而三维的话就是要保证两维都有序的情况下处理第三维。
如何保证两维都有序呢?首先我们对第一维排个序,这样就保证了一维有序,还有一维如何保证?如果我们对整个区间的第二维排序,那么第一维有序就不一定了。
能否不对整个区间排序?而取部分的区间?
首先对于一个区间\([l, r]\)是否存在答案,根据分治,答案的分布有三种可能,要么两个三元组都在 \([l, mid]\),要么都在 \([mid + 1, r]\),要么一个在左区间,一个在右区间,
前两种可能我们可以递归解决,对于第三种可能,我们就要找到两个区间的三元组的偏序关系,由于第一维事先排好序了,因此在总会有 \(a_i \leq a_j, i \in [l, mid], j \in [mid + 1, r]\) ,因此第一维可以忽略,此时就剩下第二维和第三维,问题就变成上述的二维问题,那么我们可以分别对\([l, mid],[mid + 1, r]\)的第二维排序,然后用上述提到的双指针法和后缀最大值解决。
但还有个问题,注意我们的第一维的关系是\(\leq\),而不是题目要求的严格小于,因此此时的第一维实际上不能忽略。万一第一维相等就不能成为答案,但由于我们对第二维排了序,因此第一维的大小是乱序的,似乎就没有什么规律。
细想一下,在之前的\(CDQ\)分治统计不严格偏序数量时,答案的正确性在于第一维关系是\(\leq\),符合题目要求,就可以忽略这一维,而此处的 \(\leq\)并不符合题目要求,不能忽略第一维,如果强行忽略,答案可能会不正确(考虑答案的范围就扩大成\(a_i[0] = a_j[0], a_i[1] < a_j[1], a_i[2] < a_j[2]\)),而如果能保证 \(<\),那么答案的正确性就能保证。为了保证有\(<\),考虑能否可以将\(mid\)设为 \(a_{mid + 1}[0] > a_{mid}[0]\)的地方?
但注意到, \(CDQ\)分治的时间复杂度是 \(O(n\log^2 n)\),其每次分治时的\(mid = \frac{l + r}{2}\) 是一个保证复杂度的关键之处,这样才保证每次分治处理的复杂度是\(f(n) + f(\frac{n}{2}) + f(\frac{n}{2}) + ...\),而每次分治因为涉及到排序,因此 \(f(n) = n\log n\) 。
为了找一个\(mid\)使得 \(a_{mid + 1}[0] > a_{mid}[0]\),同时又要保证复杂度,可以大胆猜想选择\(a_{mid}[0]\)的数的最左边或最右边(因为对第一维排了序,相同数都在一起,因此分界点就从中间位置左右偏移一下,找到第一个\(\neq a_{mid}[0]\)之处作为分治的分界点。虽然每次分治的区间大小不一定减半,但从值域上感觉会减半,并大胆猜想出题人不会卡,于是按此写了写发现过了。
如何找到从中间位置左右偏移的点,对第一维排序后预处理一下每个第一维值的最小下标和最大下标即可。
神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
int n;
cin >> n;
vector<array<int, 3>> a(n);
for(auto &i : a){
for(auto &j : i)
cin >> j;
sort(i.begin(), i.end());
}
sort(a.begin(), a.end(), [&](const auto& x, const auto& y){
return x[0] < y[0];
});
map<int, int> posl, posr;
for(int i = n - 1; i >= 0; -- i){
posl[a[i][0]] = i;
}
for(int i = 0; i < n; ++ i){
posr[a[i][0]] = i;
}
bool ok = 0;
function<void(int, int)> solve = [&](int l, int r){
if (l >= r - 1)
return;
int mid = posl[a[(l + r) >> 1][0]];
if (mid <= l)
mid = posr[a[(l + r) >> 1][0]] + 1;
if (mid >= r)
return;
solve(l, mid);
solve(mid, r);
sort(a.begin() + l, a.begin() + mid, [&](const auto& x, const auto&y){
return x[1] < y[1];
});
sort(a.begin() + mid, a.begin() + r, [&](const auto& x, const auto&y){
return x[1] < y[1];
});
vector<int> suf(r - l);
for(int i = r - 1; i >= l; -- i){
suf[i - l] = a[i][2];
if (i < r - 1)
suf[i - l] = max(suf[i - l], suf[i - l + 1]);
}
int pos = mid;
for(int i = l; i < mid; ++ i){
while(pos < r && a[pos][1] <= a[i][1])
++ pos;
if (pos == r)
break;
if (suf[pos - l] > a[i][2])
ok = 1;
}
};
solve(0, n);
if (ok)
cout << "Yes" << '\n';
else
cout << "No" << '\n';
return 0;
}
G - Ban Permutation (abc309 G)
题目大意
问关于\(n\)的排列的数量,满足 \(\forall i \in [1,n], |p_i - i| \geq x\)。
解题思路
<++>
神奇的代码
Ex - Simple Path Counting Problem (abc309 Ex)
题目大意
<++>
解题思路
<++>
神奇的代码