最小生成树的扩展应用

最小生成树的扩展应用

标签(空格分隔): 学术


理论基础(常用推论)

  • 任意一颗最小生成树一定可以包括无向图中权值最小的边。
  • 在最小生成树算法的实现过程中对于任意两个联通块 V ,E ,当前不在联通块内部的所有边中最小的,一定是在最小生成树中的。
    课堂截图
    注:这里加绝对值代表该联通块内节点的个数
    此处输入图片的描述

最小生成树的扩展应用

1、“杂合”其他算法类

列题[1]bzoj2654

Description
给你一个无向带权连通图,每条边是黑色或白色。让你求一棵最小权的恰好有need条白色边的生成树。
题目保证有解。
Input
第一行V,E,need分别表示点数,边数和需要的白色边数。
接下来E行,每行s,t,c,col表示这边的端点(点从0开始标号),边权,颜色(0白色1黑色)。
Output
一行表示所求生成树的边权和。
V<=50000,E<=100000,所有数据边权为[1,100]中的正整数。
Sample Input
2 2 1
0 1 1 1
0 1 2 0
Sample Output
2

solution
求一棵恰好有 \(need\) 条白边的最小生成树,根据 \(MST\) 的求解方法,一条白边是否在 \(MST\) 中只与白边的权值有关。所以我们只需对白边进行增量操作即可。假设我们对图中的每条白边的增量为 \(x\)\(MST\) 中白边的数量记为 \(f(x)\) ,可以易看出 \(f(x)\) 的大小是随 \(x\) 的增大而增大的,满足单调性,考虑二分增量。
那么这道题就可以切了。。。

code

using Simon O_O ;
int n , m , need , ans ;
struct node 
{
	int frm , to , dis ;
	bool col ; 
} ed[maxn << 1] ;
bool operator <(node x , node y)
{
	if(x.dis == y.dis)	return x.col <  y.col ;//权值相同优先选白边
	return x.dis < y.dis ;
} 
int dis[maxn] , Cnt[maxn] , fa[maxn] ;
int find(int x)
{
	if(x == fa[x])	return x ; 
	return fa[x] = find(fa[x]) ; 
}
int kru(int x)
{
	int tot = 0 ;	ans = 0 ; 
	for(int i = 1 ; i <= m ; i++ )
		if(!ed[i].col)	ed[i].dis += x ;
	fa[0] = 0 ; 
	for(int i = 1 ; i <= n ; i++ )	fa[i] = i ;                                         
	sort(ed + 1 , ed + m + 1) ;
	for(int i = 1 ; i <= m ; i++ )
	{
		int u = find(ed[i].frm) , v = find(ed[i].to) ; 
		if(u == v)	continue ; 
		fa[v] = u ; 
		if(!ed[i].col)	tot++ ; 	ans += ed[i].dis ; 		
	}
	for(int i = 1 ; i <= m ; i++ )
		if(!ed[i].col)	ed[i].dis -= x ;
	return tot ;//统计MST中白边个数
}
signed main()
{
	n = read() , m = read() , need = read() ;
	int mid = 0 ;
	for(int i = 1 ; i <= m ; i++ )
	{
		int u , v , w , col ; 
		u = read() , v = read() , w = read() , col = read() ;
		ed[i] = {u , v , w , col}  ; 
	}
	int l = -105 , r = 105 ; 
	int ans2 = 0 ; 
	while(l < r)
	{
		mid = l + r >> 1 ;
		if(kru( mid ) >= need )		l = mid + 1 , ans2 = ans - need * mid ; 
		else r = mid ; 
	}
	cout << ans - need * mid ; 
	return 0 ; 
}

例题2 ACwing1145

此处输入图片的描述
solution
题目很抽象,简化一下就是求解一个最小的 \(d\) ,使得删去权值大于 \(d\) 的边后,剩下的联通块的个数不超过 \(k\) 个。可发现本题难点就在于模型的抽象,抽象出这一点来,解题不难。我们在进行 \(Kruskal\) 算法时其实就是在维护图中联通块的个数。在每次和合并并差集的时候联通块的数量减一。所以我们只需要在求解 \(MST\) 的过程中记录一下合并的次数即可,当合并次数恰好为 \(n-k\) 时,此时加入的树边的权值就为 \(d\)

code

#include <bits/stdc++.h>
#define mk make_pair
#define x first
#define y second 
#define pii pair<int , i >
using namespace std ;
const int maxn = 1e5 + 7 ;
int n , m , cnt ; 
pii q[maxn] ;
struct node 
{
    int frm , to ;
    double dis ;
}  ed[maxn << 1] ;
bool operator <(node a , node b )
{
	return a.dis < b.dis ;
}
int fa[maxn] ; 
int find(int a)
{
    if(fa[a] == a)  return a ;
    return fa[a] = find(fa[a]) ;
}
void add(int u , int v , int w)
{
    ed[++cnt] = {u , v , w } ; 
}
double len(pii a , pii b)
{
    int x1 = a.x , x2 = b.x , y1 = a.y , y2 = b.y ; 
    return sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)) ;  
}
signed main()
{
    cin >> n >> m ;
    for(int i = 1 ; i <= n ; i++ )
	{
	   cin >> q[i].x >> q[i].y ; 
	   fa[i] = i ; 
	}
    for(int i = 1 ; i <= n ; i++ )  
        for(int j = i + 1 ; j <= n ; j++ )
            add(i , j , len(q[i] , q[j])) ; 
    sort(ed , ed + cnt + 1) ; 
    int tot = n ;   double res = 0 ;
    for(int i = 1 ; i <= cnt ; i++ )
    {
        if( tot <= m)    break ;
        int u = find(ed[i].frm) , v = find(ed[i].to) ; 
        if(u == v)  continue ;
        tot-- , res = ed[i].dis ;   fa[v] = u ; 
    }
    printf("%.2lf" , res) ;
    return 0 ; 
}

例题3黑暗城堡ACwing349

此处输入图片的描述
solution
最短路 + 最小生成树 \(\Rightarrow\) 最短路径生成树
题目要求很明确,求出有多少棵最短路径生成树。
关于最短路径生成树请点击这里。简要概括一下,最短路径生成树就是对于树中的任意一对父子节点 \(f , s\)\(dis_s = dis_f + w\)\(w\) 为(f , s) 边的权值。所以针对此题来说,我们完全可以先求出起点到各个节点的最短路路径和 \(dis_i\) ,然后暴力的去枚举每个节点,枚举这个节点连出的所有边,如果满足 \(dis_s = dis_f + w\)\(cnt_s\)++ , 其含义就为当前节点可供选择的方案数。因为对于任何一个节点 \(i\) 选择 \(cnt_i\) 中的任何一中方案都可以构成最短路径生成树, 所以根据乘法原理累乘起来的结果就为最终答案。
(虽然我没用到最小生成树,但用 \(prim\) 算法同样是可以做出来的……)

code

using Simon O_O ;
int n , m , cnt ;
int ans = 1 ; 
int head[maxn] , Cnt[maxn] , dis[maxn] ; 
bool pd[maxn] ; 
struct node 
{
	int frm , to , nxt , dis ;
} ed[maxn << 1] ;
void add(int u , int v , int w )
{
	ed[++cnt] = {u , v , head[u] , w} ; 
	head[u] = cnt ; 
}
void dij()
{
	priority_queue<pii , vector<pii> , greater<pii> > q ;
	memset(dis , 0x3f , sizeof(dis)) ;
	dis[1] = 0 ;	q.push({0 , 1}) ; 
	while( ! q.empty() )
	{
		int u = q.top().second ;
		q.pop() ;     
		if(pd[u])	continue ; 	pd[u] = true ; 
		for(int i = head[u] ; i ; i = ed[i].nxt)
		{
			int v = ed[i].to , w = ed[i].dis ;
			if(dis[v] > dis[u] + w)
			{
				dis[v] = dis[u] + w ; 
				q.push({dis[v] , v}) ; 
			}
		}
	}
}
signed main()
{
	n = read() , m = read() ; 
	while( m-- )
	{
		int u = read() , v = read() , w = read()  ;
		add(u , v , w) , add(v , u , w) ;
	}
	dij() ;

	for(int i = 1 ; i <= n ; i++ )
		for(int j = head[i] ; j ; j = ed[j].nxt )
		{
			int v =  ed[j].to , w = ed[j].dis ; 
			if(dis[v] == dis[i] + w) Cnt[v]++ ; 
		}

	for(int i = 1 ;i <= n ; i++ )
		if(Cnt[i])	ans = ans * Cnt[i] % Mod ;
	cout << ans ; 
	return 0 ; 
}

例题4 [JSOI2008]最小生成树计数

solution
求出有多少个不同的最小生成树,只要有一条边不同即不同。由于 \(Kruskal\) 算法是依据 \(sort\) 排序来保证所构建出的生成树最小,所以我们可以通过改变排序的方式来改变构建出的生成树的权值和的大小。同时我们又需要保证所构建出的生成树最小,所以我们只能用与树边权值相等的边来等效替换树边,以构建出一个不同的最小生成树。数据保证具有相同权值的边 \(sum\le10\) 所以只需要暴力搜索出每种权值边合法的方案数,乘法原理连乘起来即为答案。

2、有关虚拟源点的应用

构建虚拟源点的方式来求解图论问题,比较常用,尤其是在负环问题和差分约束问题上更为普遍。在最小生成树类的问题上同样有相似的应用。
例题ACwing1146
此处输入图片的描述
solution
求解本题的关键在于办法1上,我们可以通过构建虚拟源点 0 来解决,对于每一个 \(i\) 点,构建一条以$ 0 \(为起点,\)i$ 为终点,边权为 \(v_i\) 的边。经过这样的处理,我们就可以构建出一张图,然后在图上跑最小生成树求解答案即可。
code

#include <cstdio>
#include <vector>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>
#include <set>
#define Simon namespace
//#define int long long
#define ll long long
#define ls p << 1
#define rs p << 1 | 1
#define pii pair<int,int>
#define mk make_pair
#define eps 1e-10
#define orz cout<<"msr_AK_IOI"
using namespace std;
const int Mod=1e9+7;	const int maxn = 1e6 + 7 ;		const int MAXN = 2e5 + 50 ; 	const int INF = 2147483647 ;
namespace O_O
{
void Fre(){	freopen(".in", "r", stdin);	freopen(".out", "w", stdout);	}
inline int read(){	int x=0,f=1;	char ch=getchar();	while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();	}	while (ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}return x*f;}
inline void wrt(long long  x){	if(x<0)	{	putchar('-');	x=-x;	}	if(x>9) wrt(x/10);	putchar(x%10+48);}
template <typename T> inline T Min(T x, T y) {return x < y ? x : y;}
template <typename T> inline T Max(T x, T y) {return x > y ? x : y;}
	}
using Simon O_O ;
struct node 
{
	int frm , to , dis ;
} ed[maxn << 1] ; 
bool operator <(node x , node y)
{
	return x.dis < y.dis ;
}
int n ,cnt ;
int fa[maxn] ; 
void add(int u , int v , int w)
{
	ed[++cnt] = { u , v , w} ;
}
int find(int x)
{
//	orz ;
	if(fa[x] == x)	return x ;
	return fa[x] = find(fa[x]) ;
}
int kru()
{
	int res = 0 , tot = 0 ; 	sort(ed + 1 , ed + 1 + cnt ) ;
	for(int i = 1 ; i <= cnt ; i++ )
	{
		int v = find(ed[i].to) , u = find(ed[i].frm) ;
		if(u == v)	continue ;
		res += ed[i].dis ;	fa[v] = u ;
		if(tot == n )	break ;
	}               
	return res ;	
}
signed main()
{
	cin >> n ; 
	for(int i = 1 ; i <= n ; i++ )
	{
		int x = read() ;
		fa[i] = i ; 
		add(0 , i , x) ; add(i , 0 , x) ;
	}
	for(int i = 1 ; i <= n ; i++ )
		for(int j = 1 ; j <= n ; j++ )
		{
			int x = read() ;
			if(i <= j)	continue ; 
			add(i , j , x) ;
		}
	cout << kru() ; 
	return 0 ;
}

例题5 poj1639

题意
给定一张 \(N\) 个节点, \(M\) 条边的无向图,求出无向图中的一棵最小生成树,满足1号节点的度数不超过给定的整数 \(S\) 。 $N $ \(\le30\)
solution
咕咕~改天回来补吧,详见《算法进阶指南》P368……

次小生成树(非严格次小生成树,严格次小生成树)

方法一:

先求最小生成树,在枚举删去最小生成树的边求解。时间复杂度 \(O(mlogm + nm)\)

方法二

先求最小生成树,再枚举非树边,然后将该边加入树中,同时从树中删去一条边。复杂度可以用倍增法优化到 \(O(mlogn)\)
邻集:不难理解
总定理:一定存在一棵次小生成树和最小生成树只存在一条边不同。此处输入图片的描述

例题1秘密的奶牛运输

例题思路
1、求解最小生成树 , 统计标记每条边的性质(树边还是非树边);同时把最小生成树构建出来。
2、预处理两点之间的边权最大值(这里使用\(dfs\)实现)
3、依次枚举所有非树边,求解 \(min(sum + w - dis[a][b])\) , 满足 \(w > dis[a][b]\)

code

using Simon O_O ;
int n , m , cnt , tot , num ;
int head[maxn] , fa[maxn]  ;
int d1[510][510] , d2[510][510]  ;
struct node 
{
	int frm, to , dis ;
	bool pd ;  
}ed[maxn << 1] ; 
struct Node
{
	int to , nxt , dis ; 
}Ed[maxn << 2] ;
int find(int x)
{
	if(x == fa[x])	return x ;
	return fa[x] = find(fa[x]) ;
}
void add(int u , int v , int w )
{
	Ed[++tot].to = v ; 
	Ed[tot].dis = w ;
	Ed[tot].nxt = head[u] ;
	head[u] = tot ;
}
bool operator < (node a , node b)
{
	return a.dis < b.dis ;
}    
int vis[510];
void clear() {
	memset(vis,0,sizeof(vis));
}                                 
void dfs (int root , int u , int fa , int mid1 , int mid2)
{
	
	d1[root][u] = mid1 , d2[root][u] = mid2 ;  
	for(int i = head[u] ; i ; i = Ed[i].nxt)
	{
		int v = Ed[i].to , w = Ed[i].dis ;
		if(vis[v]) continue ;
		vis[v] = true ;
	//	int td1 = mid1 , td2 = mid2 ;
		if(w > mid1)
		{
			mid2 = mid1 ;	mid1 = w ;
		}
		else if(w > mid2)	mid2 = w  ; 
		dfs(root , v , u , mid1 , mid2) ;
	}
}
                                                                                              
signed main()
{
	n = read() , m = read() ;
	for(int i=1;i<=n;i++) fa[i] = i ;
	for(int i = 1 ; i <= m ; i++ )
	{
		int u = read() , v = read() , w = read() ; 
		ed[i] = {u , v , w , false } ;
	}
	sort(ed + 1 , ed + m + 1) ; 
	for(int i = 1 ; i <= m ; i++ )
	{
		
	//	cout << ed[i].frm << ed[i].to <<endl ; 
		int u = ed[i].frm , v = ed[i].to ;
		int fu = find(u) , fv = find(v) ;
		if(fu == fv)	continue ;
		fa[fv] = fu;
		add(u , v , ed[i].dis) ;	add(v , u , ed[i].dis) ; 
		ed[i].pd = true ; num += ed[i].dis ;
	}
//	cout << num ;//orz ;
	for(int i = 1 ; i <= n ; i++ )
	{
		clear() ;
		dfs(i , i , -1 , -INF , -INF) ;
	}

	int ans = INF ; 
	for(int i = 1 ; i <= m ; i++ )
	{
		if(! ed[i].pd)
		{      
			int w = ed[i].dis , u = ed[i].frm , v = ed[i].to ; 
			if(w > d1[u][v])	ans = min(ans , num - d1[u][v] + w ) ;
			else if(w > d2[u][v])	ans = min(ans , num - d2[u][v] + w) ;
		}
	}
	cout << ans ; 
}
posted @ 2022-04-05 22:33  Simon_...sun  阅读(111)  评论(0)    收藏  举报