POJ - 1637 Sightseeing tour 混合图欧拉回路的判断(最大流)

传送门:POJ1637

题意:给出一个混合图,判断是否存在欧拉回路。

用网络流判断混合图中是否存在欧拉回路方法及证明:

【混合图】
混合图(既有有向边又有无向边的图)中欧拉环、欧拉路径的判定需要借助网络流!


(1)欧拉环的判定:
一开始当然是判断原图的基图是否连通,若不连通则一定不存在欧拉环或欧拉路径(不考虑度数为0的点)。


其实,难点在于图中的无向边,需要对所有的无向边定向(指定一个方向,使之变为有向边),使整个图变成一个有向欧拉图(或有向半欧拉图)。若存在一个定向满足此条件,则原图是欧拉图(或半欧拉图)否则不是。关键就是如何定向?


首先给原图中的每条无向边随便指定一个方向(称为初始定向),将原图改为有向图G',然后的任务就是改变G'中某些边的方向(当然是无向边转化来的,原混合图中的有向边不能动)使其满足每个点的入度等于出度。
设D[i]为G'中(点i的出度 - 点i的入度)。可以发现,在改变G'中边的方向的过程中,任何点的D值的奇偶性都不会发生改变(设将边<i, j>改为<j, i>,则i入度加1出度减1,j入度减1出度加1,两者之差加2或减2,奇偶性不变)!而最终要求的是每个点的入度等于出度,即每个点的D值都为0,是偶数,故可得:若初始定向得到的G'中任意一个点的D值是奇数,那么原图中一定不存在欧拉环!


若初始D值都是偶数,则将G'改装成网络:设立源点S和汇点T,对于每个D[i]>0的点i,连边<S, i>,容量为D[i]/2;对于每个D[j]<0的点j,连边<j, T>,容量为-D[j]/2;G'中的每条边在网络中仍保留,容量为1(表示该边最多只能被改变方向一次)。求这个网络的最大流,若S引出的所有边均满流,则原混合图是欧拉图,将网络中所有流量为1的中间边(就是不与S或T关联的边)在G'中改变方向,形成的新图G''一定是有向欧拉图;若S引出的边中有的没有满流,则原混合图不是欧拉图。


为什么能这样建图?
考虑网络中的一条增广路径S-->i-->...-->j-->T,将这条从i到j的路径在G'中全部反向,则:i的入度加1出度减1,j的入度减1出度加1,路径中其它点的入度出度均不变。而i是和S相连的,因此初始D[i]>0,即i的出度大于入度,故这样反向之后D[i]减少2;同理,j是和T相连的,这样反向之后D[j]增加2。因此,若最大流中边<S, i>满流(流量为初始D[i]/2),此时D[i]值就变成了0,也就是i的入度等于出度。因此只要使所有S引出的边全部满流,所有初始D值>0的点的D值将等于0,又因为将边变向后所有点的D值之和不变,所有初始D值小于0的点的D值也将等于0,而初始D值等于0的D点既不与S相连也不与T相连,所以它们是网络中的中间点,而中间点的流入量等于流出量,故它们的入度和出度一直不变,即D值一直为0。因此,整个图G'成为欧拉图。


(2)欧拉路径的判定:
首先可以想到的是枚举欧拉路径的起点i和终点j,然后在图中添加边<j, i>,再求图中是否有欧拉回路即可。但是,该算法的时间复杂度达到了O(M * 最大流的时间),需要优化。
前面已经说过,在将边变向的过程中任何点的D值的奇偶性都不会改变,而一个有向图有欧拉路径的充要条件是基图连通且有且只有一个点的入度比出度少1(作为欧拉路径的起点),有且只有一个点的入度比出度多1(作为终点),其余点的入度等于出度。这就说明,先把图中的无向边随便定向,然后求每个点的D值,若有且只有两个点的初始D值为奇数,其余的点初始D值都为偶数,则有可能存在欧拉路径(否则不可能存在)。进一步,检查这两个初始D值为奇数的点,设为点i和点j,若有D[i]>0且D[j]<0,则i作起点j作终点(否则若D[i]与D[j]同号则不存在欧拉路径),连边<j, i>,求是否存在欧拉环即可(将求出的欧拉环中删去边<j, i>即可)。这样只需求一次最大流。

以上转载自:kuangbin的博客


总的来说就是按上面的方法建图,然后跑一次最大流,看看是不是满流。

代码:

//ISAP int
#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
#define ll long long
#define MAXN 220
#define inf 0x3f3f3f3f
using namespace std;
int n,m;//题目输入点数,边数
struct Edge{
	int v,next;
	int cap,flow;
}edge[MAXN*100];
int cur[MAXN],pre[MAXN],gap[MAXN],path[MAXN],dep[MAXN];
int cnt=0;//实际存储总边数 
void init()
{
	cnt=0;
	memset(pre,-1,sizeof(pre)); 
}
void add(int u,int v,int w,int rw=0)//加边 有向图三个参数,无向图四个 
{
	edge[cnt].v=v;
	edge[cnt].cap=w;
	edge[cnt].flow=0;
	edge[cnt].next=pre[u];
	pre[u]=cnt++;
	edge[cnt].v=u;
	edge[cnt].cap=rw;
	edge[cnt].flow=0;
	edge[cnt].next=pre[v];
	pre[v]=cnt++;
}
bool bfs(int s,int t)//其实这个bfs可以融合到下面的迭代里,但是好像是时间要长 
{
	memset(dep,-1,sizeof(dep));
	memset(gap,0,sizeof(gap));
	gap[0]=1;
	dep[t]=0;
	queue<int>q;
	while(!q.empty())
	q.pop();
	q.push(t);//从汇点开始反向建层次图 
	while(!q.empty())
	{
		int u=q.front();
		q.pop();
		for(int i=pre[u];i!=-1;i=edge[i].next)
		{
			int v=edge[i].v;
			if(dep[v]==-1&&edge[i^1].cap>edge[i^1].flow)//注意是从汇点反向bfs,但应该判断正向弧的余量
			{
				dep[v]=dep[u]+1;
				gap[dep[v]]++;
				q.push(v);
				//if(v==s)//感觉这两句优化加了一般没错,但是有的题可能会错,所以还是注释出来,到时候视情况而定 
				//break;
			}	
		}
	}
	return dep[s]!=-1; 
}
int isap(int s,int t)
{
	bfs(s,t);
	memcpy(cur,pre,sizeof(pre));
	int u=s;
	path[u]=-1;
	int ans=0;
	while(dep[s]<n)//迭代寻找增广路 
	{
		if(u==t)
		{
			int f=inf;
			for(int i=path[u];i!=-1;i=path[edge[i^1].v])//修改找到的增广路 
				f=min(f,edge[i].cap-edge[i].flow);
			for(int i=path[u];i!=-1;i=path[edge[i^1].v])
			{
				edge[i].flow+=f;
				edge[i^1].flow-=f;
			}
			ans+=f;
			u=s;
			continue;
		}
		bool flag=false;
		int v;
		for(int i=cur[u];i!=-1;i=edge[i].next)
		{
			v=edge[i].v;
			if(dep[v]+1==dep[u]&&edge[i].cap-edge[i].flow)
			{
				cur[u]=path[v]=i;//当前弧优化 
				flag=true;
				break;
			}
		}
		if(flag)
		{
			u=v;
			continue;
		}
		int x=n;
		if(!(--gap[dep[u]]))return ans;//gap优化 
		for(int i=pre[u];i!=-1;i=edge[i].next)
		{
			if(edge[i].cap-edge[i].flow&&dep[edge[i].v]<x)
			{
				x=dep[edge[i].v];
				cur[u]=i;//常数优化 
			}
		}
		dep[u]=x+1;
		gap[dep[u]]++;
		if(u!=s)//当前点没有增广路则后退一个点 
		u=edge[path[u]^1].v;
	 } 
	 return ans;
}
int D[MAXN];
int main()
{
	int T,u,v,w;
	cin>>T;
	while(T--)
	{
		init();
		memset(D,0,sizeof(D));
		scanf("%d%d",&n,&m);
		int source=0,sink=n+1;
		while(m--)
		{
			scanf("%d%d%d",&u,&v,&w);
			if(!w)
			add(u,v,1);//任意指定初始方向,为了好写,将无向边初始方向都定为u->v 
			D[v]--;
			D[u]++;
		}
		bool flag=0;
		for(int i=1;i<=n;i++)
		{
			if(D[i]&1)
			{
				flag=1;
				break;
			}
			if(D[i]>0)
			add(source,i,D[i]>>1);
			if(D[i]<0)
			add(i,sink,-(D[i]>>1));
		}
		if(flag)
		{
			cout<<"impossible\n";
			continue;
		}
		n=sink+1;
		isap(source,sink);
		for(int i=pre[source];i!=-1;i=edge[i].next)
		{
			if(edge[i].cap!=edge[i].flow)
			{
				flag=1;
				break;
			}
		}
		if(flag)
		cout<<"impossible\n";
		else
		cout<<"possible\n";
	}
	return 0;
}

这也算是个套路题吧。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值