[WC 2015复习](三)图论算法与经典模型

都是比较简单SB的东西,求各位去WC的神犇勿喷。

1、 二分图匹配(匈牙利算法)

(1)[BZOJ 1059][ZJOI 2007]矩阵游戏

http://www.lydsy.com/JudgeOnline/problem.php?id=1059

考虑将下图中的蓝色方框圈出的格子移到红色方框圈出的格子去。

[WC 2015复习](三)图论算法与经典模型_第1张图片

首先交换红蓝格子各自所在的行

[WC 2015复习](三)图论算法与经典模型_第2张图片

再交换红蓝格子各自所在的列

操作成功。

[WC 2015复习](三)图论算法与经典模型_第3张图片

通过这样的操作我们可以发现,实际上在这个游戏中,我们可以随心所欲地将任意一个格子移动到指定的位置上去,进一步的研究还可以发现,每一行只能有一个格子被移动,每一列也只能有一个格子被移动。那么要想达到最终这个n*n的棋盘的对角线上都是黑格子,就需要有n个黑格子(xi,yi),而且任意的xi均不相同,任意的yi均不相同。那么我们可以想到二分图建模,将行号当成x侧的点,将列号当成y侧的点,对于任意的黑格子(xi,yi),xi向yi连一条边,对这个建好的二分图跑最大匹配,最后检查所有的x点(y点)是否都被匹配上了,若都被匹配上了,则此游戏有解,否则此游戏无解。

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>
 
#define MAXV 300
#define MAXE 60000
 
struct edge
{
    int u,v,next;
}edges[MAXE];
 
int head[MAXV],nCount=0;
 
int linky[MAXV]; //记录用的链表,起点指针为-1
bool visit[MAXV];
 
int n;
 
void AddEdge(int U,int V)
{
    edges[++nCount].u=U;
    edges[nCount].v=V;
    edges[nCount].next=head[U];
    head[U]=nCount;
}
 
bool dfs(int u) //以u为起点DFS
{
    for(int p=head[u];p!=-1;p=edges[p].next)
    {
        int v=edges[p].v;
        if(visit[v]) continue;
        visit[v]=true;
        if(linky[v]==-1||dfs(linky[v])) //v还没被匹配,或者v已经被匹配了,但是拆了linky[v]和v之间的匹配边后,linky[v]还能找到其他的点
        {
            linky[v]=u;
            return true;
        }
    }
    return false;
}
 
void EdmondKrap() //匈牙利算法
{
    memset(linky,-1,sizeof(linky));
    for(int i=1;i<=n;i++)
    {
        memset(visit,false,sizeof(visit));
        dfs(i);
    }
}
 
void work() //检查y侧是否所有点都被匹配了
{
    EdmondKrap();
    for(int i=1;i<=n;i++)
        if(linky[i]==-1)
        {
            puts("No");
            return;
        }
    puts("Yes");
}
 
int main()
{
    int TestCase;
    scanf("%d",&TestCase);
    while(TestCase--)
    {
        nCount=0;
        memset(head,-1,sizeof(head));
        scanf("%d",&n);
        for(int i=1;i<=n;i++)
            for(int j=1;j<=n;j++)
            {
                int a;
                scanf("%d",&a);
                if(a) AddEdge(i,j);
            }
        work();
    }
    return 0;
}

2、 Tarjan算法(求强联通分量、割点与桥、点双联通分量和边双联通分量)

(1)[BZOJ 2730][HNOI 2012]矿场搭建

http://www.lydsy.com/JudgeOnline/problem.php?id=2730

注意到点双联通分量的性质:去掉点双中任意一个点,点双中其他点仍然双联通。因此可以发现,在整个图中,一个只含有一个割点的点双联通分量中必须建一个井口,因为若这个点双的割点塌陷,那么这个点双中所有点都将与整个图其他的联通部分不再联通,因此必须建一个井口,而且这个井口绝对不能建立在割点上(建立在割点上,那么割点塌了,这个点双中的矿工照样死翘翘大笑)。但是一个含有多个割点的点双不需要建立井口,因为如果这个点双中塌了一个割点,但是其他割点仍然安然无恙,这个点双中的矿工可以从这些完好的割点撤离。

因此我们只需要先通过Tarjan找出整个图的所有割点和点双,并给每个割点做标记。对于每个只有一个割点的点双,修一个矿井,这样第一问就解决了。

那么还有第二问,其实也比较好做,观察发现每个需要放井口的点双(只有一个割点的点双)中除了割点以外,其他位置都可以修井口,因此用乘法计数便可求出总方案数,第二问也解决了。

注意还有一个特判:如果整个图都是点双,那么只需要修建两个井口(如果只修建一个井口,有可能最终这个井口塌了,剩余的图没井口了,矿工闷死在井里出不来大笑),最终的方案数为在n个不同的点中选2个点的无序组合的方案数,也就是n*(n-1)/2。

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>
#include <vector>

#define MAXE 1010
#define MAXV 1010

using namespace std;

typedef long long int LL;

struct edge
{
	int u,v,next;
}edges[MAXE];

int head[MAXE],nCount=0;

void AddEdge(int U,int V)
{
	edges[++nCount].u=U;
	edges[nCount].v=V;
	edges[nCount].next=head[U];
	head[U]=nCount;
}

int low[MAXV],dfn[MAXV],dfs_clock=0;
int bccNo[MAXV]; //bccno[i]=点i所属点双编号
int stack[MAXV],top=0; //Tarjan时用的栈(其中保存的是边的编号)
LL ans1,ans2; //第一问与第二问的答案
bool isCut[MAXV]; //isCut[i]=true表明i是割点
int bccCnt; //bccCnt=点双个数

vector<int>bcc[MAXV]; //bcc[i]存放第i个点双中的点的编号

void Tarjan_BCC(int u,int fa) //Tarjan求点双联通分量
{
	low[u]=dfn[u]=++dfs_clock;
	int childNum=0; //u在DFS中的儿子个数
	for(int p=head[u];p!=-1;p=edges[p].next)
	{
		int v=edges[p].v;
		if(!dfn[v])
		{
			stack[++top]=p;
			childNum++;
			Tarjan_BCC(v,u);
			low[u]=min(low[u],low[v]);
			if(dfn[u]<=low[v]) //找到了一个新的点双联通分量,且u为割点
			{
				isCut[u]=true;
				bccCnt++;
				bcc[bccCnt].clear();
				while(1)
				{
					int num=stack[top--]; //取出栈顶的一条边
					if(bccNo[edges[num].u]!=bccCnt) //边上的端点u所在的点双不是当前这个点双或者还没确定它所属的点双编号
					{
						bcc[bccCnt].push_back(edges[num].u); //标记边num的端点u是当前点双中的一个点
						bccNo[edges[num].u]=bccCnt;
					}
					if(bccNo[edges[num].v]!=bccCnt) //边上的端点v所在的点双不是当前这个点双或者还没确定它所属的点双编号
					{
						bcc[bccCnt].push_back(edges[num].v); //标记边num的端点v是当前点双中的一个点
						bccNo[edges[num].v]=bccCnt;
					}
					if(edges[num].u==u&&edges[num].v==v) //又转回到了边u->v,则找完了这个点双中的所有点
						break;
				}
			}
		}
		else if(dfn[v]<dfn[u]&&v!=fa)
		{
			stack[++top]=p;
			low[u]=min(low[u],dfn[v]);
		}
	}
	if(fa<0&&childNum==1) //u下面只有一个儿子,而且u是dfs的起点,那么u肯定不是割点
		isCut[u]=false;
}

void FindBCC(int n) //在点数为n的图中找点双
{
	memset(dfn,0,sizeof(dfn));
	memset(low,0,sizeof(low));
	memset(bccNo,0,sizeof(bccNo));
	memset(isCut,false,sizeof(isCut));
	dfs_clock=bccCnt=0;
	for(int i=1;i<=n;i++)
		if(!dfn[i])
			Tarjan_BCC(i,-1);
}

int n,m;

void solve() //求出最少要多少个井口以及最小答案的不同方案数
{
	FindBCC(n);
	ans1=0,ans2=1;
	for(int i=1;i<=bccCnt;i++) //遍历第i个点双
	{
		int numOfCut=0; //这个点双中的割点个数
		for(int j=0;j<bcc[i].size();j++)
			if(isCut[bcc[i][j]]) //在第i个点双中找到一个割点
				numOfCut++;
		if(numOfCut==1) //有一个割点,则必须要在这个点双中建一口矿井
		{
			ans1++;
			ans2*=(LL)(bcc[i].size()-numOfCut); //这个井口可以建立在除割点以外点双中的任何位置,乘法计数原理统计答案
		}
	}
	if(bccCnt==1) //特判:若整个图都是点双,那么至少要建立2个井口(如果只建立了一个井口,可能这个井口塌了,矿工就出不去了),随便在哪个点建立井口都可以
	{
		ans1=2;
		ans2=bcc[1].size()*(bcc[1].size()-1)/2;
	}
	printf("%lld %lld\n",ans1,ans2);
}

int main()
{
	int TestCase=0;
	while(scanf("%d",&m)!=EOF&&m)
	{
		n=0;
		nCount=0;
		memset(head,-1,sizeof(head));
		for(int i=1;i<=m;i++)
		{
			int u,v;
			scanf("%d%d",&u,&v);
			n=max(n,u);
			n=max(n,v);
			AddEdge(u,v);
			AddEdge(v,u);
		}
		printf("Case %d: ",++TestCase);
		solve();
	}
	return 0;
}

(2)[BZOJ 1179][Apio 2009]Atm

http://www.lydsy.com/JudgeOnline/problem.php?id=1179

这道题是一道非常裸的最大收益路径问题,只是终点有P个点可以选。

注意到图上任意一个强联通分量(以下简称SCC)里,强盗是可以到处走动的,也就是说强盗到达了某个点,那么这个点所属的SCC里的所有点都能被强盗抢劫。那么我们可以用tarjan缩点,缩点后的新图中,每个点的新点权为原图中这个SCC里所有点的点权之和。然后跑SPFA即可,最大的答案就是P个可选的终点中最远路离起点距离最远的那个点的距离。

#include <iostream>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <algorithm>
 
#define MAXE 500010
#define MAXV 500010
 
using namespace std;
 
struct edge
{
    int u,v,next;
}edges1[MAXE],edges2[MAXE];
 
int head1[MAXV],nCount1=0;
int head2[MAXV],nCount2=0;
 
int money[MAXV]; //money[i]=点i的点权
 
void AddEdge1(int U,int V)
{
    edges1[++nCount1].u=U;
    edges1[nCount1].v=V;
    edges1[nCount1].next=head1[U];
    head1[U]=nCount1;
}
 
void AddEdge2(int U,int V)
{
    edges2[++nCount2].u=U;
    edges2[nCount2].v=V;
    edges2[nCount2].next=head2[U];
    head2[U]=nCount2;
}
 
int dfn[MAXV],low[MAXV],dfs_clock=0;
bool inStack[MAXV];
int stack[MAXV],top=0;
int belong[MAXV],cnt_SCC=0;
int value[MAXV]; //每个SCC的点权和
int n,m;
 
void Tarjan_SCC(int u)
{
    dfn[u]=low[u]=++dfs_clock;
    inStack[u]=true;
    stack[++top]=u;
    for(int p=head1[u];p!=-1;p=edges1[p].next)
    {
        int v=edges1[p].v;
        if(!dfn[v])
        {
            Tarjan_SCC(v);
            low[u]=min(low[u],low[v]);
        }
        else if(inStack[v])
            low[u]=min(low[u],dfn[v]);
    }
    if(dfn[u]==low[u])
    {
        int v=-1;
        cnt_SCC++;
        while(u!=v)
        {
            v=stack[top--];
            belong[v]=cnt_SCC;
            value[cnt_SCC]+=money[v];
            inStack[v]=false;
        }
    }
}
 
void rebuild() //缩点建立新图
{
    for(int i=1;i<=n;i++)
    {
        for(int p=head1[i];p!=-1;p=edges1[p].next)
        {
            int j=edges1[p].v;
            if(belong[i]!=belong[j])
                AddEdge2(belong[i],belong[j]);
        }
    }
}
 
int S,P;
int dist[MAXV];
int q[1000010];
bool inQueue[MAXV];
 
int SPFA()
{
    int h=0,t=1;
    q[h]=belong[S];
    dist[belong[S]]=value[belong[S]];
    while(h<t)
    {
        int u=q[h++];
        inQueue[u]=false;
        for(int p=head2[u];p!=-1;p=edges2[p].next)
        {
            int v=edges2[p].v;
            if(dist[u]+value[v]>dist[v])
            {
                dist[v]=dist[u]+value[v];
                if(!inQueue[v])
                {
                    inQueue[v]=true;
                    q[t++]=v;
                }
            }
        }
    }
}
 
int main()
{
    int ans=0; //最大的答案
    memset(head1,-1,sizeof(head1));
    memset(head2,-1,sizeof(head2));
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++)
    {
        int u,v;
        scanf("%d%d",&u,&v);
        AddEdge1(u,v);
    }
    for(int i=1;i<=n;i++)
        scanf("%d",&money[i]);
    for(int i=1;i<=n;i++)
        if(!dfn[i])
            Tarjan_SCC(i);
    scanf("%d%d",&S,&P);
    rebuild();
    SPFA();
    for(int i=1;i<=P;i++)
    {
        int x; //第i个可以选择结束的地方是x
        scanf("%d",&x);
        if(dist[belong[x]]>ans) ans=dist[belong[x]];
    }
    printf("%d\n",ans);
    return 0;
}

(2)[POJ 2942]Knights of the Round Table

http://poj.org/problem?id=2942

题目大意:有n个骑士要参加一个会议,其中有m对骑士互相憎恶,互相憎恶的骑士不能同时出席会议,n个骑士要坐在多个圆桌上,而且每桌至少要有3个骑士,每桌的骑士个数也必须是奇数个,问有多少骑士无论如何也不能出席会议。

整体思路:Tarjan算法求点双联通分支+DFS交叉染色法找奇环。

首先我们逆向思考此题的反问题:有多少骑士可能出席这个会议。我们对任意一对不互相憎恶的骑士连无向边,则一个骑士能出席会议当且仅当它处在某个简单奇环中,原问题转换成求有多少个点不在奇圈上。

那么,我们可以在这个图中求出它的所有点双联通分量,于是我们只需要在点双里找奇环了,点双上的奇环上的点都可以出席会议。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <algorithm>

#define MAXE 2001000
#define MAXV 1100

using namespace std;

struct edge
{
	int u,v,next;
}edges[MAXE];

int head[MAXV],nCount=1;

int stack[MAXE];
int dfn[MAXV],low[MAXV],tot=0; //tot=DFS时间
bool inStack[MAXV],visit[MAXE],mark[MAXV]; //mark[i]=true表明点i在点双里
bool flag[MAXV]; //flag[i]=true表明点i可以参加会议
int top=0;
int map[MAXV][MAXV];
int n,m;
int color[MAXV]; //color[i]=点i染的颜色

void AddEdge(int U,int V)
{
	edges[++nCount].u=U;
	edges[nCount].v=V;
	edges[nCount].next=head[U];
	head[U]=nCount;
}

void init()
{
	nCount=1;
	top=tot=0;
	memset(head,-1,sizeof(head));
	memset(low,0,sizeof(low));
	memset(dfn,0,sizeof(dfn));
	memset(color,-1,sizeof(color));
	memset(inStack,false,sizeof(inStack));
	memset(flag,false,sizeof(flag));
	memset(visit,false,sizeof(visit));
	memset(map,0,sizeof(map));
	for(int i=1;i<=m;i++)
	{
		int a,b;
		scanf("%d%d",&a,&b);
		map[a][b]=map[b][a]=-1; //标记a和b互相厌恶
	}
}

void BuildGraph()
{
	for(int i=1;i<=n;i++)
		for(int j=i+1;j<=n;j++)
			if(map[i][j]==0) //i和j不是相互仇恨,则连边
			{
				AddEdge(i,j);
				AddEdge(j,i);
			}
}

bool DFS(int u) //从点u开始DFS,对环进行黑白染色,若是奇环,返回true
{
	for(int p=head[u];p!=-1;p=edges[p].next)
	{
		int v=edges[p].v;
		if(mark[v]) //点v在点双里
		{
			if(color[v]==-1) //点v还没有被访问过
			{
				color[v]=(color[u]+1)%2; //给v染和u相反的颜色
				if(DFS(v)) return true;
			}
			else if(color[v]==color[u]) return true; //v与u的颜色相同,且v访问过了,则说明找到了一个环,而且是奇环
		}
	}
	return false;
}

void check(int x) //以x为起点
{
	memset(color,-1,sizeof(color));
	color[x]=0;
	if(DFS(x))
	{
		for(int i=0;i<=n;i++)
			if(mark[i])
				flag[i]=true;
	}
}

void tarjanBCC(int u) //tarjan求点双
{
	dfn[u]=low[u]=++tot;
	for(int p=head[u];p!=-1;p=edges[p].next)
	{
		if(visit[p]) continue;
		visit[p]=visit[p^1]=true;
		int v=edges[p].v;
		if(!dfn[v])
		{
			stack[++top]=p;
			tarjanBCC(v);
			low[u]=min(low[u],low[v]);
			if(low[v]>=dfn[u])
			{
				memset(mark,false,sizeof(mark));
				int tmp;
				int size=0; //size=当前的点双的大小
				while(1)
				{
					size++;
					tmp=stack[top--];
					mark[edges[tmp].u]=mark[edges[tmp].v]=true; //标记tmp边的u、v点都在点双里
					if(edges[tmp].u==u) break;
				}
				if(size>=3) check(u); //如果该点双的大小大于等于3,那么就要对其进行黑白染色判奇环
			}
		}
		else if(dfn[v]<dfn[u]) //!!!!!!!!!!!
		{
			stack[++top]=p;
			low[u]=min(low[u],dfn[v]);
		}
	}
}

int calSum() //求出所有可以参加会议的点数之和
{
	int sum=0;
	for(int i=1;i<=n;i++)
		if(flag[i])
			sum++;
	return sum;
}

int main()
{
	while(scanf("%d%d",&n,&m)&&!(!n&&!m))
	{
		init();
		BuildGraph();
		for(int i=1;i<=n;i++)
			if(!dfn[i])
				tarjanBCC(i);
		printf("%d\n",n-calSum());
	}
	return 0;
}





3、拓扑排序

(1)[BZOJ 1565][NOI 2009]植物大战僵尸

http://www.lydsy.com/JudgeOnline/problem.php?id=1565

一道很不错的拓扑排序+最大权闭合子图模型。

以下转自By Void大牛

问题建模

首先我们我建立图论模型,把每个植物当做一个顶点,植物携带的资源数目为顶点的权值。如果一个植物b在另一个植物a的攻击范围内,连接一条有向边<a,b>,表示a可以保护b。由于僵尸从右向左进攻,可以认为每个植物都被它右边相邻的植物保护,对于每个植物a(除最左边一列),向其左边的相邻植物b,连接一条有向边<a,b>。

题目中给出的样例建图后如下所示

观察上图发现,{5,6}互相依赖,不可能被攻击到。我们可以用拓扑排序去除图中的环依赖结构,以简化图。

算法描述
将图转置后发现,一个合法的攻击方案是一个封闭子图(Closure of a Graph),我们的目标就是最大化攻击方案的点权值和,就是要求一个最大封闭子图(Maximum Weight Closure of a Graph),只需转化为网络流模型,即可解决。
算法分析
样例的图转置后如下,其最大封闭子图为{1,2,4}。

最大封闭子图的玩网络流建模方法为:

  1. 建立附加源S和附加汇T。

  2. 图中原有的边容量设为∞。

  3. 从S向每个权值为正的点连接一条容量为该点权值的有向边。

  4. 从每个权值不为正的点向T连接一条容量为该点权值绝对值的有向边。

在网络上求S到T的网络最大流Maxflow,最大封闭子图的权值就是(所有正权点权值之和 – Maxflow)。

样例建模后如下图所示,求得Maxflow = 5,所以最大封闭子图的权值为(10 + 20 – 5) = 25。

求最大封闭子图的网络流建模方法的严格证明见胡伯涛《最小割模型在信息学竞赛中的应用》。

复杂度分析

该图点数为O(NM),边数可达O(N2M2)。拓扑排序和建图的时间为O(N2M2),用Dinic算法求网络最大流的时间为O(|V|2|E|),所以总时间复杂度为O(N4M4)。在实际测试中通过了所以测试点,得到100分。


代码:

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>
 
#define MAXE 500500
#define MAXV 650
#define INF 0x3f3f3f3f
 
using namespace std;
 
int n,m,f[MAXV],ans=0;
int inDegree[MAXV];
bool used[MAXV];
 
struct edge
{
    int u,v,cap,next;
}edges[MAXE];
 
int head[MAXV],nCount=1;
int S,T;
 
void AddEdge(int U,int V,int C)
{
    edges[++nCount].u=U;
    edges[nCount].v=V;
    edges[nCount].cap=C;
    edges[nCount].next=head[U];
    head[U]=nCount;
}
 
void add(int U,int V,int C)
{
    AddEdge(U,V,C);
    AddEdge(V,U,0);
    inDegree[U]++;
}
 
//Dinic Maxflow Algorithm
int layer[MAXV];
int q[MAXV];
 
bool CountLayer()
{
    memset(layer,-1,sizeof(layer));
    int h=0,t=1;
    layer[S]=0;
    q[h]=S;
    while(h<t)
    {
        int u=q[h++];
        for(int p=head[u];p!=-1;p=edges[p].next)
        {
            int v=edges[p].v;
            if(layer[v]==-1&&edges[p].cap&&used[v])
            {
                layer[v]=layer[u]+1;
                q[t++]=v;
                if(v==T) return true;
            }
        }
    }
    return false;
}
 
int DFS(int u,int flow)
{
    if(u==T||flow==0) return flow;
    int used=0,tmp;
    for(int p=head[u];p!=-1;p=edges[p].next)
    {
        int v=edges[p].v;
        if(edges[p].cap&&layer[v]==layer[u]+1)
        {
            tmp=DFS(v,min(flow-used,edges[p].cap));
            if(!tmp) layer[v]=0;
            used+=tmp;
            edges[p].cap-=tmp;
            edges[p^1].cap+=tmp;
        }
    }
    return used;
}
 
int Dinic()
{
    int maxflow=0;
    while(CountLayer())
        maxflow+=DFS(S,INF);
    return maxflow;
}
 
//Topological Sort
 
void Toposort()
{
    int h=0,t=0;
    for(int i=S;i<=T;i++)
        if(!inDegree[i])
            q[t++]=i;
    while(h<t)
    {
        int u=q[h++];
        used[u]=true;
        if(f[u]>0) ans+=f[u]; //累加所有不属于环上的点的边权
        for(int p=head[u];p!=-1;p=edges[p].next)
        {
            int v=edges[p].v;
            if(p&1) //
            {
                inDegree[v]--;
                if(!inDegree[v])
                    q[t++]=v;
            }
        }
    }
}
 
int main()
{
    memset(head,-1,sizeof(head));
    scanf("%d%d",&m,&n);
    S=0,T=m*n+1;
    for(int i=1;i<=m*n;i++)
    {
        int y,r,c; //y是这个植物的保护范围
        scanf("%d",&f[i]);
        if(f[i]>0) //吃这个植物可以得到能量
            add(S,i,f[i]);
        else //吃这个植物要消耗能量
            add(i,T,-f[i]);
        scanf("%d",&y);
        for(int j=1;j<=y;j++)
        {
            scanf("%d%d",&r,&c);
            add(r*n+c+1,i,INF); //不能吃
        }
        if(i%n) add(i,i+1,INF);
    }
    Toposort();
    cout<<ans-Dinic()<<endl;
    return 0;
}


4、网络流模型

(1)[BZOJ 1565][NOI 2009]植物大战僵尸 (最大流、最大权闭合子图模型)

http://www.lydsy.com/JudgeOnline/problem.php?id=1565

见3、(1)

(2)[BZOJ 1221][HNOI 2001]软件开发 (最小费用最大流、餐巾计划问题)

http://www.lydsy.com/JudgeOnline/problem.php?id=1565

非常经典的餐巾计划问题。

(以上图片来自华师一附中郭松(二价氢)神犇)

不妨将每一天的情况拆成两个点(入点和出点)来考虑,每个入点代表这一天进的被洗过的新毛巾,每个出点表示这一天用掉的旧毛巾,每一单位的流量代表一条毛巾。源代表买入新毛巾,汇表示每天毛巾的需求量。那么第i天所有得到的新毛巾,从这些毛巾的源头(上一次被用过的日期的出点)向第i天的入点连边,容量为最多能得到的毛巾个数,费用为洗一条毛巾的代价。从源向第i天的出点连边,容量为ni,费用为f,表示直接买新毛巾。第i天的入点向汇连边,容量为该天毛巾需求量,费用为0。


#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>

#define MAXE 100010
#define MAXV 2010
#define INF 0x3f3f3f3f

using namespace std;

int S,T;

struct edge
{
	int u,v,w,cap,next;
}edges[MAXE];

int head[MAXV],nCount=1;

void AddEdge(int U,int V,int W,int C)
{
	edges[++nCount].u=U;
	edges[nCount].v=V;
	edges[nCount].w=W;
	edges[nCount].cap=C;
	edges[nCount].next=head[U];
	head[U]=nCount;
}

void add(int U,int V,int W,int C)
{
	AddEdge(U,V,W,C);
	AddEdge(V,U,-W,0);
}

int pre[MAXV];
int q[MAXE];
int dist[MAXV];
bool inQueue[MAXV];

bool SPFA()
{
	memset(pre,-1,sizeof(pre));
	memset(dist,INF,sizeof(dist));
	memset(inQueue,false,sizeof(inQueue));
	int h=0,t=1;
	q[h]=S;
	dist[S]=0;
	inQueue[S]=true;
	while(h<t)
	{
		int u=q[h++];
		inQueue[u]=false;
		for(int p=head[u];p!=-1;p=edges[p].next)
		{
			int v=edges[p].v;
			if(dist[u]+edges[p].w<dist[v]&&edges[p].cap)
			{
				dist[v]=dist[u]+edges[p].w;
				pre[v]=p;
				if(!inQueue[v])
				{
					inQueue[v]=true;
					q[t++]=v;
				}
			}
		}
	}
	return pre[T]!=-1; //!!!!!!!!!!
}

int MCMF()
{
	int mincost=0;
	while(SPFA())
	{
		int flow=INF;
		for(int p=pre[T];p!=-1;p=pre[edges[p].u])
			flow=min(flow,edges[p].cap);
		for(int p=pre[T];p!=-1;p=pre[edges[p].u])
		{
			edges[p].cap-=flow;
			edges[p^1].cap+=flow;
		}
		mincost+=flow*dist[T];
	}
	return mincost;
}

int n,a,b,f,fA,fB; //解释见题目说明

int inNode(int x) //代表某一天的入点
{
	return x*2-1;
}

int outNode(int x) //代表某一天的出点
{
	return x*2;
}

int main()
{
	memset(head,-1,sizeof(head));
	S=MAXV-2,T=MAXV-1;
	scanf("%d%d%d%d%d%d",&n,&a,&b,&f,&fA,&fB);
	for(int i=1;i<=n;i++)
	{
		int ni;
		scanf("%d",&ni); //第i天参与开发的工作人员个数
		add(S,inNode(i),f,ni);
		add(inNode(i),T,0,ni);
		add(S,outNode(i),0,ni);
	}
	for(int i=1;i<=n;i++)
	{
	    if(i+1<=n) add(outNode(i),outNode(i+1),0,INF);
		if(i+a+1<=n) add(outNode(i),inNode(i+a+1),fA,INF);
		if(i+b+1<=n) add(outNode(i),inNode(i+b+1),fB,INF);
	}
	printf("%d\n",MCMF());
	return 0;
}

(3)[BZOJ 1061][NOI 2008]志愿者招募 (最大流解线性规划问题)

http://www.lydsy.com/JudgeOnline/problem.php?id=1061

强烈推荐看ByVoid大牛的题解:https://www.byvoid.com/zhs/blog/noi-2008-employee/

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>

#define MAXE 200100
#define MAXV 100100
#define INF 0x3f3f3f3f

using namespace std;

int S,T;

struct edge
{
	int u,v,w,cap,next;
}edges[MAXE];

int head[MAXV],nCount=1;

void AddEdge(int U,int V,int W,int C)
{
	edges[++nCount].u=U;
	edges[nCount].v=V;
	edges[nCount].w=W;
	edges[nCount].cap=C;
	edges[nCount].next=head[U];
	head[U]=nCount;
}

void add(int U,int V,int W,int C)
{
	AddEdge(U,V,W,C);
	AddEdge(V,U,-W,0);
}

int q[MAXE];
int dist[MAXV];
int pre[MAXV];
bool inQueue[MAXV];

bool SPFA()
{
	memset(inQueue,false,sizeof(inQueue));
	memset(dist,INF,sizeof(dist));
	memset(pre,-1,sizeof(pre));
	int h=0,t=1;
	q[h]=S;
	inQueue[S]=true;
	dist[S]=0;
	while(h<t)
	{
		int u=q[h++];
		inQueue[u]=false;
		for(int p=head[u];p!=-1;p=edges[p].next)
		{
			int v=edges[p].v;
			if(dist[u]+edges[p].w<dist[v]&&edges[p].cap)
			{
				dist[v]=dist[u]+edges[p].w;
				pre[v]=p;
				if(!inQueue[v])
				{
					inQueue[v]=true;
					q[t++]=v;
				}
			}
		}
	}
	return pre[T]!=-1;
}

int MCMF()
{
	int mincost=0;
	while(SPFA())
	{
		int flow=INF;
		for(int p=pre[T];p!=-1;p=pre[edges[p].u])
			flow=min(flow,edges[p].cap);
		for(int p=pre[T];p!=-1;p=pre[edges[p].u])
		{
			edges[p].cap-=flow;
			edges[p^1].cap+=flow;
		}
		mincost+=dist[T]*flow;
	}
	return mincost;
}

int demond[MAXV];
int n,m;

int main()
{
	S=MAXV-2,T=MAXV-1;
	memset(head,-1,sizeof(head));
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
		scanf("%d",&demond[i]);
	for(int i=1;i<=m;i++)
	{
		int s,t,c;
		scanf("%d%d%d",&s,&t,&c);
		add(s,t+1,c,INF); //如果一个变量X[i]在第j个等式中出现为X[i],在第k个等式中出现为-X[i],从顶点j向顶点k连接一条容量为∞,权值为V[i]的有向边。(很显然对于每个区间[Si,Ti],从Si向Ti+1连边)
	}
	for(int i=1;i<=n+1;i++)
	{
		int c=demond[i]-demond[i-1];
		if(c>0) add(S,i,0,c); //如果一个等式右边为非负整数c,从源点S向该等式对应的顶点连接一条容量为c,权值为0的有向边;
		else add(i,T,0,-c); //如果一个等式右边为负整数c,从该等式对应的顶点向汇点T连接一条容量为-c,权值为0的有向边。
		if(i-1>=1) add(i,i-1,0,INF); //如果一个变量Y[i]在第j个等式中出现为Y[i],在第k个等式中出现为-Y[i],从顶点j向顶点k连接一条容量为∞,权值为0的有向边。(很显然就是第i天的点连向第i-1天的点)
	}
	printf("%d\n",MCMF());
	return 0;
}

(4)[BZOJ 1070][SCOI 2007]修车 (最小费用最大流)

http://www.lydsy.com/JudgeOnline/problem.php?id=1070

有意思的是这道题在后来某年NOI出现过,不过加强了数据,可见此题有多么经典。

比较显然的思路就是先求出n个顾客总的等待时间,再求出平均等待时间。可以发现,对于每个工人而言,他修完1辆车所耗费的时间,只会对他之后修的每一辆车的主人的等待时间产生影响,这个影响就是修当前这辆车的耗费时间,也就是说,假如这个工人修当前这辆车耗费了时间T,之后还要修k-1辆车,会使n个顾客总的等待时间加上k*T。我们可以将n个汽车看成n个点,将m个工人中每个人的状态拆成n个,用Node i,j来表示第i个工人的第j个状态,表示第i个工人修完当前的车后还要修j-1辆车,共m*n个点。源向每辆车连容量为1,费用为0的边,限制整个图的最大流不超过n。每辆车向Node i,j连容量为1,费用为第i个人修第j辆车的时间*j,对于每个Node i,j,向汇连容量为1,费用为0的边,表示每个工人的修车过程随时可以结束掉。显然整个图的最大流就是n,我们在保证最大流的前提下(保证每辆车都被修),求出最小费用(最小的总的等待时间),因此用最小费用最大流便可以解决此题。

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>
#include <queue>
 
#define MAXE 500500
#define MAXV 700
#define INF 0x3f3f3f3f
 
using namespace std;
 
int S,T;
 
struct edge
{
	int u,v,w,cap,next;
}edges[MAXE];

int head[MAXV],nCount=1;

void AddEdge(int U,int V,int W,int C)
{
	edges[++nCount].u=U;
	edges[nCount].v=V;
	edges[nCount].w=W;
	edges[nCount].cap=C;
	edges[nCount].next=head[U];
	head[U]=nCount;
}

void add(int U,int V,int W,int C)
{
	AddEdge(U,V,W,C);
	AddEdge(V,U,-W,0);
}

int dist[MAXV],pre[MAXV],q[MAXE];
bool inQueue[MAXV];

bool SPFA()
{
	memset(inQueue,false,sizeof(inQueue));
	memset(dist,INF,sizeof(dist));
	memset(pre,-1,sizeof(pre));
	int h=0,t=1;
	q[h]=S;
	dist[S]=0;
	inQueue[S]=true;
	while(h<t)
	{
		int u=q[h++];
		inQueue[u]=false;
		for(int p=head[u];p!=-1;p=edges[p].next)
		{
			int v=edges[p].v;
			if(dist[u]+edges[p].w<dist[v]&&edges[p].cap)
			{
				dist[v]=dist[u]+edges[p].w;
				pre[v]=p;
				if(!inQueue[v])
				{
					inQueue[v]=true;
					q[t++]=v;
				}
			}
		}
	}
	return pre[T]!=-1;
}

int MCMF()
{
	int mincost=0;
	while(SPFA())
	{
		int flow=INF;
		for(int p=pre[T];p!=-1;p=pre[edges[p].u])
			flow=min(flow,edges[p].cap);
		for(int p=pre[T];p!=-1;p=pre[edges[p].u])
		{
			edges[p].cap-=flow;
			edges[p^1].cap+=flow;
		}
		mincost+=dist[T]*flow;
	}
	return mincost;
}

int spendTime[MAXV][MAXV];

int main()
{
    memset(head,-1,sizeof(head));
	int n,m;
	scanf("%d%d",&m,&n); //m个人,n辆车
	S=MAXV-2,T=MAXV-1;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			scanf("%d",&spendTime[i][j]);
	for(int i=1;i<=n;i++) //枚举第i辆车
	{
		add(S,i,0,1); //源向每个车连容量为1,费用为0的点,限制每个车只能被修理一次
		for(int j=1;j<=m;j++) //枚举第j个人修理这个车
			for(int k=1;k<=n;k++) //枚举第j个人在修第i辆车后还要修k-1辆车
				add(i,j*n+k,spendTime[i][j]*k,1);
	}
	for(int i=1;i<=m;i++) //每个人的n个状态均和汇连边
		for(int j=1;j<=n;j++)
			add(i*n+j,T,0,1);
	printf("%.2lf\n",(double)MCMF()/n);
    return 0;
}



5、 欧拉回路

(1)[SGU 101]Domino

http://acm.hust.edu.cn/vjudge/problem/viewProblem.action?id=16528

题目大意:有n个多米诺骨牌,每个骨牌的两面印有不同的数字,任何数字均在[0,6]范围内,给定这些骨牌初始的方向(正面是什么数字,反面是什么数字),要你按照一定的次序,将每个骨牌正着放或者反着放,使得两个相邻的骨牌面向对方的一面的数字都是相同的。如果无解输出”No solution”。

整体思路:带重边的欧拉回路。

考虑到此题的n比较大,因此我们不能把每个骨牌当成一个个点,而是要把每个数字看成一个个点,两个数字之间的边就是正反两面印有这两个数字的骨牌。因此建好的图中点的个数最多只有7个,数据规模便变小了,而且要注意由于骨牌可以正着放也可以反着放,所以所有的连边都是无向边(或者说两条方向相反的有向边),最终走欧拉回路,记录答案,输出答案时判断骨牌是正还是反着放就看走过的每条有向边和输入的数字顺序是否相反即可。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>

#define MAXE 200
#define MAXV 20

using namespace std;

struct edge
{
	int u,v,next;
}edges[MAXE],ans[MAXE]; //edges记录输入时每条边的顺序,ans=最终的答案(欧拉序列)

int n;
int cnt=0; //cnt=已经走过的边数
int degree[MAXV]; //degree[i]=第i个点的度数
int map[MAXV][MAXV];

void DFS(int u) //DFS找欧拉回路
{
	for(int v=0;v<=6;v++)
	{
		if(!map[u][v]) continue;
		map[u][v]--,map[v][u]--;
		DFS(v);
		ans[++cnt].u=u,ans[cnt].v=v;
	}
}

int main()
{
	while(scanf("%d",&n)!=EOF)
	{
		cnt=0;
		for(int i=1;i<=n;i++)
		{
			int u,v;
			scanf("%d%d",&u,&v);
			degree[u]++;
			degree[v]++;
			map[u][v]++;
			map[v][u]++;
			edges[i].u=u,edges[i].v=v;
		}
		int start=0;
		cnt=0; //cnt=度为奇数的点的个数,start=DFS的起点
		for(int i=0;i<=6;i++)
		{
			if(degree[i]&1)
			{
				cnt++;
				start=i;
			}
			else if(degree[i]&&!start) //点i度数不为0,且还没确定起点,先把点i确定为起点
				start=i;
		}
		if(cnt>2) //奇数度的点的个数>=3,无解
		{
			printf("No solution\n");
			continue;
		}
		cnt=0;
		DFS(start);
		if(cnt<n) //所有的边没全部走完,无解
		{
			printf("No solution\n");
			continue;
		}
		for(int i=cnt;i>=1;i--) //遍历欧拉回路中的第i条边
		{
			for(int j=1;j<=n;j++) //遍历n条边中的第j条边,找出欧拉回路中的第i条边是原来图中的哪条边
			{
				if(ans[i].u==edges[j].u&&ans[i].v==edges[j].v)
				{
					printf("%d +\n",j);
					edges[j].u=-1; //随便标记下第i条边已经用了
					break;
				}
				else if(ans[i].u==edges[j].v&&ans[i].v==edges[j].u)
				{
					printf("%d -\n",j);
					edges[j].u=-1;
					break;
				}
			}
		}
	}
	return 0;
}

6、 最小圈(0/1分数规划)

(1)[BZOJ 1486][HNOI 2009]最小圈

http://www.lydsy.com/JudgeOnline/problem.php?id=1486

整体思路:0/1分数规划;SPFA判负环+二分答案。

首先我们知道,将一个圈中所有的边权全部减去它们之和的平均数的话,处理后的圈的边权和为0。因此我们可以二分答案,每次二分出答案后,将图上所有的边权全部减去这个答案,如果这个答案可行的话(但是这个答案不一定是最大的),图上一定会存在负环(环的边权之和是小于等于0),因此我们用SPFA判负环,如果图上存在负环,则说明当前二分的答案大了,降低上界。否则说明当前二分的最大答案小了,增大下界。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>
#include <cmath>

#define MAXE 11000
#define MAXV 4000
#define INF 0x3f3f3f3f
#define EPS 1e-10

using namespace std;

int n,m;

struct edge
{
	int u,v,next;
	double w,w0; //w0是原来的边权,w是减去二分答案后的边权
}edges[MAXE];

int head[MAXV],nCount=0;

void AddEdge(int U,int V,double W)
{
	edges[++nCount].u=U;
	edges[nCount].v=V;
	edges[nCount].w0=W;
	edges[nCount].next=head[U];
	head[U]=nCount;
}

bool hasFind,visit[MAXV]; //hasFind=true表明SPFA已经找到了负环,visit[i]=true表明在SPFA过程中已经访问了点i
double dist[MAXV];
	
void SPFA(int u) //DFS版SPFA,方便找负环
{
	visit[u]=true;
	for(int p=head[u];p!=-1;p=edges[p].next)
	{
		int v=edges[p].v;
		if(dist[u]+edges[p].w<dist[v])
		{
			if(visit[v]) //点v是访问过的点
			{
				hasFind=true;
				return;
			}
			dist[v]=dist[u]+edges[p].w;
			SPFA(v);
			if(hasFind) return;
		}
	}
	visit[u]=false;
}

bool judge() //判断当前的图中能否找到负环
{
	hasFind=false;
	memset(visit,false,sizeof(visit));
	memset(dist,0,sizeof(dist)); //!!!!!!!
	for(int i=1;i<=n;i++)
	{
		SPFA(i);
		if(hasFind) return true;
	}
	return false;
}

int main()
{
	memset(head,-1,sizeof(head));
	double lowerBound=-1000000000.0,upperBound=1000000000.0,ans;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++)
	{
		int u,v;
		double w;
		scanf("%d%d%lf",&u,&v,&w);
		AddEdge(u,v,w);
	}
	while(fabs(upperBound-lowerBound)>EPS)
	{
		double mid=(lowerBound+upperBound)/2;
		for(int i=1;i<=n;i++)
			for(int p=head[i];p!=-1;p=edges[p].next)
				edges[p].w=edges[p].w0-mid;
		if(!judge())
		{
			ans=mid;
			lowerBound=mid;
		}
		else
			upperBound=mid;
	}
	printf("%.8lf\n",ans);
	return 0;
}








 

你可能感兴趣的:([WC 2015复习](三)图论算法与经典模型)