Kruskal 重构树 学习笔记

参考资料:Kruskal 重构树学习笔记 - ImmortalWatcher

upd on 10.26:更新了一道例题。

upd on 2023.10.26:好巧呀,竟然刚好是一年之后。又更新了一道例题。

定义

顾名思义,Kruskal 重构树就是用 Kruskal 生成树算法在无向图上得到的树所构建出的新树。

Kruskal 重构树具有良好的性质,在一些特定的问题上能够帮助我们解决问题。

构造

这里以 Kruskal 最小生成树为例。

将所有边按升序排序,用 Kruskal 算法跑出最小生成树。

接着,将最小生成树上的边逐个取出,设当前边为 \((x,y)\),若 \(x,y\) 在新树上尚未连通,就新建一个节点 \(new\),令 \(new\) 分别于 \(x,y\) 在新树上的根节点(初始时为它本身)连边。

这个过程可以在最小生成树的计算中同步完成。

    for(int i = 1;i <= n * 2;++ i)
        pre[i] = i;
    std::sort(E + 1 , E + 1 + m , [&](const node& p,const node& q) {
        return p.t < q.t;
    });
    cnt = n;
    for(int i = 1;i <= m;++ i) {
        int u = find(E[i].u),v = find(E[i].v);
        if(u == v)continue ;
        pre[u] = pre[v] = ++ cnt;
        val[cnt] = E[i].t;
        G[cnt].pb(u);
        G[cnt].pb(v);
        if(cnt == 2 * n - 1)break ;
    }

性质

  • 是一棵二叉树。

  • 可以将其视作一个堆。

  • 关键性质:原图中两个点间所有路径上的边最大权值的最小值 \(=\) 最小生成树上两点简单路径的边最大权值 \(=\) Kruskal 重构树上两点 LCA 的点权。

如果我们要找到从 \(x\) 出发经过权值不超过 \(val\) 的边所能到达的点 \(y\) 的集合,只需要建出 Kruskal 重构树,倍增找出点权 \(\le val\) 的最浅节点,它的子树内叶子结点即是 \(y\) 的集合。

类似地,如果要使最小边权最大化,只需用 Kruskal 建立最大生成树的重构树即可。

例题

[BZOJ#3732]Network

给定 \(n\) 个结点,\(m\) 条边的无向图,\(Q\) 次询问,每次问 \(a\to b\) 路径上最大边权最小值。

\(1\le n\le 1.5\times 10^4,1\le m\le 3\times 10^4,1\le Q\le 2\times 10^4\)

根据 Kruskal 重构树的性质,直接建出最小生成树的重构树,跑倍增 LCA 即可。

不知道是什么题

给定一个带权树。定义 \(d(x,y)\)\(x\to y\) 路径上的最小边权。对于 \(1\sim n\),求 \(f(i)=\sum\limits_{x\in [1,n]} d(x,i)\)

建出 Kruskal 重构树。

根据性质中的结论,两点间路径上边权最小值即为重构树上两点间 LCA 的点权。

那么我们枚举每个新点,显然它的左子树和右子树间互相有贡献,树上差分一波就行。

(因为这道题不知道是哪的,我也没写代码,以上为纯口胡,如果错了请见谅 qwq)

[CF915F]Imbalance Value of a Tree

给定一棵 \(n\) 个点的树,第 \(i\) 个点点权为 \(a_i\)

定义 \(I(i,j)\)\(i\to j\) 路径上的点权极差。求 \(\sum_{i=1}^n \sum_{j=i}^n I(i,j)\)

\(1\le n\le 10^6\)

显然,极差之和可以转化为最大值之和减去最小值之和。

根据 Kruskal 重构树的性质,两次建树,分别建出最大和最小重构树,重构的过程中计算答案即可。

// Problem: CF915F Imbalance Value of a Tree
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/CF915F
// Memory Limit: 250 MB
// Time Limit: 4000 ms
// 
// Powered by CP Editor (https://cpeditor.org)

#include <bits/stdc++.h>
#define pb emplace_back
typedef long long ll;

const int maxn = 2e6 + 5;
int n,a[maxn],cnt,pre[maxn],sz[maxn];
ll ans;
struct node {
	int u,v,t;
	node() {
		u = v = t = 0;
	}
	node(int u,int v,int t):u(u),v(v),t(t){}
}E[maxn];

int find(int x) {
	return x == pre[x] ? x : pre[x] = find(pre[x]);
}

int main() {
	scanf("%d",&n);
	for(int i = 1;i <= n;++ i)
		scanf("%d",&a[i]),sz[i] = 1;
	
	for(int i = 1;i < n;++ i) {
		scanf("%d %d",&E[i].u,&E[i].v);
		E[i].t = std::max(a[E[i].u] , a[E[i].v]);
	}
	
	std::sort(E + 1 , E + n , [&](const node& p,const node& q) {
		return p.t < q.t;
	});
	cnt = n;
	for(int i = 1;i <= n * 2;++ i)pre[i] = i;
	for(int i = 1;i < n;++ i) {
		int u = find(E[i].u),v = find(E[i].v);
		if(u == v)continue ;
		pre[u] = pre[v] = ++ cnt;
		sz[cnt] = sz[u] + sz[v];
		ans += 1ll * sz[u] * sz[v] * E[i].t;
	}
	
	for(int i = 1;i < n;++ i)
		E[i].t = std::min(a[E[i].u] , a[E[i].v]);
	
	std::sort(E + 1 , E + n , [&](const node& p,const node& q) {
		return p.t > q.t;
	});
	cnt = n;
	for(int i = 1;i <= n * 2;++ i)pre[i] = i;
	for(int i = 1;i <= n;++ i)sz[i] = 1;
	for(int i = 1;i < n;++ i) {
		int u = find(E[i].u),v = find(E[i].v);
		if(u == v)continue ;
		pre[u] = pre[v] = ++ cnt;
		sz[cnt] = sz[u] + sz[v];
		ans -= 1ll * sz[u] * sz[v] * E[i].t;
	}
	
	printf("%lld",ans);
	return 0;
}

[ONTAK2010] Peaks 加强版

给定一张 \(n\) 个点,\(m\) 条边的带权无向图,第 \(i\) 个点的点权为 \(a_i\)

\(Q\) 次询问,强制在线,每次询问点 \(u\) 经过权值不超过 \(x\) 的边所能到达的点的点权的 \(k\) 大值。

\(1\le n\le 10^5,1\le m,Q\le 5\times 10^5\)

构建最小生成树的重构树,用性质中提到的方法倍增求出 \(u\) 能到达的节点集合。

离散化一下 \(a\),跑出 dfs 序,用主席树维护区间第 \(k\) 大即可。

(在非加强版中,这个也可以用线段树合并离线解决)

// Problem: P7834 [ONTAK2010] Peaks 加强版
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P7834
// Memory Limit: 128 MB
// Time Limit: 2500 ms
// 
// Powered by CP Editor (https://cpeditor.org)

#include <bits/stdc++.h>
#define pb emplace_back

const int maxn = 1e5 + 5;
const int maxm = 5e5 + 5;
std::vector<int> G[maxn << 1];
struct node {
    int u,v,t;
    node() {
        u = v = t = 0;
    }
}E[maxm];
int n,m,Q,a[maxn],val[maxn << 1],pre[maxn << 1],cnt,res,b[maxn];

int find(int x) {
    return x == pre[x] ? x : pre[x] = find(pre[x]);
}

int f[maxn << 1][20],d[maxn << 1],lg[maxn << 1],rk[maxn << 1],L[maxn << 1],R[maxn << 1],dfn[maxn << 1],tot;

void dfs(int u) {
    if(!G[u].size()) {
        rk[dfn[u] = ++ tot] = u;
        L[u] = R[u] = dfn[u];
        return ;
    }
    L[u] = n;
    R[u] = 0;
    for(auto& v : G[u]) {
        d[v] = d[u] + 1;
        f[v][0] = u;
        for(int k = 1;(1 << k) <= d[v];++ k)
            f[v][k] = f[f[v][k - 1]][k - 1];
        dfs(v);
        L[u] = std::min(L[u] , L[v]);
        R[u] = std::max(R[u] , R[v]);
    }
    return ;
}

int rt[maxn],ls[maxn << 5],rs[maxn << 5],sum[maxn << 5],num;

int build(int l,int r) {
    int p = ++ num;
    sum[p] = 0;
    if(l == r)return p;
    int mid = l + r >> 1;
    ls[p] = build(l , mid);
    rs[p] = build(mid + 1 , r);
    return p;
}

int modify(int i,int l,int r,int val) {
    int p = ++ num;
    sum[p] = sum[i] + 1;
    if(l == r)return p;
    int mid = l + r >> 1;
    if(val <= mid)ls[p] = modify(ls[i] , l , mid , val),rs[p] = rs[i];
    else rs[p] = modify(rs[i] , mid + 1 , r , val),ls[p] = ls[i];
    return p;
}

int query(int i,int pre,int l,int r,int k) {
    if(l == r)return l;
    int mid = l + r >> 1,x = sum[ls[i]] - sum[ls[pre]];
    if(k <= x)return query(ls[i] , ls[pre] , l , mid , k);
    else return query(rs[i] , rs[pre] , mid + 1 , r , k - x);
}

int main() {
    scanf("%d %d %d",&n,&m,&Q);
    for(int i = 1;i <= n;++ i)
        scanf("%d",&a[i]),b[i] = a[i];
    std::sort(b + 1 , b + 1 + n);
    res = std::unique(b + 1 , b + 1 + n) - b - 1;
    for(int i = 1;i <= n;++ i)
        a[i] = std::lower_bound(b + 1 , b + 1 + res , a[i]) - b;

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

    for(int i = 1;i <= n * 2;++ i)
        pre[i] = i;
    std::sort(E + 1 , E + 1 + m , [&](const node& p,const node& q) {
        return p.t < q.t;
    });
    cnt = n;
    for(int i = 1;i <= m;++ i) {
        int u = find(E[i].u),v = find(E[i].v);
        if(u == v)continue ;
        pre[u] = pre[v] = ++ cnt;
        val[cnt] = E[i].t;
        G[cnt].pb(u);
        G[cnt].pb(v);
        if(cnt == 2 * n - 1)break ;
    }

    for(int i = 2;i <= n * 2;++ i)
        lg[i] = lg[i >> 1] + 1;
    dfs(cnt);
    rt[0] = build(1 , res);
    for(int i = 1;i <= n;++ i)
        rt[i] = modify(rt[i - 1] , 1 , res , a[rk[i]]);

    int ans = 0;
    while(Q --) {
        int u,x,k,v;
        scanf("%d %d %d",&u,&x,&k);
        u = (u ^ ans) % n + 1;
        k = (k ^ ans) % n + 1;
        x ^= ans;
        v = u;
        for(int p = lg[d[u]];~ p;-- p)
            if(f[v][p]&&val[f[v][p]] <= x)v = f[v][p];
        if(R[v] - L[v] + 1 < k)puts("-1"),ans = 0;
        else printf("%d\n",ans = b[query(rt[R[v]] , rt[L[v] - 1] , 1 , res , R[v] - L[v] + 1 - k + 1)]);
    }
    return 0;
}

[AGC002D] Stamp Rally

给定一个连通无向图。\(q\) 次询问,每次给定两个点 \(x,y\),求从 \(x,y\) 总共走 \(z\) 步经过的边编号最大值的最小值是多少。

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

前面的题目都对边权做了限制,但这题没有,它只对步数做了限制。

考虑二分答案,转化为判定:\(x,y\) 只走编号 \(\le k\) 的边所能到达的点是否 \(\ge z\)

仍然是建出 Kruskal 重构树,\(x,y\) 都倍增一下(注意判重),根据叶子结点的数量判断即可。

// Problem: AT_agc002_d [AGC002D] Stamp Rally
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/AT_agc002_d
// Memory Limit: 250 MB
// Time Limit: 2000 ms
// 
// Powered by CP Editor (https://cpeditor.org)

#include <bits/stdc++.h>
#define pb emplace_back

const int maxn = 1e5 + 5;
int n,m,cnt,pre[maxn << 1],val[maxn << 1];
int sum[maxn << 1],d[maxn << 1],lg[maxn << 1],f[maxn << 1][20];
std::vector<int> G[maxn << 1];

int find(int x) {
    return x == pre[x] ? x : pre[x] = find(pre[x]);
}

void dfs(int u) {
    for(auto& v : G[u]) {
        d[v] = d[u] + 1;
        f[v][0] = u;
        for(int k = 1;(1 << k) <= d[v];++ k)
            f[v][k] = f[f[v][k - 1]][k - 1];
        dfs(v);
        sum[u] += sum[v];
    }
    return ;
}

int calc(int x,int y,int mid) {
    for(int k = 17;~ k;-- k) {
        if(f[x][k]&&val[f[x][k]] <= mid)x = f[x][k];
        if(f[y][k]&&val[f[y][k]] <= mid)y = f[y][k];
    }
    return (x ^ y) ? sum[x] + sum[y] : sum[x];
}

int main() {
    scanf("%d %d",&n,&m);
    cnt = n;
    for(int i = 1;i <= n;++ i)sum[i] = 1;
    for(int i = 1;i <= 2 * n;++ i)pre[i] = i;
    for(int i = 1;i <= m;++ i) {
        int u,v;
        scanf("%d %d",&u,&v);
        u = find(u);
        v = find(v);
        if(u == v)continue ;
        pre[u] = pre[v] = ++ cnt;
        val[cnt] = i;
        G[cnt].pb(u);
        G[cnt].pb(v);
    }

    for(int i = 2;i <= 2 * n;++ i)
        lg[i] = lg[i >> 1] + 1;
    dfs(cnt);

    int Q;
    for(scanf("%d",&Q);Q --;) {
        int x,y,z;
        scanf("%d %d %d",&x,&y,&z);
        int l = 1,r = m;
        while(l <= r) {
            int mid = l + r >> 1;
            if(calc(x , y , mid) >= z)r = mid - 1;
            else l = mid + 1;
        }
        printf("%d\n",l);
    }
    return 0;
}

[NOI2018] 归程 - 洛谷

好长的题面,不简述了。

把最大生成树的重构树建出来,水位线的限制就解除了。

预处理出 1 号节点到其它点的最短路,用倍增找出从 \(v\) 乘车出发能到达的所有点。

此时问题就是静态查询子树内叶子结点的权值最小值,构树的时候就能预处理出来。

要注意一下这题的最短路,出题人刻意卡了 SPFA。关于 SPFA,它死了。

// Problem: P4768 [NOI2018] 归程
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P4768
// Memory Limit: 512 MB
// Time Limit: 4000 ms
// 
// Powered by CP Editor (https://cpeditor.org)

#include <bits/stdc++.h>
#define pb emplace_back
typedef std::pair<int,int> pii;

const int maxn = 2e5 + 5;
const int maxm = 4e5 + 5;
int n,m,cnt,pre[maxn << 1],dis[maxn << 1],val[maxn << 1];
std::vector<int> G[maxn << 1];
struct node {
    int u,v,x;
    node() {
        u = v = x = 0;
    }
    node(int u,int v,int x):u(u),v(v),x(x){}
}E[maxm];

int find(int x) {
    return pre[x] == x ? x : pre[x] = find(pre[x]);
}

std::vector<pii> S[maxn];
std::priority_queue<pii,std::vector<pii>,std::greater<pii> > q;
bool vis[maxn];

void dijkstra(int s) {
    for(int i = 1;i <= n;++ i)
        dis[i] = 2e9,vis[i] = false;
    q.emplace(dis[s] = 0 , s);
    while(!q.empty()) {
        int u = q.top().second;
        q.pop();
        if(vis[u])continue ;
        vis[u] = true;
        for(auto& [v , w] : S[u]) {
            if(dis[v] > dis[u] + w)
                q.emplace(dis[v] = dis[u] + w , v);
        }
    }
    return ;
}

int d[maxn << 1],lg[maxn << 1],f[maxn << 1][20];

void dfs(int u) {
    for(auto& v : G[u]) {
        d[v] = d[u] + 1;
        f[v][0] = u;
        for(int k = 1;(1 << k) <= d[v];++ k)
            f[v][k] = f[f[v][k - 1]][k - 1];
        dfs(v);
    }
    return ;
}

void work() {
    scanf("%d %d",&n,&m);
    for(int i = 1;i <= n;++ i)
        S[i].clear();
    for(int i = 1;i <= m;++ i) {
        int u,v,t,x;
        scanf("%d %d %d %d",&u,&v,&t,&x);
        E[i] = node(u , v , x);
        S[u].pb(v , t);
        S[v].pb(u , t);
    }

    dijkstra(1);
    cnt = n;
    for(int i = 1;i <= n * 2;++ i)
        G[i].clear(),pre[i] = i;
    std::sort(E + 1 , E + 1 + m , [&](const node& p,const node& q) {
        return p.x > q.x;
    });
    for(int i = 1;i <= m;++ i) {
        int u = find(E[i].u),v = find(E[i].v);
        if(u == v)continue ;
        pre[u] = pre[v] = ++ cnt;
        val[cnt] = E[i].x;
        dis[cnt] = std::min(dis[u] , dis[v]);
        G[cnt].pb(u);
        G[cnt].pb(v);
    }

    for(int i = 2;i <= n * 2;++ i)
        lg[i] = lg[i >> 1] + 1;
    for(int i = 1;i <= n * 2;++ i)
        for(int k = 0;k < 20;++ k)
            f[i][k] = 0;
    dfs(cnt);

    int Q,k,s,ans = 0;
    scanf("%d %d %d",&Q,&k,&s);
    while(Q --) {
        int u,p;
        scanf("%d %d",&u,&p);
        u = (u + k * ans - 1) % n + 1;
        p = (p + k * ans) % (s + 1);
        for(int j = lg[d[u]];~ j;-- j)
            if(f[u][j]&&val[f[u][j]] > p)u = f[u][j];
        printf("%d\n",ans = dis[u]);
    }
    return ;
}

int main() {
    int T;
    scanf("%d",&T);
    while(T --)work();
    return 0;
}

[CERC2016]机棚障碍 Hangar Hurdles

还是懒得概括题面。

不难发现,一个箱子从点 \(A\) 移动到点 \(B\) 的充要条件是箱子大小不超过 \(A,B\) 两点的限制。

用二分和二维前缀和预处理出来点 \((i,j)\) 的限制,设为 \(d(i,j)\)

显然,若 \((i,j)\)\((x,y)\) 相邻,那么它们之间可以连一条权值为 \(\min\{d(i,j),d(x,y)\}\) 的边。

每次询问要回答的其实就是从 \(A_k\)\(B_k\) 的最小权值的最大值。

这个模型显然就是最大生成树重构树的模型。直接套模板即可。

我参考的学习笔记中,大佬给出了 FloodFill 缩点减少时空复杂度的方法。我这里也写了一下。当然,不这样写也可以过。

// Problem: P3684 [CERC2016]机棚障碍 Hangar Hurdles
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P3684
// Memory Limit: 500 MB
// Time Limit: 4000 ms
// 
// Powered by CP Editor (https://cpeditor.org)

#include <bits/stdc++.h>
#define pb emplace_back

const int maxn = 1005;
const int maxm = 3e6 + 5;
int n,d[maxn][maxn],dfn[maxn][maxn],cnt,pre[maxm],val[maxm],sum[maxn][maxn];
char s[maxn][maxn];
std::vector<int> tmp,G[maxm];
int fx[] = {1 , 0 , -1 , 0},fy[] = {0 , 1 , 0 , -1};
struct node {
    int u,v,t;
    node() {
        u = v = t = 0;
    }
    node(int u,int v,int t):u(u),v(v),t(t){}
}E[maxm];

int find(int x) {
    return x == pre[x] ? x : pre[x] = find(pre[x]);
}

void dfs(int x,int y) {
    if(dfn[x][y])return ;
    dfn[x][y] = cnt;
    for(int k = 0;k < 4;++ k) {
        int nx = x + fx[k],ny = y + fy[k];
        if(nx < 1||nx > n||ny < 1||ny > n)continue ;
        if(!dfn[nx][ny]&&d[nx][ny] == d[x][y])dfs(nx , ny);
    }
}

int f[maxm][23],lg[maxm],dep[maxm];

void DFS(int u) {
    for(auto& v : G[u]) {
        dep[v] = dep[u] + 1;
        f[v][0] = u;
        for(int k = 1;(1 << k) <= dep[v];++ k)
            f[v][k] = f[f[v][k - 1]][k - 1];
        DFS(v);
    }
    return ;
}

int LCA(int u,int v) {
    if(dep[u] < dep[v])std::swap(u , v);
    for(int k = lg[dep[u]];~ k;-- k) {
        if(dep[u] - (1 << k) >= dep[v]) {
            u = f[u][k];
        }
        if(u == v)return u;
    }
    for(int k = lg[dep[u]];~ k;-- k) {
        if(f[u][k] != f[v][k]) {
            u = f[u][k];
            v = f[v][k];
        }
    }
    return f[u][0];
}

int main() {
    scanf("%d",&n);
    for(int i = 1;i <= n;i += 2)tmp.pb(i);
    for(int i = 1;i <= n;++ i) {
        scanf("%s",s[i] + 1);
        for(int j = 1;j <= n;++ j) {
            sum[i][j] += sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + (s[i][j] == '#');
        }
    }

    for(int i = 1;i <= n;++ i) {
        for(int j = 1;j <= n;++ j) {
            if(s[i][j] == '#') {
                d[i][j] = 0;
                continue ;
            }
            int l = 0,r = tmp.size() - 1;
            while(l <= r) {
                int mid = l + r >> 1,k = tmp[mid] >> 1;
                if(i + k > n||i - k <= 0||j + k > n||j - k <= 0) {
                    r = mid - 1;
                    continue ;
                }
                if(sum[i + k][j + k] - sum[i + k][j - k - 1] - sum[i - k - 1][j + k] + sum[i - k - 1][j - k - 1] > 0)
                    r = mid - 1;
                else l = mid + 1;
            }
            d[i][j] = tmp[r];
        }
    }

    for(int i = 1;i <= n;++ i) {
        for(int j = 1;j <= n;++ j) {
            if(!dfn[i][j]) {
                val[++ cnt] = d[i][j];
                dfs(i , j);
            }
        }
    }

    int tot = 0;
    for(int i = 1;i <= n;++ i) {
        for(int j = 1;j <= n;++ j) {
            for(int k = 0;k < 2;++ k) {//注意一下,这里不是 4 而是 2,减少了一些重复的边。
                int x = i + fx[k],y = j + fy[k];
                if(x < 1||x > n||y < 1||y > n)continue ;
                if(dfn[i][j] != dfn[x][y])
                    E[++ tot] = node(dfn[i][j] , dfn[x][y] , std::min(d[i][j] , d[x][y]));
            }
        }
    }

    std::sort(E + 1 , E + 1 + tot , [&](const node& p,const node& q) {
        return p.t > q.t;
    });
    for(int i = 1;i <= cnt * 2;++ i)pre[i] = i;
    for(int i = 1;i <= tot;++ i) {
        int u = find(E[i].u),v = find(E[i].v);
        if(u == v)continue ;
        pre[u] = pre[v] = ++ cnt;
        val[cnt] = E[i].t;
        G[cnt].pb(u);
        G[cnt].pb(v);
    }

    for(int i = 2;i <= cnt;++ i)
        lg[i] = lg[i >> 1] + 1;
    DFS(cnt);

    int Q;
    scanf("%d",&Q);
    while(Q --) {
        int x,y,nx,ny;
        scanf("%d %d %d %d",&x,&y,&nx,&ny);
        printf("%d\n",val[LCA(dfn[x][y] , dfn[nx][ny])]);
    }
    return 0;
}

[CF1706E] Qpwoeirut and Vertices

这是我在学习 Kruskal 重构树之前遇到的第一道 Kruskal 重构树的题目。

题解在这里,当时我并不会标准的 Kruskal 重构树,本质上是写了一个最小生成树上倍增。

不过这题是名副其实的 Educational 题,质量很不错,可以拿来练手。

[IOI2018] werewolf 狼人

仍然懒得写题意。

首先要发现,这个问题的本质是:

\(A\)\(S\) 经过 \(\ge L\) 的边能到达的点的集合,\(B\)\(E\) 经过 \(\le R\) 的边能到达的点的集合,判断 \(A\cap B\) 是否为空集。这个问题的加强版在 [CF1093E]Intersection of Permutations,需要了解做法。

分别构建两棵重构树,一棵为最大生成树 \(tr_0\),一棵为最小生成树 \(tr_1\)

预处理出两棵树的 dfs 序,在询问中倍增求区间,然后用主席树求解即可。

这里有个很简单,但困扰了我很长时间的问题:边权怎么设?我不会说我是因为这个问题调了 4h 的

因为 \(S\) 经过的所有点编号都要 \(\ge L\),所以 \(tr_0\) 中,边 \((u,v)\) 的权值应为 \(\min\{u,v\}\)

相应的,\(tr_1\) 中边 \((u,v)\) 的权值为 \(\max\{u,v\}\)

因为一开始这个简单的问题我没搞明白,导致代码中建树的过程乱七八糟,见谅。

// Problem: P4899 [IOI2018] werewolf 狼人
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P4899
// Memory Limit: 500 MB
// Time Limit: 4000 ms
// 
// Powered by CP Editor (https://cpeditor.org)

#include <bits/stdc++.h>
#define pb emplace_back

const int maxn = 4e5 + 5;
int n,m,Q,pre[maxn],a[maxn],rk[maxn],from[maxn],to[maxn];
struct Edge {
    int u,v,t;
    Edge() {
        u = v = t = 0;
    }
    Edge(int u,int v,int t):u(u),v(v),t(t){}
}E[maxn];

int find(int x) {
    return x == pre[x] ? x : pre[x] = find(pre[x]);
}

struct Kruskal_rebuild {
    std::vector<int> G[maxn];
    int val[maxn],cnt,num,lg[maxn],f[maxn][20],d[maxn],L[maxn],R[maxn],dfn[maxn];

    void dfs(int u) {
        if(u <= n)
            L[u] = R[u] = ++ num,dfn[num] = u,val[u] = u;
        else L[u] = num + 1;
        for(auto& v : G[u]) {
            d[v] = d[u] + 1;
            f[v][0] = u;
            for(int k = 1;(1 << k) <= d[v];++ k)
                f[v][k] = f[f[v][k - 1]][k - 1];
            dfs(v);
        }
        R[u] = num;
        return ;
    }

    void build_1() {
        for(int i = 1;i <= m;++ i)
            E[i].u = from[i],E[i].v = to[i],E[i].t = std::max(E[i].u , E[i].v);
        for(int i = 1;i <= n * 2;++ i)pre[i] = i;
        cnt = n;
        std::sort(E + 1 , E + 1 + m , [&](const Edge& p,const Edge& q) {
            return p.t < q.t;
        });
        for(int i = 1;i <= m;++ i) {
            int u = find(E[i].u),v = find(E[i].v);
            if(u == v)continue ;
            pre[u] = pre[v] = ++ cnt;
            val[cnt] = E[i].t;
            G[cnt].pb(u);
            G[cnt].pb(v);
            if(cnt == 2 * n - 1)break ;
        }

        for(int i = 2;i <= n * 2;++ i)
            lg[i] = lg[i >> 1] + 1;
        dfs(cnt);
        return ;
    }

    void build_2() {
        for(int i = 1;i <= m;++ i)
            E[i].u = from[i],E[i].v = to[i],E[i].t = std::min(E[i].u , E[i].v);
        for(int i = 1;i <= n * 2;++ i)pre[i] = i;
        cnt = n;
        std::sort(E + 1 , E + 1 + m , [&](const Edge& p,const Edge& q) {
            return p.t > q.t;
        });
        for(int i = 1;i <= m;++ i) {
            int u = find(E[i].u),v = find(E[i].v);
            if(u == v)continue ;
            pre[u] = pre[v] = ++ cnt;
            val[cnt] = E[i].t;
            G[cnt].pb(u);
            G[cnt].pb(v);
            if(cnt == 2 * n - 1)break ;
        }

        for(int i = 2;i <= n * 2;++ i)
            lg[i] = lg[i >> 1] + 1;
        dfs(cnt);
        return ;
    }

    int find_1(int x,int v) {
        for(int k = lg[d[x]];~ k;-- k)
            if(f[x][k]&&val[f[x][k]] >= v)x = f[x][k];
        return x;
    }

    int find_2(int x,int v) {
        for(int k = lg[d[x]];~ k;-- k)
            if(f[x][k]&&val[f[x][k]] <= v)x = f[x][k];
        return x;
    }
}tr[2];

int tot,ls[maxn << 5],rs[maxn << 5],sum[maxn << 5],rt[maxn];

int build(int l,int r) {
    int p = ++ tot;
    sum[p] = 0;
    if(l == r)return p;
    int mid = l + r >> 1;
    ls[p] = build(l , mid);
    rs[p] = build(mid + 1 , r);
    return p;
}

int modify(int i,int l,int r,int pos) {
    int p = ++ tot;
    sum[p] = sum[i] + 1;
    if(l == r)return p;
    int mid = l + r >> 1;
    if(pos <= mid)ls[p] = modify(ls[i] , l , mid , pos),rs[p] = rs[i];
    else rs[p] = modify(rs[i] , mid + 1 , r , pos),ls[p] = ls[i];
    return p;
}

int query(int i,int pre,int l,int r,int val) {
    if(l == r)return l == val ? sum[i] - sum[pre] : 0;
    int mid = l + r >> 1;
    if(val <= mid)return query(ls[i] , ls[pre] , l , mid , val);
    else return query(rs[i] , rs[pre] , mid + 1 , r , val) + sum[ls[i]] - sum[ls[pre]];
}

int main() {
    int Q;
    scanf("%d %d %d",&n,&m,&Q);
    for(int i = 1;i <= m;++ i) {
        scanf("%d %d",&from[i],&to[i]);
        ++ from[i];
        ++ to[i];
    }

    tr[0].build_2();//这里 build_1 和 build_2 是反过来的,看得有点难受,还是一开始没理解的锅。
    tr[1].build_1();

    for(int i = 1;i <= n;++ i)rk[tr[0].dfn[i]] = i;
    rt[0] = build(1 , n);
    for(int i = 1;i <= n;++ i)
        rt[i] = modify(rt[i - 1] , 1 , n , rk[tr[1].dfn[i]]);

    while(Q --) {
        int s,t,l,r;
        scanf("%d %d %d %d",&s,&t,&l,&r);
        ++ s;
        ++ t;
        ++ l;
        ++ r;
        int x = tr[0].find_1(s , l),y = tr[1].find_2(t , r);
        if(query(rt[tr[1].R[y]] , rt[tr[1].L[y] - 1] , 1 , n , tr[0].R[x]) - query(rt[tr[1].R[y]] , rt[tr[1].L[y] - 1] , 1 , n , tr[0].L[x] - 1))
            puts("1");
        else
            puts("0");
    }
    return 0;
}

本来还有两道例题,是 [CF1578L] Labyrinth[ARC098F] Donation ,不过我看了一下,一道 CF *2400,一道 AT *3200,感觉不好惹,先溜了,以后再补(挖坑

不知道能不能有人看到的 upd:现在我会 Donation 了,如果要挂上来还有人看吗?(

挂个 链接 先跑路咯~

posted @ 2022-10-22 15:57  ImALAS  阅读(46)  评论(0编辑  收藏  举报