Thank you for participating in my round! I hope you all enjoyed it.
What does the speed of each gear depend on?
Suppose we have some arrangement of gears where gear $$$i$$$ from the left has $$$b_i$$$ teeth. Then the speed of gear $$$n$$$ is $$$1 \cdot \frac{b_1}{b_2} \cdot \frac{b_2}{b_3} \cdot \ldots \cdot \frac{b_{n-1}}{b_n}$$$. Notice that most of the fractions cancel, leaving only $$$\frac{b_1}{b_n}$$$. So in fact, the speed of gear $$$n$$$ depends only on $$$b_n$$$ and $$$b_1$$$.
Therefore if we want the rightmost gear to have the same speed as the leftmost gear, we need to arrange them in such a way that $$$\frac{b_1}{b_n} = 1$$$, so $$$b_1 = b_n$$$. This is possible if and only if there are two gears with the same number of teeth. So we just need to find if there are any duplicated values in the given array. This can be checked in $$$\mathcal{O}(n\,\text{log}\,n)$$$ using a set, although the constraints are low enough to allow checking all pairs in $$$\mathcal{O}(n^2)$$$ as well.
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define all(x) x.begin(), x.end()
#define vecin(name, len) vector<int> name(len); for (auto &_ : name) cin >> _;
#define vecout(v) for (auto _ : v) cout << _ << " "; cout << endl;
void solve() {
int n; cin >> n;
vecin(gears, n);
set<int> seen;
for (auto gear : gears) {
if (seen.count(gear)) {
cout << "YES" << endl; return;
}
seen.insert(gear);
}
cout << "NO" << endl;
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int tt = 1;
cin >> tt;
while (tt--) solve();
return 0;
}
tt = int(input())
for t in range(tt):
n = int(input())
gears = list(map(int, input().split()))
valid = False
for i in range(n):
for j in range(i + 1, n):
if gears[i] == gears[j]:
valid = True
if valid:
print("YES")
else:
print("NO")
Who should you pair with the grumpiest villager?
Notice that each operation sets at least one of the involved villagers' grumpiness to $$$0$$$. So after one operation has been done involving each villager, each villager either has grumpiness $$$0$$$ or is friends with another villager with grumpiness $$$0$$$. Then all the villagers can be made friends with each other for free, by joining the villagers with grumpiness $$$0$$$.
So an upper bound on the answer is the cost to do at least one operation involving each villager. This is also a lower bound, since if there is a villager who was not involved in any operation, they are not friends with any other villager. Therefore we just need to find this cost.
Let's sort the array $$$g$$$ so that the grumpiest villager is villager $$$n$$$. An optimal set of operations is then to pair villagers $$$n$$$ and $$$n-1$$$, $$$n-2$$$ and $$$n-3$$$, $$$n-4$$$ and $$$n-5$$$ and so on. If villager $$$1$$$ is left over, pair it with villager $$$2$$$ as well. This costs $$$g_n + g_{n - 2} + g_{n - 4} + \ldots$$$ in total.
We can prove that this is a lower bound on the answer as follows. Consider instead the operation of paying $$$\text{max}(g_i, g_j)$$$ emeralds but setting both $$$g_i$$$ and $$$g_j$$$ to $$$0$$$; any optimal sequence with this operation has a cost of at most that of using the original operation. Now consider which villager is paired with villager $$$n$$$ for the first operation performed on them. If it is not villager $$$n - 1$$$, then some villager $$$x$$$ was paired with villager $$$n - 1$$$ on their first operation, and some other villager $$$y$$$ is paired with villager $$$n$$$ on their first operation. Replace those operations with pairing villagers $$$n$$$ and $$$n - 1$$$, and villagers $$$x$$$ and $$$y$$$. The cost does not increase because $$$\text{max}(x, y) \le g_{n-1}$$$. So there is also an optimal sequence where villagers $$$n$$$ and $$$n-1$$$ are paired with each other first for a cost of $$$g_n$$$. This leaves $$$n-2$$$ villagers with nonzero $$$g_i$$$, and clearly it is no better than an optimal sequence to pair any of them with villagers $$$n$$$ or $$$n - 1$$$ when they could be paired with other villagers among each other, so the argument can be repeated inductively on those $$$n-2$$$ villagers.
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define all(x) x.begin(), x.end()
#define vecin(name, len) vector<int> name(len); for (auto &_ : name) cin >> _;
#define vecout(v) for (auto _ : v) cout << _ << " "; cout << endl;
void solve() {
int n; cin >> n;
vecin(g, n);
ll ans = 0;
sort(all(g));
for (int i = n - 1; i >= 0; i -= 2)
ans += g[i];
cout << ans << endl;
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int tt = 1;
cin >> tt;
while (tt--) solve();
return 0;
}
tt = int(input())
for t in range(tt):
n = int(input())
grumpiness = list(map(int, input().split()))
grumpiness.sort()
tot = 0
for i in range(n - 1, -1, -2):
tot += grumpiness[i]
print(tot)
How can you find where the path should start?
How else can you use the information you gained from that?
Let's first find a suitable starting location for the longest path. We can find the length of the longest path starting from each location in $$$n$$$ queries, by querying ? i n 1 2 ... n for each $$$1 \le i \le n$$$, and can pick any location whose query returned the maximum value.
How can we also use this information to help find the rest of the path? Suppose we have some location $$$x$$$ in any longest path, whose query returned $$$k$$$. Then we know the next location is one whose query returned $$$k-1$$$. This is because if it returned a value $$$\ge k$$$, then there is a path of length at least $$$k + 1$$$ from $$$x$$$, but the query returned $$$k$$$; and if it returned a value $$$\le k - 2$$$, then if it is the next location in the path, the path starting from $$$x$$$ has length at most $$$k - 1$$$, but we know there is a path of length $$$k$$$ starting from it so it is not a longest path.
So if our path ends at a location $$$x$$$ whose query returned $$$k$$$, we can find the next location by querying ? x 2 x y for all locations $$$y$$$ whose query returned $$$k-1$$$, and choosing any location which returns $$$2$$$. We repeat this until we have the whole path. Since the second step queries each location at most once, other than the starting location, we use no more than $$$2 \cdot n - 1$$$ queries in total.
import sys
for test in range(int(input())):
layers = {}
n = int(input())
big = -1
path = []
for i in range(n):
print(f"? {i + 1} {n} {' '.join(list(map(str, list(range(1, n + 1)))))}")
sys.stdout.flush()
ans = int(input())
if ans in layers:
layers[ans].append(i + 1)
else:
layers[ans] = [i + 1]
if ans > big:
big = ans
path = [i + 1]
for depth in range(big - 1, 0, -1):
for candidate in layers[depth]:
print(f"? {path[-1]} 2 {path[-1]} {candidate}")
sys.stdout.flush()
ans = int(input())
if ans == 2:
path.append(candidate)
break
print(f"! {len(path)} {' '.join(list(map(str, path)))}")
sys.stdout.flush()
Solve in $$$\frac{5}{3} \cdot n$$$ queries.
How many times can a mob take fall damage?
If a mob takes more than $$$1$$$ fall damage, what about the mob below it?
Use DP.
Let's make a few observations. First of all, if a mob takes fall damage, it ends up at the bottom of a stack. This means a mob never takes fall damage more than once, because after taking fall damage there are no more mobs below it that it can fall off of.
Secondly, if a mob takes more than $$$1$$$ fall damage, the mob below must have been killed without taking fall damage, otherwise it would have been at the bottom of the stack and hence the mob above would only have $$$1$$$ mob below it and take $$$1$$$ fall damage. Therefore the mob below must take $$$h_i$$$ attacks to kill, regardless of whether it is in the original stack or if other mobs below it had fallen off and taken damage. This means that if a mob should take more than $$$1$$$ fall damage, it is best to do so in the original stack, as that will maximise the fall damage without incurring any additional attacks.
This seems like a very local choice: for each mob $$$i$$$ we either kill it directly, let it take $$$i - 1$$$ fall damage by killing the mob below it directly, or let it take $$$1$$$ fall damage by falling down when the mob below it dies. So we could let $$$dp[i]$$$ be the minimum attacks needed to kill all the mobs up to mob $$$i$$$. Clearly $$$dp[0] = 0$$$ and $$$dp[1] = h_1$$$. For $$$i \ge 2$$$, we either let it take $$$1$$$ fall damage which costs $$$dp[i - 1] + h_i - 1$$$ attacks, or let it take maximum fall damage which costs $$$dp[i - 2] + h_{i - 1} + \text{max}(0, h_i - (i - 1))$$$ attacks; we set $$$dp[i]$$$ to the smaller of those two values. The answer is then $$$dp[n]$$$.
t = int(input())
for test in range(t):
n = int(input())
h = list(map(int, input().split()))
dp = [0] * (n + 1)
dp[1] = h[0]
for i in range(1, n):
dp[i + 1] = min(dp[i] + h[i] - 1, dp[i - 1] + h[i - 1] + max(0, h[i] - i))
print(dp[n])
2133E - I Yearned For The Mines
Suppose the tree is a chain. Can you catch Herobrine using only operation $$$1$$$?
You can do it in only $$$n$$$ queries.
You can use all instances of operation $$$2$$$ before operation $$$1$$$.
Try tree DP/colouring.
Observe that if we have some connected component of $$$k$$$ nodes that is a path (i.e. each node has degree at most $$$2$$$), we can check it for the presence of Herobrine in $$$k$$$ operations by "sweeping" from one end of the path to the other with operation $$$1$$$. This can be proven by induction on the number of operations: after the $$$i^{th}$$$ operation, he cannot be in any of the first $$$i$$$ nodes in the path.
Also, it is always fine to do all instances of operation $$$2$$$ before any of operation $$$1$$$, as if we have any instance of operation $$$2$$$ after doing operation $$$1$$$, we could instead move that operation to the beginning of the sequence, and we would only be restricting Herobrine's movement.
So we need to find a way to split the tree into paths with at most $$$\left\lfloor\frac{n}{4}\right\rfloor$$$ of operation $$$2$$$, then check all the paths for Herobrine. There are a few different ways of doing this; the model solution does this by colouring the tree. Root the tree at node $$$1$$$. Let a green node be an endpoint of a path in its subtree, a yellow node be an intermediate node of a path in its subtree, and a black node be one on which we perform operation $$$2$$$. Then for each node:
- If it has three or more green children, or any yellow children, colour it black.
- Otherwise, if it has exactly two green children (and any other children are black), colour it yellow.
- Otherwise, colour it green.
We can prove this results in at most $$$\left\lfloor\frac{n}{4}\right\rfloor$$$ black nodes. For each black node, if it was coloured black because it had three (or more) green children, group it with those; if it was coloured black because it had a yellow child, group it with that node and its two green children. We have grouped each black node with at least $$$3$$$ other nodes that aren't grouped with any other black node, so if there are $$$x$$$ black nodes, we know $$$n \ge 4 \cdot x$$$.
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define all(x) x.begin(), x.end()
#define vecin(name, len) vector<int> name(len); for (auto &_ : name) cin >> _;
#define vecout(v) for (auto _ : v) cout << _ << " "; cout << endl;
const int MAXN = 200005;
vector<int> adj[MAXN];
int colour[MAXN];
vector<pair<int, int>> ans;
vector<int> leaves;
bool vis[MAXN];
void dfs(int node, int parent) {
vector<int> children;
for (auto a : adj[node])
if (a != parent) {
dfs(a, node);
children.push_back(a);
}
int num_1_children = 0;
bool any_2_children = false;
for (auto child : children) {
if (colour[child] == 1)
num_1_children ++;
else if (colour[child] == 2)
any_2_children = true;
}
if (any_2_children || num_1_children >= 3) {
colour[node] = 0;
ans.push_back({2, node + 1});
ans.push_back({1, node + 1});
} else if (num_1_children == 2) {
colour[node] = 2;
} else {
colour[node] = 1;
if (num_1_children == 0)
leaves.push_back(node);
}
}
void dfs2(int node) {
vis[node] = true;
ans.push_back({1, node + 1});
for (auto a : adj[node])
if (colour[a] != 0 && !vis[a])
dfs2(a);
}
void solve() {
int n; cin >> n;
for (int i = 0; i < n; i ++)
adj[i].clear(), vis[i] = false;
for (int i = 0; i < n - 1; i ++) {
int a, b; cin >> a >> b;
a --; b --;
adj[a].push_back(b);
adj[b].push_back(a);
}
ans.clear();
leaves.clear();
dfs(0, -1);
for (auto a : leaves)
if (!vis[a])
dfs2(a);
cout << ans.size() << endl;
for (auto a : ans)
cout << a.first << " " << a.second << endl;
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int tt = 1;
cin >> tt;
while (tt--) solve();
return 0;
}
Prove that $$$\left\lfloor \frac{5}{4} \cdot n \right\rfloor$$$ operations are necessary in the worst case.
Implement the checker.
#include <bits/stdc++.h>
#include "testlib.h"
using namespace std;
bool verify(int n, vector<set<int>> adj, vector<pair<int, int>> ops) {
vector<bool> inpath(n, false);
vector<bool> irrelevant(n, false);
vector<set<int>> children(n);
vector<int> parent(n, -1);
int last_op1_node = -1;
vector<int> active_heads;
function<void(int)> check_for_parent = [&](int node) {
if (irrelevant[node] || !inpath[node]) return;
if (parent[node] == -1 && (int)adj[node].size() == (int)children[node].size() + 1)
for (auto a : adj[node])
if (!children[node].count(a)) {
parent[node] = a;
children[a].insert(node);
}
};
function<void(int)> destroy = [&](int node) {
queue<int> q;
q.push(node);
while (q.size()) {
int cur = q.front(); q.pop();
if (parent[cur] != -1)
children[parent[cur]].erase(cur), parent[cur] = -1;
inpath[cur] = false;
irrelevant[cur] = true;
for (auto a : children[cur])
q.push(a);
}
};
for (int op = 0; op < ops.size(); op ++) {
int node = ops[op].second;
if (ops[op].first == 1 && !irrelevant[node]) {
if (!inpath[node]) {
if (children[node].size() > 0 || adj[node].size() <= 1) {
active_heads.push_back(node);
inpath[node] = true;
}
}
check_for_parent(node);
} else {
if (inpath[node] || node == last_op1_node) {
destroy(node);
}
for (auto a : adj[node]) {
adj[a].erase(node);
if (inpath[a] && parent[a] == node)
destroy(a);
if (inpath[a] && parent[a] == -1)
check_for_parent(a);
}
adj[node].clear();
}
vector<int> new_heads;
for (auto head : active_heads) {
if (irrelevant[head] || !inpath[head]) continue;
if (adj[head].size() == children[head].size()) {
destroy(head); continue;
}
if (ops[op].first == 1 && (head == node || parent[head] == node)) {
if (head == node)
new_heads.push_back(head);
continue;
}
if (op < ops.size() - 1 && ops[op + 1].first == 1 && ops[op + 1].second == head) {
new_heads.push_back(head); continue;
}
if (parent[head] != -1)
children[parent[head]].erase(head);
parent[head] = -1;
inpath[head] = false;
for (auto a : children[head])
new_heads.push_back(a);
}
active_heads = new_heads;
last_op1_node = (ops[op].first == 1) ? node : -1;
}
for (int i = 0; i < n; i ++)
if (!irrelevant[i])
return false;
return true;
}
const int MAXN = 200005;
int n, t;
vector<int> adj_orig[MAXN];
int max_ops() {
// floor(1.25n)
return (5 * n) / 4;
}
void readAns(InStream &in) {
vector<set<int>> adj(n);
for (int i = 0; i < n; i ++) {
for (auto a : adj_orig[i])
adj[i].insert(a);
}
int opcnt = in.readInt(1, max_ops(), "number of operations");
vector<pair<int, int>> ops;
for (int op = 0; op < opcnt; op ++) {
int tt = in.readInt(1, 2, "operation type");
int node = in.readInt(1, n, "operation node");
ops.push_back({tt, node - 1});
}
if (!verify(n, adj, ops))
in.quitf(_wa, "sequence of valid moves exists for Herobrine to not be caught");
}
int main(int argc, char *argv[]) {
registerTestlibCmd(argc, argv);
t = inf.readInt();
for (int i = 0; i < t; i ++) {
setTestCase(i + 1);
n = inf.readInt();
for (int i = 0; i < n; i ++)
adj_orig[i].clear();
for (int i = 0; i < n - 1; i ++) {
int u = inf.readInt();
int v = inf.readInt();
u --; v --;
adj_orig[u].push_back(v);
adj_orig[v].push_back(u);
}
readAns(ans);
readAns(ouf);
}
ans.quitif(!ans.seekEof(), _pe, "expected EOF after %d answers", t);
ouf.quitif(!ouf.seekEof(), _pe, "expected EOF after %d answers", t);
quitf(_ok, "very good");
}
Try solving it in $$$\mathcal{O}(n^2)$$$ first.
Use DP.
Use segment tree.
Let's first find how many detonations are required. This seems very similar to the standard problem of finding the minimum number of ranges that covers the entire array (with each creeper being the range $$$[i - e_i + 1, i + e_i - 1]$$$), but there is one key difference: two ranges cannot have intersecting midpoints. If that were the case, then whichever creeper we detonate first would kill the other, so they can't both be detonated.
A natural idea is letting $$$dp[i][j]$$$ be the minimum detonations required to kill exactly the first $$$i$$$ creepers with the rightmost creeper we use being creeper $$$j$$$. In fact, we can observe that only the states where $$$i = j + e_j - 1$$$ matter, so we can remove a dimension and instead let $$$dp[i]$$$ be the minimum detonations required to kill all the creepers up to $$$i + e_i - 1$$$ with the rightmost creeper we use being creeper $$$i$$$.
There are two cases where we can transition from some creeper $$$j \lt i$$$: either $$$i$$$ is to the right of creeper $$$j$$$'s range of detonation, so $$$i - e_i + 1 \le j + e_j$$$ and $$$j + e_j - 1 \lt i$$$, or $$$j$$$ is to the left of creeper $$$i$$$'s range of detonation, so $$$j \lt i - e_i + 1$$$ and $$$i - e_i + 1 \le j + e_j$$$. We can directly implement this to get an $$$\mathcal{O}(n^2)$$$ solution, but this is too slow.
We can optimise this further using segment tree. Let's process the creepers from $$$1$$$ to $$$n$$$ and maintain a segment tree where position $$$i$$$ stores the minimum detonations required to kill all the creepers up to $$$i$$$, using only the creepers processed so far (or $$$\infty$$$ if it's impossible). And let $$$dp[i]$$$ be the minimum detonations required to kill all the creepers up to $$$i + e_i - 1$$$, with creeper $$$i$$$ the rightmost one we use. When we reach creeper $$$i$$$, we do a point minimum update: $$$\text{segtree}[i + e_i - 1] = \text{min}(\text{segtree}[i + e_i - 1], \,dp[i])$$$.
But how do we calculate $$$dp[i]$$$? Notice that in fact we only need to perform two range minimum queries, corresponding to the two cases from earlier. For the first case, after processing the first $$$i - 1$$$ creepers, we query the range $$$[i - e_i, i - 1]$$$. And for the second case, after processing the first $$$i - e_i$$$ creepers, we query the range $$$[i - e_i, i + e_i - 1]$$$. Then $$$dp[i]$$$ is set to the smaller of those two values. We can precompute which queries need to be performed after processing each creeper to do this in $$$\mathcal{O}(n \,\text{log}\, n)$$$. The minimum detonations will then be $$$\text{min}_{i + e_i - 1 \ge n}(dp[i])$$$.
To reconstruct the set of detonated creepers, we can also have a predecessor array storing which creeper is the rightmost one to the left of creeper $$$i$$$ that should be detonated if we detonate creeper $$$i$$$, and also store that information in the segment tree. There is one more piece of the puzzle: in which order do we detonate them? If they are detonated in ascending order of explosive power, we can never get an earlier creeper in the sequence killing a later one.
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define all(x) x.begin(), x.end()
#define vecin(name, len) vector<int> name(len); for (auto &_ : name) cin >> _;
#define vecout(v) for (auto _ : v) cout << _ << " "; cout << endl;
const int INF = 1e9;
struct segtree {
int n;
vector<pair<int, int>> tree;
segtree (int _n) {
n = 1;
while (n < _n)
n *= 2;
tree = vector<pair<int, int>>(2 * n, {INF, -1});
}
void upd(int i, pair<int, int> x) {
i += n;
tree[i] = min(tree[i], x);
while (i > 1) {
i /= 2;
tree[i] = min(tree[2 * i], tree[2 * i + 1]);
}
}
pair<int, int> query(int left, int right, int index, int maxl, int maxr) {
if (left == maxl && right == maxr) {
return tree[index];
}
int mid = (maxl + maxr) / 2;
pair<int, int> ans = {INF, -1};
if (left <= mid)
ans = min(ans, query(left, min(mid, right), 2 * index, maxl, mid));
if (right > mid)
ans = min(ans, query(max(left, mid + 1), right, 2 * index + 1, mid + 1, maxr));
return ans;
}
pair<int, int> query(int left, int right) {return query(left, right, 1, 0, n - 1);}
};
struct update {
int l; int r; int i;
};
void solve() {
int n; cin >> n;
vecin(aa, n);
vector<vector<update>> upd(n + 1);
for (int i = 0; i < n; i ++) {
if (aa[i] == 0)
continue;
int left = max(0, i + 1 - aa[i]), right = min(n, i + aa[i] - 1);
upd[i].push_back((update){left, i, i + 1});
upd[left].push_back((update){left, right, i + 1});
}
vector<pair<int, int>> dp(n + 1, {INF, -1});
dp[0] = {0, -1};
segtree seg(n + 1);
seg.upd(0, {1, -1});
for (int i = 0; i <= n; i ++) {
if (i > 0)
seg.upd(min(n, i + aa[i - 1] - 1), {dp[i].first + 1, i});
for (auto xx : upd[i])
dp[xx.i] = min(dp[xx.i], seg.query(xx.l, xx.r));
}
if (seg.query(n, n).first == INF) {
cout << -1 << endl; return;
}
vector<pair<int, int>> ans;
int cur = seg.query(n, n).second;
while (cur != -1) {
ans.push_back({aa[cur - 1], cur});
cur = dp[cur].second;
}
sort(all(ans));
cout << ans.size() << endl;
for (auto a : ans)
cout << a.second << " ";
cout << endl;
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int tt = 1;
cin >> tt;
while (tt--) solve();
return 0;
}








thanks for fast editorial
Nice round with nice problems!
Beautiful interactive!
Can anybody tell me how to develop the intuition for problems like D?
Just brute force the sample cases until intuition magically comes to you.
Short answer is "solve more problems". I know this is not what you're looking for but it is what it is.
To solve problem D, you need to make some observations -- for example, realizing that having more than 2 stacks is unnecessary. The fun part of competitive programming is making such observations. It's a skill that isn't easy to improve, but you can get better by learning from problems: by reflecting on the ones you couldn't solve, analyzing how you might have solved them.
You can try swapping two consecutive operations. If they are not adjacent, then obviously the later operation is done first. Then, connect the elements to be deleted with those adjacent to the later operation. The operation times will be: {t[0], t[0] + 1, ...}, {t[1], t[1] + 1, ...}, ..., {t[k], t[k] + 1, ...}
where $$$t[0] \gt t[1] \gt \dots \gt t[k]$$$.
For each segment, the gain is calculated as follows: let the first position of the segment be $$$i$$$. If the segment length is 1, it cannot decrease; otherwise, for the second element, it can decrease by $$$\min(i, a[i+1])$$$, and all other elements decrease by 1. This way, a DP can be designed.
In other words, swapping adjacent operations often determines the structure of the operation order.
It is translated by CHAT GPT, forget my poor English.
This was the first time I came across C-type of question. I have not yet seen the answer. Any tips for attempting before viewing the answer?
Try a lot of what ifs.
Like what would happen if I picked half the vertices, or all the vertices or one vertex or two vertex, with different starting points.
I felt so good after solving C! The approach is so simple! Just do:
Query each node with all nodes allowed -> get the longest path length from each.
Pick the node with the maximum length as start.
From there, repeatedly try to move to any next node that has the longest path = current longest path — 1
Follow until the path ends!
you just summarized topo sort
I want to say D explanation leaves some room of ambiguity, especially the fall mechanism.
True, i too was confused until i saw the examples.
Ok, my D solution was really weird. The main idea was going top to bottom, but that made the dp harder and lost me a lot of time
Same, i'v noticed that, falling that happened above i, doesn't affect any fall damage for mobs below. Because of this, i thought i can do something greedy, and decided that DP idea it's wrong.
In problem C's solution, if we use binary search to find the first node whose query returned $$$k-1$$$, which is connected to the node whose query returned $$$k$$$, we may use less time to find the answer.
That was the solution to the original version of the problem, but it was found to be too hard, so we nerfed it to allow $$$2n$$$ queries. (The statement does comment that Steve is particularly generous with the query limit!)
Don't we need additional queries to check which k-1 nodes is connected to which k node and how binary search will help here?
What will be the maximum query limit required to solve this problem?
If there is only one $$$k-1$$$-node, you don't even need a query. Otherwise the worst case for binary search is when there are $$$3$$$ nodes in each layer, requiring $$$2$$$ queries. (In fact you don't really need binary search, you can just check groups of $$$2$$$ until you have at most $$$2$$$ nodes left, but binary search might be easier to implement.)
One tester mentioned it might be possible in better than $$$\frac{5}{3}n$$$ queries as well, although it sounds quite hard.
We could use this binary search, but for groups of size 3 and 5 we randomize which elements to query(just random shuffle and query first half). This way we get expected number of queries to be $$$\frac{14}{9}n$$$.
Edit: For this to work you have to query the smaller half.
What problem binary search is solving here? Are we using binary search for deciding which node to take among all k-1th level nodes after taking one from kth level node? If so then how will we decide which half to discard?
As is Sving1024's solution we query first half of elements. If the result is 1 than we know that the node we are looking for is not in this half, so it must be in the other, else it is in this half. The only change is that if on the given level there are 3 or 5 nodes we random shuffle them before we binary search and query the smaller half.
GG! Fast editorial!
Fast editorial!
I don't know if someone looked in that way, but I think D was almost identical to A very famous problem on leetcode, House Robber
Explanation :-
We want to maximize the fall damage, so we need to find a sequence of attacks that does that.
-> Kill mob[i — 1] which makes mob[i] take (i — 1) damage,
-> Kill mob[i] which makes **mob[i + 1**] take i damage
We cannot benefit from both of these operations because the stack breaks, this is why it becomes similar to HOUSE ROBBER, i.e we cannot kill adjacent mob and want to maximize the fall damage
Just because a problem's solution uses a similar thought process does not make it "almost identical". This comment is like saying that every greedy problem is "almost identical" to any tutorial lesson on greedy algorithms. What's the point?
No, you are absolutely right, but the intution behind it’s solution is same, because the idea is similar. There can be many ways to solve it, but this is one of the way someone can look at. I was just saying that.
Like for example CSES has a problem named Coin pile, that solution also works for another problem, “A knight initially at top left cell(0,0) of an infinite chess board and can only go down or right (as a knight goes in L), can it reach cell (a,b). Surprisingly if you think carefully its similar to coin pile problem.
I am talking about the institutions of solving a problem, after doing a few problems, sometimes we can just link it with some other problems. I know same or similar code doesnt mean same or similar question but institution can be similar or linked atleast.
fast editorial before GTA-6
In problem D, Is it possible to enum the $$$i$$$ from $$$n$$$ to $$$1$$$ when DP? Because if $$$i \leq j$$$, kill the j-th mob will better than kill the i-th first.
Probably hard to iterate from n to 1?
At j, it is easy to calculate the cost to let j take non-1 fall damage. But it is hard to calculate the cost to let j take 1 fall damage which depends on all i that is smaller than j.
Yes it can be done from top to bottom as well, but it becomes little messy
fast editorial :0
I really like the "Bonus(hard):Implement the checker." of the problem E. It is indeed quite interesting.
sammyuri my identical submission(https://codeforces.me/contest/2133/submission/335385249) failed in system testing but it passes post contest(https://codeforces.me/contest/2133/submission/335402623)
only an extra '//' is added in the accepted to allow submission
redacted
they are the exact same solution, it passed the pretests as well
Sorry for my earlier comment. Glad it worked out for you.
The original submission has been rejudged and it passed.
CHICKEN JOCKEY!!
C Bonus solution:
Let $$$p_u$$$ be the longest path starting at node $$$u$$$. If our solution path contains node $$$u$$$, any neighbor $$$v$$$ such that $$$p_v = p_u - 1$$$ can be the next in our solution path. The editorial looks for $$$v$$$ by linearly searching all nodes that satisfy this condition. This takes 1 query per possible neighbor and $$$n + n-1 = 2n-1$$$ queries total.
Instead, we can reduce the queries by using a smarter search. let $$$C$$$ be a set of all canidate nodes $$$v$$$ such that $$$p_v = p_u - 1$$$. Think about what happens if we query the longest path starting at $$$u$$$ using a subset $$$T \subseteq C$$$. If the longest path is 1, that means that all $$$v_i \in T$$$ are not neighbors of $$$u$$$, so we can disregard all of them. If the longest path is >1, that means that some $$$v_i \in T$$$ is a neighbor of u, and we can reduce our search to only nodes in $$$T$$$
We can "binary search" $$$C$$$ by breaking it in half into 2 subsets, $$$T_l \cup T_r = C$$$. If the result of querying $$$T_l$$$ is 1, we know we can reduce our search to only nodes in $$$T_r$$$ and vice versa. If both $$$T_l$$$ and $$$T_r$$$ return a path longer than 1, a valid path continuation exists in both sets and we can search either. It is impossible for both $$$T_l$$$ and $$$T_r$$$ to result in a path length 1. Therefore, we only need to query $$$T_l$$$ to get this information.
This solution will take $$$\sum_{i=1}^{k-1} \lceil \log_2 p_i \rceil$$$ queries. Because $$$\lceil \log_2 a \rceil \le \tfrac{2}{3} a$$$ for integers (equality at $$$a=3$$$), this solution takes $$$n + \frac{2}{3}n = \frac{5}{3}n$$$ total queries. The worst case can be achieved with a graph that looks like 1 path of length $$$k$$$ and 2 paths of length $$$k-1$$$
Solution Code: 335388268
is so relatable ;-;
yea... theres a reason I took over an hour on this problem in contest and submitted the bonus instead of intended
It's a bit of a pain to implement and way less elegant than the editorial solution, but DP also works for E with the following state (note: when I say "good" subtree below I'm referring to a subtree that has had nodes deleted such that the remaining nodes are disjoint paths):
$$$dp[v][0]$$$ — minimum operations to make v's subtree good assuming v's parent edge exists and v is not deleted
$$$dp[v][1]$$$ — minimum operations to make v's subtree good assuming v's parent edge does not exist and v is not deleted
$$$dp[v][2]$$$ — minimum operations to make v's subtree good assuming v is deleted
The key insight for this is that a forest that is a collection of disjoint paths is equivalent to a forest where all the nodes have degree $$$\leq 2$$$
335402889
Yes! You are right! I use the same way to solve E. Although I solved it after compitation. :)
DP is easier to follow.
I don't fully understand the intuition and the correctness of the coloring approach in the editorial.
I found the following more intuitive:
Our goal is to mark at most floor(n/4) nodes to be deleted such that the remaining forest will be made of chains.
Base case — there is < 4 nodes. We're done, this graph must be a chain.
Inductive step — Pick one of the deepest leaves of the tree. Let's move one node up and focus on it's parent, p. If p is already marked for deletion, remove the subtree rooted at p and continue induction on the remaining subtree.
Otherwise, there are 3 possibilities:
For every node marked as deleted, at least 3 nodes are "saved" (not marked for deletion). Therefore, we will perform op 1 on at most floor(n/4) nodes.
I also tried to use DP to solve this problem during the competition, but my code was too complicated, so I couldn't finish it during the competition. I think your code is very concise, but the output part of my code is very complicated. (I'm going to cry.)
My Code
Yeah reconstructing optimal decisions from DP is always kind of annoying, but I've generally found this pattern to be useful where I create an $$$opt$$$ array that has the same dimensions as my DP array and stores the optimal decision for each DP state — then you just recurse downward from the final DP state to reconstruct your solution.
Nice round!
Honestly, this was the best round I have ever participated in! Thank you so much for the round!
i'm sorry, but the tutorial needs another tutorial to be understandable :)
Great Problems! Came back to cf after a while and loved solving these !!
WTF is this solution for problem A ? Am I dumb, or can we just have a O(n) solution by using a frequency table ?
You can do that, although it would be $$$\mathcal{O}(n + A)$$$ where $$$A = \text{max}(a[i])$$$, so it wouldn't work if the constraints on $$$a[i]$$$ were higher. (But given the low constraints it's fine.)
using
mapis still $$$O(n \log{n})$$$using
unordered_mapis $$$O(n)$$$ worst case which might cause $$$O(n^2)$$$They are most likely referring to creating a frequency table using an array of fixed length (i.e. 99, 100, or 101 depending on implementation).
unordered_mapis unnecessary.unordered_setis enough and usingunordered_setcan't cause TLE (335440738). I think it's impossible to create a hack when $$$2 \leq a_i \leq 100$$$ because the hacks rely on hash collisions, and the possible inputs in this range can't produce many collisions (maybe even none?).Yeah that's what I did since there were small size constraints. Maybe the editorial was trying to be more general?
In hint 2 of problem D, it says
If a mob takes more than $$$1$$$ fall damage, what about the mob below it?
I think it should say
If a mob takes a fall damage of more than $$$1$$$, what about the mob below it?
Otherwise, when read alongside with hint 1, it sounds like a mob can ...
take fall damage more than once. It should be made clear that the $$$1$$$ indicates the damage amount, not the count of damage occurrence.
Missed problem E because of a one-line bug. Maybe next time...
I have a solution for D, it is a bit simple similar to "Mortal Combat" Problem the DP 1500 problem on codeforces, but in reverse. The solution basically was caring about four cases HH, HR, RH, RR and saving the best H and the best R for every i
where H represents the best score if I damaged i directly, and R represents the score if I damaged the one before i and i was just a bottom of a new stack, HH means i hit and i + 1 hit and the rest are similar things, the hardest part for me was the RR part, because we will not take R from i + 1 safely but instead of letting its prev be i it will be 1, have a look :)
I am quite curious about how the checker for E works.
What I have in mind is that for every
1operation, it checks whether the current component is a linear path and whether it started from one of the tails, moving node by node to the other tail.or am i missing something?
That's kind of the idea, but you need to keep track of potentially multiple components where Herobrine cannot be and there are some special cases that could make the checker run in $$$\mathcal{O}(n^2)$$$ if not handled properly as well. And it's not just linear paths; for example, if you had a "star" tree (one central node connected to $$$n-1$$$ others), you can also catch Herobrine using only operation $$$1$$$. (It takes more than $$$n$$$ operations, but you have to still account for this because it could be a subtree of a much larger tree that "donates" some extra operations, and a particularly weird solution might do it this way.)
I was stuck in the problem D, i thought approaching from the suffix was the best solution, however the tutorial says otherwise. Still, I really liked it — thanks to the authors <3
I wrote the following believing that the two approaches have some difference, but now I see nothing. Just posting this anyway.
We shall find the maximum fall-damage we can benefit from, and subtract this from the total sum.
First imagine that there are no monsters with health 1. Then we can select a set of monsters to apply fall damage to, by killing the monster just under it. This set cannot contain adjacent elements from the array. Also, the number of monsters between two consecutive 'selected' monsters need not be 3 or more, since we can add a monster in between to the set without affecting its validity. Now we can see that a dp from either side works for this problem:
dp[i]isfall_benefit[i]+max(dp[i+2], dp[i+3]+1)(+1 since the monster above it can be made to take fall damage 1 since we do not need to kill it directly). Note that the answer from this is incomplete, but we will fix it near the end.Now when we allow monster health to be 1, we can include
dp[i+1]as another possibility when health of monster i+1 is 1. Also, I forgot to mention another possibility we need to consider (since I thought of it only after coming up to this point): fall-benefit of 1 can be extracted from monster i+1 by taking its value, subtracting its normal fall-benefit and adding 1. Note that the 'dp[i+1] when h[i+1]=1' case is a subcase of this.fall_benefit[i] is min(i, h[i]). Final answer is sum-dp[1].
And now I realize the dp[i+3] term is unnecessary...
Problem D was awesome
Edit: Also, does anyone have a greedy solution for D?
This was my first contest and I was only able to solve Problem A. I hope to do better in future contests Any tips on how to spot hidden details and build intuition For example, Problem B turned out to be solvable but during the contest I could not figure out the right approach.
The best way to get better at spotting hidden details is to 1) thoroughly read a problem (especially when starting out) while trying to think about why they included said info, and most importantly 2) practice/experience. Most of it comes from solving a lot of problems and naturally building an intuition.
excellent contest:)
There are many thought-provoking questions in this round.I like it.
In problem E, I have made out that I need to split the tree into paths. But how can we find a way to split as the amazing answer shows? Coloring the tree just seems an impossible wild imagination for me.
You can use dp to solve it.
I hope the writer can improve the description later.
I didn't realize the D's description is wrong until 0:30......
I think the race can't appear such important problem.
Nice problem F
Is there any O(n^2) solution's code or equation of Problem F? I'm a bit confused about that.
337546641 here you go. It's tle btw
Perhaps we can find the answer to Problem E using the minimum number of operations.
Use the DP method on the tree to find the minimum number of nodes to be deleted so that the remaining portion of the tree after deleting the node consists of exactly a certain number of chains. Then remove these nodes, traverse each chain in order once, and perform operation 1.
F is hard.problem D and E is good.This is very thought-provoking content.
A really stupid solution to problem D but i like it cause of the optimization from n^2 to n
so dp[i]= min attacks to clear i...n
now to calculate dp[i] in n time we can check for every j>i.
something like
1] h[i]+dp[i+1]
2] h[i]+h[i+1]-(i+1)+dp[i+2]
3] loop j over i+2->n and find the minimum of sum till j +dp[j+1] call this above; so dp[i]= h[i]+h[i+1]-(i+1)+ above
but then it can be noticed that the value of above can only change at the current i if it does change so a simple check a[i+1]+dp[i+2]-1 will do this you can try to prove this greedily .
here: 335464375
Dang, fast editorial!
Why didn't my solution pass for D though:
https://codeforces.me/contest/2133/submission/335390561
I really wish C wasn't interactive but whatever
Is there any O(n^2) solution's code or equation of Problem F? I'm a bit confused about that.
I would say that the $$$O(n^3)$$$ limit for C is quite confusing, as this problem is easily solved in $$$O(n^2)$$$ time.
The interactor runs in $$$\mathcal{O}(n^3)$$$, as each query can take $$$\mathcal{O}(n^2)$$$ time to answer.
Can someone tell me In E why can we just split the tree into components of size 4 or less
In D.. the chicken jockey problem.. I am 100% sure my approach is correct.. I know that if a mob is to be killed its better to be killed while its in the original stack.. also.. its not beneficial to kill 2 consecutive mobs.. so what i do is i calculate the answer for an approach where i only kill the bottom-most mob in the og stack.. let the next one take 1 fall damage.. then do it till the whole stack is dead.. next i calculate for each index how much benefit i'll get if i kill that mob.. i only care if this value is negative.. coz im gonna add this to my ans... now i traverse from the right side to the left and for every continuous negative subarray i figure out which non continuous subsequence gives the minimum sum and add this to my og ans.. this however fails on the 3rd test case by 2 points.. Any help is much appreciated... (hands praying emoji)
heres my submission for reference 335568823
Can the E problem be solved using the degree of the graph? Perform the 2 operation on all degrees greater than 2, then only single nodes, double nodes, and chains remain, and then proceed with the examination
That can use up to $$$\frac{4}{3}n$$$ queries, which is too many. Here's a small case for which it doesn't work: the only solution is to perform operation $$$2$$$ on node $$$4$$$, which has degree $$$2$$$.
Someone please explain me B. Villagers ? even chatgpt can't explain it to me, I fail to understand how pairing from the last -> start after sorting them will get the most optimal emrald cost ?
You can check that for any villager pair $$$(i, j)$$$, after applying:
$$$v[i] := v[i] - \min(v[i], v[j])$$$
$$$v[j] := v[j] - \min(v[i], v[j])$$$
it's guaranteed that at least one of the values becomes $$$0$$$.
Because of this, pairing $$$\left\lfloor \frac{n}{2} \right\rfloor + (n \bmod 2)$$$ times is enough to connect all villagers, since for any pair $$$(i, j)$$$ where $$$v[i] = v[j] = 0$$$, the connection cost is $$$0$$$.
Now, if we're trying to minimize $$$\max(v[i], v[j])$$$, notice that:
for any $$$v[i] \leq v[j] \leq v[k]$$$, pairing $$$(i, k)$$$ leads to a larger or equal cost than pairing $$$(i, j)$$$.
Based on this, a greedy strategy would be:
Handling Even and Odd $$$n$$$:
If $$$n$$$ is even, the algorithm is straightforward: just pair all adjacent elements.
If $$$n$$$ is odd, there are two natural ways to handle the unpaired element:
Option 1:
Pair $$$(v[1], v[2]), (v[3], v[4]), \ldots, (v[n-2], v[n-1])$$$, then leave $$$v[n]$$$ as is.
Total cost:
$$$\sum_{i=1,\, \text{step } 2}^{n-2} \max(v[i], v[i+1]) + v[n]$$$
Option 2:
Leave $$$v[1]$$$ unpaired, and pair the rest: $$$(v[2], v[3]), (v[4], v[5]), \ldots, (v[n-1], v[n])$$$.
Total cost:
$$$v[1] + \sum_{i=2,\, \text{step } 2}^{n-1} \max(v[i], v[i+1])$$$
You can compute both and take the minimum, this will give you an AC.
But if you want to push even further, you'll notice that Option 2 is always better. Reason:
Since $$$v[1] \leq v[2], v[3] \leq v[4], \ldots$$$ it can be proofed the second sum is smaller.
My explanation isn't fully rigorous, but I hope it helps.
Thank You Brother, for your time and your explanation.
gear ratio was a very good question, learnt something new
is it possible solve F in O(n)? I got someidea but stuck at O(n log log n) by veb tree yet. wanna know if there's improve
For D, I think it would be nice to assert the fact that the top mob always takes fall damage in the optimal answer when $$$n \geq 2$$$. The dp relation makes more sense with this assertion.
F actually is far more easier than I thought,but it is hard for me still,the dp part is hard-thinking
Isn't it possible to solve problem E with this simple algorithm?
Do DFS from any vertex.
Consider the vertices in the exit time order.
If the current vertex is connected to 3 or more undeleted vertices in its subtree, delete it.
Now for every deleted vertex there will be at least 3 undeleted, meaning that we delete at most n/4 vertices. At the same time each component consists of at most 3 vertices, meaning that each vertex has degree <= 2, meaning that every component is a bamboo.