【学短向】图的存储与遍历,最短路,连通性 tarjan,树状数组

树状数组

单点修改,前缀查询。

int lbd(int x) {return (x&(-x));}
void gai(int x,int c) {for(int i=x;i<=n;i+=lbd(i)) a[i]+=c;}
int cha(int x) {int da=0;for(int i=x;i;i-=lbd(i)) da+=a[i];return da;}

上面这个模板同样是单点加/单点改/单点取max + 查前缀 max/查前缀和。
如果查后缀和只需要把 gai()cha()+- 互换。

查询信息可减就可以直接查区间(两个前缀和减一下)。

优点是代码比线段树短。

模板题。

什么?你想要更炫酷的序列操作?
哎不是你是不是为难学哥,不久有个讲线段树的学哥你去问他。


图的存储与遍历

链式前向星

const int QAQ=1001,ovo=QAQ*QAQ;
int cnt,head[QAQ],to[ovo],nex[ovo],bq[ovo];
void lian(int u,int v,int w)
{
	++cnt;
	to[cnt]=v;
	nex[cnt]=head[u];
	bq[cnt]=w;
	head[u]=cnt;
}
void dfs(int x,int dis)
{
	for(int i=head[x];i;i=nex[i])
	{
		int v=to[i],w=bq[i];
		dfs(v,dis+w);
	}
}

优点:

  1. 方便地找到反向边:初始化 cnt=1i^1 号边为 i 号边的反向边。

vector

const int QAQ=1001;
vector<int> dian[QAQ],bian[QAQ];
void lian(int u,int v,int w)
{
	dian[u].push_back(v);
	bian[u].push_back(w);
}
void dfs(int x,int dis)
{
	for(int i=0;i<dian[x].size();i++)
	{
		int v=dian[x][i],w=bian[x][i];
		dfs(v,dis+w);
	}
}

最好理解的存图。


最短路

听说大家学过了。

感觉上
dij 像贪心,稳定 \(O(n\log n)\),不能跑负边权。
spfa 像乱搞暴搜,不稳定 \(O(kn)\)\(k\) 是个很小的常数,但是在出题人想卡的情况下会卡到 \(O(n^2)\)。但是不刻意去卡会跑得很快,有负边权的时候肯定不会卡 SPFA。
弗洛伊德 像 DP,稳定 \(O(n^3)\)
bfs 只能跑 01 边权 \(O(n)\)。(实际上是可以跑整个图只有两种边权的)

【代码】SPFA:
bool vis[110000];
int n,m,s,dis[110000];
vector<int> dian[110000],bian[110000];
void sp(int s)
{
	queue<int> dui;
	dis[s]=0;
	dui.push(s);
	vis[s]=1;
	while(!dui.empty())
	{
		int x=dui.front();dui.pop();
		vis[x]=0;
		int chang=dian[x].size();
		for(int i=0;i<chang;i++)
		{
			int v=dian[x][i];
			if(dis[v]>dis[x]+bian[x][i])
			{
				dis[v]=dis[x]+bian[x][i];
				if(!vis[x]) dui.push(v),vis[v]=1;
			}
		}
	}
}
【代码】dij:
vector<int> dian[110000],bian[110000];
priority_queue<pair<int,int>,vector<pair<int,int> >,greater<pair<int,int> > > dui;
void dij(int s)
{
	dis[s]=0;
	dui.push(make_pair(0,s));
	while(!dui.empty())
	{
		int x=dui.top().second;dui.pop();
		if(vis[x]) continue;
		vis[x]=1;
		int chang=dian[x].size();
		for(int i=0;i<chang;i++)
		{
			int v=dian[x][i];
			if(dis[v]>=dis[x]+bian[x][i])
			{
				dis[v]=dis[x]+bian[x][i];
				dui.push(make_pair(dis[v],v));
			}
		}
	}
}

【代码】bfs【代码】弗洛伊德 懒得粘了。

例题:
P1462 通往奥格瑞玛的道路
P1629 邮递员送信
P3385 【模板】负环
P1144 最短路计数
P9377 [THUPC 2023 决赛] 百合
P4001 [ICPC-Beijing 2006] 狼抓兔子


连通性 tarjan

这里的连通性并非并查集之类的,把那些东西收起来!

强连通:有向图中任意两个点连通(互相走到)。
强连通分量:极大的强连通子图。(理解一下极大:能括就括,强连通分量的子图不是强连通分量)

找强连通分量

(注意别忘了我们在有向图

因为强连通分量任意两个点能互相走到,所以我们直接从一个点开始 dfs,可以构成一个 dfs 树:

返祖边:字面意思,如上图红边。
\(dfn_i\)\(i\) 的 dfs 序。
\(low_i\):在 dfs 树上,\(i\) 的子树内的点经过至多一条返祖边可以到达的最小 dfs 序。

手玩一下可以发现 \(dfn_i=low_i\) 的时候 \(i\) 为一个强连通分量的根,这时候子树内和 \(i\) 直接连通的 \(dfn_j<low_j\) 的这些点一起组成当前的强连通分量。
那么 \(dfn_j<low_j\) 如何提出来呢?
你发现父亲的 \(low\) 肯定比儿子的 \(low\) 小,宏观上看由下到上 \((dfn_j>low_j)→(dfn_j=low_j)→(dfn_j<low_j)\)
然后我们发现在 \(dfn_j=low_j\) 处会把前面的 \(dfn_j>low_j\) 统计并弹掉。
从下往上第一个强连通分量肯定是把子树都提出来并弹掉。
假设他有一个祖先为根的第二个强连通分量,这个强连通分量也是把当前没被弹走子树的点提出来弹掉。
你会发现用一个栈维护就 ok 了。
其实这个栈里的 dfs 序也是单调递增的,因为 dfs 本身就是个栈(先进先出)搜索。
栈具体的维护:每次 \(dfn_i=low_i\) 的时候开始弹栈弹到栈顶为 \(i\),然后再把栈顶也弹出来,这一轮弹出来的就是个强连通分量,至于为什么不是强连通分量的子图,大家可以手玩几个图理解(你想到的那些跑出强连通子图的情况实际上都不符合 dfs 树与 tarjan 其他操作的过程)。

void tajian(int x)
{
	dfn[x]=++hao,low[x]=dfn[x];
	zai[x]=1,zhan[++top]=x;
    // zai 数组是是否在当前点到根路径上,用来判断是否是返祖边。
    // zhan 是栈。
	for(int i=head[x];i;i=e[i].nex)
	{
		int v=e[i].to;
		if(!dfn[v]) tajian(v),low[x]=min(low[x],low[v]);
        //第一种:遇到 dfs 树上的儿子了。
		else if(zai[v]) low[x]=min(low[x],dfn[v]);
        //第二种:遇到返祖边了。
	}
	if(dfn[x]==low[x])//我是强连通分量的根。
	{
		++ke;//ke 是当前强连通分量的编号。
		while(zhan[top]!=x) kuai[zhan[top]]=ke,zai[zhan[top]]=0,top--;
        //把当前点为根的强连通分量提出来弹掉。
		kuai[zhan[top]]=ke,zai[zhan[top]]=0,top--;
	}
}

【模板】缩点

sol:

发现走到强连通分量把整个分量走一遍是最好的,所以缩点,然后剩下一个 DAG,这个显然可以按拓扑序跑个 DP。
\(f_x\):走到 \(x\) 节点的最大权值和。
\(f_v=\max(f_v,f_x+w)\)


注意下面是无向图

点双连通分量

点双连通分量:一个点删掉后仍然连通的极大子图。
上面的这个点称为割点。

找割点

如果存在 \(v\) 使得 \(low_v\ge dfn_x\),则 \(x\) 为割点
注意特判根节点如果有大于一个儿子则为割点。

std:
void tajian(int u)
{
	int ji=0;
	dfn[u]=++cnt;low[u]=cnt;
	for(int i=head[u],v;i;i=xia[i])
	{
		if(vis[i^1]) continue; vis[i]=1;
		v=to[i];
		if(!dfn[v])
		{
			tajian(v),low[u]=min(low[u],low[v]);
			if(u!=gen&&low[v]>=dfn[u]) ok[u]=1;
			ji++;
		}
		else low[u]=min(low[u],dfn[v]);
	}
	if(u==gen&&ji>1) ok[u]=1;
}

找点双连通分量

割点是点双的根,在割点退栈即可。

std:

void tajian(int u)
{
	int son=0;
	dfn[u]=++cnt;low[u]=cnt;
	zhan[++top]=u;
	for(int i=head[u],v;i;i=xia[i])
	{
		if(vis[i^1]) continue; vis[i]=1;
		v=to[i];
		if(!dfn[v])
		{
			son++,tajian(v),low[u]=min(low[u],low[v]);
			if(low[v]>=dfn[u])
			{
				std::vector<int> x;
				x.push_back(u);
				while(zhan[top]!=v) x.push_back(zhan[top--]);
				x.push_back(zhan[top--]);
				ans.push_back(x);
			}
		}
		else low[u]=min(low[u],dfn[v]);
	}
	if(gen==u&&!son)
	{
		vector<int> x;
		x.push_back(u);
		ans.push_back(x);
	}
}

边双连通分量

边双连通分量:一个边删掉后仍然连通的极大子图。
上面的这个边称为割边。

求边双连通分量

如果 \(low_x=dfn_x\),则 \(x\) 为割点
注意特判根节点如果有大于一个儿子则为割点。

std:
void tajian(int u)
{
	dfn[u]=++cnt;low[u]=cnt;
	zhan[++top]=u;
	for(int i=head[u],v;i;i=xia[i])
	{
		if(vis[i^1]) continue; vis[i]=1;
		v=to[i];
		if(!dfn[v]) tajian(v),low[u]=min(low[u],low[v]);
		else low[u]=min(low[u],dfn[v]);
	}
	if(low[u]==dfn[u])
	{
		vector<int> x=kong;
		while(zhan[top]!=u&&top) x.push_back(zhan[top--]);
		x.push_back(zhan[top--]);
		ans.push_back(x);
	}
}

求割边

std:
void tajian(int u)
{
	++hao,dfn[u]=hao,low[u]=hao;
	for(int i=head[u],v;i;i=xia[i])
	{
		if(vis[i^1]) continue;
		vis[i]=1;
		v=to[i];
		if(!dfn[v])
		{
			tajian(v),low[u]=min(low[u],low[v]);
			if(low[v]>dfn[u]) ok[i]=ok[i^1]=1;
		}
		else low[u]=min(low[u],dfn[v]);
	}
}

例题:

P2341 [USACO03FALL / HAOI2006] 受欢迎的牛 G

P1407 [国家集训队] 稳定婚姻

P3436 [POI 2006] 教授上早八

P3530 [POI 2012] FES-Festival

P3225 [HNOI2012] 矿场搭建

P8867 [NOIP2022] 建造军营

P5025 [SNOI2017] 炸弹

posted @ 2025-11-08 18:00  _a1a2a3a4a5  阅读(56)  评论(0)    收藏  举报