图论杂记

图论杂记

树哈希

设计一个多重集的哈希函数 \(F(S)\) ,可以取:

\[F(S) = c + \sum_{x \in S} f(x) \]

其中 \(c\) 为常数,\(f(x)\) 为自变量是单值的哈希函数。

\(u\) 子树的哈希值为:

\[h_u = F(\{h_v \mid v \in son(u) \}) \]

若用上面的哈希函数可以轻松做到换根操作。

无根树可以考虑换根求出每个点为根的哈希值,然后求和作为整棵树的哈希值。

P4323 [JSOI2016] 独特的树叶

给出两棵树 \(A, B\) ,其中 \(B\)\(A\) 再加一个叶子之后打乱编号得到。

求加的叶子在 \(B\) 中的编号,若有多解则取编号最小的。

\(n \le 10^5\)

先求出 \(A\) 每个点作为根的哈希值 \(hs_x\) ,并将其挂一个父亲的哈希值用 set 存储。

再求出 \(B\) 每个点作为根的哈希值,若该点为叶子且哈希值在 set 中出现,即为答案。

时间复杂度 \(O(n \log n)\)

#include <bits/stdc++.h>
typedef unsigned long long ull;
using namespace std;
const int N = 1e5 + 7;

struct Graph {
    vector<int> e[N];

    inline void clear(int n) {
        for (int i = 1; i <= n; ++i)
            e[i].clear();
    }
    
    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G;

map<ull, int> mp;

ull f[N], hs[N];

int n;

inline ull calc(ull x) {
    x = x * x * x;
    x ^= x >> 33;
    x *= 2398562385683465ull;
    x ^= x >> 17;
    x = x * (x - 1);
    return x;
}

void dfs1(int u, int fa) {
    f[u] = 2375462552572571ull;

    for (int v : G.e[u])
        if (v != fa)
            dfs1(v, u), f[u] += calc(f[v]);
}

void dfs2(int u, int fa) {
    hs[u] = f[u] + (fa ? calc(hs[fa] - calc(f[u])) : 0);

    for (int v : G.e[u])
        if (v != fa)
            dfs2(v, u);
}

signed main() {
    scanf("%d", &n);

    for (int i = 1; i < n; ++i) {
        int u, v;
        scanf("%d%d", &u, &v);
        G.insert(u, v), G.insert(v, u);
    }

    dfs1(1, 0), dfs2(1, 0);
    set<ull> st;

    for (int i = 1; i <= n; ++i)
        st.emplace(2375462552572571ull + calc(hs[i]));

    G.clear(n);

    for (int i = 1; i <= n; ++i) {
        int u, v;
        scanf("%d%d", &u, &v);
        G.insert(u, v), G.insert(v, u);
    }

    dfs1(1, 0), dfs2(1, 0);

    for (int i = 1; i <= n + 1; ++i)
        if (G.e[i].size() == 1 && st.find(hs[i]) != st.end())
            return printf("%d", i), 0;

    return 0;
}

HDU6647 Bracket Sequences on Tree

求遍历一棵无根树产生的本质不同括号序列方案数。

\(n \le 10^5\)

首先可以发现两棵不同构的有根树无法产生相同的括号序列,因此只要求出每个点为根的答案之后,去掉同构的树的答案即可。

先考虑确定根的情况,则每个儿子的遍历顺序决定了括号序列,答案即为:

\[\prod_{i = 1}^n |son(i)|! \]

但是这样会算重,需要除掉每种本质不同子树出现次数阶乘的乘积,然后不难换根求出每个点为根的答案。

#include <bits/stdc++.h>
typedef unsigned long long ull;
using namespace std;
const int Mod = 998244353;
const int N = 1e5 + 7;

struct Graph {
    vector<int> e[N];

    inline void clear(int n) {
        for (int i = 1; i <= n; ++i)
            e[i].clear();
    }
    
    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G;

map<ull, int> mp[N];

ull hs[N];
int fac[N], inv[N], invfac[N];
int f[N], g[N];

int n;

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

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

inline int mi(int a, int b) {
    int res = 1;
    
    for (; b; b >>= 1, a = 1ll * a * a % Mod)
        if (b & 1)
            res = 1ll * res * a % Mod;
    
    return res;
}

inline void prework() {
    fac[0] = fac[1] = 1;
    inv[0] = inv[1] = 1;
    invfac[0] = invfac[1] = 1;
    
    for (int i = 2; i < N; ++i) {
        fac[i] = 1ll * fac[i - 1] * i % Mod;
        inv[i] = 1ll * (Mod - Mod / i) * inv[Mod % i] % Mod;
        invfac[i] = 1ll * invfac[i - 1] * inv[i] % Mod;
    }
}

inline ull calc(ull x) {
    x = x * x * x;
    x ^= x >> 33;
    x *= 2398562385683465ull;
    x ^= x >> 17;
    x = x * (x - 1);
    return x;
}

void dfs1(int u, int fa) {
    hs[u] = 2375462552572571ull, f[u] = 1, mp[u].clear();

    for (int v : G.e[u])
        if (v != fa)
            dfs1(v, u), hs[u] += calc(hs[v]), f[u] = 1ll * f[u] * f[v] % Mod, ++mp[u][hs[v]];

    f[u] = 1ll * f[u] * fac[G.e[u].size() - (u != 1)] % Mod;

    for (auto it : mp[u])
        f[u] = 1ll * f[u] * invfac[it.second] % Mod;
}

void dfs2(int u, int fa) {
    g[u] = (fa ? 1ll * f[u] * G.e[u].size() % Mod * inv[++mp[u][hs[fa] - calc(hs[u])]] % Mod * 
        g[fa] % Mod * mi(f[u], Mod - 2) % Mod * inv[G.e[fa].size()] % Mod * mp[fa][hs[u]] % Mod : f[u]);
    hs[u] = hs[u] + (fa ? calc(hs[fa] - calc(hs[u])) : 0);

    for (int v : G.e[u])
        if (v != fa)
            dfs2(v, u);
}

signed main() {
    prework();
    int T;
    scanf("%d", &T);

    while (T--) {
        scanf("%d", &n);
        G.clear(n);

        for (int i = 1; i < n; ++i) {
            int u, v;
            scanf("%d%d", &u, &v);
            G.insert(u, v), G.insert(v, u);
        }

        dfs1(1, 0), dfs2(1, 0);
        set<ull> st;
        int ans = 0;

        for (int i = 1; i <= n; ++i)
            if (st.find(hs[i]) == st.end())
                ans = add(ans, g[i]), st.emplace(hs[i]);

        printf("%d\n", ans);
    }

    return 0;
}

[ARC194D] Reverse Brackets

给出一个长度为 \(n\) 的合法括号序列 \(S\) ,定义一次操作为:选择一个子串 \(S[l, r]\) ,满足其为合法括号序列,对于 \(i \in [l, r]\) ,若替换前 \(S[l + r - i]\)( ,则将 \(S_i\) 替换为 ) ,否则替换为 )

求任意次操作后本质不同括号串数量。

\(n \le 5000\)

先在外面套一对括号,将括号序列建成树状结构。

不难发现操作的本质就是重排儿子的顺序,答案即为:

\[\prod_{i = 1}^n |son(i)|! \]

但是这样会算重,需要除掉每种本质不同子树出现次数阶乘的乘积。

#include <bits/stdc++.h>
typedef unsigned long long ull;
using namespace std;
const int Mod = 998244353;
const int N = 5e3 + 7;

struct Graph {
    vector<int> e[N];
    
    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G;

ull hs[N];
int fac[N], inv[N], invfac[N], f[N], sta[N];
char str[N];

int n;

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

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

inline int mi(int a, int b) {
    int res = 1;
    
    for (; b; b >>= 1, a = 1ll * a * a % Mod)
        if (b & 1)
            res = 1ll * res * a % Mod;
    
    return res;
}

inline void prework(int n) {
    fac[0] = fac[1] = 1;
    inv[0] = inv[1] = 1;
    invfac[0] = invfac[1] = 1;
    
    for (int i = 2; i <= n; ++i) {
        fac[i] = 1ll * fac[i - 1] * i % Mod;
        inv[i] = 1ll * (Mod - Mod / i) * inv[Mod % i] % Mod;
        invfac[i] = 1ll * invfac[i - 1] * inv[i] % Mod;
    }
}

inline ull calc(ull x) {
    x = x * x * x;
    x ^= x >> 33;
    x *= 2398562385683465ull;
    x ^= x >> 17;
    x = x * (x - 1);
    return x;
}

void dfs(int u, int fa) {
    hs[u] = 2375462552572571ull, f[u] = 1;
    map<ull, int> mp;

    for (int v : G.e[u])
        dfs(v, u), hs[u] += calc(hs[v]), f[u] = 1ll * f[u] * f[v] % Mod, ++mp[hs[v]];

    f[u] = 1ll * f[u] * fac[G.e[u].size()] % Mod;

    for (auto it : mp)
        f[u] = 1ll * f[u] * invfac[it.second] % Mod;
}

signed main() {
    scanf("%d%s", &n, str + 2);
    str[1] = '(', str[n + 2] = ')', n += 2;
    int tot = 0;

    for (int i = 1, top = 0; i <= n; ++i) {
        if (str[i] == '(')
            sta[++top] = ++tot;
        else
            G.insert(sta[top - 1], sta[top]), --top;
    }

    prework(tot), dfs(1, 0);
    printf("%d", f[1]);
    return 0;
}

斯坦纳树

P6192 【模板】最小斯坦纳树

给定一张无向图,给定 \(k\) 个点,求使这 \(k\) 个点连通的最小代价。

\(n \le 100\)\(m \le 500\)\(k \le 10\)

首先可以发现答案的子图一定是树。设 \(f_{i, S}\) 表示以 \(i\) 为根,包含选定点的状态为 \(S\) 的情况下所需代价最小值,分类讨论转移:

  • \(i\) 的度数为 \(1\)\(f_{i, S} = \min \{ f_{j, S} + w(i, j) \}\) ,注意若 \(i\)\(j\) 是关键点,则 \(S\) 一定要包含它们。
  • \(i\) 的度数 \(> 1\)\(f_{i, S} = \min \{ f_{i, S'} + f_{i, S \oplus S'} \}\)

用 Dijkstra 优化转移,时间复杂度 \(O(n \times 3^k + 2^k \times m \log m)\)

#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 1e3 + 7;

struct Graph {
    vector<pair<int, int> > e[N];
    
    inline void insert(int u, int v, int w) {
        e[u].emplace_back(v, w);
    }
} G;

int f[N][1 << 10];

int n, m, k;

signed main() {
    scanf("%d%d%d", &n, &m, &k);

    for (int i = 1; i <= m; ++i) {
        int u, v, w;
        scanf("%d%d%d", &u, &v, &w);
        G.insert(u, v, w), G.insert(v, u, w);
    }

    vector<int> kp(k);
    memset(f, inf, sizeof(f));

    for (int i = 0; i < k; ++i)
        scanf("%d", &kp[i]), f[kp[i]][1 << i] = 0;

    for (int s = 1; s < (1 << k); ++s) {
        priority_queue<pair<int, int> > q;

        for (int i = 1; i <= n; ++i) {
            for (int t = s & (s - 1); t; t = s & (t - 1))
                f[i][s] = min(f[i][s], f[i][t] + f[i][s ^ t]);

            if (f[i][s] != inf)
                q.emplace(-f[i][s], i);
        }

        while (!q.empty()) {
            auto c = q.top();
            q.pop();

            if (-c.first != f[c.second][s])
                continue;

            int u = c.second;

            for (auto it : G.e[u]) {
                int v = it.first, w = it.second;

                if (f[v][s] > f[u][s] + w)
                    f[v][s] = f[u][s] + w, q.emplace(-f[v][s], v);
            }
        }
    }

    printf("%d", f[kp.front()][(1 << k) - 1]);
    return 0;
}

路径问题

给定一张无向图,求使得 \((1, 2), (2, 3), \cdots, (k - 1, k)\) 联通的最小边权和。

\(n \le 100\)\(m \le 1000\)\(k \le 10\)

首先可以用斯坦纳树求出 \(f_{i, S}\) 表示 \(i\)\(S\) 中所有点连通的最小代价。

\(g_S\) 表示 \(S\) 中所有点全部合法的最小边权和,其中 \((x - 1, x)\) 必须同为 \(1\)\(0\) ,分类讨论转移:

  • \(S\) 形成一个连通块:\(g_S \gets \min_i f_{i, S}\)
  • \(S\) 形成多个连通块:\(g_S \gets \min_{T \subseteq S} g_T + g_{S \setminus T}\)

时间复杂度 \(O(n \times 3^k + 2^k \times m \log m + 2^k \times n)\)

#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 1e3 + 7, K = 11;

struct Graph {
    vector<pair<int, int> > e[N];
    
    inline void insert(int u, int v, int w) {
        e[u].emplace_back(v, w);
    }
} G;

int f[N][1 << K], g[1 << K];

int n, m, k;

signed main() {
    scanf("%d%d%d", &n, &m, &k);
    
    for (int i = 1; i <= m; ++i) {
        int u, v, w;
        scanf("%d%d%d", &u, &v, &w);
        --u, --v;
        G.insert(u, v, w), G.insert(v, u, w);
    }
    
    memset(f, inf, sizeof(f));
    
    for (int i = 0; i < k; ++i)
        f[i][1 << i] = 0;
    
    for (int s = 1; s < (1 << k); ++s) {
        priority_queue<pair<int, int> > q;

        for (int i = 0; i < n; ++i) {
            for (int t = (s - 1) & s; t; t = (t - 1) & s)
                f[i][s] = min(f[i][s], f[i][t] + f[i][s ^ t]);
        
            if (f[i][s] != inf)
                q.emplace(-f[i][s], i);
        }
        
        while (!q.empty()) {
            auto c = q.top();
            q.pop();
            
            if (f[c.second][s] != -c.first)
                continue;
            
            int u = c.second;
            
            for (auto it : G.e[u]) {
                int v = it.first, w = it.second;
                
                if (f[v][s] > f[u][s] + w)
                    f[v][s] = f[u][s] + w, q.emplace(-f[v][s], v);
            }
        }
    }
    
    memset(g, inf, sizeof(g));
    
    for (int s = 1; s < (1 << k); ++s) {
        bool flag = true;
        
        for (int i = 0; i < k; i += 2)
            if ((s >> i & 1) ^ (s >> (i + 1) & 1)) {
                flag = false;
                break;
            }
        
        if (!flag)
            continue;
            
        for (int i = 0; i < n; ++i)
            g[s] = min(g[s], f[i][s]);
        
        for (int t = (s - 1) & s; t; t = (t - 1) & s)
            g[s] = min(g[s], g[t] + g[s ^ t]);
    }
    
    printf("%d", g[(1 << k) - 1]);
    return 0;
}

P7450 [THUSCH2017] 巧克力

给定 \(n, m\) 和两个 \(n \times m\) 的矩阵 \(a, c\) ,求一个矩阵的连通块使得其内部的 \(c\) 至少出现了 \(k\) 个不同的数,在最小化连通块大小的前提下最小化连通块内 \(a\) 的中位数。

\(n \times m \le 233\)\(k \le 5\)

考虑二分答案,然后将每个数归类为 \(\pm 1\)

考虑将每个数随机映射到 \([0, k)\) 的数字,然后求最小斯坦纳树。注意这里权值是放在点上的,和模板有些差别。

对于两个关键字均要最小并且有优先级的情况,可以利用余数的思想,设一个极大值 \(B\) ,第一关键字为除以 \(B\) 的商,第二关键字为除以 \(B\) 的余数。这样的好处是保证最小连通块大小为 \(ans\) 时,若求得的最小斯坦纳树的权值 \(< ans \times B\) 则说明 \(-1\) 多于 \(1\) ,二分到的中位数偏大。

一次正确的概率 \(p = \frac{k!}{k^k}\) ,做 \(N = 200\) 次正确率 \(> 99.99 \%\) ,时间复杂度 \(O(TNnm 3^k \log V)\)

#include <bits/stdc++.h>
using namespace std;
const int dx[] = {1, -1, 0, 0};
const int dy[] = {0, 0, 1, -1};
const int inf = 0x3f3f3f3f;
const int N = 3e2 + 7, K = 5;

vector<int> color;

int f[N][N][1 << K], c[N][N], a[N][N], col[N][N], val[N][N];
bool inque[N][N];

mt19937 myrand(time(0));
int n, m, k;

inline int solve(int lambda) {
    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= m; ++j)
            val[i][j] = (a[i][j] <= lambda ? N - 1 : N + 1);

    int ans = inf;

    for (int times = 1; times <= 200; ++times) {
        vector<int> id(color.size());
        iota(id.begin(), id.end(), 0);
        shuffle(id.begin(), id.end(), myrand);

        for (int i = 1; i <= n; ++i)
            for (int j = 1; j <= m; ++j) {
                memset(f[i][j], inf, sizeof(f[i][j]));

                if (~c[i][j])
                    f[i][j][1 << (id[c[i][j]] % k)] = val[i][j];
            }

        for (int s = 1; s < (1 << k); ++s) {
            queue<pair<int, int> > q;

            for (int i = 1; i <= n; ++i)
                for (int j = 1; j <= m; ++j)
                    if (~c[i][j]) {
                        for (int t = (s - 1) & s; t; t = (t - 1) & s)
                            f[i][j][s] = min(f[i][j][s], f[i][j][t] + f[i][j][s ^ t] - val[i][j]);

                        if (f[i][j][s] != inf)
                            q.emplace(i, j), inque[i][j] = true;
                    }

            while (!q.empty()) {
                int x = q.front().first, y = q.front().second;
                q.pop(), inque[x][y] = false;

                for (int i = 0; i < 4; ++i) {
                    int nx = x + dx[i], ny = y + dy[i];

                    if (1 <= nx && nx <= n && 1 <= ny && ny <= m && ~c[nx][ny] && 
                        f[nx][ny][s] > f[x][y][s] + val[nx][ny]) {
                        f[nx][ny][s] = f[x][y][s] + val[nx][ny];

                        if (!inque[nx][ny])
                            q.emplace(nx, ny), inque[nx][ny] = true;
                    }
                }
            }
        }

        for (int i = 1; i <= n; ++i)
            for (int j = 1; j <= m; ++j)
                ans = min(ans, f[i][j][(1 << k) - 1]);
    }

    return ans;
}

signed main() {
    int T;
    scanf("%d", &T);

    while (T--) {
        scanf("%d%d%d", &n, &m, &k);
        color.clear();

        for (int i = 1; i <= n; ++i)
            for (int j = 1; j <= m; ++j) {
                scanf("%d", c[i] + j);

                if (~c[i][j])
                    color.emplace_back(c[i][j]);
            }

        sort(color.begin(), color.end());
        color.erase(unique(color.begin(), color.end()), color.end());

        for (int i = 1; i <= n; ++i)
            for (int j = 1; j <= m; ++j)
                if (~c[i][j])
                    c[i][j] = lower_bound(color.begin(), color.end(), c[i][j]) - color.begin();

        for (int i = 1; i <= n; ++i)
            for (int j = 1; j <= m; ++j)
                scanf("%d", a[i] + j), val[i][j] = 1;

        int ans = solve(-1);

        if (ans == inf) {
            puts("-1 -1");
            continue;
        }

        ans /= N;
        int l = 0, r = 1e6, midnum = 0;

        while (l <= r) {
            int mid = (l + r) >> 1;

            if (solve(mid) <= ans * N)
                midnum = mid, r = mid - 1;
            else
                l = mid + 1;
        }

        printf("%d %d\n", ans, midnum);
    }

    return 0;
}

虚树

构建虚树:先将关键点按 dfn 排序,然后只要求得相邻两个节点的 LCA 即可。

考虑用单调栈维护一条虚树上的链,且栈内的节点 dfn 单调增。流程如下:

  • 先在栈中添加根,然后按 dfn 从小到大加点。
  • 若当前节点与栈顶的 LCA 即为栈顶,说明它们在一条链上,直接入栈即可。
  • 否则不断弹出栈顶元素直到栈顶元素不在 LCA 的子树内,弹出时记得连边,然后将 LCA 与该点入栈,需要特殊处理一下栈内存在 LCA 的情况。
  • 重复上述操作,最后将栈内相邻元素连边即可。
inline int build(vector<int> kp) {
    sort(kp.begin(), kp.end(), [](const int &a, const int &b) { return dfn[a] < dfn[b]; });
    kp.erase(unique(kp.begin(), kp.end()), kp.end());
    int top = 0, r = sta[++top] = LCA(kp[0], kp.back());
    T.e[r].clear();

    for (int x : kp) {
        if (x == r)
            continue;

        int lca = LCA(sta[top], x);

        if (lca != sta[top]) {
            for (; dfn[lca] < dfn[sta[top - 1]]; --top)
                T.insert(sta[top - 1], sta[top]);

            if (sta[top - 1] == lca)
                T.insert(lca, sta[top--]);
            else
                T.e[lca].clear(), T.insert(lca, sta[top]), sta[top] = lca;
        }

        T.e[x].clear(), sta[++top] = x;
    }

    for (; top > 1; --top)
        T.insert(sta[top - 1], sta[top]);
    
    return r;
}

P3320 [SDOI2015] 寻宝游戏

给出一棵 \(n\) 个点的树,边带边权。

每个点可能是关键点,每次操作改变一个点是否是关键点。

求从一个关键点开始遍历所有关键点的最短路程。

\(n \le 10^5\)

答案即为所有关键点构成的极小连通子树的边权和的两倍。

\(u_{1 \sim m}\) 为关键点按 dfn 排序后的结果,则极小连通子树的边权和为:

\[\frac{1}{2} \sum_{i = 1}^m \mathrm{dist}(u_i, u_{(i \bmod m) + 1}) \]

set 维护即可做到 \(O(n \log n)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 1e5 + 7, LOGN = 17;

struct Graph {
    vector<pair<int, int> > e[N];
    
    inline void insert(const int u, const int v, const int w) {
        e[u].emplace_back(v, w);
    }
} G;

ll dis[N];
int fa[N][LOGN], dep[N], dfn[N], id[N];
bool exist[N];

int n, m, dfstime;

void dfs(int u, int f) {
    fa[u][0] = f, dep[u] = dep[f] + 1, id[dfn[u] = ++dfstime] = u;
    
    for (int i = 1; i < LOGN; ++i)
        fa[u][i] = fa[fa[u][i - 1]][i - 1];
    
    for (auto it : G.e[u]) {
        int v = it.first, w = it.second;
        
        if (v != f)
            dis[v] = dis[u] + w, dfs(v, u);
    }
}

inline int LCA(int x, int y) {
    if (dep[x] < dep[y])
        swap(x, y);
    
    for (int h = dep[x] - dep[y]; h; h &= h - 1)
        x = fa[x][__builtin_ctz(h)];

    if (x == y)
        return x;

    for (int i = LOGN - 1; ~i; --i)
        if (fa[x][i] != fa[y][i])
            x = fa[x][i], y = fa[y][i];

    return fa[x][0];
}

inline ll dist(int x, int y) {
    return dis[x] + dis[y] - dis[LCA(x, y)] * 2;
}

signed main() {
    scanf("%d%d", &n, &m);
    
    for (int i = 1; i < n; ++i) {
        int u, v, w;
        scanf("%d%d%d", &u, &v, &w);
        G.insert(u, v, w), G.insert(v, u, w);
    }
    
    dfs(1, 0);
    set<int> st;
    ll ans = 0;
    
    while (m--) {
        int x;
        scanf("%d", &x);
        
        if (!exist[x])
            st.insert(dfn[x]);

        auto it = st.find(dfn[x]);
        
        int y = id[it == st.begin() ? *st.rbegin() : *prev(it)],
            z = id[next(it) == st.end() ? *st.begin() : *next(it)];
        
        if (exist[x])
            st.erase(dfn[x]);
        
        if (exist[x])
            exist[x] = false, ans -= dist(x, y) + dist(x, z) - dist(y, z);
        else
            exist[x] = true, ans += dist(x, y) + dist(x, z) - dist(y, z);
        
        printf("%lld\n", ans);
    }

    return 0;
}

P8339 [AHOI2022] 钥匙

给定一棵树,每个点上都有一个宝箱或钥匙。宝箱和钥匙都有各自的颜色,一把钥匙可以开同颜色的宝箱。\(m\) 次询问,每次询问从 \(s\) 不回头走到 \(t\) 最多能开的宝箱数量,询问之间相互独立。

\(n, \le 5 \times 10^5\)\(m \le 10^6\) ,同一种颜色的钥匙最多 \(5\) 把。

发现每种颜色的贡献是独立的,考虑对每种颜色都建出虚树。容易发现虚树上钥匙和宝箱的配对可以简化为括号匹配的过程。考虑对每个钥匙开始在虚树上深搜,遇到一个钥匙就 \(+1\) ,遇到一个宝箱就 \(-1\) ,第一次为 \(0\) 时则匹配成功。

考虑一对钥匙-宝箱 \((x, y)\) 会对哪些询问 \((s, t)\) 产生贡献。

  • \(x, y\) 不为祖先关系:则 \(dfn_s \in [dfn_x, dfn_x + siz_x - 1]\)\(dfn_t \in [dfn_y, dfn_y + siz_y - 1]\)
  • \(x\)\(y\) 的祖先:记 \(y\)\(x\) 的儿子 \(u\) 的子树内,则 \(dfn_s \in [1, dfn_u - 1] \cup [dfn_u + siz_u, n]\)\(dfn_t \in [dfn_y, dfn_y + siz_y - 1]\)
  • \(y\)\(x\) 的祖先:记 \(x\)\(y\) 的儿子 \(u\) 的子树内,则 \(dfn_s \in [dfn_x, dfn_x + siz_x - 1]\)\(dfn_t \in [1, dfn_u - 1] \cup [dfn_u + siz_u, n]\)

问题转化为查询一个点上有多少矩形,直接扫描线即可做到 \(O((n + m) \log n)\)

#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 7, M = 1e6 + 7, S = 5e6 + 7, LOGN = 19;

struct Graph {
    vector<int> e[N];
    
    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G, T;

struct Node {
    int op, x, l, r, k;

    inline bool operator < (const Node &rhs) const {
        return x == rhs.x ? op < rhs.op : x < rhs.x;
    }
} nd[S];

vector<int> vec[N];

int fa[N][LOGN], op[N], col[N], dep[N], in[N], out[N], sta[N], ans[M];

int n, m, dfstime, tot;

void dfs1(int u, int f) {
    fa[u][0] = f, dep[u] = dep[f] + 1, in[u] = ++dfstime;

    for (int i = 1; i < LOGN; ++i)
        fa[u][i] = fa[fa[u][i - 1]][i - 1];

    for (int v : G.e[u])
        if (v != f)
            dfs1(v, u);

    out[u] = dfstime;
}

inline int jump(int x, int h) {
    for (; h; h &= h - 1)
        x = fa[x][__builtin_ctz(h)];

    return x;
}

inline int LCA(int x, int y) {
    if (dep[x] < dep[y])
        swap(x, y);

    x = jump(x, dep[x] - dep[y]);

    if (x == y)
        return x;

    for (int i = LOGN - 1; ~i; --i)
        if (fa[x][i] != fa[y][i])
            x = fa[x][i], y = fa[y][i];

    return fa[x][0];
}

inline int build(vector<int> kp) {
    sort(kp.begin(), kp.end(), [](const int &a, const int &b) { return in[a] < in[b]; });
    kp.erase(unique(kp.begin(), kp.end()), kp.end());
    int top = 0, r = sta[++top] = LCA(kp[0], kp.back());
    T.e[r].clear();

    for (int x : kp) {
        if (x == r)
            continue;

        int lca = LCA(sta[top], x);

        if (lca != sta[top]) {
            while (top > 1 && in[lca] < in[sta[top - 1]])
                T.insert(sta[top - 1], sta[top]), T.insert(sta[top], sta[top - 1]), --top;

            if (sta[top - 1] == lca)
                T.insert(lca, sta[top]), T.insert(sta[top--], lca);
            else
                T.e[lca].clear(), T.insert(lca, sta[top]), T.insert(sta[top], lca), sta[top] = lca;
        }

        T.e[x].clear(), sta[++top] = x;
    }

    for (; top > 1; --top)
        T.insert(sta[top - 1], sta[top]), T.insert(sta[top], sta[top - 1]);

    return r;
}

void dfs2(int u, int f, int s, const int rt) {
    if (col[u] == col[rt]) {
        s += (op[u] == 1 ? 1 : -1);

        if (!s) {
            int lca = LCA(rt, u);

            if (lca == rt) {
                int x = jump(u, dep[u] - dep[rt] - 1);
                nd[++tot] = (Node){1, 1, in[u], out[u], 1};
                nd[++tot] = (Node){1, in[x], in[u], out[u], -1};
                nd[++tot] = (Node){1, out[x] + 1, in[u], out[u], 1};
            } else if (lca == u) {
                int x = jump(rt, dep[rt] - dep[u] - 1);

                if (in[x] > 1) {
                    nd[++tot] = (Node){1, in[rt], 1, in[x] - 1, 1};
                    nd[++tot] = (Node){1, out[rt] + 1, 1, in[x] - 1, -1};
                }

                if (out[x] < n) {
                    nd[++tot] = (Node){1, in[rt], out[x] + 1, n, 1};
                    nd[++tot] = (Node){1, out[rt] + 1, out[x] + 1, n, -1};
                }
            } else {
                nd[++tot] = (Node){1, in[rt], in[u], out[u], 1};
                nd[++tot] = (Node){1, out[rt] + 1, in[u], out[u], -1};
            }

            return;
        }
    }
    
    for (int v : T.e[u])
        if (v != f)
            dfs2(v, u, s, rt);
}

namespace BIT {
int c[N];

inline void modify(int x, int k) {
    for (; x <= n; x += x & -x)
        c[x] += k;
}

inline void update(int l, int r, int k) {
    modify(l, k), modify(r + 1, -k);
}

inline int query(int x) {
    int res = 0;

    for (; x; x -= x & -x)
        res += c[x];

    return res;
}
} // namespace BIT

signed main() {
    scanf("%d%d", &n, &m);

    for (int i = 1; i <= n; ++i)
        scanf("%d%d", op + i, col + i), vec[col[i]].emplace_back(i);

    for (int i = 1; i < n; ++i) {
        int u, v;
        scanf("%d%d", &u, &v);
        G.insert(u, v), G.insert(v, u);
    }

    dfs1(1, 0);

    for (int i = 1; i <= n; ++i) {
        if (vec[i].empty())
            continue;

        build(vec[i]);

        for (int it : vec[i])
            if (op[it] == 1)
                dfs2(it, 0, 0, it);
    }

    for (int i = 1; i <= m; ++i) {
        int x, y;
        scanf("%d%d", &x, &y);
        nd[++tot].op = 2, nd[tot].x = in[x], nd[tot].l = in[y], nd[tot].k = i;
    }

    sort(nd + 1, nd + tot + 1);

    for (int i = 1; i <= tot; ++i) {
        if (nd[i].op == 1)
            BIT::update(nd[i].l, nd[i].r, nd[i].k);
        else
            ans[nd[i].k] = BIT::query(nd[i].l);
    }

    for (int i = 1; i <= m; ++i)
        printf("%d\n", ans[i]);

    return 0;
}

P9479 [NOI2023] 桂花树

给出一棵 \(n\) 个点的树 \(T\) ,保证 \(fa_i < i\) 。定义一棵 \(n + m\) 个点的树 \(T'\) 是好的当且仅当:

  • \(\forall 1 \le i, j \le n, \mathrm{LCA}_T(i, j) = \mathrm{LCA}_{T'}(i, j)\)
  • \(\forall 1 \le i, j \le n + m, \mathrm{LCA}_{T'}(i, j) \le \max(i, j) + k\) ,其中 \(k\) 是给定的常数。

求好的树的数量 \(\bmod (10^9 + 7)\)

\(n \le 30000\)\(m \le 3000\)\(k \le 10\)

观察 \(T'\)\(T\) 的结构差异,不难发现 \(T'\) 中的点 \(x \in [n + 1, n + m]\) 只能在 \(T\) 的基础上加入,此时判定条件转化为:

  • \(T'\)\(1 \sim n\) 的虚树为其本身。
  • \(T'\)\(1 \sim i\) 的虚树所有点的标号 \(\le i + k\)

考虑按标号升序确定点 \(x \in [n + 1, n + m]\) ,则每次至多只有 \(k\) 个点未确定。设 \(f_{i, S}\) 表示决策了前 \(i\) 个点,其中未确定的点的状态为 \(S\) 的方案数。记 \(c = n + i + |S|\) 表示当前树的点数,\(T = \{ x + 1 \mid x \in S \}\) ,考虑 \(i + 1\) 的决策:

  • 挂在一个点上作为叶子:\(c \times f_S \to f'_T\)
  • 插到一条边中间:\((c - 1) \times f_S \to f'_T\)
  • 在一条边上新建一个未确定的点,将其挂在该点上作为叶子:\((c - 1) \times f_S \to f'_{T \cup \{ 1 \}}\)
  • 确定一个未确定的点(贪心确定最小的):\(f_S \to f'_{T \setminus \{ (\min S) + 1 \}}\)

时间复杂度 \(O(m k 2^k)\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 3e4 + 7;

int fa[N];

int n, m, k;

inline int add(int x, int y) {
    x += y;

    if (x >= Mod)
        x -= Mod;

    return x;
}

inline int dec(int x, int y) {
    x -= y;

    if (x < 0)
        x += Mod;

    return x;
}

signed main() {
    int testid, T;
    scanf("%d%d", &testid, &T);

    while (T--) {
        scanf("%d%d%d", &n, &m, &k);

        for (int i = 2; i <= n; ++i)
            scanf("%d", fa + i);

        vector<int> f(1 << k);
        f[0] = 1;

        for (int i = 0; i < m; ++i) {
            vector<int> g(1 << k);

            for (int s = 0; s < (1 << k); ++s) {
                for (int r = s; r; r = (r - 1) & r) {
                    int t = (s ^ (r & -r)) << 1;

                    if (t < (1 << k))
                        g[t] = add(g[t], f[s]);
                }

                int t = s << 1, c = n + i + __builtin_popcount(s);

                if (t < (1 << k)) {
                    g[t] = add(g[t], 1ll * f[s] * (c * 2 - 1) % Mod);
                    g[t | 1] = add(g[t | 1], 1ll * f[s] * (c - 1) % Mod);
                }
            }

            f = g;
        }

        printf("%d\n", f[0]);
    }

    return 0;
}

P5327 [ZJOI2019] 语言

给定一棵树,每个点初始都有一个空集,\(m\) 次操作,第 \(i\) 次操作向一条树上路径上所有点的集合都加入 \(i\) 。最后求有多少对点满足它们的集合交非空。

\(n, m \le 10^5\)

考虑对每个 \(u\) 统计合法的 \(v\) 的数量。

首先可以发现合法的 \(v\) 构成一个连通块,考虑这个连通块的构成,即所有包含 \(u\) 的路径的两个端点组成的极小连通块。

根据虚树的经典结论,考虑用线段树维护每个点的集合,虚树大小直接线段树合并维护即可(按 dfn 为下标建树,减去左区间 dfn 最大的点和右区间 dfn 最小的点的 LCA 深度即可)。

pushup 的时候需要求 LCA,使用欧拉序 + ST 表即可做到 \(O(n \log n)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 1e5 + 7, LOGN = 19;

struct Graph {
    vector<int> e[N];
    
    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G;

vector<int> upd[N];

int st[LOGN][N << 1], fa[N], dep[N], dfn[N], id[N << 1], pos[N];

ll ans;
int n, m, dfstime, cnt;

void dfs1(int u, int f) {
    fa[u] = f, dep[u] = dep[f] + 1, dfn[u] = ++dfstime, id[pos[u] = ++cnt] = u;

    for (int v : G.e[u])
        if (v != f)
            dfs1(v, u), id[++cnt] = u;
}

inline int cmp(const int &a, const int &b) {
    return pos[a] < pos[b] ? a : b;
}

inline int LCA(int x, int y) {
    int l = pos[x], r = pos[y];

    if (l > r)
        swap(l, r);

    int k = __lg(r - l + 1);
    return cmp(st[k][l], st[k][r - (1 << k) + 1]);
}

namespace SMT {
const int S = 3e7 + 7;

int rt[N], lc[S], rc[S], cnt[S], f[S], s[S], t[S];

int tot;

inline void pushup(int x) {
    f[x] = f[lc[x]] + f[rc[x]] - dep[LCA(t[lc[x]], s[rc[x]])];
    s[x] = s[lc[x]] ? s[lc[x]] : s[rc[x]], t[x] = t[rc[x]] ? t[rc[x]] : t[lc[x]];
}

void update(int &x, int nl, int nr, int p, int k) {
    if (!x)
        x = ++tot;

    if (nl == nr) {
        cnt[x] += k;
        f[x] = cnt[x] ? dep[p] : 0;
        s[x] = t[x] = cnt[x] ? p : 0;
        return;
    }

    int mid = (nl + nr) >> 1;

    if (dfn[p] <= mid)
        update(lc[x], nl, mid, p, k);
    else
        update(rc[x], mid + 1, nr, p, k);

    pushup(x);
}

int merge(int x, int y, int l, int r) {
    if (!x || !y)
        return x | y;

    if (l == r) {
        cnt[x] += cnt[y], f[x] |= f[y], s[x] |= s[y], t[x] |= t[y];
        return x;
    }

    int mid = (l + r) >> 1;
    lc[x] = merge(lc[x], lc[y], l, mid), rc[x] = merge(rc[x], rc[y], mid + 1, r);
    return pushup(x), x;
}

inline int query(int x) {
    return f[x] - dep[LCA(s[x], t[x])];
}
} // namespace SMT

void dfs2(int u) {
    for (int v : G.e[u])
        if (v != fa[u])
            dfs2(v), SMT::rt[u] = SMT::merge(SMT::rt[u], SMT::rt[v], 1, n);

    for (int it : upd[u])
        SMT::update(SMT::rt[u], 1, n, it, -1);

    ans += SMT::query(SMT::rt[u]);
}

signed main() {
    scanf("%d%d", &n, &m);

    for (int i = 1; i < n; ++i) {
        int u, v;
        scanf("%d%d", &u, &v);
        G.insert(u, v), G.insert(v, u);
    }

    dfs1(1, 0);
    memcpy(st[0] + 1, id + 1, sizeof(int) * cnt);

    for (int j = 1; j <= __lg(cnt); ++j)
        for (int i = 1; i + (1 << j) - 1 <= cnt; ++i)
            st[j][i] = cmp(st[j - 1][i], st[j - 1][i + (1 << (j - 1))]);

    while (m--) {
        int x, y;
        scanf("%d%d", &x, &y);
        int lca = LCA(x, y);
        SMT::update(SMT::rt[x], 1, n, x, 1), SMT::update(SMT::rt[x], 1, n, y, 1);
        SMT::update(SMT::rt[y], 1, n, x, 1), SMT::update(SMT::rt[y], 1, n, y, 1);
        upd[lca].emplace_back(x), upd[lca].emplace_back(y);
        upd[fa[lca]].emplace_back(x), upd[fa[lca]].emplace_back(y);
    }

    dfs2(1);
    printf("%lld", ans / 2);
    return 0;
}

CF1725E Electrical Efficiency

给定一棵树,点带点权。记:

  • \(f(x, y, z)\) 为树上连通 \(x, y, z\) 三个点的连通块的最小边数。
  • \(g(x, y, z)\)\(\gcd(a_x, a_y, a_z)\) 所含的不同质因子个数。

求:

\[(\sum_{x < y < z} f(x, y, z) \times g(x, y, z)) \bmod 998244353 \]

\(n, a_i \le 2 \times 10^5\)

考虑枚举质因子 \(x\) ,对 \(x \mid a_u\)\(u\) 组成的虚树一起统计贡献和。

发现 \(f(x, y, z) = \frac{1}{2} (\mathrm{dist}(x, y) + \mathrm{dist}(x, z) + \mathrm{dist}(y, z))\) ,因此只要统计 \(\sum_{x, y} \mathrm{dist}(x, y)\) 即可,枚举 LCA 不难统计。

时间复杂度 \(O(n \omega(V) \log n)\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353, inv2 = (Mod + 1) / 2;
const int N = 2e5 + 7, LOGN = 19;

struct Graph {
    vector<int> e[N];
    
    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G, T;

vector<int> vec[N];

int fa[N][LOGN], a[N], dep[N], dfn[N], sta[N];
bool flag[N];

int n, dfstime, ans;

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

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

void dfs1(int u, int f) {
    fa[u][0] = f, dep[u] = dep[f] + 1, dfn[u] = ++dfstime;

    for (int i = 1; i < LOGN; ++i)
        fa[u][i] = fa[fa[u][i - 1]][i - 1];

    for (int v : G.e[u])
        if (v != f)
            dfs1(v, u);
}

inline int LCA(int x, int y) {
    if (dep[x] < dep[y])
        swap(x, y);

    for (int h = dep[x] - dep[y]; h; h &= h - 1)
        x = fa[x][__builtin_ctz(h)];

    if (x == y)
        return x;

    for (int i = LOGN - 1; ~i; --i)
        if (fa[x][i] != fa[y][i])
            x = fa[x][i], y = fa[y][i];

    return fa[x][0];
}

inline int build(vector<int> kp) {
    sort(kp.begin(), kp.end(), [](const int &a, const int &b) { return dfn[a] < dfn[b]; });
    kp.erase(unique(kp.begin(), kp.end()), kp.end());
    int top = 0, r = sta[++top] = LCA(kp[0], kp.back());
    T.e[r].clear();

    for (int x : kp) {
        if (x == r)
            continue;

        int lca = LCA(sta[top], x);

        if (lca != sta[top]) {
            while (top > 1 && dfn[lca] < dfn[sta[top - 1]])
                T.insert(sta[top - 1], sta[top]), --top;

            if (sta[top - 1] == lca)
                T.insert(lca, sta[top--]);
            else
                T.e[lca].clear(), T.insert(lca, sta[top]), sta[top] = lca;
        }

        T.e[x].clear(), sta[++top] = x;
    }

    for (; top > 1; --top)
        T.insert(sta[top - 1], sta[top]);

    return r;
}

tuple<int, int, int> dfs2(int u) {
    tuple<int, int, int> ans = make_tuple(flag[u], flag[u] ? dep[u] : 0, 0);

    for (int v : T.e[u]) {
        auto res = dfs2(v);

        get<2>(ans) = add(get<2>(ans), 1ll * get<1>(res) * get<0>(ans) % Mod);
        get<2>(ans) = add(get<2>(ans), 1ll * get<1>(ans) * get<0>(res) % Mod);
        get<2>(ans) = dec(get<2>(ans), 2ll * dep[u] % Mod * get<0>(ans) % Mod * get<0>(res) % Mod);

        get<0>(ans) += get<0>(res), get<1>(ans) = add(get<1>(ans), get<1>(res));
        get<2>(ans) = add(get<2>(ans), get<2>(res));
    }

    return ans;
}

signed main() {
    scanf("%d", &n);

    for (int i = 1; i <= n; ++i) {
        int x;
        scanf("%d", &x);

        for (int j = 2; j * j <= x; ++j)
            if (!(x % j)) {
                vec[j].emplace_back(i);

                while (!(x % j))
                    x /= j;
            }

        if (x > 1)
            vec[x].emplace_back(i);
    }

    for (int i = 1; i < n; ++i) {
        int u, v;
        scanf("%d%d", &u, &v);
        G.insert(u, v), G.insert(v, u);
    }

    dfs1(1, 0);
    int ans = 0;

    for (int i = 1; i < N; ++i)
        if (vec[i].size() >= 2) {
            for (int it : vec[i])
                flag[it] = true;

            auto res = dfs2(build(vec[i]));

            for (int it : vec[i])
                flag[it] = false;

            ans = add(ans, 1ll * (get<0>(res) - 2) * get<2>(res) % Mod);
        }

    printf("%d", 1ll * ans * inv2 % Mod);
    return 0;
}

P6071 『MdOI R1』Treequery

给定一棵 \(n\) 个点的树,边带边权。

\(q\) 次询问,每次给出 \(x, l, r\) ,求 \(x\)\(l \sim r\) 每一个点的路径的交集长度。

\(n, q \le 2 \times 10^5\) ,强制在线

\(rt\)\(l \sim r\) 虚树的根,考虑分类讨论 \(x\)\(l \sim r\) 点的位置关系:

  • \(l \sim r\) 全部位于 \(x\) 子树内,答案即为 \(\mathrm{dist}(x, rt)\)
  • \(l \sim r\) 全部位于 \(x\) 子树外,答案即为 \(x\)\(l \sim r\) 虚树的距离。
  • 否则答案为 \(0\)

下面考虑求 \(u\)\(l \sim r\) 虚树的距离,不妨再次对 \(x\)\(rt\) 的位置关系分类讨论:

  • \(x\) 不在 \(rt\) 子树内,答案即为 \(\mathrm{dist}(x, rt)\)
  • 否则尝试将 \(x\) 插入虚树,找到 \(x\)\(l \sim r\) 中 dfs 序的前驱后继,和 \(x\) 可以求出两个 LCA,则 \(x\) 到两个 LCA距离的较小值即为答案。

在主席树上二分即可找到 \(x\)\(l \sim r\) 中 dfs 序的前驱后继,时间复杂度 \(O((n + q) \log n)\)

#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 2e5 + 7, LOGN = 19;

struct Graph {
    vector<pair<int, int> > e[N];
    
    inline void insert(int u, int v, int w) {
        e[u].emplace_back(v, w);
    }
} G;

int fa[N][LOGN], dep[N], dis[N], in[N], out[N], id[N];

int n, q, dfstime;

void dfs(int u, int f) {
    fa[u][0] = f, dep[u] = dep[f] + 1, id[in[u] = ++dfstime] = u;

    for (int i = 1; i < LOGN; ++i)
        fa[u][i] = fa[fa[u][i - 1]][i - 1];

    for (auto it : G.e[u]) {
        int v = it.first, w = it.second;

        if (v != f)
            dis[v] = dis[u] + w, dfs(v, u);
    }

    out[u] = dfstime;
}

inline int LCA(int x, int y) {
    if (dep[x] < dep[y])
        swap(x, y);

    for (int h = dep[x] - dep[y]; h; h &= h - 1)
        x = fa[x][__builtin_ctz(h)];

    if (x == y)
        return x;

    for (int i = LOGN - 1; ~i; --i)
        if (fa[x][i] != fa[y][i])
            x = fa[x][i], y = fa[y][i];

    return fa[x][0];
}

namespace SMT {
const int S = 3e7 + 7;

int rt[N], lc[S], rc[S], s[S], mx[S];

int tot;

int insert(int x, int nl, int nr, int p) {
    int y = ++tot;
    lc[y] = lc[x], rc[y] = rc[x], s[y] = s[x] + 1;

    if (nl == nr)
        return mx[y] = p, y;

    int mid = (nl + nr) >> 1;

    if (in[p] <= mid)
        lc[y] = insert(lc[x], nl, mid, p);
    else
        rc[y] = insert(rc[x], mid + 1, nr, p);

    mx[y] = max(mx[lc[y]], mx[rc[y]]);
    return y;
}

int querysum(int x, int y, int nl, int nr, int l, int r) {
    if (l <= nl && nr <= r)
        return s[y] - s[x];

    int mid = (nl + nr) >> 1;

    if (r <= mid)
        return querysum(lc[x], lc[y], nl, mid, l, r);
    else if (l > mid)
        return querysum(rc[x], rc[y], mid + 1, nr, l, r);
    else
        return querysum(lc[x], lc[y], nl, mid, l, r) + querysum(rc[x], rc[y], mid + 1, nr, l, r);
}

int querymn(int x, int nl, int nr, int p) {
    if (nl == nr)
        return mx[x];

    int mid = (nl + nr) >> 1;
    return lc[x] && mx[lc[x]] >= p ? querymn(lc[x], nl, mid, p) : querymn(rc[x], mid + 1, nr, p);
}

int querymx(int x, int nl, int nr, int p) {
    if (nl == nr)
        return mx[x];

    int mid = (nl + nr) >> 1;
    return rc[x] && mx[rc[x]] >= p ? querymx(rc[x], mid + 1, nr, p) : querymx(lc[x], nl, mid, p);
}

int querypre(int x, int nl, int nr, int p1, int p2) {
    if (!x || mx[x] < p1 || nl > p2)
        return -1;

    if (nl == nr)
        return mx[x];

    int mid = (nl + nr) >> 1, res = querypre(rc[x], mid + 1, nr, p1, p2);
    return ~res ? res : querypre(lc[x], nl, mid, p1, p2);
}

int querynxt(int x, int nl, int nr, int p1, int p2) {
    if (!x || mx[x] < p1 || nr < p2)
        return -1;

    if (nl == nr)
        return mx[x];

    int mid = (nl + nr) >> 1, res = querynxt(lc[x], nl, mid, p1, p2);
    return ~res ? res : querynxt(rc[x], mid + 1, nr, p1, p2);
}
} // namespace SMT

signed main() {
    scanf("%d%d", &n, &q);

    for (int i = 1; i < n; ++i) {
        int u, v, w;
        scanf("%d%d%d", &u, &v, &w);
        G.insert(u, v, w), G.insert(v, u, w);
    }

    dfs(1, 0);

    for (int i = 1; i <= n; ++i)
        SMT::rt[i] = SMT::insert(SMT::rt[i - 1], 1, n, i);

    int lstans = 0;

    while (q--) {
        int x, l, r;
        scanf("%d%d%d", &x, &l, &r);
        x ^= lstans, l ^= lstans, r ^= lstans;
        int lca = LCA(SMT::querymn(SMT::rt[r], 1, n, l), SMT::querymx(SMT::rt[r], 1, n, l));

        if (in[x] <= in[lca] && in[lca] <= out[x])
            printf("%d\n", lstans = dis[lca] - dis[x]);
        else if (SMT::querysum(SMT::rt[l - 1], SMT::rt[r], 1, n, in[x], out[x]))
            printf("%d\n", lstans = 0);
        else if (in[lca] <= in[x] && in[x] <= out[lca]) {
            int pre = SMT::querypre(SMT::rt[r], 1, n, l, in[x]), nxt = SMT::querynxt(SMT::rt[r], 1, n, l, in[x]);
            printf("%d\n", lstans = min(~pre ? dis[x] - dis[LCA(x, pre)] : inf, ~nxt ? dis[x] - dis[LCA(x, nxt)] : inf));
        } else
            printf("%d\n", lstans = dis[x] + dis[lca] - dis[LCA(x, lca)] * 2);
    }

    return 0;
}

长链剖分

长链剖分在维护有关深度的信息时具有显著优势。

定义长儿子为子树内深度最大的儿子,不难使用类似重链剖分的方式求出长儿子:

void dfs1(int u, int f) {
    fa[u] = f, len[u] = 1;

    for (int v : G.e[u]) {
        if (v == f)
            continue;

        dfs1(v, u);

        if (len[v] >= len[u])
            son[u] = v, len[u] = len[v] + 1;
    }
}

性质:

  • 一个节点到所在长链底部的路径为其到子树内所有节点的路径中最长的一条。
  • 任意节点 \(u\)\(k\) 级祖先 \(f\) 所在链的长度一定 \(\ge k\)
  • 任意节点到达根节点经过的长链数是 \(O(\sqrt{n})\) 的,即最多经过 \(O(\sqrt{n})\) 条虚边。

对于一类下标为深度的 DP,如果直接朴素实现,则可以通过构造一条链将时间复杂度卡到 \(O(n^2)\)

考虑直接 \(O(1)\) 继承一个长儿子的信息,并添加当前点的信息,接着暴力合并其他儿子的信息。

由于暴力合并的复杂度为短链的长度,所有链的长度之和是 \(O(n)\) 的,故暴力合并带来的总复杂度为 \(O(n)\)

O(1) 继承一个长儿子的信息主要有三种实现:

  • 给每个 DP 数组开指针,然后直接继承长儿子的指针。
  • 给每个 DP 数组开成 vector ,然后直接和长儿子的 vectorswap
  • 按 dfs 序存储 DP 状态,钦定一条长链的 dfs 序递增即可。

P5903 【模板】树上 k 级祖先

给定一棵树,\(q\) 次询问,每次给出 \(x, k\) ,求 \(x\)\(k\) 级祖先。

\(n \le 5 \times 10^5\)\(q \le 5 \times 10^6\)

考虑预处理:

  • 每条长链的顶点和深度。
  • 倍增求出每个点的 \(2^k\) 级祖先。
  • 对于每条长链,记其长度为 \(len\) ,那么在顶点处记录顶点向上的 \(len\) 个祖先和向下的 \(len\) 个链上的点。

询问时先利用倍增数组将 \(x\) 跳到 \(x\)\(2^{h_k}\) 级祖先(其中 \(h_k\) 表示 \(k\) 在二进制下的最高位),设剩下还有 \(k^{\prime}\) 级,显然有 \(k^{\prime} < 2^{h_k}\) 。而此时 \(x\) 所在的长链长度一定 \(\ge 2^{h_k} > k^{\prime}\) ,因此可以先将 \(x\) 跳到 \(x\) 所在链的顶点,若之后剩下的级数为正,则利用向上的数组求出答案,否则利用向下的数组求出答案。

时间复杂度 \(O(n \log n + q)\)

#include <bits/stdc++.h>
typedef unsigned int uint;
typedef long long ll;
using namespace std;
const int N = 5e5 + 7, LOGN = 19;

struct Graph {
    vector<int> e[N];
    
    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G;

vector<int> up[N], down[N];

int fa[N][LOGN], dep[N], len[N], son[N], top[N];

ll ans;
uint s;
int n, q, root;

inline uint get(uint x) {
    return x ^= x << 13, x ^= x >> 17, x ^= x << 5, s = x;
}

void dfs1(int u) {
    dep[u] = dep[fa[u][0]] + 1, len[u] = 1;

    for (int i = 1; i < LOGN; ++i)
        fa[u][i] = fa[fa[u][i - 1]][i - 1];

    for (int v : G.e[u]) {
        dfs1(v);

        if (len[v] + 1 > len[u])
            son[u] = v, len[u] = len[v] + 1;
    }
}

void dfs2(int u, int topf) {
    top[u] = topf;

    if (u == topf) {
        up[u].resize(len[u] + 1);

        for (int i = 1, x = fa[u][0]; i <= len[u]; ++i, x = fa[x][0])
            up[u][i] = x;

        down[u].resize(len[u] + 1);

        for (int i = 1, x = son[u]; i <= len[u]; ++i, x = son[x])
            down[u][i] = x;
    }

    if (son[u])
        dfs2(son[u], topf);

    for (int v : G.e[u])
        if (v != son[u])
            dfs2(v, v);
}

inline int query(int x, int k) {
    if (!k)
        return x;

    x = fa[x][__lg(k)], k -= 1 << __lg(k);

    if (!k)
        return x;
    
    k -= dep[x] - dep[top[x]], x = top[x];
    return k ? (k > 0 ? up[x][k] : down[x][-k]) : x;
}

signed main() {
    scanf("%d%d%u", &n, &q, &s);

    for (int i = 1; i <= n; ++i) {
        scanf("%d", fa[i] + 0);

        if (fa[i][0])
            G.insert(fa[i][0], i);
        else
            root = i;
    }

    dfs1(root), dfs2(root, root);
    int lstans = 0;

    for (int i = 1; i <= q; ++i) {
        int x = (get(s) ^ lstans) % n + 1, k = (get(s) ^ lstans) % dep[x];
        ans ^= 1ll * i * (lstans = query(x, k));
    }

    printf("%lld", ans);
    return 0;
}

CF1009F 2Dominant Indices

求子树内深度的众数。

\(n \le 10^6\)

\(f_{i, j}\)\(i\) 的子树内到 \(i\) 距离为 \(j\) 的节点数量,则:

\[\begin{aligned} f_{i, 0} &= 1 \\ f_{i, j} &= \sum_{v \in son_u} f_{v, j - 1} \end{aligned} \]

考虑优化,对于一个节点,先遍历它的重儿子,继承重儿子的结果,并添加当前点的信息。然后遍历轻儿子,将轻儿子的结果并到当前点上。

因为每条重链都只合并一次,时间复杂度 \(O(n)\)

#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 7;

struct Graph {
    vector<int> e[N];
    
    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G;

int buf[N], *f[N], *now = buf;
int fa[N], len[N], son[N], ans[N];

int n;

void dfs1(int u, int f) {
    fa[u] = f, len[u] = 1;

    for (int v : G.e[u]) {
        if (v == f)
            continue;

        dfs1(v, u);

        if (len[v] >= len[u])
            son[u] = v, len[u] = len[v] + 1;
    }
}

void dfs2(int u) {
    auto cmp = [&](const int &a, const int &b) {
        return (f[u][a] == f[u][b] ? a < b : f[u][a] > f[u][b]) ? a : b;
    };

    f[u][0] = 1, ans[u] = 0;

    if (son[u])
        f[son[u]] = f[u] + 1, dfs2(son[u]), ans[u] = cmp(ans[u], ans[son[u]] + 1);

    for (int v : G.e[u]) {
        if (v == fa[u] || v == son[u])
            continue;

        f[v] = now, now += len[v], dfs2(v);

        for (int i = 0; i < len[v]; ++i)
            f[u][i + 1] += f[v][i], ans[u] = cmp(ans[u], i + 1);
    }
}

signed main() {
    scanf("%d", &n);

    for (int i = 1; i < n; ++i) {
        int u, v;
        scanf("%d%d", &u, &v);
        G.insert(u, v), G.insert(v, u);
    }

    dfs1(1, n);
    f[1] = now, now += len[1], dfs2(1);

    for (int i = 1; i <= n; ++i)
        printf("%d\n", ans[i]);

    return 0;
}

P3899 [湖南集训] 更为厉害

给定一棵 \(n\) 个节点的有根树。定义:

  • \(a \ne b\)\(a\)\(b\) 的祖先,那么称“\(a\)\(b\) 更为厉害”。
  • \(a \ne b\)\(a\)\(b\) 在树上的距离不超过某个给定常数 \(x\),那么称“ \(a\)\(b\) 彼此彼此”。

\(q\) 次询问,每次给出 \(p, k\),求有多少个有序二元组 \((b, c)\) 满足:

  • \(p, b, c\) 互异。
  • \(p\)\(b\) 都比 \(c\) 更为厉害。
  • \(p\)\(b\) 彼此彼此,这里彼此彼此中的常数为给定的 \(k\)

\(n, q, \le 3\times 10^5\)

首先,\(b\)\(p\) 更为厉害的情况是好处理的,\(b\) 能任取 \(p\) 向上 \(k\) 个点,\(c\) 能在 \(p\) 子树中任取。下面讨论 \(p\)\(b\) 更为厉害的情况。

\(f_{u, k} = \sum_{v \in T(u) - \{ u \}} [dist(u, v) \le k] (siz_v - 1)\) 表示对于所有 \(v\) 满足 \(u\)\(v\) 更为厉害且彼此彼此的答案,则:

\[f_{u, k} = \sum_{v \in son_u} f_{v, k - 1} + siz_v - 1 \]

长链剖分优化即可,实现时对于 \(siz_v - 1\) 的部分可以打全局标记实现。

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 3e5 + 7;

struct Graph {
    vector<int> e[N];
    
    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G;

vector<pair<int, int> > qry[N];

ll buf[N], *f[N], *now = buf, tag[N], ans[N];
int fa[N], dep[N], len[N], son[N], siz[N];

int n, m;

void dfs1(int u, int f) {
    fa[u] = f, dep[u] = dep[f] + 1, len[u] = 1, siz[u] = 1;
    
    for (int v : G.e[u]) {
        if (v == f)
            continue;

        dfs1(v, u), siz[u] += siz[v];
        
        if (len[v] >= len[u])
            son[u] = v, len[u] = len[v] + 1;
    }
}

void dfs2(int u) {
    if (son[u])
        f[son[u]] = f[u] + 1, dfs2(son[u]), tag[u] = tag[son[u]] + siz[son[u]] - 1;
    
    for (int v : G.e[u]) {
        if (v == fa[u] || v == son[u])
            continue;

        f[v] = now, now += len[v], dfs2(v);
        tag[u] += tag[v] + siz[v] - 1;
        
        for (int j = 0; j < len[v]; ++j)
            f[u][j + 1] += f[v][j];
    }
    
    f[u][0] = -tag[u];
    
    for (auto it : qry[u])
        ans[it.second] += f[u][min(it.first, len[u] - 1)] + tag[u];
}

signed main() {
    scanf("%d%d", &n, &m);
    
    for (int i = 1; i < n; ++i) {
        int u, v;
        scanf("%d%d", &u, &v);
        G.insert(u, v), G.insert(v, u);
    }
    
    dfs1(1, 0);
    
    for (int i = 1; i <= m; ++i) {
        int x, k;
        scanf("%d%d", &x, &k);
        ans[i] = 1ll * min(dep[x] - 1, k) * (siz[x] - 1);
        qry[x].emplace_back(k, i);
    }
    
    f[1] = now, now += len[1], dfs2(1);
    
    for (int i = 1; i <= m; ++i)
        printf("%lld\n", ans[i]);
    
    return 0;
}

P5904 [POI2014] HOT-Hotels 加强版

给出一棵树,求有多少个无序三元组 \((i,j,k)\) 满足 \(i,j,k\) 两两之间的距离都相等。

\(n \le 10^5\)

设:

  • \(f_{i, j}\) 表示满足 \(x\)\(i\) 子树中且 \(\mathrm{dist}(x, i) = j\) 的点的数量。
  • \(g_{i, j}\) 表示满足 \(x, y\)\(i\) 的子树内且 \(\mathrm{dist}(\mathrm{lca}(x, y), x) = \mathrm{dist}(\mathrm{lca}(x, y), y) = \mathrm{dist}(\mathrm{lca}(x, y), i) + j\) 的无序数对 \((x, y)\) 的数量。

考虑转移:

  • \(\sum_{v \in son(u)} g_{v, i} \to g_{u, i - 1}\)
  • \(\sum_{v, w \in son(u), v \ne w} f_{v, i} \times f_{w, i} \to g_{u, i + 1}\)
  • \(\sum_{v \in son(u)} f_{v, i} \to f_{u, i + 1}\)

再考虑对答案的贡献:

  • \(g_{u, 0} \to ans\)
  • \(\sum_{v, w \in son(u), v \ne w} f_{v, i - 1} \times g_{w, i + 1} \to ans\)

关于 \(g\) 开空间的解释:由于 g[son[u]] = g[u] - 1 ,所以 \(g_u\) 前面要开 \(len_u\) ;由于 \(g_{u, i}\)\(i\) 的取值为 \([0, len_u)\) ,所以 \(g_i\) 后面也要开 \(len_u\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 1e5 + 7;

struct Graph {
    vector<int> e[N];
    
    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G;

ll buf[N * 3], *f[N], *g[N];
int fa[N], len[N], son[N];

ll *now = buf, ans;
int n;

void dfs1(int u, int f) {
    fa[u] = f, len[u] = 1;
    
    for (int v : G.e[u]) {
        if (v == f)
            continue;
        
        dfs1(v, u);
        
        if (len[v] + 1 > len[u])
            len[u] = len[v] + 1, son[u] = v;
    }
}

void dfs2(int u) {
    if (son[u]) {
        f[son[u]] = f[u] + 1, g[son[u]] = g[u] - 1;
        dfs2(son[u]);
    }
    
    f[u][0] = 1;
    
    for (int v : G.e[u]) {
        if (v == fa[u] || v == son[u])
            continue;
        
        f[v] = now, now += len[v];
        now += len[v], g[v] = now, now += len[v];
        dfs2(v);
        
        for (int i = 0; i < len[v]; ++i)
            ans += g[u][i + 1] * f[v][i];
        
        for (int i = 1; i + 1 < len[v]; ++i)
            ans += f[u][i] * g[v][i + 1];

        for (int i = 1; i < len[v]; ++i)
            g[u][i - 1] += g[v][i];
        
        for (int i = 0; i < len[v]; ++i)
            g[u][i + 1] += f[u][i + 1] * f[v][i];

        for (int i = 0; i < len[v]; ++i)
            f[u][i + 1] += f[v][i];
    }

    ans += g[u][0];
}

signed main() {
    scanf("%d", &n);
    
    for (int i = 1; i < n; ++i) {
        int u, v;
        scanf("%d%d", &u, &v);
        G.insert(u, v), G.insert(v, u);
    }
    
    dfs1(1, 0);
    f[1] = now, now += len[1];
    now += len[1], g[1] = now, now += len[1];
    dfs2(1);
    printf("%lld", ans);
    return 0;
}

光明

给出一棵树,设 \(f(u, i)\) 表示 \(u\) 子树内与 \(u\) 深度差为 \(i\) 的点的数量,求前 \(k\) 大的 \(f(u, i)\) 的和。

\(n \le 3 \times 10^6\)

\(f(u, i)\) 不难用长链剖分 \(O(n)\) 求出,一个暴力是每次把一段 \(f(u, 0 \sim len_u - 1)\) 放入桶中,时间复杂度 \(O(n^2)\)

考虑优化,设 \(tag_{u, i}\) 表示当前 \(f(u, i)\) 要放入桶的数量,则每次更新 \(f(u, i)\) 时下传标记即可。

时间复杂度 \(O(n)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 3e6 + 7;

struct Graph {
    vector<int> e[N];
    
    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G;

ll cnt[N];
int buc[N << 1], *f[N], *tag[N], *now = buc;
int fa[N], len[N], son[N];

ll m;
int n;

void dfs1(int u) {
    len[u] = 1;

    for (int v : G.e[u]) {
        dfs1(v);

        if (len[v] + 1 > len[u])
            son[u] = v, len[u] = len[v] + 1;
    }
}

inline void pushdown(int u, int i) {
    cnt[f[u][i]] += tag[u][i];

    if (i + 1 < len[u])
        tag[u][i + 1] += tag[u][i];

    tag[u][i] = 0;
}

void dfs2(int u) {
    if (son[u])
        f[son[u]] = f[u] + 1, tag[son[u]] = tag[u] + 1, dfs2(son[u]);
    
    ++f[u][0];

    for (int v : G.e[u]) {
        if (v == son[u])
            continue;

        f[v] = now, now += len[v];
        tag[v] = now, now += len[v];
        dfs2(v);

        for (int i = 0; i < len[v]; ++i)
            pushdown(u, i + 1), pushdown(v, i), f[u][i + 1] += f[v][i];
    }

    ++tag[u][0];
}

signed main() {
    scanf("%d%lld", &n, &m);

    for (int i = 2; i <= n; ++i)
        scanf("%d", fa + i), G.insert(fa[i], i);

    dfs1(1);
    f[1] = now, now += len[1];
    tag[1] = now, now += len[1];
    dfs2(1);

    for (int i = 0; i < len[1]; ++i)
        pushdown(1, i);

    ll ans = 0;

    for (int i = n; i && m; --i)
        ans += 1ll * i * min<ll>(cnt[i], m), m -= min<ll>(cnt[i], m);

    printf("%lld", ans);
    return 0;
}

数树上块

给定一棵树,求直径 \(\le k\) 的连通块数量 \(\bmod 998244353\)

\(n \le 5 \times 10^5\)

\(f_{u, i}\) 表示 \(u\) 子树内包含 \(u\) 的连通块最大深度(到 \(u\) 的距离)为 \(i\) 的方案数,则统计答案就是枚举短链,在长链上区间查询,转移就是 \(f_{u, i} \times f_{v, j} \to f_{u, \max(i, j + 1)}\)

注意到第二维为深度,可以利用长链剖分进行优化。按长剖 dfn 为下标存储 DP 值,这样一条长链就是连续的一段,用线段树维护 DP 值即可快速转移。

转移时分类讨论 \(\max\) 取到前者还是后者即可,修改为单点加、区间乘,查询为区间和,时间复杂度 \(O(n \log n)\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 5e5 + 7;

struct Graph {
    vector<int> e[N];
    
    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G;

int fa[N], len[N], son[N], dfn[N], f[N], g[N];

int n, m, dfstime, ans;

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

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

void dfs1(int u, int f) {
    fa[u] = f;
    
    for (int v : G.e[u])
        if (v != f) {
            dfs1(v, u);
            
            if (len[v] + 1 > len[u])
                len[u] = len[v] + 1, son[u] = v;
        }
}

void dfs2(int u) {
    dfn[u] = ++dfstime;
    
    if (son[u])
        dfs2(son[u]);
    
    for (int v : G.e[u])
        if (v != fa[u] && v != son[u])
            dfs2(v);
}

namespace SMT {
int s[N << 2], tag[N << 2];

inline int ls(int x) {
    return x << 1;
}

inline int rs(int x) {
    return x << 1 | 1;
}

inline void pushup(int x) {
    s[x] = add(s[ls(x)], s[rs(x)]);
}

inline void spread(int x, int k) {
    s[x] = 1ll * s[x] * k % Mod, tag[x] = 1ll * tag[x] * k % Mod;
}

inline void pushdown(int x) {
    if (tag[x] != 1)
        spread(ls(x), tag[x]), spread(rs(x), tag[x]), tag[x] = 1;
}

void build(int x, int l, int r) {
    s[x] = 0, tag[x] = 1;
    
    if (l == r)
        return;
    
    int mid = (l + r) >> 1;
    build(ls(x), l, mid), build(rs(x), mid + 1, r);
}

void modify(int x, int nl, int nr, int p, int k) {
    if (nl == nr) {
        s[x] = add(s[x], k);
        return;
    }
    
    pushdown(x);
    int mid = (nl + nr) >> 1;
    
    if (p <= mid)
        modify(ls(x), nl, mid, p, k);
    else
        modify(rs(x), mid + 1, nr, p, k);
    
    pushup(x);
}

void update(int x, int nl, int nr, int l, int r, int k) {
    if (l <= nl && nr <= r) {
        spread(x, k);
        return;
    }

    pushdown(x);
    int mid = (nl + nr) >> 1;
    
    if (l <= mid)
        update(ls(x), nl, mid, l, r, k);
    
    if (r > mid)
        update(rs(x), mid + 1, nr, l, r, k);
    
    pushup(x);
}

int query(int x, int nl, int nr, int l, int r) {
    if (l <= nl && nr <= r)
        return s[x];
    
    pushdown(x);
    int mid = (nl + nr) >> 1;

    if (r <= mid)
        return query(ls(x), nl, mid, l, r);
    else if (l > mid)
        return query(rs(x), mid + 1, nr, l, r);
    else
        return add(query(ls(x), nl, mid, l, r), query(rs(x), mid + 1, nr, l, r));
}
} // namespace SMT

void dfs3(int u) {
    if (son[u])
        dfs3(son[u]);
    
    SMT::modify(1, 1, n, dfn[u], 1);
    ans = add(ans, SMT::query(1, 1, n, dfn[u], dfn[u] + min(m, len[u])));
    
    for (int v : G.e[u]) {
        if (v == fa[u] || v == son[u])
            continue;
        
        dfs3(v);
        int lim = min(len[v], m - 1);
        
        for (int i = 0; i <= lim; ++i) {
            f[i] = SMT::query(1, 1, n, dfn[u], dfn[u] + min(i, m - i - 1));
            g[i] = SMT::query(1, 1, n, dfn[v] + i, dfn[v] + i);
            ans = add(ans, 1ll * g[i] * SMT::query(1, 1, n, dfn[u], dfn[u] + min(len[u], m - i - 1)) % Mod);
        }
        
        map<int, int> pre;
        pre[0] = 1, pre[len[u] + 1] = 0;
        
        for (int i = 0; i <= lim && i + 1 <= m - i - 1; ++i) {
            pre[i + 1] = add(pre[i + 1], g[i]);
            pre[min(m - i, len[u] + 1)] = dec(pre[min(m - i, len[u] + 1)], g[i]);
        }
    
        int sum = 0;
        
        for (auto a = pre.begin(), b = next(a); b != pre.end(); ++a, ++b)
            SMT::update(1, 1, n, dfn[u] + a->first, dfn[u] + b->first - 1, sum = add(sum, a->second));
        
        for (int i = 0; i <= lim; ++i)
            SMT::modify(1, 1, n, dfn[u] + i + 1, 1ll * f[i] * g[i] % Mod);
    }
}

signed main() {
    scanf("%d%d", &n, &m);
    
    for (int i = 1; i < n; ++i) {
        int u, v;
        scanf("%d%d", &u, &v);
        G.insert(u, v), G.insert(v, u);
    }
    
    dfs1(1, 0), dfs2(1), SMT::build(1, 1, n), dfs3(1);
    printf("%d", ans);
    return 0;
}

优化建图技巧

  • 状态数量优化:对于一类题目,可以列出来的状态数量很多,但是有用的状态并不多,可以考虑从状态数量入手,优化时间复杂度。

  • 线段树优化建图:

    对于一类题目,连边方式为:点对点、点对区间、区间对点、区间对区间。

    考虑建立一棵线段树,则一段区间可以被拆为 \(O(\log n)\) 个线段树上的点,分别连边即可。

    此时点对点需要连 \(O(1)\) 条边,点对区间、区间对点需要连 \(O(\log n)\) 条边,区间对区间需要连 \(O(\log^2 n)\) 条边。

  • 前后缀优化建图:

    对于一类题目,连边的区间都形如前缀或后缀。考虑新建 \(2n\) 个点表示前缀与后缀,分别连边即可。

    在 2-SAT 问题中,前缀优化建图可以用于表示前缀或,用于处理多个点只能选一个的限制。

  • 分块优化建图:

    对于一般图区间对区间的连边,分块优化建图不如线段树。

    但是在区间长度固定时,记区间长度为 \(L\) ,则可以将 \([1, n]\)\(L\) 为块长分块块,那么每次区间对区间的连边都可以转化为一个整块或相邻块的前后缀,建边总边数降为线性。

  • (树上)倍增优化建图:注意到倍增的可重复贡献性,若连的边满足该性质,则可以考虑,此时点数为 \(O(n \log n)\) ,边数线性,优于线段树优化建图。

  • 并查集优化建图:常用于维护连通性和二分图染色,每次可以把一段区间缩成一个点,然后暴力跳下一个点复杂度就是对的。

  • 树剖优化建图:本质就是把树上路径拆分为 \(O(\log n)\) 个区间,然后对应连边。

  • k-D Tree 优化建图:和线段树本质相同。

P3645 [APIO2015] 雅加达的摩天楼

\(n\) 个点排在一条直线上,编号为 \(0 \sim n - 1\)

\(m\) 个人,编号为 \(0 \sim n - 1\) 。第 \(i\) 个人最初在 \(b_i\) 处,移动能力为 \(p_i\)

\(i\) 个人可以移动到 \(x_i \pm p_i\) 且在 \(0 \sim n - 1\) 范围内的点上,其中 \(x_i\) 表示移动前第 \(i\) 个人的位置。

现在第 \(0\) 个人需要发送消息给第 \(1\) 个人,任何一个收到消息的人可以选择下面二者之一:

  • 移动到其他点。
  • 将信息发送给该点上的其他人。

求最少移动次数。

\(n, m \le 3 \times 10^4\)

设状态 \((i, j)\) 表示当前在 \(i\) ,移动能力为 \(j\) 。考虑分析状态数量:

  • \(j \le \sqrt{n}\) :只有 \(O(n \sqrt{n})\) 个状态。
  • \(j > \sqrt{n}\) :只有 \(O(m \sqrt{n})\) 个状态,因为每个人此时可行位置只有 \(O(\frac{n}{j})\) 个。

因此状态数量是 \(O((n + m) \sqrt{n})\) 的。

bitset 存状态,每次到达一个点后将这个点的所有初始状态(人)放进队列即可。

#include <bits/stdc++.h>
using namespace std;
const int N = 3e4 + 7;

vector<int> vec[N];
bitset<N> vis[N];

int n, m;

inline int bfs(int S, int T) {
    if (S == T)
        return 0;

    queue<tuple<int, int, int> > q;

    auto insert = [&](int x, int k, int d) {
        if (vis[x].none()) {
            for (int it : vec[x])
                vis[x].set(it), q.emplace(x, it, d);
        }

        if (~k && !vis[x].test(k))
            vis[x].set(k), q.emplace(x, k, d);
    };

    insert(S, -1, 0);

    while (!q.empty()) {
        int x = get<0>(q.front()), k = get<1>(q.front()), d = get<2>(q.front());
        q.pop();

        if (x == T)
            return d;

        if (x - k >= 0)
            insert(x - k, k, d + 1);

        if (x + k < n)
            insert(x + k, k, d + 1);
    }

    return -1;
}

signed main() {
    scanf("%d%d", &n, &m);
    int S, T;

    for (int i = 0; i < m; ++i) {
        int b, p;
        scanf("%d%d", &b, &p);
        vec[b].emplace_back(p);

        if (!i)
            S = b;
        else if (i == 1)
            T = b;
    }

    printf("%d", bfs(S, T));
    return 0;
}

[ABC210F] Coprime Solitaire

给定 \(n\) 对数 \((a_i, b_i)\) ,每对数选一个,求是否存在方案使得最终 \(n\) 个数两两互质。

\(n \le 3 \times 10^4\)\(a_i, b_i \le 10^6\)

显然考虑拿出所有数字的质因子,那么包含该质因子的若干数只能选择一个。

对于一个质因子,包含它的数为 \(u_{1 \sim k}\) 。考虑按顺序表示限制 \(u_i\) ,记 \(las\) 点表示前一个前缀至少选一个的方案,则:

  • 若选该点则必须选 \(\neg las\) ;若选 \(las\) 则必须选 \(\neg u_i\)
  • 对于相邻前缀的连边,显然前一个为真则后一个也为真,后一个为假则前一个也为假。
  • 对于该点对应的前缀,若选该点则前缀或为真,若前缀或为假则不选该点。

时间复杂度 \(O(n \log V)\)

#include <bits/stdc++.h>
using namespace std;
const int N = 5e6 + 7;

struct Graph {
    vector<int> e[N];
    
    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G;

vector<int> vec[N];

int dfn[N], low[N], sta[N], leader[N];

int n, tot, dfstime, top, scc;

void Tarjan(int u) {
    dfn[u] = low[u] = ++dfstime, sta[++top] = u;

    for (int v : G.e[u]) {
        if (!dfn[v])
            Tarjan(v), low[u] = min(low[u], low[v]);
        else if (!leader[v])
            low[u] = min(low[u], dfn[v]);
    }

    if (low[u] == dfn[u]) {
        ++scc;

        while (sta[top] != u)
            leader[sta[top--]] = scc;

        leader[sta[top--]] = scc;
    }
}

signed main() {
    scanf("%d", &n);

    for (int i = 1; i <= n; ++i) {
        int a, b;
        scanf("%d%d", &a, &b);

        auto insert = [](int x, int id) {
            for (int i = 2; i * i <= x; ++i)
                if (!(x % i)) {
                    vec[i].emplace_back(id);

                    while (!(x % i))
                        x /= i;
                }

            if (x > 1)
                vec[x].emplace_back(id);
        };

        insert(a, i * 2), insert(b, i * 2 + 1);
    }

    tot = n * 2 + 1;

    for (int i = 1; i < N; ++i) {
        int las = 0;

        for (int it : vec[i]) {
            if (las) {
                G.insert(las, it ^ 1), G.insert(it, las ^ 1);
                G.insert(las, tot + 1), G.insert(tot + 2, las ^ 1);
                G.insert(it, tot + 1), G.insert(tot + 2, it ^ 1);
                las = tot + 1, tot += 2;
            } else
                las = it;
        }
    }

    for (int i = 1; i <= tot; ++i)
        if (!dfn[i])
            Tarjan(i);

    for (int i = 1; i <= n; ++i)
        if (leader[i * 2] == leader[i * 2 + 1])
            return puts("No"), 0;

    puts("Yes");
    return 0;
}

P3295 [SCOI2016] 萌萌哒

求有多少 \(n\) 位十进制整数满足给出的 \(m\) 条限制,每条限制形如 \([l_1, r_1]\) 位和 \([l_2, r_2]\) 位完全相同(保证 \(r_1 - l_1 = r_2 - l_2\) ),答案对 \(10^9 + 7\) 取模。

\(n, m \le 10^5\)

考虑一条限制带来的影响,实际就是钦定了一些位置必须相同。对于一条限制,考虑连 \(O(len)\) 条点对点的平行边。记连通块数量为 \(s\) ,则答案为 \(9 \times 10^{s - 1}\) (最高位非 \(0\) )。

考虑优化建图,建立 ST 表结构。将区间拆为两个极长的 \(2^k\) 区间,在 ST 表的第 \(k\) 层连两条平行边。判定连通性时,只要将该层的连通块的情况下传到下一层即可。

时间复杂度 \(O(n \log n)\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 1e5 + 7, LOGN = 17;

struct DSU {
    int fa[N];
    
    inline void prework(int n) {
        iota(fa + 1, fa + n + 1, 1);
    }
    
    inline int find(int x) {
        while (x != fa[x])
            fa[x] = fa[fa[x]], x = fa[x];
    
        return x;
    }
    
    inline void merge(int x, int y) {
        fa[find(y)] = find(x);
    }
} dsu[LOGN];

int n, m;

signed main() {
    scanf("%d%d", &n, &m);

    for (int i = 0; i <= __lg(n); ++i)
        dsu[i].prework(n);

    for (int i = 1; i <= m; ++i) {
        int l1, r1, l2, r2;
        scanf("%d%d%d%d", &l1, &r1, &l2, &r2);
        int k = __lg(r1 - l1 + 1);
        dsu[k].merge(l1, l2), dsu[k].merge(r1 - (1 << k) + 1, r2 - (1 << k) + 1);
    }

    for (int j = __lg(n); j; --j)
        for (int i = 1; i + (1 << j) - 1 <= n; ++i) {
            int anc = dsu[j].find(i);

            if (anc != i)
                dsu[j - 1].merge(i, anc), dsu[j - 1].merge(i + (1 << (j - 1)), anc + (1 << (j - 1)));
        }

    int ans = 0;

    for (int i = 1; i <= n; ++i)
        if (dsu[0].find(i) == i)
            ans = (ans ? 10ll * ans % Mod : 9);

    printf("%d", ans);
    return 0;
}

QOJ9904. 最小生成树

给定 \(a_{3 \sim 2n - 1}\) ,定义完全图 \(G\) ,其中 \(i, j\) 之间的边权为 \(a_{i + j}\) ,求 MST。

\(n \le 2 \times 10^5\)

显然将 \(a\) 排序后依次连边,下面考虑优化连边的复杂度。

由于连边形如回文,因此考虑先将并查集的大小开到 \(2n\) ,其中 \(1 \sim n\) 表示原来的点,\(n + 1 \sim 2n\) 表示翻转后的点。一开始先将 \(i, 2n - i + 1\) 连上边,表示位置相对应。

一个想法是用线段树维护区间哈希值,每次二分找到第一个哈希值不同(不连通)的对应位置连边,时间复杂度 \(O(n \log^2 n)\)

另一个想法是用 ST 表优化,具体想法和 P3295 [SCOI2016] 萌萌哒 是类似的。

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 4e5 + 7, LOGN = 19;

struct DSU {
    int fa[N];
    
    inline void prework(int n) {
        iota(fa + 1, fa + n + 1, 1);
    }
    
    inline int find(int x) {
        while (x != fa[x])
            fa[x] = fa[fa[x]], x = fa[x];
    
        return x;
    }
    
    inline void merge(int x, int y) {
        fa[find(y)] = find(x);
    }
} dsu[LOGN];

int a[N];

ll ans;
int n;

void merge(int x, int y, int k, int val) {
    if (dsu[k].find(x) == dsu[k].find(y))
        return;

    dsu[k].merge(x, y);

    if (k)
        merge(x, y, k - 1, val), merge(x + (1 << (k - 1)), y + (1 << (k - 1)), k - 1, val);
    else
        ans += val;
}

signed main() {
    scanf("%d", &n);

    for (int i = 3; i <= n * 2 - 1; ++i)
        scanf("%d", a + i);

    vector<int> id(n * 2 - 3);
    iota(id.begin(), id.end(), 3);

    sort(id.begin(), id.end(), [](const int &x, const int &y) {
        return a[x] < a[y];
    });

    for (int i = 0; i <= __lg(n); ++i)
        dsu[i].prework(n * 2);

    for (int i = 1; i <= n; ++i)
        dsu[0].merge(i, n * 2 - i + 1);

    for (int it : id) {
        int l = (it <= n ? 1 : it - n), r = (it <= n ? it - 1 : n), k = __lg(r - l + 1);
        merge(l, n * 2 - r + 1, k, a[it]), merge(r - (1 << k) + 1, n * 2 - (l + (1 << k) - 1) + 1, k, a[it]);
    }

    printf("%lld", ans);
    return 0;
}

CF1007D Ants

给定一棵树,有 \(m\) 对链,每对链选择其中一个,使得选择的链不相交,判断有无解,若有解还需要构造方案。

\(n \le 10^5\)\(m \le 10^4\)

考虑建立 2-SAT 模型,将一条链拆为 \(O(\log n)\) 个区间,把这些区间放在线段树上得到 \(O(\log^2 n)\) 个区间,限制条件即为选了一个线段树上的点就不能选其祖先,新建点表示子树或即可。时间复杂度 \(O(m \log^2 n)\)

#include <bits/stdc++.h>
using namespace std;
const int N = 5e6 + 7;

struct Graph {
    vector<int> e[N];
    
    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G;

int dfn[N], low[N], sta[N], leader[N];

int n, m, tot, dfstime, top, scc;

namespace SMT {
const int N = 1e5 + 7;

vector<int> vec[N << 2];

inline int ls(int x) {
    return x << 1;
}

inline int rs(int x) {
    return x << 1 | 1;
}

void insert(int x, int nl, int nr, int l, int r, int k) {
    if (l <= nl && nr <= r) {
        vec[x].emplace_back(k);
        return;
    }

    int mid = (nl + nr) >> 1;

    if (l <= mid)
        insert(ls(x), nl, mid, l, r, k);

    if (r > mid)
        insert(rs(x), mid + 1, nr, l, r, k);
}

int dfs(int x, int l, int r) {
    int las = 0;

    if (l != r) {
        int mid = (l + r) >> 1, a = dfs(ls(x), l, mid), b = dfs(rs(x), mid + 1, r);

        if (!a || !b)
            las = a | b;
        else {
            G.insert(a, tot + 1), G.insert(tot + 2, a ^ 1);
            G.insert(b, tot + 1), G.insert(tot + 2, b ^ 1);
            las = tot + 1, tot += 2;
        }
    }

    for (int it : vec[x]) {
        if (las) {
            G.insert(las, it ^ 1), G.insert(it, las ^ 1);
            G.insert(las, tot + 1), G.insert(tot + 2, las ^ 1);
            G.insert(it, tot + 1), G.insert(tot + 2, it ^ 1);
            las = tot + 1, tot += 2;
        } else
            las = it;
    }

    return las;
}
} // namespace SMT

namespace Tree {
Graph G;

int fa[N], dep[N], siz[N], son[N], top[N], dfn[N];

int dfstime;

void dfs1(int u, int f) {
    fa[u] = f, dep[u] = dep[f] + 1, siz[u] = 1;

    for (int v : G.e[u]) {
        if (v == f)
            continue;

        dfs1(v, u), siz[u] += siz[v];

        if (siz[v] > siz[son[u]])
            son[u] = v;
    }
}

void dfs2(int u, int topf) {
    top[u] = topf, dfn[u] = ++dfstime;

    if (son[u])
        dfs2(son[u], topf);

    for (int v : G.e[u])
        if (v != fa[u] && v != son[u])
            dfs2(v, v);
}

inline void insert(int x, int y, int id) {
    while (top[x] != top[y]) {
        if (dep[top[x]] < dep[top[y]])
            swap(x, y);

        SMT::insert(1, 1, n, dfn[top[x]], dfn[x], id), x = fa[top[x]];
    }

    if (x != y) {
        if (dep[x] > dep[y])
            swap(x, y);

        SMT::insert(1, 1, n, dfn[x] + 1, dfn[y], id);
    }
}
} // namespace Tree

void Tarjan(int u) {
    dfn[u] = low[u] = ++dfstime, sta[++top] = u;

    for (int v : G.e[u]) {
        if (!dfn[v])
            Tarjan(v), low[u] = min(low[u], low[v]);
        else if (!leader[v])
            low[u] = min(low[u], dfn[v]);
    }

    if (low[u] == dfn[u]) {
        ++scc;

        while (sta[top] != u)
            leader[sta[top--]] = scc;

        leader[sta[top--]] = scc;
    }
}

signed main() {
    scanf("%d", &n);

    for (int i = 1; i < n; ++i) {
        int u, v;
        scanf("%d%d", &u, &v);
        Tree::G.insert(u, v), Tree::G.insert(v, u);
    }

    Tree::dfs1(1, 0), Tree::dfs2(1, 1);
    scanf("%d", &m);

    for (int i = 1; i <= m; ++i) {
        int a, b, c, d;
        scanf("%d%d%d%d", &a, &b, &c, &d);
        Tree::insert(a, b, i << 1), Tree::insert(c, d, i << 1 | 1);
    }

    tot = m * 2 + 1, SMT::dfs(1, 1, n);

    for (int i = 1; i <= tot; ++i)
        if (!dfn[i])
            Tarjan(i);

    for (int i = 1; i <= m; ++i)
        if (leader[i << 1] == leader[i << 1 | 1])
            return puts("NO"), 0;

    puts("YES");

    for (int i = 1; i <= m; ++i)
        printf("%d\n", leader[i << 1] < leader[i << 1 | 1] ? 1 : 2);

    return 0;
}

CF1956F Nene and the Passing Game

\(n\) 个点,第 \(i\) 个点有一个区间 \([l_i, r_i]\)

对于任意两点 \(i, j\) ,它们之间有边当且仅当 \(|i - j| \in [l_i + l_j, r_i + r_j]\)

求连通块个数。

\(n \le 2 \times 10^6\)

有边条件推式子得到其等价于 \([i + l_i, i + r_i] \cap [j - r_j, j - l_j] \ne \emptyset\)

设区间 \(L_i = [i - r_i, i - l_i], R_i = [i + l_i, i + r_i]\) ,那么 \(i, j\) 之间有边当且仅当 \((R_i \cap L_j) \cup (L_i \cap R_j) \ne \emptyset\)

问题转化为点和区间之间的连边,最后查询连通块数量,不难用并查集做到 \(O(n \alpha(n))\)

但是这样并不是正确的,因为会出现 \(L_i \cap L_j \ne \emptyset\)\(R_i \cap R_j \ne \emptyset\) 的情况,而这并不等价于 \(i, j\) 之间有边。考虑只保留满足 \(\exist i, j, x \in L_i \and x \in R_j\) 的点 \(x\) ,这样若 \(i, j\) 通过 \(L_i \cap L_j \ne \emptyset\) 建立联系,则不断切换 \(L, R\) 连通,这是合法的。

#include <bits/stdc++.h>
using namespace std;
const int N = 2e6 + 7;

struct DSU {
    int fa[N * 2];
    
    inline void prework(int n) {
        iota(fa + 1, fa + n + 1, 1);
    }
    
    inline int find(int x) {
        while (x != fa[x])
            fa[x] = fa[fa[x]], x = fa[x];
    
        return x;
    }
    
    inline void merge(int x, int y) {
        fa[find(y)] = find(x);
    }
} dsu;

pair<int, int> L[N], R[N];

int cl[N], cr[N], pre[N], nxt[N];
bool vis[N];

int n;

signed main() {
    int T;
    scanf("%d", &T);

    while (T--) {
        scanf("%d", &n);
        memset(cl + 1, 0, sizeof(int) * n), memset(cr + 1, 0, sizeof(int) * n);

        for (int i = 1; i <= n; ++i) {
            int l, r;
            scanf("%d%d", &l, &r);
            L[i] = make_pair(max(1, i - r), max(1, i - l + 1)), ++cl[L[i].first], --cl[L[i].second];
            R[i] = make_pair(min(n, i + l), min(n, i + r + 1)), ++cr[R[i].first], --cr[R[i].second];
        }

        for (int i = 1; i <= n; ++i)
            cl[i] += cl[i - 1], cr[i] += cr[i - 1];

        int tot = 0;

        for (int i = 1; i <= n; ++i)
            pre[i] = nxt[i] = (cl[i] && cr[i] ? ++tot : 0);

        for (int i = 2; i <= n; ++i)
            if (!pre[i])
                pre[i] = pre[i - 1];

        for (int i = n - 1; i; --i)
            if (!nxt[i])
                nxt[i] = nxt[i + 1];

        dsu.prework(n + tot);

        for (int i = 1; i <= n; ++i) {
            if (nxt[L[i].first] && pre[L[i].second]) {
                for (int j = dsu.find(nxt[L[i].first]) + 1; j <= pre[L[i].second - 1]; ++j)
                    dsu.merge(j, j - 1), j = dsu.find(j);
            }

            if (nxt[R[i].first] && pre[R[i].second]) {
                for (int j = dsu.find(nxt[R[i].first]) + 1; j <= pre[R[i].second - 1]; ++j)
                    dsu.merge(j, j - 1), j = dsu.find(j);
            }
        }

        for (int i = 1; i <= n; ++i) {
            if (nxt[L[i].first] && pre[L[i].second] && nxt[L[i].first] <= pre[L[i].second - 1])
                dsu.merge(i + tot, nxt[L[i].first]);

            if (nxt[R[i].first] && pre[R[i].second] && nxt[R[i].first] <= pre[R[i].second - 1])
                dsu.merge(i + tot, nxt[R[i].first]);
        }

        memset(vis + 1, false, sizeof(bool) * n);

        for (int i = 1; i <= n; ++i)
            vis[dsu.find(i + tot) - tot] = true;

        printf("%d\n", (int)count(vis + 1, vis + n + 1, true));
    }

    return 0;
}

P3209 [HNOI2010] 平面图判定

给定一个包含 \(n\) 个点的环和 \(m\) 条弦,判定该图是否为平面图。

\(n \le 200\)\(m \le 10000\)

最终每条弦要么在环的内部,要么在环的外部。将原图每条弦看成点,严格相交关系看成边,则问题转化为二染色的合法性。

考虑断环为链,那么弦变成链上的一段区间,限制条件仍然是严格相交的区间不能放在一侧,因此直接将边变为区间即可,接下来就是对区间进行二染色。

按照左端点从大到小扫描,每次将靠右的区间连向靠左的。连边的时候会连向右端点在一个区间范围内的区间,这些区间在二分图中处于同一部分。用 map 按照右端点维护所有区间,并查集维护同色区间即可做到 \(O(m \log m)\)

#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 5e5 + 7;

struct Edge {
    int u, v;

    inline bool operator < (const Edge &rhs) const {
        return v == rhs.v ? u > rhs.u : v < rhs.v;
    }
} e[N];

struct Graph {
    vector<int> e[N];

    inline void clear(int n) {
        for (int i = 1; i <= n; ++i)
            e[i].clear();
    }
    
    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G;

struct DSU {
    int fa[N];
    
    inline void prework(int n) {
        iota(fa + 1, fa + n + 1, 1);
    }
    
    inline int find(int x) {
        while (x != fa[x])
            fa[x] = fa[fa[x]], x = fa[x];
    
        return x;
    }
    
    inline void merge(int x, int y) {
        fa[find(y)] = find(x);
    }
} dsu;

int id[N], col[N];
bool ban[N];

int n, m;

int dfs(int u) {
    for (int v : G.e[u]) {
        if (col[v] == col[u])
            return false;
        else if (col[v] == -1) {
            col[v] = col[u] ^ 1;

            if (!dfs(v))
                return false;
        }
    }

    return true;
}

signed main() {
    int T;
    scanf("%d", &T);

    while (T--) {
        scanf("%d%d", &n, &m);

        for (int i = 1; i <= m; ++i)
            scanf("%d%d", &e[i].u, &e[i].v);

        for (int i = 1; i <= n; ++i) {
            int x;
            scanf("%d", &x);
            id[x] = i;
        }

        for (int i = 1; i <= m; ++i) {
            e[i].u = id[e[i].u], e[i].v = id[e[i].v];
        
            if (e[i].u > e[i].v)
                swap(e[i].u, e[i].v);
        }

        sort(e + 1, e + m + 1, [](const Edge &a, const Edge &b) {
            return a.u == b.u ? a.v < b.v : a.u > b.u;
        });

        memset(ban + 1, false, sizeof(bool) * m);
        map<Edge, int> mp;

        for (int i = 1; i <= m; ++i) {
            if (e[i].u == e[i].v || mp.find(e[i]) != mp.end())
                ban[i] = true;
            else
                mp[e[i]] = i;
        }

        dsu.prework(m), G.clear(m);

        for (int i = 1; i <= m; ++i) {
            if (ban[i])
                continue;

            mp.erase(e[i]);
            vector<int> vec;
            auto it = mp.lower_bound((Edge){inf, e[i].v});

            while (it != mp.begin() && (--it)->first.v > e[i].u) {
                int x = dsu.find(it->second);
                G.insert(x, i), G.insert(i, x);
                vec.emplace_back(x), it = mp.lower_bound(e[x]);
            }

            for (int j = 0; j + 1 < vec.size(); ++j)
                dsu.merge(vec.back(), vec[j]);
        }

        memset(col + 1, -1, sizeof(int) * m);
        bool flag = true;

        for (int i = 1; i <= m && flag; ++i)
            if (col[i] == -1)
                col[i] = 0, flag &= dfs(i);

        puts(flag ? "YES" : "NO");
    }
    
    return 0;
}

AT_joisc2017_b 港湾設備 (Port Facility)

有两个栈和 \(n\) 个物品,给出每个物品的入栈、出栈时间,求给每个物品分配栈的合法方案数。

\(n \le 10^6\)

将每个时间段看作线段,将相交的线段连边,问题转化为连通块数量,或判定图不为二分图。

考虑从小到大扫右端点,并维护右端点更大的集合 \(S\) 。设当前扫描的线段为 \([l, r]\) ,则其会向 \(S\) 中左端点在 \([l, r]\) 中的点连边。

观察到由于是二染色问题,因此这些点最终染上的染色相同。考虑用并查集维护区间,遇到一个左端点就加入,遇到一个右端点就删除,并与当前访问到的左端点 \(> l\) 的点连边,维护每个位置最远的与其同色的点可以做到 \(O(n \alpha(n))\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 1e6 + 7;

struct DSU {
    int fa[N];
    
    inline void prework(int n) {
        iota(fa + 1, fa + n + 1, 1);
    }
    
    inline int find(int x) {
        while (x != fa[x])
            fa[x] = fa[fa[x]], x = fa[x];
    
        return x;
    }
    
    inline void merge(int x, int y) {
        fa[find(y)] = find(x);
    }
} dsu;

struct Graph {
    vector<int> e[N];
    
    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G;

int id[N << 1], nxt[N], sta[N], L[N], col[N];

int n, top;

int dfs(int u) {
    for (int v : G.e[u]) {
        if (col[v] == col[u])
            return false;
        else if (col[v] == -1) {
            col[v] = col[u] ^ 1;

            if (!dfs(v))
                return false;
        }
    }

    return true;
}

signed main() {
    scanf("%d", &n);

    for (int i = 1; i <= n; ++i) {
        int l, r;
        scanf("%d%d", &l, &r);
        id[l] = id[r] = i;
    }

    iota(nxt + 1, nxt + n + 1, 1), dsu.prework(n + 1);

    for (int i = 1; i <= n * 2; ++i) {
        int u = id[i];

        if (!L[u])
            sta[L[u] = ++top] = u;
        else {
            dsu.merge(L[u] + 1, L[u]); // remove u

            for (int j = dsu.find(L[u]); j <= top; ) {
                G.insert(u, sta[j]), G.insert(sta[j], u);
                int x = dsu.find(nxt[j] + 1);
                nxt[j] = top, j = x;
            }
        }
    }

    memset(col + 1, -1, sizeof(int) * n);
    int ans = 1;

    for (int i = 1; i <= n; ++i)
        if (col[i] == -1) {
            ans = 2ll * ans % Mod, col[i] = 0;

            if (!dfs(i))
                return puts("0"), 0;
        }

    printf("%d\n", ans);
    return 0;
}

P5471 [NOI2019] 弹跳

二维平面上有 \(n\) 个城市,第 \(i\) 个城市在 \((x_i, y_i)\) ,城市标号为 \(1 \sim n\)

\(m\) 个弹跳装置,第 \(i\) 个弹跳装置在标号 \(p_i\) 的点,表示从 \(p_i\) 出发,可以花费 \(t_i\) 的时间到达 \(x \in [L_i, R_i], y \in [D_i, U_i]\) 的城市。

\(1\) 到所有点的最短路。

\(n \le 70000\)\(m \le 1.5 \times 10^5\) ,ML = 125MB

直接用 k-D Tree 优化建图,边数是 \(O(m \sqrt{n})\) 级别,空间无法接受。

考虑不显式建图,开 \(2n\) 个点,\(1 \sim n\) 表示原来的点,\(n + 1 \sim 2n\) 表示 K-D Tree 上的子树。

考虑 Dijkstra 算法求最短路,每次取出堆顶点 \(u\) 时:

  • \(u \le n\) :用 \(u\) 处所有弹跳装置在 k-D Tree 上做类似矩形查询的操作,松弛若干子树和若干单点。注意子树标记无需下传,当前标记不小于原先标记时退出。
  • \(u > n\) :直接暴力松弛子树内的所有点即可。

不难发现 k-D Tree 上的每个点只会被放入堆中一次(因为祖先到它的距离为 \(0\) ,因此只会被最小的祖先松弛),因此时间复杂度为 \(O(n \log n + m \sqrt{n})\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = 0x3f3f3f3f3f3f3f3f;
const int N = 7e4 + 7;

struct Point {
    int x, y;
} p[N];

struct Device {
    ll t;
    int xl, xr, yl, yr;
};

vector<Device> device[N];
priority_queue<pair<ll, int> > q;

ll dis[N << 1];

int n, m, w, h;

namespace KDT {
int id[N], lc[N], rc[N], lx[N], rx[N], ly[N], ry[N];

int root;

inline void pushup(int x) {
    lx[x] = rx[x] = p[x].x, ly[x] = ry[x] = p[x].y;
    
    if (lc[x]) {
        lx[x] = min(lx[x], lx[lc[x]]), rx[x] = max(rx[x], rx[lc[x]]);
        ly[x] = min(ly[x], ly[lc[x]]), ry[x] = max(ry[x], ry[lc[x]]);
    }
    
    if (rc[x]) {
        lx[x] = min(lx[x], lx[rc[x]]), rx[x] = max(rx[x], rx[rc[x]]);
        ly[x] = min(ly[x], ly[rc[x]]), ry[x] = max(ry[x], ry[rc[x]]);
    }
}

int build(int l, int r, int tp) {
    if (l > r)
        return 0;

    int mid = (l + r) >> 1;

    nth_element(id + l, id + mid, id + r + 1, [&](const int &a, const int &b) {
        return tp ? p[a].x < p[b].x : p[a].y < p[b].y;
    });

    int x = id[mid];
    lc[x] = build(l, mid - 1, tp ^ 1), rc[x] = build(mid + 1, r, tp ^ 1);
    return pushup(x), x;
}

void maintain(int x, Device k) {
    if (!x || rx[x] < k.xl || lx[x] > k.xr || ry[x] < k.yl || ly[x] > k.yr || dis[x + n] <= k.t)
        return;
    else if (k.xl <= lx[x] && rx[x] <= k.xr && k.yl <= ly[x] && ry[x] <= k.yr) {
        dis[x + n] = k.t, q.emplace(-dis[x + n], x + n);
        return;
    }

    if (k.xl <= p[x].x && p[x].x <= k.xr && k.yl <= p[x].y && p[x].y <= k.yr) {
        if (dis[x] > k.t)
            dis[x] = k.t, q.emplace(-dis[x], x);
    }

    maintain(lc[x], k), maintain(rc[x], k);
}

void dfs(int x, ll k) {
    if (!x)
        return;

    if (dis[x] > k)
        dis[x] = k, q.emplace(-dis[x], x);

    dfs(lc[x], k), dfs(rc[x], k);
}
} // namespace KDT

using namespace KDT;

inline void Dijkstra(int S) {
    memset(dis + 1, inf, sizeof(ll) * (n * 2));
    dis[S] = 0, q.emplace(-dis[S], S);

    while (!q.empty()) {
        auto c = q.top();
        q.pop();

        if (-c.first != dis[c.second])
            continue;

        int u = c.second;

        if (u <= n) {
            for (Device it : device[u])
                it.t += dis[u], maintain(root, it);
        } else
            dfs(u - n, dis[u]);
    }
}

signed main() {
    scanf("%d%d%d%d", &n, &m, &w, &h);

    for (int i = 1; i <= n; ++i)
        scanf("%d%d", &p[i].x, &p[i].y);

    iota(id + 1, id + n + 1, 1), root = build(1, n, 1);

    for (int i = 1; i <= m; ++i) {
        int x, t, l, r, d, u;
        scanf("%d%d%d%d%d%d", &x, &t, &l, &r, &d, &u);
        device[x].emplace_back((Device){t, l, r, d, u});
    }

    Dijkstra(1);

    for (int i = 2; i <= n; ++i)
        printf("%lld\n", dis[i]);

    return 0;
}

其他

  • 托兰定理:在 \(n\) 个点的图 \(G\) 中,满足 \(r\) 阶完全图 \(K_r\) 不是 \(G\) 的子图的 \(G\) 的最大边数为 \(\lfloor (1 - \frac{1}{r - 1}) \times \frac{n^2}{2} \rfloor\)

  • Erdős–Gallai Theorem:\(d_1 \ge d_2 \ge \cdots \ge d_n\) 是一个合法的度数序列的充要条件是 \(\sum d_i\) 是偶数且 \(\forall 1 \le k \le n\) ,有:

    \[\sum_{i = 1}^k d_i \le k(k - 1) + \sum_{i = k + 1}^n \min(k, d_i)\sum_{i = 1}^k d_i \le k(k - 1) + \sum_{i = k + 1}^n \min(k, d_i) \]

    也不难理解,对前 \(k\) 个点分配度数,除了两两能连 \(\frac{k(k - 1)}{2}\) 条边外,剩下度数由后面点的度数补。

posted @ 2025-07-09 16:37  wshcl  阅读(51)  评论(0)    收藏  举报