图论 模板(2)

版权声明:https://blog.csdn.net/huashuimu2003 https://blog.csdn.net/huashuimu2003/article/details/84427016

Tarjan算法

割点(割顶)

#include<cstdio>
#include<cstring>
#include<algorithm>
#define _ 200010
using namespace std;
struct node{int x,y,next;} a[_];
int n,m,len=0,id=0,ans=0;
int last[_],low[_],dfn[_];
/*dfn[i]表示点i被访问的时间戳
low[i]表示点i及i的子树中所有结点能到达的结点中dfn最小的结点的时间戳*/
bool bz[_];//是不是割点
void ins(int x,int y)
{
	a[++len].x=x;a[len].y=y;a[len].next=last[x];last[x]=len;
}
void dfs(int x,int root)
//x表示当前访问到第x个点,root表示以root为根节点的子树的根
{
	int tot=0;//入度(及子树数) 
	low[x]=dfn[x]=++id;
//记录时间戳
	for(int i=last[x];i;i=a[i].next)
	{
		int y=a[i].y;
		if(!dfn[y])
		{
			dfs(y,root);
			low[x]=min(low[x],low[y]);
//更新当前节点的low值
			if(low[y]>=dfn[x]&&x!=root) bz[x]=true;
/*非根且子树能达到的dfn最小的结点的时间>=自己的时间时,
说明它的子树中最早能访问到的结点都比它后访问,此时只要不为根就一定是割点*/
			if(x==root) tot++;
//更新入度
		}
		low[x]=min(low[x],dfn[y]);
//把点x及x的子树可以达到的dfn的最小结点更新
	}
	if(x==root&&tot>=2) bz[root]=true;
/*如果一个点为根且入度>=2(即有两个子树),则一定为割点,
因为一棵树的根一删,那么它的子树一定不连通了*/
}
int main()
{
	int x,y;
	scanf("%d %d",&n,&m);
	for(int i=1;i<=m;i++)
	{
		scanf("%d %d",&x,&y);
		ins(x,y);
		ins(y,x);
	}
	for(int i=1;i<=n;i++)
		if(!dfn[i]) dfs(i,i);
	for(int i=1;i<=n;i++)
		if(bz[i]) ans++;
	printf("%d\n",ans);//割点的总数
	for(int i=1;i<=n;i++)
		if(bz[i]) printf("%d ",i);//输出割点
	return 0;
}

割边(桥)

#include<bits/stdc++.h>
#define _ 110000
using namespace std;

struct rec
{
    int y, next;
}edge[_],cut[_];
int n,m,id=0,len=0;
int dfn[_], low[_];
int head[_], bz[_], link[_];

void Add(rec *edge,int x,int y,int *head)
{
    edge[++len].y = y,edge[len].next = head[x],head[x] = len;
}

void tarjan(int x)
{
    dfn[x]=low[x]=++id;
    for (int i =head[x]; i; i =edge[i].next) 
	{
        int y = edge[i].y;
        if (bz[x] == y) continue;
        if (!dfn[y]) 
		{
            bz[y] = x;//是y的爸爸为x!!
            tarjan(y);
            low[x] = min(low[x], low[y]);
            if (low[y] > dfn[x]) Add(cut, x, y, link);
        }//此处无=号!!
        low[x] = min(low[x], dfn[y]);
    }
}

int main()
{
    cin >> n >> m;
    for (int i = 1; i <= m; ++i) 
	{
        int x, y;
        cin >> x >> y;
        Add(edge,x,y,head);
        Add(edge,y,x,head);
    }
    for (int i = 1; i <= n; ++i) 
		if (!dfn[i]) tarjan(i);
    for (int x = 1; x <= n; ++x) 
		for (int i = link[x]; i; i = cut[i].next)
            printf("%d %d\n",x,cut[i].y);
}

有向图的强连通分量

//强连通分量
#include<bits/stdc++.h>
#define _ 100010
#define $ 1000010
using namespace std;
inline int read()
{
	int f=1,num=0;
	char ch=getchar();
	while (ch<'0'||ch>'9') { if (ch=='-') f=-1; ch=getchar(); }
	while (ch>='0'&&ch<='9') num=(num<<1)+(num<<3)+ch-'0', ch=getchar();
	return num*f;
}

int ver[$],Next[$],link[_],len=0;
void add(int x,int y)
{
	ver[++len]=y,Next[len]=link[x],link[x]=len;
}
int dfn[_],low[_],id=0,belong[_];
int Stack[_],top=0,tot=0;
int instack[_];
vector<int>scc[_];
void tarjan(int x)
{
	dfn[x]=low[x]=++id;
	instack[x]=1;
	Stack[++top]=x;
	for(int i=link[x];i;i=Next[i])
	{
		int y=ver[i];
		if(!dfn[y])
		{
			tarjan(y);
			low[x]=min(low[x],low[y]);
		}
		else if(instack[y])
		low[x]=min(low[x],dfn[y]);
	}
	if(dfn[x]==low[x])
	{
		int k;
		++tot;
		do
		{
			k=Stack[top--], instack[k]=0;
			belong[k]=tot,	scc[tot].push_back(k);
		}while (k!=x);
	}
}
int main()
{
	int n=read(),m=read();
	for(int i=1;i<=m;++i)
	{
		int x=read(),y=read();
		add(x,y);
	}
	for(int i=1;i<=n;++i)
		if(!dfn[i])	tarjan(i);
	for (int i=1;i<=tot;++i)
	{
		printf("SCC #%d:",i);
		for (int j=0;j<scc[i].size();++j)
			printf(" %d",scc[i][j]);
		puts(" ");
	}
	return 0;
}

附上一个求最大强连通分量的例题

洛谷 p1726
描述 Description
在幻想乡,上白泽慧音是以知识渊博闻名的老师。春雪异变导致人间之里的很多道路都被大雪堵塞,使有的学生不能顺利地到达慧音所在的村庄。因此慧音决定换一个能够聚集最多人数的村庄作为新的教学地点。人间之里由N个村庄(编号为1…N)和M条道路组成,道路分为两种一种为单向通行的,一种为双向通行的,分别用1和2来标记。如果存在由村庄A到达村庄B的通路,那么我们认为可以从村庄A到达村庄B,记为(A,B)。当(A,B)和(B,A)同时满足时,我们认为A,B是绝对连通的,记为<A,B>。绝对连通区域是指一个村庄的集合,在这个集合中任意两个村庄X,Y都满足<X,Y>。现在你的任务是,找出最大的绝对连通区域,并将这个绝对连通区域的村庄按编号依次输出。若存在两个最大的,输出字典序最小的,比如当存在1,3,4和2,5,6这两个最大连通区域时,输出的是1,3,4。
输入格式 Input Format
第1行:两个正整数N,M
第2…M+1行:每行三个正整数a,b,t, t = 1表示存在从村庄a到b的单向道路,t = 2表示村庄a,b之间存在双向通行的道路。保证每条道路只出现一次。
输出格式 Output Format
第1行: 1个整数,表示最大的绝对连通区域包含的村庄个数。
第2行:若干个整数,依次输出最大的绝对连通区域所包含的村庄编号。
样例输入 Sample Input

5 5
1 2 1
1 3 2
2 4 2
5 1 2
3 5 1

样例输出 Sample Output

3
1 3 5

时间限制 Time Limitation
1s
注释 Hint
对于60%的数据:N <= 200且M <= 10,000
对于100%的数据:N <= 5,000且M <= 50,000

#include<bits/stdc++.h>
#define _ 100010
using namespace std;
inline int read()
{
	int f=1,num=0;
	char ch=getchar();
	while (ch<'0'||ch>'9') { if (ch=='-') f=-1; ch=getchar(); }
	while (ch>='0'&&ch<='9') num=(num<<1)+(num<<3)+ch-'0', ch=getchar();
	return num*f;
}
int head[_],ver[_],Next[_],len;
void add(int x,int y)
{
    ver[++len]=y,Next[len]=head[x],head[x]=len;
}
int dfn[_],low[_],id=0;
int belong[_],siz[_];
int Stack[_],top=0,tot=0;
int instack[_];
void tarjan(int x)
{	
    low[x]=dfn[x]=++id;
    instack[x]=1;
	Stack[++top]=x;
    for(int i=head[x];i;i=Next[i])
	{
		int y=ver[i];
        if(!dfn[y])
		{
            tarjan(y);
            low[x]=min(low[x],low[y]);
        }
        else if(instack[y])
			low[x]=min(low[x],dfn[y]);
    }
	int k;
    if(low[x]==dfn[x])
	{
    	++tot;
    	do
    	{
    		k=Stack[top];
			siz[tot]++;
    		--top;
    		instack[k]=0;
    		belong[k]=tot;
		} while (k!=x);
    }
}
int maxn=-1;
int main()
{
    int n=read(),m=read();
    for(int i=1;i<=m;i++)
	{
        int x=read(),y=read(),z=read();
        add(x,y);
        if(z==2) add(y,x);
    }
    for(int i=1;i<=n;i++)
        if(!dfn[i]) tarjan(i);
    int dcc;
    for(int i=1;i<=n;i++)
        if(siz[belong[i]]>maxn)
			maxn=siz[belong[i]],dcc=i;
    printf("%d\n",maxn);
    for(int i=1;i<=n;i++)
        if(belong[i]==belong[dcc]) printf("%d ",i);
    return 0;
}

无向图的双连通分量

E-DCC (边-双连通分量) 模板

/*
*边双连通分量(e-DCC)的求法
*/
#include<bits/stdc++.h>
#define _ 100010
using namespace std;
int head[_],ver[_*2],Next[_*2];
int dfn[_],low[_],n,m,tot,id;
bool bridge[_*2];
void add(int x,int y)
{
	ver[++tot]=y,Next[tot]=head[x],head[x]=tot;
}
void tarjan(int x,int inEdge)
{
	dfn[x]=low[x]=++id;
	for (int i=head[x];i;i=Next[i])
	{
		int y=ver[i];
		if (!dfn[y])
		{
			tarjan(y,i);
			low[x]=min(low[x],low[y]);
			if (low[y]>dfn[x])
				bridge[i]=bridge[i^1]=true;
		}
		else if (i!=(inEdge^1))
			low[x]=min(low[x],dfn[y]);
	}
}
int c[_],dcc;
void dfs(int x)
{
	c[x]=dcc;
	for (int i=head[x];i;i=Next[i])
	{
		int y=ver[i];
		if (c[y]||bridge[i]) continue;
		dfs(y);
	}
}
//E-DCC 缩点 
int hc[_],vc[_*2],nc[_*2],tc;
void addC(int x,int y)
{
	vc[++tc]=y,nc[tc]=hc[x],hc[x]=tc;
}
int main()
{
	cin>>n>>m;
	tot=1;
	for (int i=1;i<=m;++i)
	{
		int x,y;
		scanf("%d%d",&x,&y);
		add(x,y),add(y,x);
	}
	for (int i=1;i<=n;++i)
		if (!dfn[i]) tarjan(i,0);
	printf("\n");
	for (int i=2;i<tot;i+=2)
		if (bridge[i])
			printf("%d %d\n",ver[i^1],ver[i]);
	puts("are cut-edge");
	for (int i=1;i<=n;++i)
		if (!c[i])
		{
			++dcc;
			dfs(i);
		}
	printf("There are %d e-DCCs.\n",dcc);
	for (int i=1;i<=n;++i)
		printf("%d belongs to DCC %d.\n",i,c[i]);
	//E-DCC 缩点
	tc=1;
	for (int i=2;i<=tot;++i)
	{
		int x=ver[i^1],y=ver[i];
		if (c[x]==c[y]) continue;
		addC(c[x],c[y]); 
	}
	printf("缩点之后的森林,点数:%d,边数:%d(可能有重边)\n",dcc,tc/2);
	for (int i=2;i<=tc;++i)
		printf("%d %d\n",vc[i^1],vc[i]);
	return 0;
}

V-DCC (点-双连通分量) 模板

/*
*点双连通分量(v-DCC)的求法
*/
#include<bits/stdc++.h>
#define _ 100010
using namespace std;
int head[_],ver[_*2],Next[_*2];
int dfn[_],low[_],Stack[_],top;
int n,m,tot,id,root,cnt;
bool cut[_];
void add(int x,int y)
{
	ver[++tot]=y,Next[tot]=head[x],head[x]=tot;
}
vector<int>dcc[_];
void tarjan(int x)
{
	dfn[x]=low[x]=++id;
	Stack[++top]=x;
	if (x==root&&head[x]==0)//孤立点 
	{
		dcc[++cnt].push_back(x);
		return ;
	}
	int flag=0;
	for (int i=head[x];i;i=Next[i])
	{
		int y=ver[i];
		if (!dfn[y])
		{
			tarjan(y);
			low[x]=min(low[x],low[y]);
			if (low[y]>=dfn[x])
			{
				flag++;
				if (x!=root||flag>1) cut[x]=true;
				cnt++;
				int z;
				do
				{
					z=Stack[top--];
					dcc[cnt].push_back(z);
				}while (z!=y);
				dcc[cnt].push_back(x);
			}
		}
		else low[x]=min(low[x],dfn[y]);
	}
}
int newid[_],hc[_],nc[_*2],vc[_*2],c[_],tc;
void addC(int x,int y)
{
	vc[++tc]=y,nc[tc]=hc[x],hc[x]=tc;
}
int main()
{
	cin>>n>>m;
	tot=1;
	for (int i=1;i<=m;++i)
	{
		int x,y;
		scanf("%d%d",&x,&y);
		if (x==y) continue;
		add(x,y),add(y,x);
	}
	for (int i=1;i<=n;++i)
		if (!dfn[i]) root=i,tarjan(i);
	for (int i=1;i<=n;++i)
		if (cut[i]) printf("%d ",i);
	puts("are cut-vertexes");
	for (int i=1;i<=cnt;++i)
	{
		printf("e-DCC #%d:",i);
		for (int j=0;j<dcc[i].size();++j)
			printf(" %d",dcc[i][j]);
		puts(" ");
	}
	//V-DCC 缩点 
	//给每一个割点一个新的编号(编号从cnt+1开始) 
	int num=cnt;
	for (int i=1;i<=n;++i)
		if (cut[i]) newid[i]=++num;
	//建新图,从每个V-DCC到它包含的所有割点连边 
	tc=1;
	for (int i=1;i<=cnt;++i)
		for (int j=0;j<dcc[i].size();++j)
		{
			int x=dcc[i][j];
			if (cut[x])
				addC(i,newid[x]),addC(newid[x],i);
			else c[x]=i;//除割点外,其他点仅属1个V-DCC 
		}
	printf("缩点之后的森林,点数:%d,边数:%d\n",num,tc/2);
	printf("编号1~%d的为原图的V-DCC,编号>%d的为原图的割点\n",cnt,cnt);
	for (int i=2;i<tc;i+=2)
		printf("%d %d\n",vc[i^1],vc[i]);
	return 0;
}

2-SAT 模板

/*
*luogu.org p4728  
*/
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+10;
inline int read()
{
	int f=1,num=0;
	char ch=getchar();
	while (ch<'0'||ch>'9') { if (ch=='-') f=-1; ch=getchar(); }
	while (ch>='0'&&ch<='9') num=(num<<1)+(num<<3)+ch-'0', ch=getchar();
	return num*f;
}
int n,m;
int ver[maxn<<2],Next[maxn<<2],head[maxn<<1],len;
void add(int x,int y)
{
    ver[++len]=y,Next[len]=head[x],head[x]=len;
}
int low[maxn<<1],dfn[maxn<<1],id=0;
int Stack[maxn<<1],belong[maxn<<1],top=0,tot=0;
bool instack[maxn<<1];
void tarjan(int x)
{
    low[x]=dfn[x]=++id;
    Stack[++top]=x;
	instack[x]=1;
    for(int i=head[x]; i; i=Next[i])
    {
        int y=ver[i];
        if(!dfn[y])
        {
            tarjan(y);
            low[x]=min(low[x],low[y]);
        }
		else if(instack[y])
            low[x]=min(low[x],dfn[y]);
    }
    if(low[x]==dfn[x])
    {
        int k;
        ++tot;
        do
        {
            k=Stack[top];
            --top;
            instack[k]=0;
            belong[k]=tot;
        }while(k!=x);
    }
}
bool two_SAT()
{
    for(int i=1; i<=2*n; i++)
        if(!dfn[i])
            tarjan(i);//tarjan找强连通分量 
    for(int i=1; i<=n; i++)
        if(belong[i]==belong[i+n])//a条件和非a条件在同一个强连通分量,原问题无解 
            return 0;
    return 1;
}

int main()
{
    n=read(),m=read();
    for(int i=1; i<=m; ++i)
    {
        int a=read(),aval=read(),b=read(),bval=read();
        int nota=aval^1,notb=bval^1;//这里我用a表示条件a选0的情况,a+n表示条件a选1的情况
        add(a+nota*n,b+bval*n);//连边(非a,b) 
        add(b+notb*n,a+aval*n);//连边(非b,a) 
    }
    if(two_SAT())
    {
        printf("POSSIBLE\n");
        for(int i=1; i<=n; ++i)
            printf("%d ",belong[i]>belong[i+n]);
    }
	else printf("IMPOSSIBLE");
    return 0;
}

二分图

此类型现在不太会。。。。。。。。。。。。 ,所以先贴模板吧

二分图最大匹配 匈牙利算法 模板

/*洛谷 p3386 
题目描述:给定一个二分图,结点个数分别为n,m,边数为e,求二分图最大匹配数
输入格式:
	第一行,n,m,e,第二至e+1行,每行两个正整数u,v,表示u,v有一条连边(单向),u,v分属两个集合,
	1<=u<=n,1<=v<=m,n,m<=1000但是可能有坑
输出格式:共一行,二分图最大匹配
*/
#include<bits/stdc++.h>//注意一点:我们这里把第一个集合的点定为[1,n],第二个集合为[n+1,m]
using namespace std;
const int _=1000010;
int vis[2010],ver[_*2],Next[_*2],head[2010],tot;
int match[2010];

char buf[1<<15],*fs,*ft;
inline char getc(){return (ft==fs&&(ft=(fs=buf)+fread(buf,1,1<<15,stdin),ft==fs))?0:*fs++;}
inline int read()
{
	int num=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){num=(num<<3)+(num<<1)+(ch^48);ch=getchar();}
	return num*f;
}
inline void add(int x,int y)
{
	ver[++tot]=y,Next[tot]=head[x],head[x]=tot;
}
int dfs(int x)//算法核心 
{
	for(int i=head[x];i;i=Next[i])
	{
		int y=ver[i];
		if(!vis[y])
		{
			vis[y]=1;
			if((!match[y])||dfs(match[y]))//当前节点的指向节点没有匹配,自然return 1;否则尝试更改指向节点的指向节点的匹配 ,腾出来令当前节点与指向节点匹配 
			{
				match[y]=x;
				return 1;
			}
		}
	}
	return 0;
}
int main()
{
	register int ans=0;
	register int n=read(),m=read(),e=read();
	for(register int i=1;i<=e;++i)
	{
		register int u=read(),v=read();
		if(u>n||v>m||u<1||v<1) continue;
		add(u,v+n);
	}
	for(register int i=1;i<=n;++i)
	{
		memset(vis,0,sizeof(vis));//注意清空vis数组 
		if(dfs(i)) ++ans;
	}
	/*for(i=n+1;i<=m+n;++i)//输出方案
    {
        if(match[i]) cout<<match[i]<<" "<<i-n<<endl;
    }*/ 
	printf("%d",ans);
	return 0;
}
/*补充定义和定理:
*最大匹配数:最大匹配的匹配边的数目
*最小点覆盖数:选取最少的点,使任意一条边至少有一个端点被选择
*最大独立数:选取最多的点,使任意所选两点均不相连
*最小路径覆盖数:对于一个 DAG(有向无环图),选取最少条路径,使得每个顶点属于且仅属于一条路径。
 路径长可以为 0(即单个点)。
*最大独立集:选出一些顶点使得这些顶点两两不相邻,则这些点构成的集合称为独立集。
 找出一个包含顶点数最多的独立集称为最大独立集。
定理1:最大匹配数 = 最小点覆盖数(这是 Konig 定理)
定理2:最大匹配数 = 最大独立数
定理3:最小路径覆盖数 = 顶点数 - 最大匹配数
定理4:最大独立集=所有顶点数-最小顶点覆盖=顶点数-最大匹配数
*/

二分图带权匹配 KM算法 模板

/*
*例题:入门题:HDU2255 
*/ 
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 305;
const int INF = 0x3f3f3f3f;
int love[MAXN][MAXN];   // 记录每个妹子和每个男生的好感度
int ex_girl[MAXN];      // 每个妹子的期望值
int ex_boy[MAXN];       // 每个男生的期望值
bool vis_girl[MAXN];    // 记录每一轮匹配匹配过的女生
bool vis_boy[MAXN];     // 记录每一轮匹配匹配过的男生
int match[MAXN];        // 记录每个男生匹配到的妹子 如果没有则为-1
int slack[MAXN];        // 记录每个汉子如果能被妹子倾心最少还需要多少期望值
int N;
bool dfs(int girl)
{
    vis_girl[girl] = true;

    for (int boy = 0; boy < N; ++boy)
	{
		if (vis_boy[boy]) continue; // 每一轮匹配 每个男生只尝试一次

        int gap = ex_girl[girl] + ex_boy[boy] - love[girl][boy];

        if (gap == 0)
		{  // 如果符合要求
            vis_boy[boy] = true;
            if (match[boy] == -1 || dfs( match[boy] ))
			{    // 找到一个没有匹配的男生 或者该男生的妹子可以找到其他人
                match[boy] = girl;
                return true;
            }
        }
		else slack[boy] = min(slack[boy], gap);
			// slack 可以理解为该男生要得到女生的倾心 还需多少期望值 取最小值 备胎的样子【捂脸】 
    }
    return false;
}

int KM()
{
    memset(match, -1, sizeof match);    // 初始每个男生都没有匹配的女生
    memset(ex_boy, 0, sizeof ex_boy);   // 初始每个男生的期望值为0

    // 每个女生的初始期望值是与她相连的男生最大的好感度
    for (int i = 0; i < N; ++i)
	{
        ex_girl[i] = love[i][0];
        for (int j = 1; j < N; ++j)
			ex_girl[i] = max(ex_girl[i], love[i][j]);
    }

    // 尝试为每一个女生解决归宿问题
    for (int i = 0; i < N; ++i)
	{
        fill(slack, slack + N, INF);    // 因为要取最小值 初始化为无穷大

        while (1)
		{   // 为每个女生解决归宿问题的方法是 :如果找不到就降低期望值,直到找到为止

            // 记录每轮匹配中男生女生是否被尝试匹配过
            memset(vis_girl, false, sizeof vis_girl);
            memset(vis_boy, false, sizeof vis_boy);

            if (dfs(i)) break;  // 找到归宿 退出

            // 如果不能找到 就降低期望值
            // 最小可降低的期望值
            int d = INF;
            for (int j = 0; j < N; ++j)
                if (!vis_boy[j]) d = min(d, slack[j]);

            for (int j = 0; j < N; ++j)
			{  // 所有访问过的女生降低期望值
                if (vis_girl[j]) ex_girl[j] -= d;

                // 所有访问过的男生增加期望值
                if (vis_boy[j]) ex_boy[j] += d;
                // 没有访问过的boy 因为girl们的期望值降低,距离得到女生倾心又进了一步!
                else slack[j] -= d;
            }
        }
    }

    // 匹配完成 求出所有配对的好感度的和
    int res = 0;
    for (int i = 0; i < N; ++i)
        res += love[ match[i] ][i];

    return res;
}

int main()
{
    while (~scanf("%d", &N))
	{
        for (int i = 0; i < N; ++i)
            for (int j = 0; j < N; ++j)
                scanf("%d", &love[i][j]);

        printf("%d\n", KM());
    }
    return 0;
}

猜你喜欢

转载自blog.csdn.net/huashuimu2003/article/details/84427016