[图论]最小生成树

nannandbk / 2023-08-11 / 原文

最小生成树

一、最小生成树定义

最小生成树定义:在一张带权无向图中,最小生成树是一棵生成树,它的边权值之和最小

什么是生成树?

生成树是一棵包含原图所有顶点的树,它的边的集合是原图的一个子集,并且任意两点之间有且只有一条简单路径。

二、常见求最小生成树的两种算法

1.Kruscal算法

\(Kruscal\)算法的主要思想是:按照边权的大小依次选边,如果这条边的两个端点没在一个连通块里面,那就把这条边加到最小生成树的边集里面。

具体步骤:

  1. 边按照权值排序
  2. 按照顺序依次加边
  3. 重复以上操作直到有最小生成树边集里面有\(n-1\)条边为止。

复杂度:\(O(m\log m)\),其中\(m\)为边数。

注意:\(Kruscal\)存边是存的单向边,适用于稀疏图

P3366 【模板】最小生成树

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 50100,M = 2e5+10;

struct Node
{
	int x,y,v;
	bool operator <(const Node &A)const{
		return v<A.v;
	}
}a[M+1];

int n,m,fa[N+1];

int find(int x)
{
	if(x==fa[x])return x;
	return fa[x] = find(fa[x]);
}

int kruskal()
{
	for(int i = 1;i<=n;i++)fa[i] = i;
	sort(a+1,a+1+m);
	int ans = 0,cnt = n;
	for(int i = 1;i<=m;i++)
	{
		int x = find(a[i].x),y = find(a[i].y);
		if(x!=y)
		{
			fa[x] = y;
			ans += a[i].v;
			--cnt;
		}
		if(cnt==1)break;
	}
	if(cnt!=1)return -1;
	return ans;
}

int main()
{
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	cin>>n>>m;
	for(int i = 1;i<=m;i++)
	{
		cin>>a[i].x>>a[i].y>>a[i].v;
	}
	int ans = kruskal();
	if(ans==-1)cout<<"orz\n";
	else cout<<ans<<endl;
	return 0;
}

2.Prim算法

\(Prim\)算法是主要思想是:从任意一个顶点开始,每次选择一个与当前连通块相邻的权值最小的边,并把它加到最小生成树的边集里面,直到所有顶点都加入。

具体步骤:

  1. 随机选择一个顶点作为起点
  2. 将于起点相邻的边按照边权从小到大排序,并选择权值最小的边加入到最小生成树的边集里面。
  3. 将新加入的点加入到连通块中,并将于它相邻的边按照边权从小到大排序。
  4. 重复2,3步骤直到所有点是加入。

时间复杂度:\(O(n^2+m)\),其中\(n\)为顶点数。若用堆优化时间复杂度\(O((n+m)\log n)\)

注意:\(Prim\)存的是双向边,适合稠密图

朴素写法:\(O(n^2+m)\)

完整写法,不一定是连通图情况

#include<bits/stdc++.h>
using namespace std;
struct Node{
	int y,v;
	Node(int _y,int _v){y = _y;v = _v;};
};
std::vector<Node>edge[N+1];
int n,m,dist[N+1];
bool b[N+1];

int Prim()
{
	memset(b,false,sizeof(b));
	memst(dist,127,sizeof(dist));
	dist[1] = 0;
	int ans = 0,tot = 0;
	while(1)
	{
		int x = -1;
		for(int i = 1;i<=n;i++)
		{
			if(!b[i]&&dist[i]<(1<<30))
			{
				if(x==-1||dist[i]<dist[x])
					x=i;
			}
		}
		if(x==-1)break;//找不到符合条件的能加入c中的了
		++tot;
		ans += dist[x];
		b[x]=true;
		for(auto i:edge[x])
		{
			dist[i.y] = min(dist[i.y],i.v);//dist记录的是当前这个点到c中的边权最小值
		}
	}
		if(tot == n)return ans;
		else return -1;//不连通
}
int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	return 0;
}

堆优化写法\(O((n+m)\log n)\)

\(dijkstra\) 和 $prim $的区别
差别只在于最后循环体内的松弛操作。

最小生成树只关心所有边的和最小,所以有\(v.key = w(u, v)\),即每个点直连其他点的最小值(最多只有两个节点之间的权值和)
最短路径树只搜索权值最小,所以有\(v.key = w(u, v) + u.key\),即每个点到其他点的最小值(最少是两个节之间的权值和)

#include<bits/stdc++.h>
using namespace std;
struct Node
{
	int y,v;
	Node(int _y,int _v){y = _y;v = _v;};
};
const int N = 50000;
int n,m,dist[N+1];
std::vector<Node> edge[N+1];
bool b[N+1];
set<pair<int,int>>q;

void Prim()
{
	memset(b,false,sizeof(b));
	memset(dist,127,sizeof(dist));
	dist[1] = 0;
	q.clear();
	for(int i = 1;i<=n;i++)
	{
		q.insert({dist[i],i});
	}
	int ans = 0;
	while(!q.empty())
	{
		int x= q.begin()->second;
		q.erase(q.begin());
		ans += dist[x];
		b[x]=true;
		for(auto i:edge[x])
		{
			if(!b[i.y]&&i.v<dist[i.y])
			{
				q.erase({dist[i.y],i.y});
				dist[i.y]=i.v;
				q.insert({dist[i.y],i.y});
			}
		}
	}
	cout<<ans<<endl;
}

int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	cin>>n>>m;
	for(int i= 1;i<=m;i++)
	{
		int x,y,v;
		cin>>x>>y>>v;
		edge[x].push_back({y,v});
		edge[y].push_back({x,v});
	}
	Prim();
	return 0;
}

三、最小生成树常见应用

(1)对于Kruscal原理的理解:

例题1:P1396 营救

题意:给你起点\(s\)和终点\(t\),问你在通往\(s\)\(t\)的路径上最大拥挤度最小是多少?

思路:因为我们知道用\(Kruscal\)建立最小生成树的时候是从小往大加边的,那么第一次加入且使得\(s\)\(t\)连通的边就是答案了。

#include<bits/stdc++.h>
using namespace std;
const int N = 10000,M = 20000;
struct Node
{
	int x,y,v;
	bool operator <(const Node &A)const{
		return v<A.v;
	}
}a[M+1];//存边

int n,m,s,t,fa[N+1];
int findset(int i)
{
	if(i==fa[i])return i;
	return fa[i]=findset(fa[i]);
}

void Kruskal()
{
	for(int i =1 ;i<=n;i++)
	{
		fa[i] =i;
	}
	sort(a+1,a+1+m);
	for(int i = 1;i<=m;i++){
		int x= findset(a[i].x),y = findset(a[i].y);
		if(x!=y)
		{
			fa[x] = y;
		}
		if(findset(s)==findset(t)){
			cout<<a[i].v<<endl;
			return;
		}
	}
}

int main()
{
	cin>>n>>m>>s>>t;
	for(int i = 1;i<=m;i++)
	{
		cin>>a[i].x>>a[i].y>>a[i].v;
	}
	Kruskal();
	return 0;
}

当然了,这道题的解法还有很多。求最大值最小,二分+\(dp\)当然可以搞一搞。或者用\(Kruscal\)重构树也是可以哒(后面会讲)。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5+10,M = 2e5+10;
const int LOGN = 20;
struct Node
{
    int x,y,v;
}a[M+1];

int n,m,s,t,now,fa[N][LOGN+2],val[N],dep[N];
vector<int>e[N];


//////////////////////////////DSU//////////////////////////////////////
struct Dsu {
    int fa[N];
    void init(int x) {for(int i = 1;i<=x;i++) fa[i] = i;}
    int find(int x) {return x == fa[x] ? x : fa[x] = find(fa[x]);}
    void merge(int x, int y) {
        int u = find(x), v = find(y);
        if(u == v) return;
        fa[u] = v;
    }
}dsu;
/////////////////////////Kruscal///////////////////////////////////////

void kruskalRebuildTree()
{
    dsu.init(2*n-1);
    sort(a+1, a+1+m, [](const Node& x, const Node& y) {
        return x.v < y.v;
    });
    now = n;
    for(int i = 1;i<=m;i++) {
        ll u = dsu.find(a[i].x), v = dsu.find(a[i].y), w = a[i].v;
        if(u != v) {
            val[++now] = w;
            dsu.merge(u, now);
            dsu.merge(v, now);
            e[now].push_back(u);
            e[now].push_back(v);
        }
    }
}

/////////////////////////LCA/////////////////////////////////////////
void dfs(int u, int from)
{
    dep[u] += dep[from] + 1;
    for(auto v : e[u]) {
        if(v == from) continue;
        fa[v][0] = u;
        dfs(v, u);
    }
}
void lca_init(int now)
{
    for(int j = 1; j < LOGN; j++)
        for(int i = 1; i <= now; i++)
            fa[i][j] = fa[fa[i][j - 1]][j - 1];
}
int lca_query(int u, int v)
{
    if(dep[u] < dep[v])   swap(u, v);
    int d = dep[u] - dep[v];
    for(int j = LOGN; j >= 0; j--)
        if(d & (1 << j))
            u = fa[u][j];

    if(u == v)    return u;
    for(int j = LOGN; j >= 0; j--)
    {
        if(fa[u][j] != fa[v][j])
            u = fa[u][j],v = fa[v][j];
    }

    return fa[u][0];
}
/////////////////////////////Main//////////////////////////////////////////////
int main()
{
    cin>>n>>m>>s>>t;
    for(int i = 1;i<=m;i++)
    {
        cin>>a[i].x>>a[i].y>>a[i].v;
    }
    kruskalRebuildTree();
    
    //可能是森林
    for(int i = now;i>=1;i--)
    {
        if(!dep[i])
            dfs(i,0);
    }
    lca_init(now);

   int lca  = lca_query(s,t);
    if(!lca)cout<<-1<<"\n";
    else cout<<val[lca]<<"\n";
    return 0;
}

例题2:P2700 逐个击破

题意:花费最小代价,使得给出的\(k\)个点不连通。

思路:可以反向思考,我们要花费最小代价使得他们不连通,那是不是可以反着思考?

删除边权小的转化为加入边权大的。用\(Kruscal\)的贪心思想,加入花费大的边。最后答案就是总花费-生成树花费。

❗记得,如果把这条边加进来了,如果有一个是\(k\)个中的某个,与它相连的端点要标记。因为两个在\(k\)里面的是不能连起来的,两个有标记的也不能连起来,因为连起来了会使得\(k\)中的元素连通。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5+10,M = 2e6+10;

struct Node
{
	int x,y,v;
	bool operator <(const Node &A)const{
		return v>A.v;
	}
}a[M+1];

int n,m,k,fa[N+1];
set<int>c;
int cnt = 0;
int find(int x)
{
	if(x==fa[x])return x;
	return fa[x] = find(fa[x]);
}

ll kruskal()
{
	for(int i = 1;i<=n;i++)fa[i] = i;
	sort(a+1,a+1+m);
	ll ans = 0;
	for(int i = 1;i<=m;i++)
	{
		int x = find(a[i].x),y = find(a[i].y);
		if(c.find(x)!=c.end()&&c.find(y)!=c.end())continue;
		if(x!=y)
		{
			fa[x] = y;
			ans += a[i].v;
		}
		if(c.find(x)!=c.end())
			c.insert(y);
		else if(c.find(y)!=c.end())
			c.insert(x);
	}
	return ans;
}
int main()
{
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	cin>>n>>k;
	m = n-1;
	ll res = 0;
	for(int i = 1;i<=k;i++)
	{
		int x;
		cin>>x;
		x++;
		
		c.insert(x);
	}
	
	for(int i = 1;i<=m;i++)
	{
		int x,y,v;
		cin>>x>>y>>v;
		x++,y++;
		res += v;
		a[i].x = x,a[i].y = y,a[i].v = v;
	}
	ll ans = kruskal();
	cout<<res-ans<<endl;
	//cout<<"ans = "<<ans<<endl;
	return 0;
}

(2)结合超级源点:

例题1:P1194 买礼物

题意:要买\(b\)样东西,原价都是\(a\)。最近促销:买了\(I\),再买\(J\),就可以只用花\(K_{I,J}\),问最少花多少钱?

思路:

  1. 将每件物品看作一个结点,物品之间有费用或者优惠为权值的边。
  2. 设置一个超级源点\(0\)号结点,物品与该节点相连表示直接花费\(a\)元去买。
  3. \(0\)向每个结点连权值为\(a\)的边,其他的按照优惠正常连边
  4. \(Kruskal\)
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 5010,M = 2e6+10;

struct Node
{
    int x,y,v;
    bool operator <(const Node &A)const{
        return v<A.v;
    }
}a[M+1];

int n,m,A,fa[N+1];
int cnt;
int find(int x)
{
    if(x==fa[x])return x;
    return fa[x] = find(fa[x]);
}

ll kruskal()
{
    for(int i = 1;i<=n;i++)fa[i] = i;
    sort(a+1,a+1+cnt);
    ll ans = 0;
    for(int i = 1;i<=cnt;i++)
    {
        int x = find(a[i].x),y = find(a[i].y);
        if(x!=y)
        {
            fa[x] = y;
            ans += a[i].v;
        }
    }
    return ans;
}

int main()
{
    ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    cin>>A>>n;
    for(int i = 1;i<=n;i++)
    {
        a[++cnt].x = 0;
        a[cnt].y = i;
        a[cnt].v = A;
    }
    for(int i = 1;i<=n;i++)
    {
        for(int j = 1;j<=n;j++)
        {
            int v;
            cin>>v;
            if(v==0||i>=j)continue;
            a[++cnt].x = i;
            a[cnt].y = j;
            a[cnt].v = v;
        }
    }
    ll ans = kruskal();
    cout<<ans<<endl;
    return 0;
}

例题2:P1550Watering Hole G

思路:和上题思路一样,同样是建立一个超级源点,与它相连的话表示挖一口井,其他供水正常连边,跑\(Kruskal\)就ok啦。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5+10,M = 2e6+10;
int n,cnt,fa[N+1],w[N];
struct Node
{
	int x,y,v;
	bool operator <(const Node &A)const{
		return v<A.v;
	}
}a[M+1];



int find(int x)
{
	if(x==fa[x])return x;
	return fa[x] = find(fa[x]);
}

int kruskal()
{
	for(int i = 1;i<=n;i++)fa[i] = i;
	sort(a+1,a+1+cnt);
	int ans = 0;
	for(int i = 1;i<=cnt;i++)
	{
		int x = find(a[i].x),y = find(a[i].y);
		if(x!=y)
		{
			fa[x] = y;
			ans += a[i].v;
		}
	}
	return ans;
}

int main()
{
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	cin>>n;
	for(int i = 1;i<=n;i++)
	{
		cin>>w[i];
		a[++cnt].x = 0,a[cnt].y = i,a[cnt].v = w[i]; 
	}	
	for(int i = 1;i<=n;i++)
	{
		for(int j = 1;j<=n;j++)
		{
			int x;
			cin>>x;
			if(i<=j)continue;
			a[++cnt].x = i,a[cnt].y = j,a[cnt].v = x;
		}
	}
	ll ans = kruskal();
	cout<<ans<<endl;
	return 0;
}

例题3:Shichikuji and Power Grid

思路和上面类似,只不过两个之间的花费需要自己计算出来,这里就不赘述啦QAQ。

#include<bits/stdc++.h>
using namespace std;
#define int long long
typedef long long ll;
const int N = 2e3+10;

struct Node
{
	int x,y;
	ll v;
	bool operator <(const Node &A)const{
		return v<A.v;
	}
}a[N*N];

struct ty
{
	int x,y;
}p[N];

int n,m,fa[N+1],cnt,k[N];

int find(int x)
{
	if(x==fa[x])return x;
	return fa[x] = find(fa[x]);
}
set<int>s;
set<pair<int,int>>l;
ll kruskal()
{
	for(int i = 1;i<=n;i++)fa[i] = i;
	sort(a+1,a+1+cnt);
	ll ans = 0;
	for(int i = 1;i<=cnt;i++)
	{
		int x = find(a[i].x),y = find(a[i].y);
		
		
		if(x!=y)
		{
			fa[x] = y;
			ans += a[i].v;
			if(a[i].x==0)
				s.insert(a[i].y);
			else
				l.insert({a[i].x,a[i].y});
		}
	}
	return ans;
}

ll get(ty a,ty b)
{
	return 1ll*abs(a.x-b.x)+abs(a.y-b.y);
}

signed main()
{
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	cin>>n;
	for(int i = 1;i<=n;i++)
	{
		cin>>p[i].x>>p[i].y;
	}
	for(int i = 1;i<=n;i++)
	{
		int x;
		cin>>x;
		a[++cnt].x = 0,a[cnt].y = i,a[cnt].v = x;
	}
	for(int i = 1;i<=n;i++)
		cin>>k[i];
	for(int i = 1;i<=n;i++)
	{
		for(int j = i+1;j<=n;j++)
		{
			ll cost = k[i]+k[j];
			ll dis = get(p[i],p[j]);
			cost *= dis;
			a[++cnt].x = i,a[cnt].y = j,a[cnt].v = cost;
		}
	}
	cout<<kruskal()<<"\n";
	cout<<s.size()<<"\n";
	for(auto& x:s)
		cout<<x<<" ";
	if(s.size())
		cout<<"\n";
	cout<<l.size()<<"\n";
	for(auto& [x,y]:l)
		cout<<x<<" "<<y<<"\n";
	return 0;
}

(3)Kruskal重构树

分析:\(Kruskal\)重构树是由原树建立起来的一棵二叉树,它将边权信息转换为点权信息

我们来讲解\(Kruskal\)重构树的经典应用:求图中任意两点间所有路径中最小边权的最大值。

构造方法如下:

  • 按照边权大小排序(按你的需求,如果是找点\(x\)路径上的边最小值权值的最大值需要按照从大到小排序,因为这样建出来的是小根堆。反之从小到大排序的话,是一个大根堆,可以找到点\(x\)的简单路径上的的边的最大权值的最小值)
  • 如果\(x,y\)以边权\(v\)的边相连,求出并查集中\(x,y\)的代表元\(fx,fy\)。如果他们不在一个集合里面,我们就新建一个结点,设该节点为\(u\),点权是\(w\)【这步实现了边权到点权的转化】,然后把\(fx,fy\)在并查集中的代表元都设为\(u\)。那么,询问\(x,y\)的答案的时候,二者的\(lca\)的点权就是答案。

以下面这题为例,

例题1:P1967 货车运输

按照样例建出的树:

//样例
1 2 4
2 3 3
3 1 1

image

关于正确性?

如果\(x,y\)必须要加入边权为\(w\)的边才能连通,那么答案就是\(w\)

按照我们的求法能得到类似答案——它们一开始不相连,在这一步后代表元素(两个根结点)连接权值为 \(w\)的点,才使得它们相连,所以最近公共祖先权值是新结点,权值 \(w\)。而在更后面的连边中,又不会更改它们的\(lca\)的值,那么\(lca\)的权值就是答案。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5+10,M = 2e5+10;
const int LOGN = 20;
struct Node
{
	int x,y,v;
}a[M+1];

int n,m,q,now,fa[N][LOGN+2],val[N],dep[N];
vector<int>e[N];


//////////////////////////////DSU//////////////////////////////////////
struct Dsu {
	int fa[N];
	void init(int x) {for(int i = 1;i<=x;i++) fa[i] = i;}
	int find(int x) {return x == fa[x] ? x : fa[x] = find(fa[x]);}
	void merge(int x, int y) {
		int u = find(x), v = find(y);
		if(u == v) return;
		fa[u] = v;
	}
}dsu;
/////////////////////////Kruscal////////////////////////////////////

void kruskalRebuildTree()
{
	dsu.init(2*n-1);
	sort(a+1, a+1+m, [](const Node& x, const Node& y) {
		return x.v > y.v;//重构最大生成树
	});
	now = n;
	for(int i = 1;i<=m;i++) {
		ll u = dsu.find(a[i].x), v = dsu.find(a[i].y), w = a[i].v;
		if(u != v) {
			val[++now] = w;
			dsu.merge(u, now);
			dsu.merge(v, now);
			e[now].push_back(u);
			e[now].push_back(v);
		}
	}
}

/////////////////////////LCA///////////////////////////////
void dfs(int u, int from)
{
	dep[u] += dep[from] + 1;
	for(auto v : e[u]) {
		if(v == from) continue;
		fa[v][0] = u;
		dfs(v, u);
	}
}
void lca_init(int now)
{
	for(int j = 1; j < LOGN; j++)
		for(int i = 1; i <= now; i++)
			fa[i][j] = fa[fa[i][j - 1]][j - 1];
}
int lca_query(int u, int v)
{
	if(dep[u] < dep[v])   swap(u, v);
	int d = dep[u] - dep[v];
	for(int j = LOGN; j >= 0; j--)
		if(d & (1 << j))
			u = fa[u][j];

	if(u == v)    return u;
	for(int j = LOGN; j >= 0; j--)
	{
		if(fa[u][j] != fa[v][j])
			u = fa[u][j],v = fa[v][j];
	}

	return fa[u][0];
}
///////////////////////////////////////////////////////////////
int main()
{
	cin>>n>>m;
	for(int i = 1;i<=m;i++)
	{
		cin>>a[i].x>>a[i].y>>a[i].v;
	}
	kruskalRebuildTree();
	
	//可能是森林
	for(int i = now;i>=1;i--)
	{
		if(!dep[i])
			dfs(i,0);
	}
	lca_init(now);

	cin>>q;
	while(q--)
	{
		int s,t;
		cin>>s>>t;
		int lca  = lca_query(s,t);
		if(!lca)cout<<-1<<"\n";
		else cout<<val[lca]<<"\n";
	}
	return 0;
}

例题2:Labyrinth

题意:

给定一张 \(n\) 个点,\(m\) 条边的无向图,每条边有一个宽度限制 \(w_i\)。每个点上有一个糖果,吃了第 \(i\) 个点上的糖果会使身体增宽 \(c_i\)。你也可以经过一个点而不吃掉它的糖果。你能够通过一个边 \(i\),当且仅当你当前身体的宽度 \(\le w_i\)

你需要从点 \(1\) 开始,吃掉所有点上的糖果,但不是必须要返回点 \(1\)。请求出你最大的初始身体宽度,使得你能吃掉所有糖果。

注意初始身体宽度必须是正整数。若无解输出 \(-1\)

思路:瓶颈是每个连通块里面限制最小的边,我们进行\(Kruscal\)重构,重构树的点权就是子树边权的最小值了。那么对于一棵以\(u\)为根节点的子树,它的情况是怎么样呢?

我们不知道先走左树还是右树,所以都需要考虑。

如果先走左边,那么答案是\(min(dp[rs[u]],val[u])-sum[ls[u]]\)。为什么呢?因为我需要满足\(dp[u]+sum[ls[u]]\le val[u]\)\(dp[u]+sum[ls[u]]\le dp[rs[u]]\)

那么以上表达式需要同时满足,我们的答案\(dp[u] = min(dp[rs[u]],val[u])-sum[ls[u]]\)。先走右边是同理的。那么对两边取\(max\)就行。

最终就是:\(dp[u] = max(min(dp[rs[u]],val[u])-sum[ls[u]],min(dp[ls[u]],val[u])-sum[rs[u]])\)

#include<bits/stdc++.h>
#define INF 0x3f3f3f3f
#define int long long 
using namespace std;
typedef long long ll;
const int N = 4e5+10,M = 5e5+10;
const int LOGN = 20;
struct Node
{
	int x,y,v;
}a[M+1];

ll n,m,q,now,fa[N][LOGN+2],val[N],dep[N];
vector<int>e[N];
ll ls[N],rs[N],sum[N],c[N],dp[N];

//////////////////////////////DSU//////////////////////////////////////
struct Dsu {
	int fa[N];
	void init(int x) {for(int i = 1;i<=x;i++) fa[i] = i;}
	int find(int x) {return x == fa[x] ? x : fa[x] = find(fa[x]);}
	void merge(int x, int y) {
		int u = find(x), v = find(y);
		if(u == v) return;
		fa[u] = v;
	}
}dsu;
/////////////////////////Kruskal////////////////////////////////////

void kruskalRebuildTree()
{
	dsu.init(2*n-1);
	sort(a+1, a+1+m, [](const Node& x, const Node& y) {
		return x.v > y.v;//重构最大生成树
	});
	now = n;
	for(int i = 1;i<=m;i++) {
		ll u = dsu.find(a[i].x), v = dsu.find(a[i].y), w = a[i].v;
		if(u != v) {
			val[++now] = w;
			dsu.merge(u, now);
			dsu.merge(v, now);
//			e[now].push_back(u);
//			e[now].push_back(v);
			ls[now] = u;
			rs[now] = v;
		}
	}
}


////////////////////////////DP///////////////////////////////////

void dfs(int u)
{
	if(!ls[u]||!rs[u])
	{
		sum[u] = c[u];
		dp[u] = INF;
		return;
	}
	dfs(ls[u]),dfs(rs[u]);
	sum[u] = sum[ls[u]] + sum[rs[u]];
	//               先吃ls[u]
	dp[u] = max(min(dp[rs[u]],val[u])-sum[ls[u]],min(dp[ls[u]],val[u])-sum[rs[u]]);
}

////////////////////////////Main///////////////////////////////////
signed main()
{
	cin>>n>>m;
	for(int i = 1;i<=n;i++)cin>>c[i];
	for(int i = 1;i<=m;i++)
	{
		cin>>a[i].x>>a[i].y>>a[i].v;
	}
	kruskalRebuildTree();
	dfs(now);
	if(dp[now]<=0)
		cout<<-1<<"\n";
	else cout<<dp[now]<<"\n";
	
	return 0;
}

例题3:[ARC098F] Donation

这是一道和上一题很像的题目。

题意:

给出一个\(N\)个点\(M\)条边的无向连通图,每个点的标号为\(1\)\(n\), 且有两个权值\(A_i,B_i\).第\(i\)条边连接了点\(u_i\)\(v_i\).

最开始时你拥有一定数量的钱,并且可以选择这张图上的任意一个点作为起始点,之后你从这个点开始沿着给定的边遍历这张图。每当你到达一个点\(v\)时,你必须拥有至少\(A_v\)元。而当你到达了这个点后,你可以选择向它捐献\(B_v\)元(当然也可以选择不捐献),当然,你需要保证在每次捐献之后自己剩余的钱\(\geq 0\)

你需要对所有的\(n\)个点都捐献一次,求你一开始至少需要携带多少钱。

思路:我们令\(C[i] = max(A[i]-B[i],0)\),即对于点\(i\),每个时刻的钱数都应该满足\(\ge C[i]\)

我们按照\(C[i]\)为边权进行\(Kruskal\)重构树,建大根堆。建完树以后考虑如何\(DP\)

和上题思路类似,差不多就是反过来。

思路:瓶颈是每个连通块里面限制最大的边,我们进行\(Kruscal\)重构,重构树的点权就是子树边权的最大了。那么对于一棵以\(u\)为根节点的子树,它的情况是怎么样呢?

我们不知道先走左树还是右树,所以都需要考虑。

如果先走左边,那么答案是\(max(dp[rs[u]],val[u])+sum[ls[u]]\)。为什么呢?因为我需要满足\(dp[u]-sum[ls[u]]\ge val[u]\)\(dp[u]-sum[ls[u]]\ge dp[rs[u]]\)

那么以上表达式需要同时满足,我们的答案\(dp[u] = max(dp[rs[u]],val[u])+sum[ls[u]]\)。先走右边是同理的。那么对两边取\(max\)就行。

最终就是: $ dp[u]=min(max(dp[rs[u]],val[u])+sum[ls[u]],max(dp[ls[u]],val[u])+sum[rs[u]])$

还要注意一下叶子节点的处理,对于一个叶子节点,它的\(dp[u] = C[i]+B[i],sum[u] = B[i]\).

以上就大功告成啦!你将获得一份和上一次几乎一样滴代码\((bushi)\)

#include<bits/stdc++.h>
#define INF 0x3f3f3f3f
#define int long long 
using namespace std;
typedef long long ll;
const int N = 4e5+10,M = 5e5+10;
const int LOGN = 20;
struct Node
{
	int x,y,v;
}a[M+1];

ll n,m,q,now,fa[N][LOGN+2],val[N],dep[N];
vector<int>e[N];
ll ls[N],rs[N],sum[N],c[N],dp[N],A[N],B[N];

//////////////////////////////DSU//////////////////////////////////////
struct Dsu {
	int fa[N];
	void init(int x) {for(int i = 1;i<=x;i++) fa[i] = i;}
	int find(int x) {return x == fa[x] ? x : fa[x] = find(fa[x]);}
	void merge(int x, int y) {
		int u = find(x), v = find(y);
		if(u == v) return;
		fa[u] = v;
	}
}dsu;
/////////////////////////Kruskal////////////////////////////////////

void kruskalRebuildTree()
{
	dsu.init(2*n-1);
	sort(a+1, a+1+m, [](const Node& x, const Node& y) {
		return x.v < y.v;
	});
	now = n;
	for(int i = 1;i<=m;i++) {
		ll u = dsu.find(a[i].x), v = dsu.find(a[i].y), w = a[i].v;
		if(u != v) {
			val[++now] = w;
			dsu.merge(u, now);
			dsu.merge(v, now);
//			e[now].push_back(u);
//			e[now].push_back(v);
			ls[now] = u;
			rs[now] = v;
		}
	}
}


////////////////////////////DP///////////////////////////////////

void dfs(int u)
{
	if(!ls[u]||!rs[u])
	{
		sum[u] = B[u];
		dp[u] = c[u]+B[u];
		return;
	}
	dfs(ls[u]),dfs(rs[u]);
	sum[u] = sum[ls[u]] + sum[rs[u]];
	dp[u] = min(max(dp[rs[u]],val[u])+sum[ls[u]],max(dp[ls[u]],val[u])+sum[rs[u]]);
}

//////////////////////////////Main/////////////////////////////////
signed main()
{
	cin>>n>>m;
	for(int i = 1;i<=n;i++){
		cin>>A[i]>>B[i];
		c[i] = max(A[i]-B[i],0ll);
	}
	for(int i = 1;i<=m;i++)
	{
		cin>>a[i].x>>a[i].y;
		a[i].v = max(c[a[i].x],c[a[i].y]);
	}
	kruskalRebuildTree();
	dfs(now);
	cout<<dp[now]<<"\n";
	return 0;
}