图论 简单学习笔记

网络流另开了一个专题,所以在这里就不详细叙述了。


一般表示为\(G=(V,E)\),V表示点集,E表示边集
定义图G为简单图,当且仅当图G没有重边和自环。
对于图G=(V,E)和图G2=(V2,E2) ,若V2是V的子集,E2是E的子集,那么图G2是图G的子图

拓扑排序问题

对于有向图,如果不存在环,则称为有向无环图,缩写为DAG。
拓扑排序是对DAG找出一个点的序列,使得如果x->y,那么x在序列中在y的前面。
拓扑排序是非常简单的,我们初始将所有入度为0的点丢入队列中,然后进行bfs。
每次取出队头x,对于所有x->y,将y的入度减一,如果此时y入度为0则将y加入队尾。最后队列中就是合法的拓扑序列。
常见的应用还有,如果对一个有向图做拓扑排序无法访问到所有点,可以证明这个有向图是有环的。

欧拉回路

入度必须都是偶数,如果有奇数的,一定不是欧拉回路。
如何输出方案?直接按照度数递归下去即可,注意输出方案一定要在回溯之后。

inline void search(int x)
{
	for(int i=1;i<=50;i++)
		if(done[x][i])
		{
			done[x][i]--,done[i][x]--;
			search(i);
			printf("%d %d\n",i,x);
		}
}

最短路

floyd 时间复杂度\((n^3)\)

从大到小枚举中间点k,然后枚举任意两个点i,j,用dist[i][k]+dist[k][j]去更新dist[i][j]。

dij 时间复杂度\((n^2+m)\)

适用性:没有负边权的单元最短路问题
过程:设起点为s,令\(f[x]\)表示从s到x的最短路,显然初始f[s]=0,其余f[x]=inf。初始所有点均为白点,每次选择f最小的一个白点作为当前点。枚举当前点的出边,用当前点更新其余的d,将当前点染黑。重复上述过程直至所有点都是黑点。
正确性证明:因为所有的边权均为非负整数,那么对于s->x最短路的一个方案,若最后一条边是y->x,显然这个方案由s->y加上y->x组成,即先走到y的最短路,再从y走到x。
因此我们用黑点表示最短路已经确定了的点,到最后所有的点都是黑点的时候算法结束。

dij+堆优化 时间复杂度\(O((n+m)logn)\)


SPFA算法(她死了

对于有负权,而没有负环的图,最短路仍然是有意义的,但是dij无法处理(大家想一下它的工作原理)
SPFA算法就是用一个队列来维护当前点,每次将队头当作当前点进行更新,并将被更新而不在队列中的点加入队尾,直至队列为空。

时间复杂度为\(O(nm)\)

数据结构优化建图

有些图的边数过于巨大,往往让我们很难直接在上面应用最短路算法。所以我们就可以用数据结构优化建图。
原理是保证图的等价性,而缩小边数。比如说点x向所有编号在区间[l,r]内的点都连相同边权的有向边,就可以对这些点建立线段树,每个节点向两个儿子连边权为0的有向边,每个叶子向对应原节点也这样连。那么对于x连向[l,r],显然可以变成x连向logn个线段树节点。


差分约束

其实就是利用最短路那个不等式,然后建立模型即可。
这里有一个写的很好的博客qwq


最小生成树

切割性质

将点集v分成s和v-s,一端在s内另一端在v-s内边权最小的边,一定出现在最小生成树中。(这里默认所有边权都不相同)

Prim算法

初始选定一个点为黑点,其余点为白点,每次找到一条连接黑点和白点的边权最小的边,将该边加入到生成树中,并将白点变为黑点。重复上述过程直到所有点都是黑点,最小生成树即求出。 堆优化之后可以做到\(O((n+m)logm)\)

Kruskal算法

一开始每个点自成一个联通块。将边按照边权从小到大排序,依次扫描,如果一条边的两端点不在一个联通块,加入并合并联通块。最终形成的就是最小生成树。复杂度瓶颈是将边排序。、

Boruvka算法

一开始每个点自成一个联通块,每次所有联通块都找一条边权最小的边,其中一端在该联通块而另一端不在,接下来加入这些边并合并联通块。(如果这些边成环则最大边权的边不该加入)
只剩一个联通块时算法结束。
正确性有保证是因为利用了切割性质。
算法复杂度与每一轮的执行时间有关,最多执行logn轮。


prufer序列

对于一个n个节点的带标号无根树,定义其prufer序是一个长度为n-2,值域为[1,r]的序列。

对于一个确定的n个节点的带标号无根树,得到其prufer序列的方法:

1、将树中标号最小的叶子删除,并将其连接的点的标号加入当前序列的末尾。
2、重复上述过程直到剩下两个节点。

一颗带标号无根树对于一个prufer序,一个prufer也对应一个带标号无根树,也就是说这是一个双射。

可以由上面得出:n个节点的不同带标号无根树的个数为\(n^{n-2}\)


无向图的一些定义

割点

若删掉某点P(及与其相连的边)后,原图G分裂成两个或以上的子图。那么P为原图的一个割点

割边(桥)

若删掉某边E后,原图G分裂成两个或以上的子图。那么E是原图的一个割边

点双连通图

如果一个图上没有割点,那么该图为点双连通图。在一个点双连通图里,任意两点间存在至少两条不经过重复点的路径
对于一个连通图,如果任意两点之间至少存在两条“点不重复”的路径,就说这个图是点双联通的(一般称作双联通)。这个要求等价于任意两条边都在同一个简单环中。

inline int tarjan(int x,int fa)
{
	int lowx=dfn[x]=++tot;
	int child=0;
	for(int i=head[x];i;i=edge[i].nxt)
	{
		int v=edge[i].to;
		Line e=(Line){x,v};
		if(dfn[v]==0)
		{
			s.push(e);
			child++;
			int lowv=tarjan(v,x);
			lowx=min(lowx,lowv);
			if(lowv>=lowx)
			{
				bcc[++cnt].clear();
				iscut[x]=1;
				for(;;)
				{
					Line cur=s.top();s.pop();
					if(id[cur.u]!=cnt) id[cur.u]=cnt,bcc[cnt].push_back(cur.u);
					if(id[cur.v]!=cnt) id[cur.v]=cnt,bcc[cnt].push_back(cur.v);
					if(cur.u==x&&cur.v==v) break;
				}
			}
		}
		else if(dfn[v]<dfn[x]&&v!=fa)
		{
			lowx=min(lowx,dfn[v]);
			s.push(e);
		}
	}
	if(child==1&&fa<0) iscut[x]=0;
	return lowx;
}

边双联通图

如果一个图上没有割边,那么该图为边双连通图。
在一个边双连通图里,任意两点间存在至少两条不经过重复边的路径
G的某个极大点/边双联通子图为G的点/双联通分量

一个有桥的连通图,如何把它通过加边变成边双连通图?

首先求出所有的桥,然后删除这些桥边,剩下的每个连通块都是一个双连通子图。把每个双连通子图收缩为一个顶点,再把桥边加回来,最后的这个图一定是一棵树,边连通度为1。

统计出树中度为1的节点的个数,即为叶节点的个数,记为ans。则至少在树上添加(ans+1)/2条边,就能使树达到边二连通,所以至少添加的边数就 是(ans+1)/2。

首先把两个最近公共祖先最远的两个叶节点之间连接一条边,这样可以把这两个点到祖先的路径上所有点收缩到一起,因为 一个形成的环一定是双连通的。然后再找两个最近公共祖先最远的两个叶节点,这样一对一对找完,恰好是(ans+1)/2次,把所有点收缩到了一起。


无向图的tarjan

这个算法可以用以找出无向图中所有点/边双联通分量。而其主要思路是遍历dfs树求出dfn和low。

(实现时无需先求出dfs树才做Tarjan算法,可以合并)

对于点i定义:
dfn[i]为dfs时i的时间戳,即i是第几个被dfs到的节点。
low[i]为从i出发,每个点只能通过树边往子树走,或通过非树边走到祖先,能够走到的dfn最小值。

具体实现:枚举从i出发的边(i,j),如果dfs树上j是i的儿子,那么low[i]=min(low[i],low[j]),否则low[i]=min(low[i],dfn[j])(此处需满足该边为返祖边,即没有枚举到dfs树上连接i与其父亲的边)。


无向图上求割点

对于一个点i,有以下情况:
i是dfs树的根,如果i有两个或以上的儿子,那么i是割点
i不是树根,如果存在i的一个儿子j,满足low[j]≥dfn[i],那么i是割点(此时j不能通过返祖边与i的祖先联通)

//核心代码
void tarjan(int u,int f)
{
    dfn[u]=low[u]=++cnt;
    int child=0;
    for(int i=head[u];i;i=edge[i].next)
    {
        int v=edge[i].to;
        if(!dfn[v])
        {
            tarjan(v,f);
            low[u]=min(low[u],low[v]);
            if(low[v]>=dfn[u]&&u!=f)
               cut[u]=1;
            if(u==f)
               child++;
        }
        low[u]=min(low[u],dfn[v]);
    }
    if(child>=2&&u==f)
       cut[u]=1;
}

无向图求点双

在Tarjan算法过程中,用栈存dfs过的点。
即每dfs一个新的点将其加入栈中。
其含义是我们用栈保存了按照dfn排序的目前还能进入新的点双里的非割点。
对于点u,如果它的儿子v满足low[v]≥dfn[u],那么u是割点,这时应不断退栈直到v被弹出,那么弹出的所有点和u形成一个点双。
一个割点可以存在于多个点双中(比如说:菊花图)

性质:对于大于2个点组成的点双,任意两点间存在两条除起点和终点外点不相交的路径。(若不存在,相交部分显然为割点,则不符合点双定义)


无向图求割边

首先非树边不可能是割边。
对于一条树边(i,j),其中父亲是i,儿子是j,那么如果low[j]>low[i],那么该树边是割边。
这可以表述为:如果low[i]=dfn[i],那么i与其父亲连接的边为割边。


无向图求边双

在Tarjan算法过程中,用栈存dfs过的点。
在对u的子树dfs完后,如果dfn[u]=low[u],那么u与其父亲连接的树边为割边。此时应不断弹栈直到弹出u,此时弹出的所有点形成一个边双。

割边不存在于任何一个边双里,其它边都恰好存在于一个边双里


强连通分量

在有向图G中,若两个顶点u,v之间能够相互到达,则称u,v是强连通的,若有向图G的每两个顶点都强连通,则G是一个强连通图。

有向图的极大强连通子图,被称为强连通分量。

首先强连通分量是针对于有向图的+有向图G中每一个点都属于且仅属于一个强连通分量+具有传递性。(即AB一个,BC一个的话,AC也在同一个)】

和上面针对于无向图提出的算法过程差不多。具体可以看下下面的缩点代码qwqwq

缩点

缩点之后的图一定是一个有向无环图。就是求出强连通分量之后给在同一个强连通分量的点重新染色。之后再把不同颜色的点重新连起来就完成了缩点。

//核心代码
void tarjan(int p)
{
    s[++tot]=p;
    ex[p]=1;
    dfn[p]=low[p]=++cnt;
    for(int i=head[p];i;i=edge[i].nxt)
    {
        int v=edge[i].to;
        if(!dfn[v])
        {
            tarjan(v);
            low[p]=min(low[p],low[v]);
        }
        else if(ex[v])
           low[p]=min(low[p],dfn[v]);
    }
    if(dfn[p]==low[p])
    {
        num++;
        while(s[tot+1]!=p)
        {
            id[s[tot]]=num;
            sum[num]+=val[s[tot]];
            ex[s[tot--]]=0;
        }
    }
}

圆方树

圆方树是对于连通无向图定义的一颗树,其中每个原图中的点都叫圆点,而我们为每个点双新建一个方点,而圆方树上所有边都是由一个方点和一个属于其对应点双的圆点相连这种形式。

性质

  • 只有圆点连向方点的边,任意圆点和方点之间都没有边。
  • 对于给定点对,其在圆方树路径上的所有圆点都是必经点。

在图中找环

有向图

有向图判断是否有负环——直接spfa即可,如果一个点入队次数超过n,就确定它存在负环。
有向基环树找环——直接上dfs即可。、
有向基环树找环代码:

inline void search(int x,int pre)
{
	if(flag==false) return;
	fa[x]=pre;
	done[x]=1;
	for(int i=head[x];i;i=edge[i].nxt)
	{
		int v=edge[i].to;
		if(done[v])
		{
			int now=x;
			vec.push_back(v);
			while(now!=v)
			{
				vec.push_back(now); 
				now=fa[now];
				
			}
			flag=false;
			return;
		}
		else search(v,x);
	}
}

有向图找环——和有向基环树差不多,就是每次找到一个环之后要把它锁起来,然后再找。时间复杂度上不太优秀。。。

无向图

无向基环树找环——直接拓扑排序,到最后度数>=2的就是环上的点。
无向图找环代码:
拓扑排序

inline void toposort()
{
	queue<int>q;
	for(int i=1;i<=n;i++)
		if(in[i]==1)
			q.push(i);
	while(!q.empty())
	{
		int u=q.front();q.pop();
		for(int i=head[u];i;i=edge[i].nxt)
		{
			int v=edge[i].to;
			in[v]--;
			if(in[v]==1) q.push(v);
		}
	}
	for(int i=1;i<=n;i++)
		if(in[i]>=2)
			v.push_back(i);//v里面的就是环上的点 
}

tarjan(其实也就是求出来割边即可,注意割边和割点的不同)

#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<cstdio>
#include<vector>
#define MAXN 100010
using namespace std;
int n,m,t=1,cnt,tot;
int head[MAXN<<1],dfn[MAXN],low[MAXN],cut[MAXN],color[MAXN];
vector<int>vec[MAXN];
struct Edge{int nxt,to;}edge[MAXN<<1];
inline void add(int from,int to){edge[++t].nxt=head[from],edge[t].to=to,head[from]=t;}
inline void tarjan(int x,int f)
{
	dfn[x]=low[x]=++cnt;
	for(int i=head[x];i;i=edge[i].nxt)
	{
		int v=edge[i].to;
		if(!dfn[v]) 
		{
			tarjan(v,i);
			low[x]=min(low[x],low[v]);
			if(low[v]>dfn[x]) cut[i]=cut[i^1]=1;
		}
		else if(i!=(f^1))
			low[x]=min(low[x],dfn[v]);
	}
}
inline void search(int x)
{
	color[x]=tot;
	vec[tot].push_back(x);
	for(int i=head[x];i;i=edge[i].nxt)
	{
		if(cut[i]) continue;
		int v=edge[i].to;
		if(color[v]) return;
		search(v);
	}
}
int main()
{
	#ifndef ONLINE_JUDGE
	freopen("ce.in","r",stdin);
	#endif
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++)
	{
		int u,v;
		scanf("%d%d",&u,&v);
		add(u,v),add(v,u);
	}
	for(int i=1;i<=n;i++)
		if(!dfn[i])
			tarjan(i,0);
	for(int i=1;i<=n;i++)
		for(int j=head[i];j;j=edge[j].nxt)
			if(cut[j]&&edge[j].to>i)
				printf("cut %d->%d\n",i,edge[j].to);
	for(int i=1;i<=n;i++) ++tot,search(i);
	for(int i=1;i<=tot;i++)
	{
		for(int j=0;j<vec[i].size();j++)
			printf("%d ",vec[i][j]);
		puts("");
	}
	return 0;
}

三元环计数

给定一个n个点m条边的无向图,问有多少个三元组(u,v,w)满足两两之间有边相连。
我们先把无向图转成有向图,并给每个点定义一个双关键字\((deg_i,id_i)\),其中\(deg\)表示度数,\(id\)表示标号,这样对于每一对点都能严格比较出大小。

我们把每一条边重定向成从度数大的点连向度数小的点,我们就可以得到一张有向无环图。

枚举一个点i,将所有i点连出的点标记为i。
枚举一个i连出的点j。
枚举一个j连出的点k,如果k的标记是i,那么就找到了一组三元环\((i,j,k)\)
分析每一个三元环只会在\(i\)这个点被算到一次答案。

四元环计数

先和三元环一样,把每个点排出来rank。

然后枚举两条边,找每个点距离2的点x,将x的标记加入答案,然后往上面标记+1


SAT问题

2-SAT问题

时间复杂度一般为\(O(n+m)\),如果是要求字典序最小,就更暴力了,是\(O(nm)\)
一般就是如果存在“选A必须选B”,那么从A向B连一条边。
2-SAT的构图具有对称性。
如果最后一遍tarjan下来同一个东西的两种决策(选或者不选)在一个强连通分量里,那么就是不存在一个合法解。
如果存在合法解的话,直接构造出来就行了。

如果是输出方案的话,要选择拓扑序小的先输出,再输出拓扑序大的。
当然我们也不需要重新搞一次拓扑序,在tarjan求强连通分量的时候,我们已经算过一遍拓扑序了,所以我们只需要将缩点之后的强连通分量标号倒着找输出即可。

3-SAT问题

这是NPC问题qwq


竞赛图与哈密顿回路

竞赛图:指任意两个顶点间恰有一条有向边的有向图
哈密顿回路:指除起点和终点外经过所有顶点恰好一次且起点和终点相同的路径

n个点的竞赛图中哈密顿回路的总数为\((n-1)!2^{\frac{(n-1)n}{2}-n}\)

因为我们形成一个哈密顿回路的话是一个排列,一共有\(n!\)种情况,但又因为是一个圆排列,所以要除以n
然后连边的话是有\(n(n-1)/2\)种,去掉形成环的n个,就是\(n(n-1)/2-n\)种,然后因为有向,所以是2的次方。

仙人掌图

如果某个无向连通图的任意一条边至多只出现在一条简单回路(simple cycle)里,我们就称这张图为仙人图(cactus)。

posted @ 2019-01-17 10:52  风浔凌  阅读(844)  评论(0编辑  收藏  举报