图论(1)
本篇主要讲解图论中的直径求法,最近公共祖先,割点和桥,连通性
(1)直径求法
分为两种,第一种通过dfs求最大边以及次大边
void dfs(int x)
{
v[x]=1;
for (int i=head[x];i;i=e[i].fr)
{
int y=e[i].to;
if (v[y]) continue;
dfs(y);
if (d[y]+1>d[x])//如果最大边能被更新
{
d2[x]=d[x],d[x]=d[y]+1;//更新最大边以及次大边
c[x]=y;//记录x在直径上的后节点为y
}
else if (d[y]+1>d2[x])//如果次大边能被更新
{
d2[x]=d[y]+1;
}
}
}
for (int i=0;i<n;i++) ans=max(ans,d[i]+d2[i]);
第二种为两次bfs,先随便找一个点,求出到这个点最大距离的点p,再以p为起点,寻找与p相距最远的点q,pq即/直径。
void dfs(int x)
{
v[x]=1;
for (int i=head[x];i;i=e[i].fr)
{
int y=e[i].to;
if (!v[y])
{
if (d[x]+e[i].val>ans)
{
ans=d[x]+e[i].val;
qd=y;
pre[x]=y;//记录路径
}
d[y]=d[x]+e[i].val;
dfs(y);
}
}
}
void qzj()
{
v[1]=1;
q.push(1);
ans=0;
while (!q.empty())//第一遍bfs
{
int x=q.front();
q.pop();
for (int i=head[x];i;i=e[i].fr)
{
int y=e[i].to;
if (!v[y])
{
if (d[x]+e[i].val>ans)
{
ans=d[x]+e[i].val;
zd=y;
}
d[y]=d[x]+e[i].val;
q.push(y),v[y]=1;
}
}
}
ans=0;
memset(v,0,sizeof(v));
memset(d,0,sizeof(d));
dfs(zd);//第二遍bfs
}
(2)最近公共祖先求法
有两种方法,第一种利用了树上倍增,详细看代码。(这种比较常用好理解)
void dfs(int t)
{
for (int i=head[t];i;i=e[i].fr)
{
int now=e[i].to;
if (!v[now])
{
v[now]=1;
d[now]=d[t]+1;
dis[now]=dis[t]+e[i].val;
f[now][0]=t;//表示now的父节点为t
dfs(now);
}
}
}
int lca(int x,int y)
{
if (d[x]>d[y]) swap(x,y);//保证y的深度更大
for (int i=15;i>=0;i--)
{
if (d[f[y][i]]>=d[x]) y=f[y][i];//使x,y高度相等或差1
}
if (x==y) return x;
for (int i=15;i>=0;i--)
{
if (f[x][i]!=f[y][i]) x=f[x][i],y=f[y][i];//使x,y共同攀升
}
return f[x][0];
}
int main()
{
d[1]=1;//深度
v[1]=1;//标记1为已经访问
dfs(1);//1为根节点
for (int j=1;j<=15;j++)
for (int i=1;i<=n;i++)
f[i][j]=f[f[i][j-1]][j-1];//外层直接初始化f
for (int i=1;i<=m;i++)
{
scanf("%d%d",&x,&y);
int result=dis[x]+dis[y]-2*dis[lca(x,y)];//求两点间最短距离公式
printf("%d\n",result);
}
return 0;
}
第二种利用了tarjan,是并查集对线上标记法的优化,离线算法,所以这里用到vector储存。(因为作者也不是很懂所以暂时略过)
int find(int xx)
{
if (fa[xx]!=xx) fa[xx]=find(fa[xx]);
return fa[xx];
}
void tarjan(int x)
{
v[x]=1;
for (int i=head[x];i;i=e[i].fr)
{
int y=e[i].to;
if (!v[y])
{
d[y]=d[x]+e[i].val;
tarjan(y);//先递归再合并
fa[y]=x;
}
}
for (int i=0;i<q[x].size();i++)
{
int y=q[x][i],id=qid[x][i];
if(v[y]==2){
int lca=find(y);
ans[id]=d[x]+d[y]-2*d[lca];
}
}
v[x]=2;
}
(3)割点与桥
割点:割了这个点,图被分为不连通的两块以上。
桥:割了这条边,图被分为不连通的两块以上。
因为桥比较重要,这里先讲桥。
(x,y)为桥,则dfn[x]<low[y]!!!(非常重要)
简单来讲就x的 子孙节点 的 最近祖先 没有大于x的。
void dfs(int u,int and_edge)
{
times++;
dfn[u]=low[u]=times;//初始化时间戳和u的最近祖先
vis[u]=1;
for (int i=head[u];i;i=e[i].fr)
{
int v=e[i].to;
if (!vis[v])
{
dfs(v,i);
low[u]=min(low[v],low[u]);
if (low[v]>dfn[u]){//满足桥成立的条件
e[i].flag=e[i^1].flag=true;
}
}
else if (i!=(and_edge^1)) low[u]=min(low[u],dfn[v]);//若已经访问,则更新low[u]
}
}
for (int i=1;i<=n;i++)
{
if(!dfn[i]) dfs(i,0);//防止原始图就不连通
}
下面讲割点判定法则:dfn【x】<=low【y】注意,分两种情况,第一种若x不为根节点,则一个子节点满足即可。第二种若x为根节点,则必须有两个子节点满足。
void dfs(int u)
{
times++;
dfn[u]=low[u]=times;//初始化时间戳和u的最近祖先
vis[u]=1;
int flag=0;//第二种情况
for (int i=head[u];i;i=e[i].fr)
{
int v=e[i].to;
if (!vis[v])
{
dfs(v);
low[u]=min(low[v],low[u]);
if (low[v]>=dfn[u]){//满足割点成立的条件
flag++;
if (x!=root || flag>1) cut[u]=true;
}
}
else low[u]=min(low[u],dfn[v]);//若已经访问,则更新low[u]
}
}
for (int i=1;i<=n;i++)
{
if(!dfn[i]) dfs(i);//防止原始图就不连通
}
(4)连通问题
连通问题大体分为有向图与无向图的连通。
无向图:分为“点双连通”(无割点),“边双连通”(无桥)。
有向图:强连通分量,即x可以到y,y也可以到x。
下面只对有向图强连通分量进行讲解。
void tarjan(int u)
{
times++;
dfn[u]=low[u]=times;//初始化时间戳和u的最近祖先
stack[++top]=u;//将u入栈
vis[u]=1;
ins[u]=1;//表示u在栈内
int flag=0;
for (int i=head[u];i;i=e[i].fr)
{
int v=e[i].to;
if (!vis[v])
{
tarjan(v);
low[u]=min(low[v],low[u]);
}
else if (ins[v]) low[u]=min(low[u],dfn[v]);//如果v已经在栈内在更新low【u】
}
if (dfs[u]==low[u])//如果构成了一个强联通分量
{
cnt++;
int y;
do{
y=stack[top--];
ins[y]=0;
c[y]=cnt;//表示y节点所在强联通分量编号为cnt
//scc[cnt].push_back(y);这一步操作可以进行缩点
}(y!=u);
}
}
for (int i=1;i<=n;i++)
{
if(!dfn[i]) dfs(i);//防止原始图就不连通
}