题目大意:

给出一个无向图,求解至少删去多少个点,才能使网络不连通。

思路:

就是求割点集中点的数量。利用网络流求解,以任意一点为源点,遍历汇点,最大流的最小值即是需要删去的点的个数。但是需要额外注意三个地方:

需要拆点,将每个点拆成两个相互之间通路容量为1的点。不同点之间建立的通路的容量则为无限。

由于源点可能是割点集中的点,这样计算出的结果可能会大于实际,所以源点也要遍历。

再就是输入样例用第三个例子,这算是特殊情况。整个网络就是一个割点集时,源点汇点无论如何取,结果都会比实际值大。这样的情况也有比较明显的特征,即是最大流的最小值会大于INF,换句话说,网络中的各点都是两两相连的。这时,直接输出节点数即可。

代码:

#include<iostream>
using namespace std;
#define DATASIZE 50*2
#define INF 1000
int map[DATASIZE+1][DATASIZE+1],backup_map[DATASIZE+1][DATASIZE+1],f[DATASIZE+1],pre[DATASIZE+1],queue[DATASIZE],start,end,n,m;
//n表示边的个数,m表示点的个数,map表示残留网络,f[i]表示处于i点时的最大剩余容量。 
int bfs()
{
	memset(pre,0,sizeof(pre));
	memset(queue,0,sizeof(queue));
	int now=0,tail=0;
	f[start]=INF;
	queue[tail++]=start;
	while(tail-now)
	{
		int head=queue[now++];
		if(head==end)
			return f[end];
		for(int i=1;i<=m*2;i++)
		//由于拆点操作,节点个数翻倍。 
			if(!pre[i]&&map[head][i])
			{
				f[i]=f[head]<map[head][i]?f[head]:map[head][i];
				queue[tail++]=i;
				pre[i]=head;
			}
	}
	return -1;
}
int Edmonds_karp()
{
	int temp_flow,max_flow=0;
	while((temp_flow=bfs())!=-1)
	{
		max_flow+=temp_flow;
		for(int i=end;i!=start;i=pre[i])
		{
			map[pre[i]][i]-=temp_flow;
			map[i][pre[i]]+=temp_flow;
		}
	}
	return max_flow;
}
int solved(int s,int e)
//计算s为源点,e为汇点的最大流。 
{
	start=s;
	end=e;
	//start和end为计算最大流的接口,非别表示源点和汇点。 
	for(int i=1;i<=m*2;i++)
		for(int j=1;j<=m*2;j++)
		//每次计算前都要初始化残留网络 
			map[i][j]=backup_map[i][j];
	return Edmonds_karp();
	//初始化完start和end以及残留网络map之后,就可以调用Edmonds_karp(),其返回值为最大流。 
}
void add_edge(int u,int v)
//在节点u、v之间建立通路。 
{
	backup_map[v][v+m]=1;
	backup_map[u][u+m]=1;
	//拆点操作,将u(v)拆成u(v)和u+m(v+m)两个点,之间通路的容量为1。 
	backup_map[u+m][v]=INF;
	backup_map[v+m][u]=INF;
}
int main()
{
	while(~scanf("%d%d",&m,&n))
	{
		if(m<=1)
		{
			printf("%d\n",m);
			continue;
		}
		if(n==0)
		{
			printf("0\n");
			continue;
		}
		memset(backup_map,0,sizeof(backup_map));
		for(int i=0,u,v;i<n;i++)
		{
			scanf(" (%d,%d)",&u,&v);
			add_edge(u+1,v+1);
		}
		int min=INF;
		for(int i=1;i<=m;i++)
			for(int j=i+1;j<=m;j++)
			//由于是无向图,源点汇点交换位置不影响最大流的值,所以j从i+1开始。 
			{
				int res=solved(i+m,j);
				min=min<res?min:res;
			}
		printf("%d\n",min>=INF?m:min);
		//输出时判断下min与INF是否相等,目的是判断当前是否处于整个网络就是一个割点集这种特殊情况。 
	}
}

后记:

提交后时间为250ms,由于是EK是O(VE^2)的复杂度,相比较SAP的O(EV^2)有不小的差距。曾设想进行优化,由于每次求最大流都要两层循环backup_map[][]初始化残留网络map[][],于是就想从矩阵复制入手。

思考了会,想起了memcpy,它可以进行连续空间的复制,但由于数组只有一维度上是连续的,所以还是免不了外层循环。满怀期待的尝试,结果却令人大跌眼镜,782ms!这货比for循环还慢,跟它兄弟memset差太多了。

之前看过NotOnlySuccess的《网络流的几个常见算法》,结论上说基本没题会去卡SAP,而却EK很悲剧,看来又得重新写模板了。

下面是用《网络流的几个常见算法》里的“sap+gap优化”模板写的代码,时间从250ms降到了141ms,非常可观的优化。美中不足的是这模板毕竟属于别人,风格上差别太大。虽然代码短小,但是使用了goto无疑是种缺憾。想手动改成递归,效率低些不要紧,但发现无从下手,我对sap的理解还太嫩,不会改啊。

#include<iostream>
using namespace std;
#define MAXN 50*2
#define INF MAXN+1
int map[MAXN][MAXN],backup_map[MAXN][MAXN];
int gap[MAXN],dis[MAXN],pre[MAXN],cur[MAXN];
//非负整数dis[i]描述i点到汇点的最少弧的数量,初始化均为0。 
int m,n;
int sap(int s,int t,int total)
//s源点,t汇点,total节点的个数 
{
	memset(cur,0,sizeof(cur));
	memset(dis,0,sizeof(dis));
	memset(gap,0,sizeof(gap));
	int u=pre[s]=s,maxflow=0,aug=-1;
	gap[0]=total;
	while(dis[s]<total) 
	{
		loop:
		for(int v=cur[u];v<total;v++)
			if(map[u][v]&&dis[u]==dis[v]+1)
			{
				if(aug==-1||aug>map[u][v])
					aug=map[u][v];
				pre[v]=u;
				u=cur[u]=v;
				if(v==t) 
				{
					maxflow+=aug;
					for(u=pre[u];v!=s;v=u,u=pre[u])
					{
						map[u][v]-=aug;
						map[v][u]+=aug;
					}
					aug=-1;
				}
				goto loop;
			}
		int mindis=total-1;
		for(int v=0;v<total;v++)
			if(map[u][v]&&mindis>dis[v])
			{
				cur[u]=v;
				mindis=dis[v];
			}
		if(!(--gap[dis[u]]))
			break;
		gap[dis[u]=mindis+1]++;
		u=pre[u];
	}
	return maxflow;
}
int solved(int s,int e)
//计算s为源点,e为汇点的最大流。 
{
	for(int i=0;i<m*2;i++)
		for(int j=0;j<m*2;j++)
		//每次计算前都要初始化残留网络 
			map[i][j]=backup_map[i][j];
	return sap(s,e,2*m);
	//初始化完start和end以及残留网络map之后,就可以调用sap(),其返回值为最大流。 
}
void add_edge(int u,int v)
//在节点u、v之间建立通路。 
{
	backup_map[v][v+m]=1;
	backup_map[u][u+m]=1;
	//拆点操作,将u(v)拆成u(v)和u+m(v+m)两个点,之间通路的容量为1。 
	backup_map[u+m][v]=INF;
	backup_map[v+m][u]=INF;
}
int main()
{
	while(~scanf("%d%d",&m,&n))
	{
		if(m<=1)
		{
			printf("%d\n",m);
			continue;
		}
		if(n==0)
		{
			printf("0\n");
			continue;
		}
		memset(backup_map,0,sizeof(backup_map));
		for(int i=0,u,v;i<n;i++)
		{
			scanf(" (%d,%d)",&u,&v);
			add_edge(u,v);
		}
		int min=INF;
		for(int i=0;i<m;i++)
			for(int j=i+1;j<m;j++)
			//由于是无向图,源点汇点交换位置不影响最大流的值,所以j从i+1开始。 
			{
				int res=solved(i+m,j);
				min=min<res?min:res;
			}
		printf("%d\n",min>=INF?m:min);
		//输出时判断下min与INF是否相等,目的是判断当前是否处于整个网络就是一个割点集这种特殊情况。 
	}
}