前言
这只蒟蒻由于一道需要最小生成树的题只会打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))。
给 (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;
}