Here is a quick editorial for the problems from WEOI 2024, in order from easiest expected problem to hardest, along with my solutions.

You can attempt these problems on the mirror contest page on Codeforces.

Mazes (mazes)

Full Solution

From subtask 44, we can come up with a pattern to solve for all powers of two. The construction I used created the following pattern for K=8K = 8 and is easily generalisable for all powers of two:

...#####
.#.#####
......##
###.#.##
###...##
#####...
#####.#.
#####...

We can add an “early-exit” for powers of 22 that are used in the binary representation of KK, and bring them directly to the end of the maze.

For example, for K=5=20+22K = 5 = 2^0 + 2^2, we can add an early exit at the K=20K = 2^0 and K=22K = 2^2 squares.

...#####
.#.#####
......##
.##.#.##
.##...##
.####.##
.####.##
........

This will allow us to create a construction for any valid KK.

Full Solution Code
#include <vector>
using namespace std;

vector<vector<char>> solve(long long K) {
    if (K == 1) {
        return {{'.'}};
    }
    vector<vector<char>> ans(200, vector<char>(200, '#'));
    ans[0][0] = '.';
    ans[199] = vector<char>(200, '.');
    int x = 0;
    long long done = 0;
    for (int p = 0; done != K; ++p) {
        for (int i = 0; i < 3; ++i) {
            for (int j = 0; j < 3; ++j) {
                ans[x + i][x + j] = '.';
            }
        }
        ans[x + 1][x + 1] = '#';
        if ((1LL << p) & K) {
            for (int i = x; i < 200; ++i) {
                ans[i][x] = '.';
            }
            done ^= 1LL << p;
        }
        x += 2;
    }
    return ans;
}

Double Agents (trees)

Full Solution

to-do

Full Solution Code
#include <vector>
using namespace std;

const int MOD = 1e9 + 7;

int add(int x, int y) {
  x += y;
  if (x >= MOD) {
    x -= MOD;
  }
  return x;
}

int sub(int x, int y) {
  x -= y;
  if (x < 0) {
    x += MOD;
  }
  return x;
}

int mul(int a, int b) {
  return (1LL * a * b) % MOD;
}

void dfs(int cur, int par, vector<vector<int>>& e, vector<int>& ways, vector<int>& ways_constrained) {
  ways[cur] = 1;
  int cnt1 = 0, cnt2 = 1;
  for (int child : e[cur]) {
    if (child == par) {
      continue;
    }
    dfs(child, cur, e, ways, ways_constrained);
    // fill root and one child subtree
    ways[cur] = add(ways[cur], ways_constrained[child]);
    // do not fill root, fill at least two child subtrees
    cnt2 = mul(cnt2, add(ways_constrained[child], 1));
    ways[cur] = sub(ways[cur], ways_constrained[child]);
    // do not fill root, fill only one child subtree
    cnt1 = add(cnt1, ways[child]);
  }
  ways[cur] = add(sub(ways[cur], 1), add(cnt1, cnt2));
  ways_constrained[cur] = 1;
  int cnt = 1;
  for (int child : e[cur]) {
    if (child == par) {
      continue;
    }
    cnt = mul(cnt, add(ways_constrained[child], 1));
  }
  ways_constrained[cur] = add(ways_constrained[cur], sub(cnt, 1));
}

int count_sets(int N, vector<int> u, vector<int> v) {
  vector<vector<int>> e(N);
  vector<int> ways(N), ways_constrained(N);
  for (int i = 0; i + 1 < N; ++i) {
    e[u[i]].push_back(v[i]);
    e[v[i]].push_back(u[i]);
  }
  dfs(0, -1, e, ways, ways_constrained);
  return ways[0];
}

Parcel Post (multihop)

Full Solution

Root the tree arbitrarily. Note that we can calculate the cost of moving from a node UU to its immediate ancestor (i.e. parent) VV very easily, it’s just min(Ai,Bi+C)\min(A_i, B_i + C), where ii is the indice of the edge connecting UU and VV.

If we keep track of some additional information, such as whether a path from a node to its ancestor starts and/or ends using a high power firing, we can keep track of 44 states for each node from itself to its parent. Now, since we have the cost to get from a node to its 20=12^0 = 1-st ancestor, we can use binary lifting to calculate the cost, for each station, to its ii-th ancestor for all 1i161 \le i \le 16, with a little bit of casework for matching the states (you can see it in my code below!).

We can do the same binary lifting-dp to keep track of costs for downwards paths in the tree. Now, the cost of a path from UU to VV can be broken down into two paths: one upwards path from UU to LL, the lowest common ancestor of UU and VV, and one downwards path from LL to VV. Combining these two paths will tell us the lowest common cost for sending a parcel on this path.

Time complexity: O(nlogn)O(n \log n)

Full Solution Code
#include <vector>
#include <array>
using namespace std;

struct min_cost {
    array<array<long long, 2>, 2> mc;
    // prefix/suffix closed/open
    void init() {
        for (int i = 0; i < 2; ++i) {
            for (int j = 0; j < 2; ++j) {
                mc[i][j] = 1e18;
            }
        }
    }
};

const int N = 1e5, L = 17;
int p[N], h[N], a[N], b[N], c, lift[L][N];
vector<int> e[N];
min_cost dp_up[L][N], dp_down[L][N];

min_cost merge(min_cost first, min_cost second) {
    min_cost ans;
    ans.init();
    for (int i = 0; i < 2; ++i) {
        for (int j = 0; j < 2; ++j) {
            for (int k = 0; k < 2; ++k) {
                ans.mc[i][k] = min(ans.mc[i][k], first.mc[i][j] + second.mc[j][k]);
            }
        }
    }
    return ans;
}

void dfs(int cur) {
    for (int i = 1; (1 << i) <= h[cur]; ++i) {
        lift[i][cur] = lift[i - 1][lift[i - 1][cur]];
        dp_up[i][cur] = merge(dp_up[i - 1][cur], dp_up[i - 1][lift[i - 1][cur]]);
        dp_down[i][cur] = merge(dp_down[i - 1][lift[i - 1][cur]], dp_down[i - 1][cur]);
    }
    for (int i : e[cur]) {
        if (i == p[cur]) {
            continue;
        }
        p[i] = cur;
        h[i] = h[cur] + 1;
        lift[0][i] = cur;
        dp_up[0][i].mc[0][0] = min(a[i], b[i] + c);
        dp_up[0][i].mc[0][1] = min(a[i] + b[cur], b[i] + c);
        dp_up[0][i].mc[1][0] = min(c, a[i]);
        dp_up[0][i].mc[1][1] = min(c, a[i] + b[cur]);
        dp_down[0][i].mc[0][0] = min(a[cur], b[cur] + c);
        dp_down[0][i].mc[0][1] = min(a[cur] + b[i], b[cur] + c);
        dp_down[0][i].mc[1][0] = min(c, a[cur]);
        dp_down[0][i].mc[1][1] = min(c, a[cur] + b[i]);
        dfs(i);
    }
}

void init(int n, int C, vector<int> A, vector<int> B, vector<int> u, vector<int> v) {
    for (int i = 0; i < n; ++i) {
        a[i] = A[i];
        b[i] = B[i];
    }
    c = C;
    for (int i = 0; i < n - 1; ++i) {
        e[u[i]].push_back(v[i]);
        e[v[i]].push_back(u[i]);
    }
    dfs(0);
}

int lca(int x, int y) {
    if (h[x] < h[y]) {
        swap(x, y);
    }
    for (int i = 0; i < L; ++i) {
        if ((h[x] - h[y]) & (1 << i)) {
            x = lift[i][x];
        }
    }
    if (x == y) { return x; }
    for (int i = L - 1; i >= 0; --i) {
        if (lift[i][x] != lift[i][y]) {
            x = lift[i][x];
            y = lift[i][y];
        }
    }
    return lift[0][x];
}

long long query(int x, int y) {
    int z = lca(x, y);
    min_cost cur_ans;
    cur_ans.init();
    cur_ans.mc[0][0] = 0;
    vector<array<int, 2>> v;
    for (int i = 0; i < L; ++i) {
        if ((h[x] - h[z]) & (1 << i)) {
            cur_ans = merge(cur_ans, dp_up[i][x]);
            x = lift[i][x];
        }
        if ((h[y] - h[z]) & (1 << i)) {
            v.push_back({y, i});
            y = lift[i][y];
        }
    }
    while (!v.empty()) {
        cur_ans = merge(cur_ans, dp_down[v.back()[1]][v.back()[0]]);
        v.pop_back();
    }
    return cur_ans.mc[0][0];
}

Make All Equal (equal)

Full Solution

From Subtask 33, we can come up with a method for making N=2N = 2 piles of stones equal: starting from i=19i = 19 and working down to 00, we can add 2i2^i stones to the smaller pile. After the ii-th operation, we can be sure that the difference between H0H_0 and H1H_1 is no more than 2i2^i, that is, H0H12i| H_0 - H_1 | \le 2^i. Our last operation ensures that the difference between the two is at most 20=12^0 = 1, after which we can use a singular one of our compare\textit{compare} operations to check whether H0H_0 and H1H_1 are equal. If they are not, we add 11 stone to H0H_0, and after doing so, H0=H1H_0 = H_1, as desired.

An intuitive idea follows: we can use a divide-and-conquer-esque method with segment merging, and solve our problem layer by layer, i.e. before solving for [1,2i+1][1, 2^{i + 1}], solve for [1,2i][1, 2^i] and [2i+1,2i+1][2^i + 1, 2^{i + 1}], and make each subsegment equal first before making the new segment equal. This however, takes about 20×204720 \times 2047 add\textit{add} operations, which is not sufficient.

We notice that on each layer - each segment of length 2i2^i - all merges of subsegments are independent, and thus can occur at the same time. Now, for each layer, we perform about 2020 (plus some extra, I’ll leave it as an implementation detail :P) add\textit{add} operations, which solves the problem.

Full Solution Code
#include "equal.h"
#include <vector>
using namespace std;
 
void make_all_equal(int N, int, int) {
    for (int p = 2; p <= N; p *= 2) {
        vector<int> query_indices;
        for (int i = 0; i + p - 1 < N; i += p) {
            for (int j = i; j < i + p / 2; ++j) {
                query_indices.push_back(j);
            }
        }
        for (int i = 18 + __lg(p); i >= 0; --i) {
            add(query_indices, 1 << i);
        }
        query_indices.clear();
        for (int i = 0; i + p - 1 < N; i += p) {
            if (!compare(i, i + p - 1)) {
                for (int j = i; j < i + p / 2; ++j) {
                    query_indices.push_back(j);
                }
            }
        }
        if (!query_indices.empty()) {
            add(query_indices, 1);
        }
    }
}