最大权闭合子图

闭合图

首先,先了解什么是闭合图。闭合图一般指一个图中点的集合,从该集合中所有的点出发,能到达的点要求都必须在该点集中。也就是说,从该集合中出发,一定要回到该集合中,不能出到集合外。

最大权闭合子图,顾名思义,就是所有闭合子图中点权之和最大的那个,注意这里的权指的是点权,因为闭合图是对于点集而言的。


最大权闭合子图

了解完概念后,要知道如何求最大权闭合子图。大体的思路是借助最小割模型,让每一种闭合图通过变形后都能和一种割相对应,这样求最大权就是求最小割。

具体做法

构造一个新的流网络,建一个源点s和汇点t,从s向原图中所有点权为正数的点建一条容量为点权的边,从点权为负数的点向t建一条容量为点权绝对值的边,原图中各点建的边都建成容量为正无穷的边。然后求从s到t的最小割,再用所有点权为正的权值之和减去最小割,就是我们要求的最大权值和了。

证明

简单证明,如果不想看证明的可跳过。
如何从最大权闭合子图转化到最小割的呢?首先我们要知道简单割的定义,简单割指的是割集的所有边都是从s出发的边或终点是t的边。由此,我们知道在上述建图方式中的最小割一定是一个简单割,因为其图内部的边的流量是正无穷,所以最小割集一定不包含内部的边,是一个简单割。
然后我们需要证明闭合子图和简单割是一一对应的,从而我们求闭合子图的最值就是简单割的最小值,也就是建的新图的最小割。
我们假设一个闭合子图是v,流网络分成两个集合S和T,构成一个割集的情况。S包含的是v+s(闭合子图的点+源点);T包含的是剩下的所有点(闭合子图外的点+汇点)。在这种情况下闭合子图v就和这个割[S,T]相对应,且这个割是个简单割:当前割的割集是S集合到T集合的所有边的集合,割集内一定不会出现原图内部的边,因为从S集合出发,若起点是s,则一定不包含原图内部的边,若起点是v,根据闭合子图的定义,回到的一定是v中的点,所以不包含到T集合中的点,因此该割一定是简单割。

这样我们就将闭合子图问题转化成了割的问题了,然后我们就要看看数量关系,找到最大权闭合子图和最小割间的数量表达式,就可以借助最小割模型计算最大权闭合子图了。
首先我们要找到最小割的计算表达式,下面是割集的所有情况:
在这里插入图片描述
我们将原图中的点分为两个集合,v1和v2,v1是闭合子图的点集,v2是原图中剩下的所有点,因此割的所有情况共有四种(S集合到T集合共四种情况),其中有两种情况不存在,第①种情况不存在是因为我们在建图时就不会建s到t的边,第②种情况不存在是根据闭合子图的定理,从v1出发的点一定会回到v1,不会到v2。这样割集就只剩两种情况s到v2和v1到t,根据见图方式可得:前者的容量和是v2中所有点权为正的和,后者容量和是v1中所有点权为负数的和的相反数,割集的容量之和就是v2中所有点权为正的和+v1中所有点权为负数的和的相反数

然后我们再看我们要求的闭合子图权值之和的计算表达式:最大权值和就等于v1中所有的权值和,即v1中所有点权为正的和+v1中所有点权为负的和

我们将上面求出来的两个表达式相加可得:割集容量之和+闭合子图权值之和=原图中所有点权为正数的点权之和(后面的相加后刚好抵消)。而等式的右边是个定值,我们为使闭合子图权值最大化,所以我们就要使得割集容量之和最小化,即求最小割的大小,再用所有点权为正的权值之和减去最小割,就是我们要求的最大权值了。


例题

原题链接

Acwing961:最大获利

思路

我们将中转站的成本记为负值,记成该点的点权,再将用户的收益记为记为该点的点权,然后从每一个用户向两个所需的中转站连一条有向边。之后就是求最大权闭合子图的模板了。最大权之和就是最大收益。

#include<iostream>
#include<queue>
#include<cstring>
using namespace std;
const int N = 55010,M = (N+2*50000)*2+10,INF = 0x3f3f3f3f;

int n,m,s,t,sum,dis[N],cur[N];
int e[M],w[M],ne[M],h[N],idx;	//链式前向星建图

void add(int a,int b,int c)	//建图模板
{
	e[idx] = b;
	w[idx] = c;
	ne[idx] = h[a];
	h[a] = idx++;
	e[idx] = a;
	w[idx] = 0;
	ne[idx] = h[b];
	h[b] = idx++;
}

bool bfs()		//dinic模板
{
	queue<int> q;
	memset(dis,-1,sizeof dis);
	q.push(s);
	dis[s] = 0;
	cur[s] = h[s];
	while(q.size())
	{
		int u = q.front();
		q.pop();
		for(int i = h[u];~i;i = ne[i])
		{
			int v = e[i];
			if(dis[v] == -1 && w[i])
			{
				dis[v] = dis[u] + 1;
				cur[v] = h[v];
				if(v == t)
					return 1;
				q.push(v);
			}
		}
	}
	return 0;
}

int dfs(int u,int limit)
{
	if(u == t)
		return limit;
	int flow = 0;
	for(int i = cur[u];~i;i = ne[i])
	{
		int v = e[i];
		cur[u] = i;
		if(dis[v] == dis[u]+1 && w[i])
		{
			int minf = dfs(v,min(w[i],limit-flow));
			w[i] -= minf;
			w[i^1] += minf;
			flow += minf;
			if(flow == limit) 
				return flow;
		}
	}
	return flow;
}

int dinic()
{
	int ans = 0;
	while(bfs())
		ans += dfs(s,INF);
	return ans;
}

int main()
{
	cin >> n >> m;
	memset(h,-1,sizeof h);
	s= 0,t = n+m+1;
	for(int i = 1;i <= n;i++)
	{
		int p;
		cin >> p;
		add(m+i,t,p);		//中转站到汇点建边
	}
	for(int i = 1;i <= m;i++)
	{
		int a,b,c;
		cin >> a >> b >> c;
		add(i,m+a,INF);		//内部的边建成无穷大
		add(i,m+b,INF);
		add(s,i,c);		//源点到用户建边
		sum += c;		//计算点权为正的权值之和
	}
	cout << sum-dinic() << endl;	//正数的点权和减去最小割就是答案(最大权闭合子图)
 
	return 0;
}
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值