模板:图论

这篇博客详细介绍了图论的各种算法,包括树的链剖分、最小生成树、连通性、最短路、网络流、二分图及其相关算法,如Floyd、Dijkstra、BellmanFord、匈牙利算法等,旨在帮助读者理解和掌握图论在ACM竞赛中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

图论

这里用类似邻接表的方法存图。有的算法可能需要邻接矩阵,详见模板·线性代数

struct Graph
{
	struct Vertex
	{
		vector<int> a,b;//相关出边和入边编号
		int siz,dep,top,dfn;//树链剖分中使用,依次代表子树节点数、深度、所在链的顶端节点、dfs序
	};
	struct Edge
	{
		int from,to;
		ll dist,cap;//边长、容量,图论算法使用
	};
	vector<Vertex> v;//点集
	vector<Edge> e;//边集
	Graph(int n):v(n) {}
	void add(const Edge &ed)
	{
		if(ed.from==ed.to)return;//如果有需要请拆点
		v[ed.from].a.push_back(e.size());
		v[ed.to].b.push_back(e.size());
		e.push_back(ed);
	}
};

struct Tree:Graph
{
	Tree(int n):Graph(n) {}
	int fa(int k,int i=0)
	{
		return e[v[k].b[i]].from;
	}
	int ch(int k,int i=0)
	{
		return e[v[k].a[i]].to;
	}
	void build(int u,const Graph &g)//无向图dfs建树,且重边在最前,u为根节点
	{
		v[u].siz=1;
		for(int i=0,w,k; i!=g.v[u].a.size(); ++i)
			if(k=g.v[u].a[i],w=g.e[k].to,!v[w].siz)//没访问过的点siz默认0
			{
				build(w,g);
				v[u].siz+=v[w].siz;
				add(g.e[k]);
				if(v[ch(u)].siz<v[w].siz)//重边移到最前
					swap(v[u].a.front(),v[u].a.back());
			}
	}
};

树链剖分与LCA

struct Diagram:Tree
{
	Fenwick data;//暂用树状数组作为默认数据结构
	Diagram(const Graph &g,int root):
		Tree(g.v.size()),data(g.v.size())
	{
		build(root,g);
		int cnt=v[root].dfn=v[root].dep=1;
		dfs(v[root].top=root,cnt);
	}
	void dfs(int u,int &cnt)
	{
		for(int i=0,w; i!=v[u].a.size(); ++i)
		{
			v[w=ch(u,i)].dfn=++cnt;
			v[w].top=i?w:v[u].top;
			v[w].dep=v[u].dep+1;
			dfs(w,cnt);
		}
	}
	int lca(int x,int y)
	{
		for(; v[x].top!=v[y].top; x=fa(v[x].top))
			if(v[v[x].top].dep<v[v[y].top].dep)swap(x,y);
		if(v[x].dep<v[y].dep)swap(x,y);
		return y;
	}
	ll ask(int x,int y)
	{
		ll ans=0;
		for(; v[x].top!=v[y].top; x=fa(v[x].top))
		{
			if(v[v[x].top].dep<v[v[y].top].dep)swap(x,y);
			ans+=data.ask(v[v[x].top].dfn,v[x].dfn);
		}
		if(v[x].dep<v[y].dep)swap(x,y);
		return ans+=data.ask(v[y].dfn,v[x].dfn);
	}
	void add(int x,int y,ll pv)
	{
		for(; v[x].top!=v[y].top; x=fa(v[x].top))
		{
			if(v[v[x].top].dep<v[v[y].top].dep)swap(x,y);
			data.add(v[v[x].top].dfn,v[x].dfn,pv);
		}
		if(v[x].dep<v[y].dep)swap(x,y);
		data.add(v[y].dfn,v[x].dfn,pv);
	}
};

点剖(点分治)

零号点为虚节点。

struct TreeDiv:Graph
{
	int root;
	vector<int> vis,siz,mx;
	TreeDiv(int n):Graph(n),vis(n),siz(n),mx(n,n) {}
	void dfsRoot(int u,int fa)
	{
		for(int i=mx[u]=siz[u]=0,k,to; i<v[u].a.size(); ++i)
			if(k=v[u].a[i],to=e[k].to,to!=fa&&!vis[to])
				if(dfsRoot(to,u),siz[u]+=siz[to],mx[u]<siz[to])
					mx[u]=siz[to];
		if(mx[u]<mx[0]-++siz[u])mx[u]=mx[0]-siz[u];
		if(mx[root]>mx[u])root=u;
	}
	void dfsDist(int u,int fa,ll d)
	{
		//用d更新答案
		for(int i=0,k,to; i<v[u].a.size(); ++i)
			if(k=v[u].a[i],to=e[k].to,to!=fa&&!vis[to])
				dfsDist(to,u,d+e[k].dist);
	}
	int cal(int u,ll d)//返回符合要求的点对数
	{
		return dfsDist(u,0,d),/*得到答案*/;
	}
	void dfs(int u=1)
	{
		dfsRoot(u,root=0),ans+=cal(u=root,0),vis[u]=1;
		for(int i=0,k,to; i<v[u].a.size(); ++i)
			if(k=v[u].a[i],to=e[k].to,!vis[to])
				ans-=cal(to,e[k].dist),mx[0]=siz[to],dfs(to);
	}
};

最小生成树

无向图

同时给出Prim算法(生成新树)、Kruskal算法(消耗小)。

struct Prim:Tree
{
	struct DistGreater
	{
		bool operator()(const Edge &e1,const Edge &e2)
		{
			return e1.dist>e2.dist;
		}
	};
	ll ans;
	vector<int> vis;
	priority_queue<Edge,vector<Edge>,DistGreater> q;
	Prim(const Graph &g,int root):Tree(n),ans(0),vis(g.v.size(),0)//生成新树,每条边都要有等长反向边
	{
		for(insert(root,g); !q.empty();)
		{
			Edge ed=q.top();
			if(q.pop(),!vis[ed.to])
			{
				insert(ed.to,g);
				ans+=ed.dist;
				add(ed);
			}
		}
	}
	void insert(int u,const Graph &g)//把点和对应的相连的边加入集合
	{
		vis[u]=1;
		for(int i=0,k; i!=g.v[u].a.size(); ++i)
			if(k=g.v[u].a[i],!vis[g.e[k].to])
				q.push(g.e[k]);
	}
};
ll kruskal(vector<Edge> &e,int n)//会清空边集e,每条边被认作无向边
{
	ll ret=0;
	UnionFindSet ufs(n);
	for(sort(e.begin(),e.end(),DistGreater()); !e.empty(); e.pop_back())
		if(ufs.fa(e.back().from)!=ufs.fa(e.back().to))
		{
			ufs.merge(e.back().from,e.back().to);
			ret+=e.back().dist;
		}
	return /*ufs.siz>1?INF:*/ret;//视情况选择去注释
}
有向图

指定以root为根,如果没有限定根那么新建一个虚拟点作为根,向所有边连边长最大边长+1的边,在最后生成的图中去掉此边。时间复杂度 O ( V E ) O(VE) O(VE)

ll zhuLiu(vector<Edge> &e,int root,int n)//不存在返回INF
{
	for(ll ret=0;;)
	{
		vector<ll> in(n,INF);
		vector<int> pre(n,NPOS);
		for(int i=0,to; i<e.size(); ++i)
		{
			if(e[i].from==(to=e[i].to))
				swap(e[i--],e.back()),e.pop_back();
			else if(in[to]>e[i].dist)
				in[to]=e[i].dist,pre[to]=e[i].from;
		}
		for(int i=in[root]=0; i<n; ++i)
			if(in[i]==INF)return INF;
		vector<int> id(n,NPOS),vis(n,NPOS);
		int tn=0;
		for(int i=0,v; i<n; ++i)
		{
			for(ret+=in[v=i]; vis[v]!=i&&id[v]==NPOS&&v!=root; v=pre[v])
				vis[v]=i;
			if(v!=root&&id[v]==NPOS)
			{
				for(int u=pre[v]; u!=v; u=pre[u])
					id[u]=tn;
				id[v]=tn++;
			}
		}
		if(!tn)return ret;
		for(int i=0; i<n; ++i)
			if(id[i]==NPOS)id[i]=tn++;
		for(int i=0,v; i<e.size(); ++i)
			if((e[i].from=id[e[i].from])!=(e[i].to=id[v=e[i].to]))
				e[i].dist-=in[v];
		n=tn,root=id[root];
	}
}

连通性

无向图求割和双连通分量

割边:在连通图中,删除了连通图的某条边后,图不再连通。这样的边被称为割边,也叫做桥。
割点:在连通图中,删除了连通图的某个点以及与这个点相连的边后,图不再连通。这样的点被称为割点。
构造dfs搜索树,在树上有两类节点可以成为割点:
对根节点u,若其有两棵或两棵以上的子树,则该根结点u为割点;
对非根非叶节点u,若其中的某棵子树的节点均没有指向u的祖先节点的回边,说明删除u之后,根结点与该棵子树的节点不再连通;则节点u为割点。
对于一个无向图的子图,当删除其中任意一条边后,不改变图内点的连通性,这样的子图叫做边的双连通子图。而当子图的边数达到最大时,叫做边的双连通分量。原理是图中所有割边再求一次SCC,可直接使用下面求SCC的代码。
对于一个无向图的子图,当删除其中任意一个点后,不改变图内点的连通性,这样的子图叫做点的双连通子图。而当子图的边数达到最大时,叫做点的双连通分量。下面给出求点双连通分量的代码。

struct BCC:Graph//Biconnected Connected Componenet
{
	vector<int> low,bid,stak,cutPoint,cutEdge;//连通块最早dfs序,边的端点所属双连通块
	int bcc_siz;
	BCC(int n):Graph(n) {}
	void ask()
	{
		low.assign(v.size(),NPOS);
		bid.assign(e.size(),NPOS);
		cutPoint.assign(v.size(),0);
		cutEdge.assign(e.size(),0);
		for(int i=bcc_siz=0,cnt=0; i<v.size(); ++i)
			if(low[i]==NPOS)
				dfs(i,NPOS,cnt);
	}
	void dfs(int u,int fa,int &cnt)
	{
		low[u]=v[u].dfn=++cnt;
		for(int i=0,k,to,ch=0; i<v[u].a.size(); ++i)
			if(k=v[u].a[i],to=e[k].to,to!=fa)
			{
				if(low[to]==NPOS)
				{
					++ch;
					stak.push_back(k);
					dfs(to,u,cnt);
					low[u]=min(low[u],low[to]);
					if(low[to]>=v[u].dfn)
						for(++bcc_siz,cutPoint[u]=fa!=NPOS||ch>1;;)
						{
							int x=stak.back();
							stak.pop_back();
							bid[x]=bid[x^1]=bcc_siz-1;
							if(x==k)break;
						}
					if(low[to]>v[u].dfn)cutEdge[k]=cutEdge[k^1]=1;
				}
				else if(v[to].dfn<v[u].dfn)
				{
					stak.push_back(k);
					low[u]=min(low[u],v[to].dfn);
				}
			}
	}
};
双连通图的构造

先求出所有的桥,然后删除这些桥边,剩下的每个连通块都是一个双连通子图。把每个双连通子图收缩为一个顶点,再把桥边加回来,最后的这个图一定是一棵树,边连通度为1。统计出树中度为1的节点的个数,即为叶节点的个数,记为leaf。至少在树上添加(leaf+1)/2条边,就能使树达到边双连通:先把两个最近公共祖先最远的两个叶节点之间连接一条边,这样可以把这两个点到祖先的路径上所有点收缩到一起,因为一个形成的环一定是双连通的;然后再找两个最近公共祖先最远的两个叶节点,这样一对一对找完,恰好是(leaf+1)/2次,把所有点收缩到了一起。

有向图求强连通分量

如果是无向图,求出来的还是边双连通分量。

struct SCC:Graph//Strongly Connected Componenet
{
	vector<int> low,sid,stak;//连通块最早dfs序,点所属连通块
	int scc_siz;
	SCC(int n):Graph(n) {}
	void ask()
	{
		low.assign(v.size(),NPOS);
		sid.assign(v.size(),NPOS);
		for(int i=scc_siz=0,cnt=0; i!=v.size(); ++i)
			if(low[i]==NPOS)
				dfs(i,NPOS,cnt);
	}
	void dfs(int u,int fa,int &cnt)
	{
		low[u]=v[u].dfn=++cnt;
		stak.push_back(u);
		for(int i=0,k,to; i!=v[u].a.size(); ++i)
			if(k=v[u].a[i],to=e[k].to,to!=fa,1)//求边双连通分量把",1"注释掉,即不许走回边
			{
				if(low[to]==NPOS)
					dfs(to,u,cnt),low[u]=min(low[u],low[to]);
				else if(sid[to]==NPOS)
					low[u]=min(low[u],v[to].dfn);
			}
		if(low[u]==v[u].dfn)
			for(++scc_siz;;)
			{
				int x=stak.back();
				stak.pop_back();
				sid[x]=scc_siz-1;
				if(x==u)break;
			}
	}
};
2-SAT

n个布尔变量 x 0 … x n − 1 x_0\ldots x_{n-1} x0xn1,逻辑表达式 Y = ( A 0 + B 0 ) ( A 1 + B 1 ) … ( A m − 1 + B m − 1 ) Y=(A_0+B_0)(A_1+B_1)\ldots(A_{m-1}+B_{m-1}) Y=(A0+B0)(A1+

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值