zoukankan      html  css  js  c++  java
  • 货车运输

    思路:

    首先是要建最大生成树,不难发现,对于两点\(u,v\),如果\(u->v\)中最小的边的权值最大,那么这条路径\(u->v\)一定在最大生成树上。

    考虑用\(Kruskal\)建最大生成树,那么首先对边按照从大到小进行排序。

    然后用并查集维护两个点的联通性,如果\(u\)\(v\)不在同一个并查集中就合并,注意到合并的同时,我们保证了这样的一个信息:

    在对第\(i\)条边操作的时候,假设合并了并查集\(S1\)与并查集\(S2\),那么显然有:(记第\(i\)条边的边权为\(V_i\)


    性质1.集合\(S1\)内点,集合\(S2\)内点分别可以相互到达 (由前面\(Kruskal\)建最大生成树的流程得到的)

    性质2.集合\(S1,S2\)内,任意两点之间的边权的权值都比\(Ki\)大(因为已经按照边权排序了)


    那么我们不难发现,从集合\(S1\)内的点出发,到集合\(S2\)内的点,所经过的路径中,边权最小的即为\(V_i\),换而言之,我们现在的目标是维护这个东西。也就是说,对于某一次询问\(u\)\(v\),我能告诉你\(u\)\(v\)两个点被合并到一个集合时,通过的边的边权。(这一段感性理解,因为打字不好描述)

    如何维护?/流程,做法

    为了方便起见,我们认为\(V_i\)表示第\(i\)条边的边权。(貌似这个做法又被叫做\(Kruskal\)重构树?)

    仍然按照\(Kruskal\)建最大生成树的方法,对边按照边权排序。

    //下面是我自己可能也看不懂的解释,但还是强行解释了一波流程...\(QAQ\)

    (您可以忽略这一段,直接跳到下面清晰好看的字体附近)


    第一步,首先考虑连接两个点的边,假设边\(i\)连接了点\(u\)\(v\),那么我可以建立一个节点,这个点的点权为\(V_i\),然后其拥有两个儿子,分别指向\(u\)\(v\),那么我询问\(u\)\(v\)的路径长度,就是\(u\)\(v\)两点的父亲。然后把\(u\)\(v\)合并到一个并查集中,表示\(u\)\(v\)联通。然后记这个并查集为\(S1\),其最顶上的根节点记为\(root1\),这样做,我们就建出了一个二叉树。

    然后考虑假设边\(i\)连接了两个大小为\(2\)的并查集\(S1\)\(S2\),这个大小为\(2\)的并查集即第一步建立的并查集,那么我建立一个节点\(k\),其点权为\(V_i\),那么其两个儿子则分别指向并查集\(S1,S2\)\(root1,root2.\)不难发现,从并查集\(S1\)中的点到并查集\(S2\)中的点,必须要经过点\(k\)。那么集合\(S1\)中的点到集合\(S2\)中的点所经过的点,就是在以此法建出来的树中,经过的点的点权最小值,也就是\(k\)的点权。

    依次类推,对于一条边\(i\),如果其连接了并查集\(S1\)\(S2\),仍然有之前得到的性质\(1\)和性质\(2\),所以建立一个节点\(k\),令其二个儿子分别指向\(S1\)\(S2\)\(root\),然后令其点权为\(V_i\)


    简化建树流程如下:


    
    > 1.按照边权排序
    
    > 2.对于一条边所指向的两个点,判断其是否在同一集合,若不在,合并两个集合,把这条边当作一个树点,新建节点,权值为这条边的边权,令其两个儿子指向这两个集合的树的祖先root。
    
    > 3.不难发现,对于u->v的路径,其在最大生成树中唯一,且在重构树中也唯一,即从叶子节点u到叶子节点v的路径。
    
    > 4.同时,由于建树流程只是在Kruskal建树流程加了一步,所以我仍然有前面所提到的性质1和性质2,所以我们有对于一个父亲节点,若两个儿子为边,则儿子权值一定比父亲大(性质2)。所以询问集合S1到集合S2的最小限重,就是这个点的点权。
    
    > 5.对于第4点反过来考虑,询问u->v的答案,实际上就是询问重构树中,点u和点v的LCA的权值
    

    即对于询问\(u->v\)最小限重,我们求出其\(LCA\),然后返回\(LCA\)的权值即可。

    所以我们的问题变成,在重构的树上求\(LCA\)了。。。

    所以用\(Trajan\)算法呗......

    复杂度貌似就排序时会有\(log\),总复杂度\(O(MlogM)\)

    代码://笔者写得比较丑......因为弱.......

    #include<bits/stdc++.h>
    using namespace std;
    int read(){
    	char cc = getchar(); int cn = 0;
    	while(cc > '9' || cc < '0') cc = getchar();
    	while(cc >= '0' && cc <= '9') cn = cn * 10 + cc - '0', cc = getchar();
    	return cn;
    }
    const int M = 50000 * 2 + 5;
    struct E{
    	int to, w, next, from;
    	bool operator < (const E &a) const{
    		return w > a.w;
    	}// 重定义< 号 
    }e[M * 2];
    struct Tree{
    	int son[2], fa, val, p, root; //p表示这个点为边还是点 
    }t[M * 2];
    int head[M], cnt, book[M], n, m, fa[M], tot, ans[M], top[M];
    int Head[M], To[M], Next[M], Id[M], tot1;
    //加边 
    void add(int x, int y, int z)
    {
    	e[++cnt].to = y;  e[cnt].from = x;  e[cnt].w = z;
    	e[cnt].next = head[x];  head[x] = cnt;
    } 
    //---并查集 
    //查找 
    int find(int x){
    	if(x == fa[x]) return x;
    	return fa[x] = find(fa[x]);
    }
    //合并 
    void merge(int x, int y){
    	int u = find(x), v = find(y);
    	fa[u] = v;
    }
    //---重构树--- 
    int find_top(int x){ //询问root,即当前点x的祖先 
    	while(t[x].fa != 0) x = t[x].fa;
    	return x;
    }
    void merge1(int x, int y, int w){//新建节点,同时将两棵树变为此点儿子 
    	t[++tot].son[0] = x;  t[tot].son[1] = y;
    	t[tot].val = w;
    	t[x].fa = tot; t[y].fa = tot;
    	t[x].root = 0, t[y].root = 0, t[tot].root = 1;
    }
    void Kruskal(){//重构树 
    	for(int i = 1; i <= n; i++){
    		fa[i] = i;//并查集初始化 
    		t[++tot].p = 1; t[tot].val = i; t[tot].root = 1, top[i] = i;//每个节点对应的树初始化 
    	}
    	sort(e + 1, e + cnt + 1);//排序 
    	for(int i = 1; i <= cnt; i++){
    		int u = find(e[i].from), v = find(e[i].to);
    		if(u != v){//不在同一个并查集 
    			merge(u, v);
    			int u1 = find_top(top[e[i].from]); //top记录了i号点,在当前情况下,祖先是谁,在询问i号的时候更新其top,防止跳跃次数比较多,算是一个小优化 
    			int v1 = find_top(top[e[i].to]);
    			merge1(u1, v1, e[i].w);
    			top[e[i].to] = tot; top[e[i].from] = tot;
    		}
    	}
    }
    //---询问---Trajan 
    void add_q(int x, int y, int z){
    	To[++tot1] = y;	Id[tot1] = z;
    	Next[tot1] = Head[x]; Head[x] = tot1;
    }
    void dfs(int now){
    	book[now] = 1;
    	if(t[now].p){
    		for(int i = Head[t[now].val]; i; i = Next[i])
    			if(book[To[i]])  ans[Id[i]] = t[find(To[i])].val;
    		return ;
    	}
    	dfs(t[now].son[0]);  fa[t[now].son[0]] = now;
    	dfs(t[now].son[1]);  fa[t[now].son[1]] = now;
    }
    void input(){
    	n = read(); m = read();
    	int x, y, z;
    	for(int i = 1; i <= m; i++)
    	{
    		x = read(); y = read(); z = read();
    		add(x, y, z);
    		add(y, x, z); 
    	}
    }
    void solve(){
    	int p, x, y; 
    	p = read();
    	for(int i = 1; i <= p; i++)
    	{
    		x = read(); y = read();
    		add_q(x, y, i); add_q(y, x, i);
    	}
    	for(int i = 1; i <= tot; i++) fa[i] = i;
    	t[0].val = -1; //因为可能图并不联通,所以会建出多棵重构树,然后令这些重构树的父亲都为0号点。那么其实通过0号点到达点,本质上是并不连通的,LCA为0,其LCA的权值为-1 
    	for(int i = 1; i <= tot; i++){
    		if(t[i].root == 1)//这个点为一个根节点 
    			dfs(i);//dfs,trajan 
    			fa[i] = 0; //然后把这个点父亲改为0 
    	}
    	for(int i = 1; i <= p; i++) printf("%d\n", ans[i]);
    }
    signed main()
    {
    	input();
    	Kruskal();
    	solve(); 
    	return 0;
    } 
    
  • 相关阅读:
    C#泛型类的简单创建与使用
    线程、委托、lambda运算符的简单示例
    异步编程中使用帮助类来实现Thread.Start()的示例
    C#操作INI配置文件示例
    C#“简单加密文本器”的实现
    Java设计模式之模板模式(Template )
    java提取出一个字符串里面的Double类型数字
    阿里云服务器配置SSL证书成功开启Https(记录趟过的各种坑)
    Gson解决字段为空是报错的问题
    shiro 单点登录原理 实例
  • 原文地址:https://www.cnblogs.com/Soulist/p/10332010.html
Copyright © 2011-2022 走看看