CF2247F — A More Intuitive Dominator-Tree Solution
Difference between en2 and en3, changed 0 character(s)
# CF2247F — A More Intuitive Dominator-Tree Solution (Different from the Official Editorial)↵
↵
> This post was **AI-translated from Chinese**.↵
> ↵
> If you can read Chinese, I recommend the original Chinese write-up:↵
> ↵
> https://dk-qwq.github.io/blog/posts/cf2247/cf2247f/↵
> ↵
> or↵
> ↵
> https://www.luogu.com.cn/article/3l93nkdh↵
↵
## Problem↵
↵
You are given a grid graph where you may only move **right** or **down**.↵
Each cell is marked with `0/1`, indicating whether it is usable.↵
$(1,1)$ is the start and $(n,m)$ is the destination.↵
↵
We need to count how many **non-empty sets of cells** $S$ satisfy:↵
↵
$\forall x, y \in S$, if a path from $(1,1)$ to $(n,m)$ passes through $x$, then it must also pass through $y$.↵
↵
---↵
↵
## Solution↵
↵
I want to present a solution that is much easier to come up with than the official one, although it requires a bit more code.↵
↵
### 1. Two types of valid sets↵
↵
A valid set $S$ can only be of two kinds:↵
↵
- all cells in $S$ are blocked / unreachable / not contained in any valid $(1,1)\to(n,m)$ path;↵
- all cells in $S$ are cells that lie on valid paths.↵
↵
For the first kind, the answer is obviously:↵
↵
$$↵
2^{\text{count}} - 1↵
$$↵
↵
where `count` is the number of cells that are **not on any valid start-to-end path**.↵
↵
So from now on, we only consider the second kind.↵
↵
---↵
↵
### 2. The relation between two cells↵
↵
For any two cells $x, y$ in such a set, we must have:↵
↵
- every valid path passing through $x$ also passes through $y$;↵
- every valid path passing through $y$ also passes through $x$.↵
↵
This “must pass through” relation is exactly what **dominator trees** describe.↵
↵
Since the grid with only right/down moves is a DAG, we can build a dominator tree for:↵
↵
- the forward graph (from top-left to bottom-right),↵
- and the reversed graph (equivalently, considering the bottom-right to top-left direction).↵
↵
Then the relation between $x$ and $y$ can be converted into domination relations on the forward and reverse dominator trees.↵
↵
---↵
↵
### 3. Why DSU works↵
↵
Checking the domination relation for every pair $(x,y)$ would be too complicated.↵
↵
Instead, observe the following:↵
↵
Suppose↵
↵
$$↵
u \ dom \ w \ dom \ v↵
$$↵
↵
and $u$ and $v$ are mutually related (in the sense required by the problem).↵
Then by the definition on the original graph, every path between $u$ and $v$ must also pass through $w$, so $w$ must belong to the set as well.↵
↵
This means that the set of mutually related vertices must form a connected chain on the dominator tree, connected through parent-child edges.↵
↵
Let:↵
↵
- `fa1[u]` be the parent of $u$ in the forward dominator tree,↵
- `fa2[u]` be the parent of $u$ in the reverse dominator tree.↵
↵
Then the condition that two adjacent vertices are mutually related is equivalent to:↵
↵
```cpp↵
fa2[fa1[u]] == u↵
```↵
↵
So we can simply check this condition for every vertex, and if it holds, merge `u` with `fa1[u]` in a DSU.↵
↵
After that, each DSU component gives one maximal group of mutually valid cells.↵
If a component has size `siz`, then it contributes:↵
↵
$$↵
2^{\text{siz}} - 1↵
$$↵
↵
because any non-empty subset of this component is valid.↵
↵
So the total contribution from cells that lie on valid paths is:↵
↵
$$↵
\sum (2^{\text{siz}} - 1)↵
$$↵
↵
over all DSU components.↵
↵
Finally, don’t forget to add the contribution from cells that are not on any valid path:↵
↵
$$↵
2^{\text{count}} - 1↵
$$↵
↵
Be careful not to double count any special edge case.↵
↵
---↵
↵
## Complexity↵
↵
Let $V = nm$.↵
↵
Building the two dominator trees on DAGs and processing the DSU takes roughly:↵
↵
$$↵
O(V \log V)↵
$$↵
↵
since each grid cell has only constant out-degree.↵
↵
---↵
↵
[Submission](https://codeforces.me/contest/2247/submission/383524429)↵
↵
```cpp↵
#include <bits/stdc++.h>↵
#include <cassert>↵
#include <format>↵
#define debug(x) cout << #x << " = " << x << endl↵
using namespace std;↵
using ll = long long;↵
↵
class DAGDominatorTree {↵
public:↵
    // vertices are numbered from 1 to n↵
    // g[u] contains directed edges u -> v in the original graph↵
    // the returned dominator tree has size n + 1, where 0 is a virtual root↵
    vector<vector<int>> build(int n, const vector<vector<int>>& g) {↵
        this->n = n;↵
        LOG = __lg(max(1, n)) + 2;↵
↵
        G.assign(n + 1, {});↵
        RG.assign(n + 1, {});↵
        ST.assign(n + 1, {});↵
        indeg.assign(n + 1, 0);↵
        dep.assign(n + 1, 0);↵
        fa.assign(n + 1, vector<int>(LOG, 0));↵
↵
        // copy the graph and build the reverse graph↵
        for (int u = 1; u <= n; ++u) {↵
            for (int v : g[u]) {↵
                G[u].push_back(v);↵
                RG[v].push_back(u);↵
                ++indeg[v];↵
            }↵
        }↵
↵
        // add virtual root: 0 -> all vertices with indegree 0↵
        for (int u = 1; u <= n; ++u) {↵
            if (indeg[u] == 0) {↵
                G[0].push_back(u);↵
                RG[u].push_back(0);↵
                ++indeg[u];↵
            }↵
        }↵
↵
        topoBuild();↵
↵
        // build the dominator tree↵
        for (int u = 1; u <= n; ++u) {↵
            ST[fa[u][0]].push_back(u);↵
        }↵
        return ST;↵
    }↵
↵
private:↵
    int n, LOG;↵
    vector<vector<int>> G, RG, ST, fa;↵
    vector<int> indeg, dep;↵
↵
    int lca(int x, int y) {↵
        if (x == 0 || y == 0) return x | y;  // 0 acts as “null”↵
        if (dep[x] < dep[y]) swap(x, y);↵
↵
        int d = dep[x] - dep[y];↵
        for (int k = 0; k < LOG; ++k) {↵
            if (d >> k & 1) x = fa[x][k];↵
        }↵
↵
        if (x == y) return x;↵
↵
        for (int k = LOG - 1; k >= 0; --k) {↵
            if (fa[x][k] != fa[y][k]) {↵
                x = fa[x][k];↵
                y = fa[y][k];↵
            }↵
        }↵
        return fa[x][0];↵
    }↵
↵
    void topoBuild() {↵
        queue<int> q;↵
        q.push(0);  // virtual root first↵
↵
        int cnt = 0;↵
        while (!q.empty()) {↵
            int u = q.front();↵
            q.pop();↵
            ++cnt;↵
↵
            if (u != 0) {↵
                int p = 0;↵
                for (int pre : RG[u]) p = lca(p, pre);↵
↵
                fa[u][0] = p;↵
                dep[u] = dep[p] + 1;↵
↵
                for (int k = 1; k < LOG; ++k) {↵
                    fa[u][k] = fa[fa[u][k - 1]][k - 1];↵
                }↵
            }↵
↵
            for (int v : G[u]) {↵
                if (--indeg[v] == 0) q.push(v);↵
            }↵
        }↵
↵
        // the original graph is not a DAG↵
        if (cnt != n + 1) {↵
            throw runtime_error("Input graph is not a DAG.");↵
        }↵
    }↵
};↵
↵
struct DSU{↵
    vector<int> fa, siz;↵
    void init(int n) {↵
        fa.assign(n + 1, 0), siz.assign(n + 1, 1);↵
        siz[0] = 0;↵
        iota(fa.begin(), fa.end(), 0);↵
    }↵
    int find(int x) {↵
        return fa[x] == x? x: fa[x] = find(fa[x]);↵
    }↵
    bool Union(int x, int y) {↵
        x = find(x), y = find(y);↵
        if(x == y) return false;↵
        if(siz[x] > siz[y]) swap(x, y);↵
        fa[x] = y, siz[y] += siz[x];↵
        return true;↵
    }↵
}D;↵
↵
const ll mod = 998244353;↵
ll qpow(ll x, ll k) {↵
    ll res = 1;↵
    while(k) {↵
        if(k & 1ll) res = res * x % mod;↵
        k >>= 1, x = x * x % mod;↵
    }↵
    return res;↵
}↵
↵
void solve() {↵
    DAGDominatorTree solver;↵
↵
    int n, m; cin >> n >> m;↵
    int N = n * m;↵
    auto encode = [&](int x, int y) -> int{↵
        return (x - 1) * m + y;↵
    };↵
    auto decode = [&](int S) -> string {↵
        int x = S / m, y = S % m;↵
        if(y == 0) x--, y += m;↵
        return format("({}, {})", x + 1, y);↵
    };↵
↵
    vector a(n + 1, vector<int>(m + 1));↵
    for(int i = 1; i <= n; i++) {↵
        string s; cin >> s;↵
        for(int j = 1; j <= m; j++)↵
            a[i][j] = s[j - 1] - '0';↵
    }↵
↵
    vector reach(n + 1, vector<bool>(m + 1, false));↵
    reach[1][1] = true;↵
    for(int i = 1; i <= n; i++) for(int j = 1; j <= m; j++) {↵
        if(a[i][j] == 0) continue;↵
        if(reach[i - 1][j] || reach[i][j - 1])↵
            reach[i][j] = true;↵
    }↵
↵
    if(!reach[n][m]) {↵
        return cout << qpow(2, n * m) - 1 << endl, void();↵
    }↵
↵
    vector g(N + 1, vector<int>());↵
    for(int i = 1; i <= n; i++) for(int j = 1; j <= m; j++) {↵
        if(!reach[i][j]) continue;↵
        if(reach[i - 1][j])↵
            g[encode(i - 1, j)].push_back(encode(i, j));↵
        if(reach[i][j - 1])↵
            g[encode(i, j - 1)].push_back(encode(i, j));↵
    }↵
↵
    vector reach2(n + 1 + 1, vector<bool>(m + 1 + 1, false));↵
    reach2[n][m] = true;↵
    for(int i = n; i >= 1; i--) for(int j = m; j >= 1; j--) {↵
        if(a[i][j] == 0) continue;↵
        if(reach2[i + 1][j] || reach2[i][j + 1])↵
            reach2[i][j] = true;↵
    }↵
↵
↵
    vector gg(N + 1, vector<int>());↵
    for(int i = 1; i <= n; i++) for(int j = 1; j <= m; j++) {↵
        if(!reach2[i][j]) continue;↵
        if(reach2[i - 1][j])↵
            gg[encode(i, j)].push_back(encode(i - 1, j));↵
        if(reach2[i][j - 1])↵
            gg[encode(i, j)].push_back(encode(i, j - 1));↵
    }↵
↵
    auto ST1 = solver.build(N, g);↵
↵
    auto ST2 = solver.build(N, gg);↵
↵
    vector<int> fa1(N + 1, 0);↵
    vector<int> fa2(N + 1, 0);↵
↵
    for(int u = 1; u <= N; u++) for(auto v: ST1[u])↵
        fa1[v] = u;↵
    for(int u = 1; u <= N; u++) for(auto v: ST2[u])↵
        fa2[v] = u;↵
↵
    D.init(N);↵
    for(int u = 1; u <= N; u++) {↵
        if(fa2[fa1[u]] != u) continue;↵
        assert(fa1[u] > 0);↵
        D.Union(u, fa1[u]);↵
    }↵
↵
    ll ans = 0;↵
↵
    vector<bool> vis(N + 1, false);↵
    for(int i = 1; i <= n; i++) for(int j = 1; j <= m; j++) {↵
        if(!reach[i][j] || !reach2[i][j]) continue;↵
        int r = D.find(encode(i, j));↵
        if(vis[r]) continue;↵
        vis[r] = true;↵
        int sz = D.siz[r];↵
        ans += qpow(2, sz) - 1;↵
        ans %= mod;↵
    }↵
↵
    int count = 0;↵
    for(int i = 1; i <= n; i++) for(int j = 1; j <= m; j++)↵
        if(!reach[i][j] || !reach2[i][j]) count++;↵
↵
    if(count)↵
        cout << (ans + qpow(2, count) - 1) % mod << endl;↵
    else↵
        cout << ans << endl;↵
}↵
int main() {↵
    ios::sync_with_stdio(false);↵
    cin.tie(nullptr), cout.tie(nullptr);↵
    int T = 1;↵
    cin >> T;↵
    while(T--) solve();↵
}↵
```

History

 
 
 
 
Revisions
 
 
  Rev. Lang. By When Δ Comment
en3 English dkdk 2026-07-20 08:01:53 0 (published)
en2 English dkdk 2026-07-20 08:00:31 22
en1 English dkdk 2026-07-20 07:59:32 10387 Initial revision (saved to drafts)