AtCoder Beginner Contest 355
A - Who Ate the Cake? (abc355 A)
题目大意
三人有偷吃蛋糕的嫌疑,现告诉两个目击证人的澄清对象。问谁偷吃蛋糕。
解题思路
两个对象相同就不知道是谁,否则就是剩下的那一个了。
神奇的代码
#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;
if (a == b)
cout << "-1" << '\n';
else
cout << 1 + 2 + 3 - a - b << '\n';
return 0;
}
B - Piano 2 (abc355 B)
题目大意
给定两个数组\(a,b\),定义数组 \(c\),为数组 \(a,b\)拼接后排序。
问\(c\)中相邻两个数,均在数组 \(a\)中出现的数量。
解题思路
预处理出\(exist[i]\)表示数字 \(i\)是否在数组 \(a\)出现过,然后枚举 \(c\)的相邻数判断即可。
神奇的代码
#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<int> a(n), b(m);
for (auto& i : a)
cin >> i;
for (auto& i : b)
cin >> i;
vector<int> c = a;
c.insert(c.end(), b.begin(), b.end());
sort(c.begin(), c.end());
vector<int> ina(*max_element(a.begin(), a.end()) + 1, 0);
for (auto i : a)
ina[i] = 1;
bool ok = false;
for (int i = 0; i < n + m - 1; ++i) {
ok |= ina[c[i]] && ina[c[i + 1]];
}
if (ok)
cout << "Yes" << endl;
else
cout << "No" << endl;
return 0;
}
C - Bingo 2 (abc355 C)
题目大意
二维网格,有\(t\)次操作,每次操作涂黑一个格子。
问多少操作后,有一行或一列或对角线的格子全部被涂黑。
解题思路
分别记录每行、每列、对角线中被涂黑的格子数量\(row[i], col[i], diag1, diag2\),每次操作后检查该格子对应的行列对角线的黑格子数是否是 \(n\)即可。
神奇的代码
#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, t;
cin >> n >> t;
vector<int> row(n), col(n);
int diag1 = 0, diag2 = 0;
int ans = -1;
auto tr = [&](int x) -> pair<int, int> { return {x / n, x % n}; };
for (int i = 0; i < t; i++) {
int x;
cin >> x;
--x;
auto [r, c] = tr(x);
row[r]++;
col[c]++;
if (r + c == n - 1)
diag1++;
if (r == c)
diag2++;
if (row[r] == n || col[c] == n || diag1 == n || diag2 == n) {
ans = i + 1;
break;
}
}
cout << ans << '\n';
return 0;
}
D - Intersecting Intervals (abc355 D)
题目大意
给定\(n\)个区间\([l_i, r_i]\),问俩俩区间有重叠的数量。交点重叠也算重叠。
解题思路
将这些区间按照左端点排序,然后依次枚举区间,考虑如何计算重叠数量\(cnt_j\)。
假设当前枚举的第\(j\)个区间,我要求 \(i < j\)的数量,满足第 \(i\)个区间与第 \(j\)个区间有重叠。
考虑满足什么条件算重叠,由于\(l_i \leq l_j\),所以只要\(r_i \geq l_j\),那么第 \(i\)个区间就与第 \(j\)个区间重叠。
因此\(cnt_j\)就是满足 \(i < j, r_i \geq l_j\)的数量。
这是一个二维偏序问题,但与之前不同的是条件\(l_j\)是单调递增的。因此我们可以维护一个小根堆的优先队列,将 \(i < j\)的 \(r_i\)丢进去, 一旦堆顶的\(r_i < l_j\),说明不重叠,之后也不会重叠,就丢弃。 \(cnt_j\)就是优先队列里的元素个数。
神奇的代码
#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, 2>> a(n);
for (auto& x : a) {
cin >> x[0] >> x[1];
}
sort(a.begin(), a.end());
priority_queue<int, vector<int>, greater<int>> team;
LL ans = 0;
for (int i = 0; i < n; i++) {
while (!team.empty() && team.top() < a[i][0]) {
team.pop();
}
ans += team.size();
team.push(a[i][1]);
}
cout << ans << '\n';
return 0;
}
E - Guess the Sum (abc355 E)
题目大意
交互题。
有一个长度为\(2^n\)的隐藏数组\(a\),你需要用最少的询问,问出 \(\sum_{i=l}^{r} a_i\)的和。对 \(100\)取模。
每次询问给定\(i,j\),回答 \(l=2^ij, r = 2^i(j+1),\sum_{i=l}^{r-1} a_i\)的和,对 \(100\)取模。
解题思路
一开始参考abc349d,结果\(wa\)了,应该是询问的次数不是最少。
可以考虑一反例,询问 \([1,7]\),如果按照上面的思路,则是询问 \([1,1] + [2, 3] + [4,7]\) 共三个询问,即\((0, 1)(1,1)(2,1)\)。
而实际上可以做到两个:询问\([0,7] - [0, 0]\) 。即\((3,0)(0,0)\)
因此此处除了区间加,还有可能区间减。换句话说就像是先退一步(\(1 \to 0\)),才能走的更远 (\(0 \to 7\))。
由此我们可以假想在一张有\(2^n\)个点的无向图上,我们从点\(l\)出发,花最小的步数到达点 \(r+1\)。点与点之间通过询问连边。比如我们可以询问 \((3,0)\),即区间 \([0,8)\),因此点\(0\)和点 \(8\)就连一条无向边。而询问数只有 \(O(n2^n)\),也即边数,点数有 \(O(2^n)\),边权均为 \(1\)。因此在该图上直接从 点\(l\)进行一次 \(BFS\),记录转移点,然后询问即可。
按上面的为例,我们从点 \(1\)出发,最终到达点\(8\),一个最短的方案是 \(1 \to 0 \to 8\),第一条就是询问 \([0,1)\),要减去该值,第二条就是询问 \([0,8)\),要加上该值。
神奇的代码
#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, l, r;
cin >> n >> l >> r;
int up = (1 << n);
vector<vector<int>> edge(up + 1);
for (int i = 0; i <= n; ++i) {
for (int l = 0; l < up; l += (1 << i)) {
int r = l + (1 << i);
edge[l].push_back(r);
edge[r].push_back(l);
}
}
queue<int> team;
vector<int> pre(up + 1, -1);
pre[l] = 0;
team.push(l);
while (!team.empty()) {
int cur = team.front();
team.pop();
if (cur == r + 1) {
break;
}
for (int next : edge[cur]) {
if (pre[next] == -1) {
pre[next] = cur;
team.push(next);
}
}
}
vector<array<int, 2>> query;
for (int i = r + 1; i != l; i = pre[i]) {
query.push_back({pre[i], i});
}
reverse(query.begin(), query.end());
int ans = 0;
for (auto& [a, b] : query) {
int sign = 1;
if (a > b) {
sign = -1;
swap(a, b);
}
int i = __builtin_ctz(b - a);
int j = a >> i;
cout << "? " << i << " " << j << endl;
int sum;
cin >> sum;
ans += sum * sign;
}
ans = (ans % 100 + 100) % 100;
cout << "! " << ans << endl;
return 0;
}
F - MST Query (abc355 F)
题目大意
给定一棵树,边有边权,不断加边,求每次加边后的最小生成树的边权和。
边权$ \leq 10$。
解题思路
当给树加了一条边,此时就出现了个环,而只要去除环上边权最大的边,就又变回一棵树,同时也是一个最小生成树。
因此我们要动态维护一棵树,维护俩点之间的边权最大值,这是Link-Cut Tree所擅长的——动态连边、断边、快速求出一条路径的权值最大值。
LCT
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const int maxn = 1e6;
const int inf = 1e9 + 7;
struct Splay {
int ch[maxn][2], fa[maxn], tag[maxn], val[maxn], maxx[maxn];
void clear(int x) {
ch[x][0] = ch[x][1] = fa[x] = tag[x] = val[x] = maxx[x] = 0;
}
int getch(int x) { return ch[fa[x]][1] == x; }
int isroot(int x) { return ch[fa[x]][0] != x && ch[fa[x]][1] != x; }
void maintain(int x) {
if (!x)
return;
maxx[x] = x;
if (ch[x][0]) {
if (val[maxx[ch[x][0]]] > val[maxx[x]])
maxx[x] = maxx[ch[x][0]];
}
if (ch[x][1]) {
if (val[maxx[ch[x][1]]] > val[maxx[x]])
maxx[x] = maxx[ch[x][1]];
}
}
void pushdown(int x) {
if (tag[x]) {
if (ch[x][0])
tag[ch[x][0]] ^= 1, swap(ch[ch[x][0]][0], ch[ch[x][0]][1]);
if (ch[x][1])
tag[ch[x][1]] ^= 1, swap(ch[ch[x][1]][0], ch[ch[x][1]][1]);
tag[x] = 0;
}
}
void update(int x) {
if (!isroot(x))
update(fa[x]);
pushdown(x);
}
void print(int x) {
if (!x)
return;
pushdown(x);
print(ch[x][0]);
printf("%d ", x);
print(ch[x][1]);
}
void rotate(int x) {
int y = fa[x], z = fa[y], chx = getch(x), chy = getch(y);
fa[x] = z;
if (!isroot(y))
ch[z][chy] = x;
ch[y][chx] = ch[x][chx ^ 1];
fa[ch[x][chx ^ 1]] = y;
ch[x][chx ^ 1] = y;
fa[y] = x;
maintain(y);
maintain(x);
if (z)
maintain(z);
}
void splay(int x) {
update(x);
for (int f = fa[x]; f = fa[x], !isroot(x); rotate(x))
if (!isroot(f))
rotate(getch(x) == getch(f) ? f : x);
}
void access(int x) {
for (int f = 0; x; f = x, x = fa[x])
splay(x), ch[x][1] = f, maintain(x);
}
void makeroot(int x) {
access(x);
splay(x);
tag[x] ^= 1;
swap(ch[x][0], ch[x][1]);
}
int find(int x) {
access(x);
splay(x);
while (ch[x][0])
x = ch[x][0];
splay(x);
return x;
}
void link(int x, int y) {
makeroot(x);
fa[x] = y;
}
void cut(int x, int y) {
makeroot(x);
access(y);
splay(y);
ch[y][0] = fa[x] = 0;
maintain(y);
}
} st;
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n, q;
cin >> n >> q;
int ans = 0;
for (int i = 1; i <= n; ++i)
st.val[i] = -inf, st.maintain(i);
int id = n + 1;
vector<array<int, 2>> edge;
for (int i = 0; i < n - 1; ++i) {
int u, v, w;
cin >> u >> v >> w;
st.val[id] = w;
st.maintain(id);
st.link(u, id);
st.link(id, v);
edge.push_back({u, v});
++id;
ans += w;
}
while (q--) {
int u, v, w;
cin >> u >> v >> w;
edge.push_back({u, v});
st.val[id] = w;
st.maintain(id);
st.makeroot(u);
st.access(v);
st.splay(v);
int x = st.maxx[v];
int maxx = st.val[x];
auto [l, r] = edge[x - n - 1];
if (w < maxx) {
st.cut(l, x);
st.cut(x, r);
st.link(u, id);
st.link(id, v);
ans -= maxx;
ans += w;
}
++id;
cout << ans << '\n';
}
return 0;
}
不过这题有个特别的地方就是边权\(\leq 10\),会有不需要 \(lct\)的做法。
由于边权个数只有\(10\),我们可以考虑每个边权选择的数量之类的,这样可以算出生成树边权,但删边的话无从下手,因为不知道加边的两个点之间边权。
或许可以维护边权为 \(i\)的边所形成的连通性, 这样加一条边权为\(i\)时,看有没有边权 \(> i\)的使得那两点连通,从而决定删除哪条边。但会发现不好维护,边权为 \(i\)的边所形成的连通性会依赖于边权\(< i\)的选择结果。
为了不依赖其他情况,我们可以维护边权 \(\leq i\)的边所形成的连通性情况,这用一个并查集\(d_i\)维护,这样我们就建立 \(10\)个这样的并查集,并查集之间的依赖关系就没有了。
每加一条边就只用更新这\(O(10)\)个并查集的连通性,而如何计算最小生成树的边权呢?关键是求出每个边权的使用个数。
由于每加一条边,连通块的个数就会少一。要求出边权为\(i\)使用的个数,那就看 \(d_i\)维护的连通块数量与\(d_{i-1}\)的连通块数量,两者的差值就是边权为\(i\)的使用个数。
这样求答案的时间也是 \(O(10)\)。
神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
class dsu {
public:
vector<int> p;
int n;
int block;
dsu(int _n) : n(_n), block(_n) {
p.resize(n);
iota(p.begin(), p.end(), 0);
}
inline int get(int x) { return (x == p[x] ? x : (p[x] = get(p[x]))); }
inline bool unite(int x, int y) {
x = get(x);
y = get(y);
if (x != y) {
p[x] = y;
--block;
return true;
}
return false;
}
};
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n, q;
cin >> n >> q;
vector<dsu> d(10, dsu(n));
vector<array<int, 3>> edge(n - 1);
for (auto& [u, v, w] : edge) {
cin >> u >> v >> w;
--u;
--v;
}
sort(edge.begin(), edge.end(),
[](auto& a, auto& b) { return a[2] < b[2]; });
for (int i = 0; i < 10; ++i) {
int up = i + 1;
auto& di = d[i];
for (auto& [u, v, w] : edge) {
if (w > up)
break;
di.unite(u, v);
}
}
while (q--) {
int u, v, w;
cin >> u >> v >> w;
--u, --v, --w;
for (int i = w; i < 10; ++i) {
d[i].unite(u, v);
}
int ans = 0, la = 0;
for (int i = 0; i < 10; ++i) {
int use = n - d[i].block;
ans += (use - la) * (i + 1);
la = use;
}
cout << ans << '\n';
}
return 0;
}
G - Baseball (abc355 G)
题目大意
给定一个关于\(n\)的排列\(p\),高桥从中取\(k\)个数\(x_i\)。青木则以一定概率取一个数\(y \in [1,n]\),取的\(y=i\)的概率是 \(\frac{p_i}{\sum p_j}\)。
青木的分数即为\(\min |x_i - y|\)。
求高桥的最优策略,使得青木的期望分数最小。输出对应的最小分数的\(\sum p_j\)倍。
解题思路
<++>
神奇的代码