图论笔记

图论

最小生成树

(N)个城市,(M)条可修的公路,每条公路有一个修的成本(w_i),要使(N)个城市连通,所需要的最低成本?

最少需要(N-1)条边,构成一棵树。

Kruskal算法证明

对图的顶点数(n)做归纳,证明(Kruskal)算法对任意(n)阶图都适用

归纳基础

(n=1),显然能找到最小生成树

归纳过程

例题

P1550 [USACO08OCT]打井Watering Hole

思路就是建立一个超级源点往每个点建一条边权为(w_i)的边,然后跑最小生成树

怎么证明合数都可以分成素数的乘积

就不写了,我太菜了

在最大生成树上求两点路径中边权的最小值

int f[N][20]; //i的第2^j祖先
int mn[N][20]; //i往上跳2^j祖先所经过的边的最小值 
int query(int u,int v) {
	int ans=inf;
	if(dep[u]>dep[v]) swap(u,v);
	for(int i=0,k=dep[v]-dep[u];i<=LG[k];++i)
		if(k>>i&1) {
			ans=min(ans,mn[v][i]);
			v=f[v][i];
		}
	if(u==v) return ans;
	for(int i=LG[dep[u]];i>=0;--i)
		if(f[u][i]!=f[v][i]) {
			ans=min(ans,min(mn[u][i],mn[v][i]));
			u=f[u][i],v=f[v][i];
		}
	ans=min(ans,min(mn[u][0],mn[v][0]));
	return ans;
}

kruskal重构树

orz lgj 学长,竟然是学长的博客

最短路

SPFA判负环

queue<int> q;
bool inq[N];
int dis[N], len[N];
bool spfa() { //返回值为true表示有负环 否则没有负环 
	fill(dis+1,dis+n+1,inf);
	dis[1]=0,len[1]=1;
	q.push(1);
	inq[1]=true;
	while(!q.empty()) {
		int u=q.front() ; q.pop();
		inq[u]=false;
		for(int i=0;i<e[u].size();++i) {
			int v=e[u][i];
			int w=W[u][i];
			if(dis[u]+w<dis[v]) {
				dis[v]=dis[u]+w;
				len[v]=len[u]+1;
				if(len[v]>=n) return true;
				if(!inq[v]) q.push(v),inq[v]=true;
			}
		}
	}
	return false;
}

建图技巧

通过添加虚拟点等手段将问题转化

从而达到减少边数等目的。

骚操作,长见识了

拓扑排序

在一个(DAG)有向无环图) 中,我们将图中的顶点以线性方式进行排序,使得对于任何的顶点 (u)(v) 的有向边 ((u,v)) , 都可以有 (u)(v) 的前面。

//拓扑排序 O(n^2)
int ind[N]; //每个点的入度
int seq[N],cnt; //求出的拓扑序
bool vis[N];
bool toposort() { //有环返回false 
	while(true) {
		if(cnt==n) return true;
		int k=0;
		for(int i=1;i<=n;++i)
			if(!vis[i]&&ind[i]==0) {
				k=i; break;
			}
		if(k==0) return false;
		seq[++cnt]=k; vis[k]=true;
		for(int i=0;i<e[k].size();++i) {
			int u=e[k][i];
			--ind[u];
		}
	}
}

queue优化

(O(n+m))

//拓扑排序 O(n+m)
queue<int> q; //q中维护入度为0的点 
int ind[N],seq[N],cnt;
bool toposort() { //有环返回false
	for(int i=1;i<=n;++i)
		if(!ind[i]) q.push(i);
	while(!q.empty()) {
		int u=q.front(); q.pop();
		seq[++cnt]=u;
		for(int i=0;i<e[u].size();++i) {
			int v=e[u][i];
			--ind[v];
			if(ind[v]==0) q.push(v);
		}
	}
	return cnt==n;
}

tarjian

推荐洛谷日报
Tarjan算法运行过程
1.按照深度优先遍历的方式遍历这张图。

2.遍历当前节点所出的所有边。在遍历过程中:

( 1 ) 如果当前边的终点还没有访问过,访问。回溯回来之后比较当前节点的low值和终点的low值。将较小的变为当前节点的low值。(因为遍历到终点时有可能触发了2)

( 2 ) 如果已经访问过,那我们一定走到了一个之前已经走过的点(终点的时间戳一定比当前的小)

3.则比较当前节点的low值和终点的dfn值。将较小的变为当前节点的low值

4,在回溯过程中,对于任意节点u用其出边的终点v的low值来更新节点u的low值。因为节点v能够回溯到的已经在栈中的节点,节点u也一定能够回溯到。因为存在从u到v的直接路径,所以v能够到的节点u也一定能够到。

当一个节点的dfn值和low值相等时,这个节点是一个强联通分量的“根”。压栈,输出。

int dfn[N],low[N],dfsclock;
int s[N],top;
int cnt; //当前联通块的编号
int bl[N]; //bl[i]表示i所在的强联通分量编号 
vector<int> scc[N]; //scc[i]中存储编号为i强联通分量中的所有点
void dfs(int u) {
	low[u]=dfn[u]=++dfsclock;
	s[++top]=u;
	for(int i=0;i<e[u].size();++i) {
		int v=e[u][i];
		if(dfn[v]) low[u]=min(low[u],dfn[v]);
		else {
			dfs(v);
			low[u]=min(low[u],low[v]);
		}
	}
	if(low[u]==dfn[u]) {
		++cnt;
		int v=s[top];
		while(v!=u) {
			bl[v]=cnt;
			scc[cnt].push_back(v);
			v=s[--top];
		}
		bl[u]=cnt;
		scc[cnt].push_back(u);
	}
}

缩点

一个强连通分量里的点如果看成是一个大点的话,图就可以变成有向无环图((DAG)),然后递推什么的就方便了

差分约束

以后再写吧,感觉就知道是吧不等式组建了个图...我太弱了

环套树/基环树

我们知道,树是最简单的连通无向图。

树中任意两点之间有且只有一条路径

环套树/基环树:树中加一条边的图。

设加的一条边为((u,v)),那么由之前(u,v)间有一条路径,加了这条边之后这条路径和这条边构成一个环。

这也是形成的唯一一个环

我们可以把环放在图中心,这样看起来环上每个点都是一个树根。
找到环的位置很重要

方法类似(noip2016 信息传递)

基环树找环

找环的步骤:

1.随意找一个点开始当成树进行(dfs),并记录每个结点访问的时间戳(dfn)

2.(dfs)的过程中一定会有一个点往(dfn)比自己小的点连了边,那么这条边可以看成加上的那条。记录下这条边((u,v))

3.暴力让(u)(v)往上爬到根,记录他们分别经过的点。

4.深度最大的他们都经过的点为他们的(lca)(u->lca)之间的所有点(+v->lca)之间的所有点即构成环。

//求基环树中的环 方法一
int fa[N]; //f[i]为i在搜索树中的父亲结点 
bool in[N],vis[N]; //表示i是否在环中
int cir[N],cnt;
int x,y;
void dfs(int u,int f) { //f为u的父亲结点 
	fa[u]=f; vis[u]=1;
	for(int i=0 ;i<e[u].size();++i) {
		int v=e[u][i];
		if(v==f) continue;
		if(vis[v]) x=u,y=v;
		else dfs(v,u);
	}
}
void solve() { //x到y一定是返祖边 
	dfs(1,0);
	while(x!=y) {
		cir[++cnt]=x;
		x=fa[x];
	}
	cir[++cnt]=y;
}
//方法二
queue<int>q;
int deg[N],n;
void solve()
{
    for(int i=1;i<=n;++i) in[i]=1;
    for(int i=1;i<=n;++i)
        if(deg[i]==1) q.push(i);
    while(!q.empty())
    {
        int u=q.front();
        q.pop();
        int[u]=0;
        for(int i=0;i<e[u].size();++i)
        {
            int v=e[u][i];
            deg[v]--;
            if(deg[v]==0) q.push(v);
        }
	}
    int u;//u作为环的起点
    for(u=1;u<=n;++u)
   	if(in[u]) break;
    int v=u,las=0;//把u放环里
    do{
        cir[++cnt]=v;
        int k;
        for(i=0;i<e[v].size();++i)
        {
            k=e[v][i];
            if(in[k] && k!=las) break;
		}
        la
        v=k;
    }while(v!=u)
}

欧拉图

通过图中所有边一次且仅一次行遍所有顶点的通路称为欧拉通路。

通过图中所有边一次且仅一次行遍所有顶点的回路称为欧拉回路。

具有欧拉回路的图称为欧拉图。

具有欧拉通路的图称为半欧拉图。

有向图的时候可以类似地定义。

(G)是欧拉图当且仅当(G)是连通的且没有奇度顶点。

$ G(是半欧拉图当且仅当)G$中恰有两个奇度顶点。

证:

连通且没有奇度顶点=>一定存在一个环

那么我们把这个环上的边删掉,剩下的图仍满足连通且没有奇数顶点

由连通性这些环可以是有公共点的,可以拼起来

圈套圈算法

(dfs)搜索,不能再往下走(不能重复使用一条边,但可以重复经过一个点)便回溯,回溯时记录路径,回溯时不清除对边的标记,最后求出来的路径就是欧拉回路。

给边编号,用(vis)数组记录每个边是否访问过

//欧拉回路 圈套圈算法
struct edge
{
    int v,nex,id;
};
bool vis[N];
int s[N],top;
int seq[N],cnt;
void dfs(int u)
{
    s[++top]=u;
    for(int i=head[u];i;i=e[i].nex)
    {
        if(vis[e[i].id]) continue;
        vis[e[i].id]=true;
        dfs(e[i].v);
	}
    --top;
    seq[++top]=e[i].v;
}
原文地址:https://www.cnblogs.com/pyyyyyy/p/11329739.html