最小生成树

前言

这只蒟蒻由于一道需要最小生成树的题只会打Kruskal而逼近超时

心血来潮学了Prim

其实都挺简单的(为什么我当初就不会Prim)

update 2021.7.17 新搞懂了 (Boruvka) 算法。

一、Kruskal算法

适用于稀疏图。

其实很简单的,你只要对于每一条边,按边权从小打大排序,如果两个点没有在一个连通块中,那么连接,累加权值,连接用并查集维护。

连几条边呢?

当然是n-1条。

Kruskal
void MST()
{
	n = Read();
	m = Read();
	for(int i = 1;i <= n;++ i)
		f[i] = i;
	for(int i = 1;i <= m;++ i)
	{
		int dz1 = Read(),dz2 = Read(),dz3 = Read();
		q.push(node(dz1,dz2,dz3));
	}
	int tot = 0,ans = 0;
	while(tot < n - 1)
	{
		node t = q.top();
		q.pop();
		int u1 = findSet(t.u),v1 = findSet(t.v);
		if(u1 != v1)
		{
			f[u1] = v1;
			ans += t.s;
			++ tot;
		}
	}
	printf("%d",ans);
}

时间复杂度:(O(mlog(m)))((m)为边数)。

如果是完全图,时间复杂度就不能忍受了:(O(n^2/2*log(n^2/2)))

其实就是:(O(n^2log(n^2)))

那么就要引出今天的主角:

二、Prim算法

适用于稠密图。

其实这个也不难,我们需要一个dis数组存当前如果要连到第i个点需要的最小权值,vis数组存这个点是否已经加入了最小生成树。

我们把起点默认设为一号点,预处理出一号点到到其他点的最小距离,即预处理出(dis)数组,(vis[1])当然要赋值为1,然后开始加边,当然也是n-1次,每次找出最小的(dis[i])且i没有被加入到最小生成树中,加入它(自行YY怎么写),然后更新(dis[i])

时间复杂度:(O(n^2))

是不是很快啊,少了一个(log(n))呢!

Prim
void MST()
{
	n = Read();
	m = Read();
	for(int i = 1;i <= m;++ i)
	{
		int x = Read(),y = Read(),z = Read();
		Add_Edge(x,y,z);
		Add_Edge(y,x,z);
	}
	memset(dis,INF,sizeof(dis));
	vis[1] = 1;
	for(int i = head[1]; i ;i = e[i].nxt)
		dis[e[i].v] = Min(dis[e[i].v],e[i].s);//有重边要用Min值 
	for(int i = 1;i < n;++ i)
	{
		int ID,MIN = INF;
		for(int j = 1;j <= n;++ j)
			if(!vis[j] && dis[j] < MIN)
			{
				MIN = dis[j];
				ID = j;
			}
		ans += MIN;
		vis[ID] = 1;
		for(int i = head[ID]; i ;i = e[i].nxt)
			dis[e[i].v] = Min(dis[e[i].v],e[i].s);
	}
	printf("%d",ans);
}

以上两份代码要建立在是连通图上,否则会出现问题!

三、Boruvka算法

其实跟前面两种都差不多,本质上还是贪心。

我们考虑维护每个连通块,初始时 (n) 个点自然就有 (n) 个连通块。

每一次操作 (O(m)) 求出每个连通块连向其它连通块的最短边,然后连起来。

可以发现一次操作下来最劣情况连通块减半,最优情况直接连通,所以时间复杂度是 (O(mlog_2n)) 的。

实现的时候其实是 (O((n+m)log_2n)) 的。

Boruvka
struct edge
{
	int u,v,w;
	void Get(){u = Read();v = Read();w = Read();}
}e[MAXM];
int f[MAXN],dis[MAXN],to[MAXN];
int findSet(int x)
{
	if(f[x] != x) f[x] = findSet(f[x]);
	return f[x];
}
bool unionSet(int x,int y)
{
	int u = findSet(x),v = findSet(y);
	if(u == v) return 0;
	if(u > v) swap(u,v);
	f[v] = u;
	return 1;
}

int main()
{
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	n = Read(); m = Read();
	for(int i = 1;i <= m;++ i) e[i].Get();
	for(int i = 1;i <= n;++ i) f[i] = i,dis[i] = INF,to[i] = 0;
	int cnt = n;
	while(cnt > 1)
	{
		for(int i = 1,u,v;i <= m;++ i)
		{
			u = findSet(e[i].u);
			v = findSet(e[i].v);
			if(u != v)
			{
				if(e[i].w < dis[u]) dis[u] = e[i].w,to[u] = v;
				if(e[i].w < dis[v]) dis[v] = e[i].w,to[v] = u;
			}
		}
		bool f = 0;
		for(int i = 1;i <= n;++ i)
		{
			if(findSet(i) == i && to[i])
				if(unionSet(i,to[i]))
					ans += dis[i],cnt--,f = 1;
			dis[i] = INF; to[i] = 0; 
		}
		if(!f) {printf("orz");return 0;}
	}
	Put(ans);
	return 0;
}

但是 Boruvska 算法更有前途,为什么这么说呢?因为在某些题里面,它一轮更新的时间复杂度并不需要 (O(m))

比如CF888G Xor-MST(洛谷)

(n) 个点,每个点有权值 (a_i),两个点 (i,j) 连边的代价为 (a_i oplus a_j),求最小生成树。

对于这道题,上面两个算法显得无能为力,而普通版本的 Boruvka 也做不了,但是改一改就行了。

我们注意到在一轮更新的时候每个点可以在 (O(log_2 a_i)) 的时间内通过权值线段树/Trie找到最小的连向其它连通块的边,至于维护连通性可以用并查集,顺便也可以用线段树合并维护其它点的Trie。

时间复杂度可以做到 (O(nlog_2a_ilog_2n))

或者我们把这玩意叫做最小异或生成树

还有个小常数的权值分治做法,可以在这里的T3看到更详细的讲解。

最小异或生成树
//12252024832524
#include <cstdio>
#include <cstring>
#include <algorithm>
#define TT template<typename T>
using namespace std; 

typedef long long LL;
const int MAXN = 200005;
const int INF = 1 << 30;
int n;

LL Read()
{
	LL x = 0,f = 1;char c = getchar();
	while(c > '9' || c < '0'){if(c == '-')f = -1;c = getchar();}
	while(c >= '0' && c <= '9'){x = (x*10) + (c^48);c = getchar();}
	return x * f;
}
TT void Put1(T x)
{
	if(x > 9) Put1(x/10);
	putchar(x%10^48);
}
TT void Put(T x,char c = -1)
{
	if(x < 0) putchar('-'),x = -x;
	Put1(x); if(c >= 0) putchar(c);
}
TT T Max(T x,T y){return x > y ? x : y;}
TT T Min(T x,T y){return x < y ? x : y;}
TT T Abs(T x){return x < 0 ? -x : x;}

bool vis[MAXN];
int od[MAXN],f[MAXN],val[MAXN];
bool cmp(int x,int y){return val[x] < val[y];}
int findSet(int x)
{
	if(f[x]^x) f[x] = findSet(f[x]);
	return f[x];
}

int rt[MAXN],tot,who;
int ch[MAXN*60][2],siz[MAXN*60],ID[MAXN*60];
void Add(int &x,int v,int rk)
{
	if(!x) x = ++tot;
	++siz[x];
	if(rk < 0) {ID[x] = who;return;}
	bool to = v >> rk & 1;
	Add(ch[x][to],v,rk-1);
}
int mge(int x,int y)
{
	if(!x || !y) return x|y;
	siz[x] += siz[y];
	ch[x][0] = mge(ch[x][0],ch[y][0]);
	ch[x][1] = mge(ch[x][1],ch[y][1]);
	return x;
}
void unionSet(int u,int v)
{
	u = findSet(u); v = findSet(v);
	if(u^v) f[v] = u,rt[u] = mge(rt[u],rt[v]);
}
int MIN[MAXN],CID[MAXN],ori;
void Query(int lst,int x,int v,int rk)
{
	if(v >= MIN[who]) return;
	if(rk < 0)
	{
		CID[who] = ID[lst];
		MIN[who] = v;
		return;
	}
	bool to = val[ori] >> rk & 1;
	if(siz[ch[lst][to]] - siz[ch[x][to]]) Query(ch[lst][to],ch[x][to],v,rk-1);
	else Query(ch[lst][to^1],ch[x][to^1],v|(1<<rk),rk-1);
}

int main()
{
//	freopen("xor.in","r",stdin);
//	freopen("xor.out","w",stdout);
	n = Read();
	for(int i = 1;i <= n;++ i) val[i] = Read(); 
	for(int i = 1;i <= n;++ i) od[i] = f[i] = i,MIN[i] = INF;
	int cnt = n;
	sort(od+1,od+n+1,cmp);
	for(int i = 2;i <= n;++ i)
		if(val[od[i]] == val[od[i-1]])
			unionSet(od[i-1],od[i]),--cnt;
	for(int i = 1;i <= n;++ i)
		if(findSet(i) == i)
			who = i,Add(rt[i],val[i],29),Add(rt[0],val[i],29);
	LL ans = 0;
	while(cnt > 1)
	{
		for(int i = 1;i <= n;++ i)
		{
			who = findSet(i); ori = i;
			Query(rt[0],rt[who],0,29);
		}
		for(int i = 1;i <= n;++ i)
		{
			if(MIN[i] < INF && findSet(i) != findSet(CID[i]))
			{
				unionSet(i,CID[i]);
				ans += MIN[i];
				MIN[i] = INF;
				--cnt;
			}
		}
	}
	Put(ans);
	return 0;
}

练习

板题(洛谷)

CF888G Xor-MST(洛谷)

原文地址:https://www.cnblogs.com/PPLPPL/p/13395344.html