Tarjan算法笔记

news2025/1/19 10:47:33

Tarjan

内容概要

dfs 搜索树

首先,我们要知道,Tarjan 算法来源于搜索树,那是什么呢,顾名思义就是按照搜索的顺序来遍历,所产生的顺序构成的树。首先我们可以来举个有向图的例子:

graph (4).png
graph (5).png

所以我们可以知道 dfs 生成树有一下 4 4 4 种边:

  • 树边(tree edge):图中黑色边表示。表示搜索访问到未访问过的结点。
  • 回祖边(back edge):图中橙色边表示。表示该边指向先前访问过的祖先。
  • 叉边(横向边)(cross edge):图中蓝色边表示。表示该边指向先前访问过的非祖先结点。
  • 前向边(forward edge):图中红色边表示。表示该边指向先前访问过的孩子结点。

但是,虽然有向图有四种,可是无向图却只有 2 2 2 种,分别是树边和回祖边。这里就不举例子了。

如果有人问为什么无向图偏偏少了叉边和前向边呢?好,我们来证明一下。

  • 如果存在叉边 u ⇒ v u \Rightarrow v uv ,其中 u u u 为当前搜索访问到的点, v v v 为之前访问过的非祖先节点,那么我们根据定义可知,访问 v v v 的时候,就应该访问过 u u u 了。但是这就与 u u u 的定义所矛盾,所以不存在叉边。
  • 如果存在前向边 u ⇒ v u \Rightarrow v uv ,那么在访问 v v v 时,该边就会变成一条回祖边指向 u u u ,不会作为一条前向边。

好,那知道了 dfs 搜索树,接下来就可以学习 tarjan 了。

Tarjan

经典题目

然后我们先说一下桥的定义:无向图中,若删去一条边会使得这个图的极大连通分量数增加,则该边被称为桥。

也可以理解为无向图的一个连通块中,若删除一条边会使得至少两点之间无法相互到达,该边被称为桥。我们不妨讲得形象一点,也就是你去西湖,有苏堤对吧,如果我用大炮把苏堤炸了,是不是你也就无法走进去了,对吧?1.png

也就是分成了一个外围,一个内湖,所以说苏堤就可以算一个桥。好我们来举个例子:

graph (6).png

那么,也就是在以上这个图中: 1 ⇒ 2 1 \Rightarrow 2 12 5 ⇒ 6 5 \Rightarrow 6 56 就是这个图的桥。而且我们不难发现一个非常有用的结论:只有树边才可以成为桥,而回边 4 ⇒ 2 4 \Rightarrow 2 42 和 $ 2 \Rightarrow 3 \Rightarrow 5 \Rightarrow 4$​ 沿途上的树边都不可以成为桥,因此我们可以知道:只有一条不能被回边标记的树边才有可能成为桥。

但是,我们怎么用 Tarjan 算法来求桥呢?我们可以对每个点 u u u 按 dfs 序打上时间戳 d f n u dfn_u dfnu ,同时记录 l o w u low_u lowu 表示 u u u 不经过他的父亲能到达最小的时间戳。那么一条树边 u ⇒ v u \Rightarrow v uv 不被回边标记的充要条件就是 l o w v > d f s u low_v>dfs_u lowv>dfsu 。比如

low[2]=2,dfn[1]=1;

所以 1 ⇒ 2 1 \Rightarrow 2 12 就是一座桥。代码如下:

void tarjan(int u, int fa) {
    dfn[u] = low[u] = ++num; // 初始化为num+1,表示已访问
    for (int i = head[u]; i; i = e[i].next) { // 遍历 u 所有邻接点
        int v = e[i].to; // 边的终点
        if (v == fa) continue;
        if (!dfn[v]) { // v 未访问
            tarjan(v, u); // 处理下一节点
            low[u] = min(low[u], low[v]); // 更新追溯值low[u]
            if (low[v] > dfn[u]) // 边(u,v)是桥
                cout << u << "——>" << v << endl; // 输出结果
        }
        else // 如果v已经被访问过
            low[u] = min(low[u], dfn[v]); // 更新当前节点u的追溯点值low[u]
    }
}

割点

定义:无向图中,若删去一个点会使得这个图的极大连通分量数增加,这个点被称为割点。

也可以理解为无向图的一个连通块中,若删除一个点会使得至少两点之间无法相互到达,该点被称为割点。,其他的话根桥有一点点相似。

类似于上面一张图片, 2 2 2 号就是一个割点。但是我们还有一种情况也是割点,就是当 u u u 作为根节点时,若 u u u 有两个及以上的儿子时, u u u​ 为割点。

代码如下:

void Tarjan(int u,int fa)
{
    dfn[u]=low[u]=++id;
    int child=0;
    for(int i=head[u];i;i=p[i].nxt)
    {
        int v=p[i].to;
        if(!dfn[v])
        {
            Tarjan(v,u);
            low[u]=min(low[u],low[v]);
            if(u!=root&&low[v]>=dfn[u])
                point[u]=1;
            if(u==root&&++child>=2)
                point[u]=1;
        }
        else
            low[u]=min(low[u],dfn[v]);    
    }

}
强联通分量

首先,我们要明确一个事:强连通分量(SCC,Strongly Connected Components)是在单向图中的。

强联通子图,定义为:在 单向图 中,该子图上的任意两点之间能互相到达。

强联通分量,定义为:极大连通子图

graph (8).png

那么这张图的强联通分量 { 1 , 2 , 3 , 4 , 5 , 6 , 7 } \{ 1,2,3,4,5,6,7 \} {1,2,3,4,5,6,7}

前向边是没有一点用处的,一定牢记!!!

  • 如果 u u u 是当前 scc 中的点,那么该 scc 的节点一定在 dfs 生成树中而且是以 u u u​ 为根的子树内。怎么证明呢?若存在该 scc 中的点 v v v 不在 u u u 子树内,则 u u u v v v 的路径中一定存在叉边或回边,根据叉边、回边的定义,则 v v v u u u 之前已经被访问过,与 u u u​ 是当前 scc 第一个被访问的结点矛盾。
  • 如果我们把单个节点也算作是强联通分量,那么所有的 scc 的根应该都满足 l o w u = d f n u low_u=dfn_u lowu=dfnu
缩点

模版题

在无向图的一些问题中,若将所有 scc 合并为一个点,那么这张无向图就会变成一张有向无环图(DAG,Directed Acyclic Graph)因此我们只需保留连接两个不同 scc 的边来构成缩点后的新图。也就是把一个个环变成一个个大点。

#include<bits/stdc++.h>
#define maxn 100001
#define maxm 500001
using namespace std;
struct node{
    int to,next,from;
}edge[maxm];
queue <int> q;
vector <int> cb[maxn];
vector <int> rdr[maxn];
int ans[maxn],totq,x,y,v,rd[maxn],u,n,m,sum,vis[maxn],dis_[maxn],dis[maxn];
int dfn[maxn],low[maxn],f[maxn],times,cntqq;
int stack_[maxn],heads[maxm],fuck[maxn],cnt,tot,index_;
void add(int x,int y)
{
    edge[++cntqq].next=heads[x];
    edge[cntqq].from=x;
    edge[cntqq].to=y;
    heads[x]=cntqq;
   	return;
}
void tuopu()
{
	for(int i=1;i<=tot;i++)
    {
        if(rd[i]==0)
        q.push(i);	
    }
    while(!q.empty())
    {
        int u=q.front();
        q.pop();
        ans[++totq]=u;
        for(int i=1;i<=cb[u].size();i++)
        {
            v=cb[u][i-1]; 
            rd[v]--;
            if(rd[v]==0)q.push(v);
        }
    }
}
void tarjan(int x)
{
    dfn[x]=low[x]=++times;
    stack_[++index_]=x;	
    fuck[x]=1;
   	for(int i=heads[x];i!=-1;i=edge[i].next)
    {
        if(!dfn[edge[i].to])
        {
           	tarjan(edge[i].to);
            low[x]=min(low[x],low[edge[i].to]);
       	}
       	else 
        if(fuck[edge[i].to])
        low[x]=min(low[x],dfn[edge[i].to]);
    }
    if(low[x]==dfn[x])
   	{
   		tot++; 
   		while(1)
       	{
       		vis[stack_[index_]]=tot;
       		dis_[tot]+=dis[stack_[index_]];	
       		fuck[stack_[index_]]=0;index_--;
       		if(x==stack_[index_+1])break;
       	}
    }
}
int main(){
    memset(heads,-1,sizeof(heads));
    int n,m,x,y;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
		scanf("%d",&dis[i]);
    for(int i=1;i<=m;i++)
    {
        scanf("%d%d",&x,&y);
       	add(x,y);
    }
    for(int i=1;i<=n;i++)
		if(!dfn[i])
			tarjan(i);
    for(int i=1;i<=cntqq;i++){
        if(vis[edge[i].from]!=vis[edge[i].to])
        {
            x=vis[edge[i].from];y=vis[edge[i].to];
			rd[y]++;cb[x].push_back(y);rdr[y].push_back(x);
        }
    }
    tuopu();
    for(int i=1;i<=tot;i++)
    {
        int w=ans[i];
        f[w]=dis_[w];
        for(int j=1;j<=rdr[w].size();j++)
        f[w]=max(f[w],f[rdr[w][j-1]]+dis_[w]);
    }
    for(int i=1;i<=tot;i++)
    	sum=max(f[i],sum);
    printf("%d\n",sum); 
    return 0;
}
双联通分量

双联通分量包含了两种,分别是点双联通分量边双联通分量

首先,我们先说一下定义:

  • 点双联通:对于点 u u u v v v ,删去图上任意一个点, u u u v v v 始终连通,则称 u u u v v v 点双联通。

    容易发现,若一个子图点双连通,那么这个子图上肯定没有割点。

  • 边双联通:对于点 u u u v v v ,删去图上任意一条边, u u u v v v 始终连通,则称 u u u v v v 边双联通。

    容易发现,若一个子图点双连通,那么这个子图上肯定没有桥。

  • 点双连通分量:极大点双连通子图。

  • 边双连通分量:极大边双连通子图。

那么,我们又应该怎么实现呢?

边双连通分量:在找出原图上的桥后,将其在原图中去掉,剩下的连通块即为边双连通分量。

点双联通分量:我们将每个边双看作一个新节点,原图上的桥看作连接边双之间的边,这样新图会构成一棵树。

边双实现:

int cnt; 
bool vis[N];
vector <int> dcc[N];
void dfs(int u)
{
	vis[u] = 1;
	dcc[cnt].push_back(u);
	for(int i=head[u]; i; i=e[i].nxt)
	{
		if(vis[e[i].v] or bridge[i]) continue;
		dfs(e[i].v);
	}
}

点双实现:我太菜了,就是圆方树,不想写!!!——QWQ

例题讲解

缩点

额,一道模版题,不说了,自己看上面。

行星与王国

这道题目其实就是求强联通分量,上面没听懂的来看一下。好我们再次掏出一张图。

graph (10).png

好,再来一张,不难得出下面那张图就是 dfs 生成树。然后圆圈圈出来的就是强联通分量。
graph (9).png

再定睛一看,那一条红边就是回祖边,那么从一个点出发,一直向下遍历,然后忽得找到一个点,那个点竟然有条指回这一个点的边。说明什么,这成了一个环啦!那么,这个环上所有的点都是强联通的

但是这只是强联通啊,我们需要求的可是强连通分量啊!QWQ

那怎么办呢?我们只需要再想一下,什么时候一个点和他的所有子孙节点中的一部分构成强连通分量?他的子孙再也没有指向他的祖先的边,却有指向他自己的边因为只要他的子孙节点有指向祖先的边,显然可以构成一个更大的强联通图。

2.png

再举一个例子,红色是强联通分量,但是蓝色只是强联通图。那么我们只需要知道这个点 u u u 下面的所有子节点有没有连着这个点的祖先就行了。但是呢?我们怎么知道这个点u它下面的所有子节点一定是都与他强联通的呢?但是这好像有是不对的, ~~自己打草稿去。~~那怎么办呢?开个栈记录就行了。你不早说

如果在这个点之后被遍历到的点已经能与其下面的一部分点(也可能就只有他一个点)已经构成强连通分量,即它已经是最大的。那么把它们一起从栈里弹出来就行了。好了,那么代码怎么实现呢?我们只需要开这么几个数组

  • d f s i dfs_i dfsi ,表示这个点在dfs时的时间戳。
  • l o w i low_i lowi ,表示这个点以及其子孙节点连的所有点中 d f n i dfn_i dfni​ 最小的值
  • s t a i sta_i stai ,表示当前所有可能能构成是强连通分量的点。
  • v i s i vis_i visi ,表示一个点是不是在栈里面。

然后再说一下 tarjan 的步骤:

  1. 首先初始化 d f n u = l o w u dfn_u=low_u dfnu=lowu​ 第几个被 dfs 到
  2. u u u 存入栈中,并将 v i s u vis_u visu 设为 t r u e true true
  3. 遍历 u u u 的每一个能到的点,如果这个点 d f n dfn dfn 0 0 0 ,即仍未访问过,那么就对点 v v v 进行 dfs ,然后 l o w u = min ⁡ ( l o w u , l o w ) low_u = \min (low_u,low_) lowu=min(lowu,low)

于是我们得到了一个强联通分量。还有一个特别的点:tarjan 一遍是搜不完所有的点的,因为存在一些孤立点,所以我们要对一趟跑下来还没有被访问到的点继续跑 tarjan 。而怎么判断呢?只需要看看 d f s dfs dfs 是否为 0 0 0 就好了。所以说 tarjan 的时间复杂度就是 O ( n ) O(n) O(n) 。至于为什么我就不多说了,好了,上代码。

#include<bits/stdc++.h>
using namespace std;
#define inf 0x3f3f3f3f
vector<int> g[100005];
int color[100005],dfn[200020],low[200020],sta[200020],vis[100005],cnt[100005];
int deep,top,n,m,sum,ans;
void tarjan(int u)
{
    dfn[u]=++deep;
    low[u]=deep;
    vis[u]=1;
    sta[++top]=u;
    int sz=g[u].size();
    for(int i=0;i<sz;i++)
    {
        int v=g[u][i];
        if(!dfn[v])
        {
            tarjan(v);
            low[u]=min(low[u],low[v]);
        }
        else
        {
            if(vis[v])
            {
                low[u]=min(low[u],low[v]);
            }
        }
    }
    if(dfn[u]==low[u])
    {
        color[u]=++sum;
        vis[u]=0;
        while(sta[top]!=u)
        {
            color[sta[top]]=sum;
            vis[sta[top--]]=0;
        }
        top--;
    }
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++)
    {
        int from,to;
        scanf("%d%d",&from,&to);
        g[from].push_back(to);
    }
    for(int i=1;i<=n;i++)
    {
        if(!dfn[i])
        {
            tarjan(i);
        }
    }
    for(int i=1;i<=n;i++)
    {
        cnt[color[i]]++;
    }
    for(int i=1;i<=sum;i++)
    {
        if(cnt[i]>=1)
        {
            ans++;
        }
    }
    printf("%d\n",ans);
    for(int i=1;i<=n;i++)
    {
		printf("%d ",color[i]);
	}
}
最大半连通子图

一个有向图 G = ( V , E ) G=\left(V,E\right) G=(V,E) 称为半连通的 (Semi-Connected),如果满足: ∀ u , v ∈ V \forall u,v\in V u,vV,满足 u → v u\to v uv v → u v\to u vu,即对于图中任意两点 u , v u,v u,v,存在一条 u u u v v v 的有向路径或者从 v v v u u u 的有向路径。

G ′ = ( V ′ , E ′ ) G'=\left(V',E'\right) G=(V,E) 满足 V ′ ⊆ V V'\subseteq V VV E ′ E' E E E E 中所有跟 V ′ V' V 有关的边,则称 G ′ G' G G G G 的一个导出子图。若 G ′ G' G G G G 的导出子图,且 G ′ G' G 半连通,则称 G ′ G' G G G G 的半连通子图。若 G ′ G' G G G G 所有半连通子图中包含节点数最多的,则称 G ′ G' G G G G 的最大半连通子图。

给定一个有向图 G G G,请求出 G G G 的最大半连通子图拥有的节点数 K K K,以及不同的最大半连通子图的数目 C C C。由于 C C C 可能比较大,仅要求输出 C C C X X X​ 的余数。


那么这一题我们就是一道普通的缩点,然后缩完点以后,此时图就变成 DAG ,然后进行一遍 dfs 扫描,记得使用记忆化存储,然后边扫边记录得到求最大值数组和求个数数组,最后统计答案即可。但是必须注意时刻取模不要问我怎么知道的,QWQ。然后因为要求种类个数,所以不可以重边

然后代码如下:

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+2,M=2e6+2;
int n,m,tot,MOD,top,num,scc,TOT,Head[N],ver[M],Next[M],dfn[N],low[N],Stack[N],belong[N];
int size[N],HEAD[N],VER[M],NEXT[M],f[N],g[N],used[N];
bool instack[N];
void add(int x,int y) {
	ver[++tot]=y;
	Next[tot]=Head[x];
	Head[x]=tot;
}
void ADD(int x,int y) {
	VER[++TOT]=y;
	NEXT[TOT]=HEAD[x];
	HEAD[x]=TOT;
}
void Tarjan(int x) {
	dfn[x]=low[x]=++num;
	Stack[++top]=x;
	instack[x]=true;
	for(int i=Head[x];i;i=Next[i]) {
		int y=ver[i];
		if(!dfn[y]) {
			Tarjan(y);
			low[x]=min(low[x],low[y]);
		}else if(instack[y]) {
			low[x]=min(low[x],low[y]);
		}
	}
	if(dfn[x]==low[x]) {
		scc++;
		int k=-1;
		while(k!=x) {
			k=Stack[top--];
			belong[k]=scc;
			size[scc]++;
			instack[k]=false;
		}
	}
}
int main() {
	cin>>n>>m>>MOD;
	for(int i=1,x,y;i<=m;i++) 
	{
		cin>>x>>y;
		add(x,y);
	}
	for(int i=1;i<=n;i++) {
		if(!dfn[i]) {
			Tarjan(i);
		}
	}
	for(int x=1;x<=n;x++) {
		f[x]=size[x];
		g[x]=1;
		for(int i=Head[x];i;i=Next[i]) {
			int y=ver[i];
			if(belong[x]==belong[y]) 
				continue;
			ADD(belong[x],belong[y]);
		}
	}
	for(int x=scc;x>=1;x--) {
		for(int i=HEAD[x];i;i=NEXT[i]) {
			int y=VER[i];
			if(used[y]==x) continue;
			used[y]=x;
			if(f[y]<f[x]+size[y]) {
				f[y]=f[x]+size[y];
				g[y]=g[x];
			}
			else if(f[y]==f[x]+size[y]) {
				g[y]+=g[x];
				g[y]%=MOD;
			}
		}
	}
	int ans=0,tmp=0;
	for(int i=1;i<=scc;i++) {
		if(f[i]>ans) {
			ans=f[i];
			tmp=g[i];
		}
		else if(f[i]==ans) {
			tmp+=g[i];
			tmp%=MOD;
		}
	}
	printf("%d\n%d",ans,tmp);
}
再来点boss

给定一个 n n n 个点 m m m 条边的无向图,保证图连通。找到两个点 s , t s,t s,t ,使得 s s s t t t​ 必须经过的边最多(一条边无论走哪条路线这一条边都经过,这条边就是必须经过的边)。


首先我们要知道知道同一个边双连通分量中,任意两点之间存在至少两条无重边的简单路径。

我们可以发现同一个边双内的点之间没有必须经过的边。所以在这一道题目中,我们只需要把给我们的图给缩点缩成一棵树,然后跑两边 dfs 求树的直径即可,也就是 2 2 2 个板子套一下就可以了。

#include<bits/stdc++.h>
using namespace std;
const int N=1200100;
int n,m,idx=1,cnt,in1,in2,num,p,q;
int to[N],nxt[N],head[N],rhead[N];
int dfn[N],low[N],bridge[N],rid[N];
int dis[N];
void add(int h[],int u,int v)
{
    idx++;
    to[idx]=v;
    nxt[idx]=h[u];
    h[u]=idx;
}
void tarjan(int s,int last)//缩点板子,不必多说
{
    dfn[s]=low[s]=++cnt;
    for(int i=head[s];i;i=nxt[i])
	{
        int v=to[i];
        if(i==(last^1)) 
			continue;
        if(!dfn[v])
		{
            tarjan(v,i);
            low[s]=min(low[s],low[v]);
            if(dfn[s]<=low[v])//直接就是一座桥
			{
				bridge[i]=bridge[i^1]=1;
			}
        }
        else 
			low[s]=min(low[s],dfn[v]);
    }
}
void dfs_0(int s)//跑2遍dfs,求树的直径
{
    rid[s]=num;
    for(int i=head[s];i;i=nxt[i])
    {
        int v=to[i];
        if(bridge[i]||rid[v]) 
            continue;
        dfs_0(v);
    }
}
void dfs_1(int s,int fa,int &u)
{
    for(int i=rhead[s];i;i=nxt[i])
    {
        int v=to[i];
        if(v==fa) 
            continue;
        dis[v]=dis[s]+1;
        if(dis[0]<dis[v])
        {
            dis[0]=dis[v];
            u=v;
        }
        dfs_1(v,s,u);
    }
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++)
    {
        scanf("%d%d",&in1,&in2);
        add(head,in1,in2);
        add(head,in2,in1);
    }
    tarjan(1,-1);
    for(int i=1;i<=n;i++)
    {
        if(!rid[i])
        {
            num++;
            dfs_0(i);
        }
    }
    for(int s=1;s<=n;s++)
    {
        for(int i=head[s];i;i=nxt[i])
        {
            if(rid[s]!=rid[to[i]])
            {
                add(rhead,rid[s],rid[to[i]]);
            }
        }
    }
    dis[1]=0;
    dfs_1(1,0,p);
    dis[0]=dis[p]=0;
    dfs_1(p,0,q);
    cout<<dis[0]<<endl;
    return 0;
}
旅行者

Cyberland 有 n n n 座城市,编号从 1 1 1 n n n ,有 m m m 条双向道路连接这些城市。第 j j j 条路连接城市 a j a_j aj b j b_j bj 。每天,都有成千上万的游客来到 Cyberland 游玩。

在每一个城市,都有纪念品售卖,第 i i i 个城市售价为 w i w_i wi 。这个售价有时会变动。

每一个游客的游览路径都有固定起始城市和终止城市,且不会经过重复的城市。

他们会在路径上的城市中,售价最低的那个城市购买纪念品。

你能求出每一个游客在所有合法的路径中能购买的最低售价是多少吗?


那么这一道题目就是经典的圆方树好题了,什么是圆方树呢?还记得我们在双联通分量那一块空出来的点双吗?没错,圆方树跟点双有千丝万缕的关系好的,我们来正式讲解圆方树。然后提醒一下,这道题目需要用到:tarjan,树剖,线段树。这些如果不会的大部分可以移步我之前的总结。但是,有同学就会问了,点双和圆方树有什么关系呢?好,因为圆方树圆方树,圆方圆方,所以他的点自然就有圆形和方形了,那么对应什么呢?就是每一个点对应圆形,每一个点双联通分量就对应方形。

3.png

4.png

那么也就是说,圆方树的点数肯定小于 2 n 2n 2n 。因为点双跟割点有关,而割点的数量总是小于 n n n 。所以就有一个坑点:所有数组的大小必须开2倍。这时肯定有聪明的同学发现了,这个圆方树,看起来是树,事实上其实只有原图联通,圆方树才是一棵树。而且如果原图有 k k k 个连通分量,则它的圆方树也会形成 k k k 棵树形成的森林。好的,那么定义说完了,接下来我们就说一下怎么构造。如果说它不是联通图,那么就把它分成几个连通图。所以我们就只讨论连通图的问题,但是因为圆方树是基于点双连通分量的,而点双连通分量又基于割点,所以只需要用类似求割点的方法即可,而割点又用的是 tarjan 。所以根据理论来说,只要你会割点,那么你就会圆方树。好,首先,我们首先 dfs 一遍图,构造出 dfs 生成树,跟 tarjan 迷之相似,定义的数组 d f n , l o w dfn,low dfn,low 跟 tarjan 中的一模一样,不做过多赘述,接下来来考虑点双和 DFS 树以及这两个数组之间的关联。不难发现,每个点双在 DFS 树上是一棵连通子树,并至少包含两个点。但是有一个反例,最顶端节点仅往下接一个点。同时还可以发现每条树边恰好在一个点双内。不得不评论一句,谁发明了这个算法,这么牛逼!!!%%%我们先考虑一个点双在 DFS 树中的根 u u u u u u 这个地方确定点双联通分量,为什么呢?因为 u u u 包含了所有他的子树的信息,所以 dfs 中可以确定哪里存在点双,但是不能确定点双的点集合,但是我们要用啊?所以我们只需要略微地想亿想,就知道可以用 stl 中的栈,存储还未确定所属点双(可能有多个)的节点。那该怎么处理呢?在找到点双 u u u 以外的其他的点都集中在栈顶端,只需要不断弹栈直到弹出 为止即可。好的,那么我们就完成了圆方树的构建了。好,那么这一道题目基本上差不多就完成了,就是建一个圆方树再用树链剖分和线段树维护一下就好了。OK,很不容易啊。我们上代码:

#include<bits/stdc++.h>
#define INF 2147483647
using namespace std;
vector<int>G1[200010],G2[200010]; 
multiset<int>S[200010];
int minn[800010];
int dep[200010],f[200010],siz[200010],id[200010],cnt,h[200010],top[200010],loc[200010];
int pos,dfn[200010],low[200010],topp,sta[200010];
int n,m,q,dis[200010],ext;
void swap(int &x,int &y){
	int t=x;x=y;y=t;
}
int min(int a,int b){
	return a<b?a:b;
}
void build_tree(int o,int l,int r){
	if(l==r){
		minn[o]=dis[loc[l]];
		return ;
	}
	int mid=l+r>>1;
	build_tree(o<<1,l,mid);
	build_tree(o<<1|1,mid+1,r);
	minn[o]=min(minn[o<<1],minn[o<<1|1]);
}
void update(int o,int l,int r,int x,int k){
	if(l==r){
		minn[o]=k;
		return ;
	}
	int mid=l+r>>1;
	if(x<=mid)update(o<<1,l,mid,x,k);
	else update(o<<1|1,mid+1,r,x,k);
	minn[o]=min(minn[o<<1],minn[o<<1|1]);
}
int query(int o,int l,int r,int x,int y){
	if(x<=l&&r<=y)
		return minn[o];
	int mid=l+r>>1,ret=INF;
	if(x<=mid)ret=min(ret,query(o<<1,l,mid,x,y));
	if(mid<y)ret=min(ret,query(o<<1|1,mid+1,r,x,y));
	return ret;
}
void dfs1(int x,int fa){
	f[x]=fa;
	dep[x]=dep[fa]+1;
	siz[x]=1;
	int tmp=-1;
	for(int i=0;i<G2[x].size();i++){
		int v=G2[x][i];
		if(v==fa)
			continue;
		dfs1(v,x);
		siz[x]+=siz[v];
		if(siz[v]>tmp){
			tmp=siz[v];
			h[x]=v;
		}
	}
}
void dfs2(int x,int fa){
	top[x]=fa;
	id[x]=++cnt;
	loc[cnt]=x;
	if(!h[x])
		return ;
	dfs2(h[x],fa);
	for(int i=0;i<G2[x].size();i++){
		int v=G2[x][i];
		if(v==f[x]||v==h[x])
			continue;
		dfs2(v,v);
	}
}
void tarjan(int x){
	dfn[x]=low[x]=++pos;
	sta[++topp]=x;
	for(int i=0;i<G1[x].size();i++){
		int v=G1[x][i];
		if(!dfn[v]){
			tarjan(v);
			low[x]=min(low[x],low[v]);
			if(low[v]==dfn[x]){
				ext++;
				for(int j=0;j!=v;topp--){
					j=sta[topp];
					G2[ext].push_back(j);
					G2[j].push_back(ext);
				}
				G2[ext].push_back(x);
				G2[x].push_back(ext);
			}
		}
		else low[x]=min(low[x],dfn[v]);
	}
}
int querypath(int x,int y){
	int ret=INF;
	while(top[x]!=top[y]){
		if(dep[top[x]]<dep[top[y]])
			swap(x,y);
		ret=min(ret,query(1,1,ext,id[top[x]],id[x]));
		x=f[top[x]];
	}
	if(dep[x]>dep[y])
		swap(x,y);
	ret=min(ret,query(1,1,ext,id[x],id[y]));
	if(x>n)
		ret=min(ret,dis[f[x]]);
	return ret;
}
int main(){
	scanf("%d%d%d",&n,&m,&q);
	ext=n;
	for(int i=1;i<=n;i++)
		scanf("%d",&dis[i]);
	for(int i=1;i<=m;i++){
		int u,v;
		scanf("%d%d",&u,&v);
		G1[u].push_back(v);
		G1[v].push_back(u);
	}
	tarjan(1);
	dfs1(1,0);
	dfs2(1,1);
	for(int i=1;i<=n;i++)
		if(f[i])
			S[f[i]].insert(dis[i]);
	for(int i=n+1;i<=ext;i++)
		dis[i]=*S[i].begin();
	build_tree(1,1,ext);
	while(q--){
		char s[5];
		scanf("%s",s);
		if(s[0]=='C'){
			int x,y;
			scanf("%d%d",&x,&y);
			update(1,1,ext,id[x],y);
			if(f[x]){
				S[f[x]].erase(S[f[x]].lower_bound(dis[x]));
				S[f[x]].insert(y);
				if(dis[f[x]]!=*S[f[x]].begin()){
					dis[f[x]]=*S[f[x]].begin();
					update(1,1,ext,id[f[x]],dis[f[x]]);
				}
					
			}
			dis[x]=y;
		}
		else if(s[0]=='A'){
			int x,y;
			scanf("%d%d",&x,&y);
			printf("%d\n",querypath(x,y));
		}
	}
	return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2278884.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

socket网络通信基础

目录 一、套接字编程基本流程 二、TCP流式协议及Socket编程的recv()和send() 三、读写无阻塞-完美掌握I/O复用 select&#xff08;&#xff09;函数详解 poll&#xff08;&#xff09;函数详解 epoll () 函数详解 一、套接字编程基本流程 原文链接&#xff1a;Socket编程…

接口防篡改+防重放攻击

接口防止重放攻击&#xff1a;重放攻击是指攻击者截获了一次有效请求(如交易请求),并在之后的时间里多次发送相同的请求&#xff0c;从而达到欺骗系统的目的。为了防止重放攻击&#xff0c;通常需要在系统中引入一种机制&#xff0c;使得每个请求都有一个唯一的标识符(如时间戳…

庄小焱——2024年博文总结与展望

摘要 大家好&#xff0c;我是庄小焱。岁末回首&#xff0c;2024 年是我在个人成长、博客创作以及生活平衡方面收获颇丰的一年。这一年的经历如同璀璨星辰&#xff0c;照亮了我前行的道路&#xff0c;也为未来的发展奠定了坚实基础。 1. 个人成长与突破 在 2024 年&#xff0c…

在线base64转码工具

在线base64转码工具&#xff0c;无需登录&#xff0c;无需费用&#xff0c;用完就走。 官网地址&#xff1a; https://base64.openai2025.com 效果&#xff1a;

鸿蒙学习构建视图的基本语法(二)

一、层叠布局 // 图片 本地图片和在线图片 Image(https://developer.huawei.com/allianceCmsResource/resource/HUAWEI_Developer_VUE/images/080662.png) Entry Component//自适应伸缩 设置layoutWeight属性的子元素与兄弟元素 会按照权重进行分配主轴的空间// Position s…

OA-CNN:用于 3D 语义分割的全自适应稀疏 CNN

大家读完觉得有帮助记得及时关注和点赞&#xff01;&#xff01;&#xff01; 1介绍 2相关工作 基于点的学习。 基于 CNN 的学习。 动态卷积。 3全能自适应 3D 稀疏 CNN 3.1空间适应性感受野 赋予动机。 体素网格。 金字塔网格分区。 Adaptive 聚合器。 3.2自适应关…

利用 LNMP 实现 WordPress 站点搭建

部署MySQL数据库 在主机192.168.138.139主机部署数据库服务 包安装数据库 apt-get install mysql-server 创建wordpress数据库和用户并授权 mysql> create database wordpress;#MySQL8.0要求指定插件 mysql> create user wordpress192.168.138.% identified with mys…

Vue2.0的安装

1.首先查看是否已经安装了node.js 选择以管理员方式打开命令提示符&#xff08;权限较高&#xff09;&#xff0c;或者通过cmd的方式打开 打开后输入node -v 查看自己电脑是否安装node&#xff0c;以及版本号 node -v 如果没有的话&#xff0c;请查看Node.js的安装 2.Vue和脚…

OpenEuler学习笔记(一):常见命令

OpenEuler是一个开源操作系统&#xff0c;有许多命令可以用于系统管理、软件安装、文件操作等诸多方面。以下是一些常见的命令&#xff1a; 一、系统信息查看命令 uname 用途&#xff1a;用于打印当前系统相关信息&#xff0c;如内核名称、主机名、内核版本等。示例&#xff…

无纸化同屏解决方案探究和技术展望

好多开发者&#xff0c;在了解到我们在无纸化同屏、智慧教育场景的碾压式行业积累后&#xff0c;希望我们做些无纸化同屏相关的技术探讨&#xff0c;实际上这块方案并不复杂&#xff0c;很容易做到实际使用场景契合的方案&#xff0c;主要是如何达到客户期望的功能和体验。 无…

nss刷题3

[SWPUCTF 2022 新生赛]webdog1__start level1&#xff1a; 打开环境后什么也&#xff0c;没有&#xff0c;查看源码&#xff0c;看到第一关是MD5值&#xff0c;要get传参web&#xff0c;然后web的值的MD5和它原来值相等&#xff0c;0e开头的字符在php中都是0&#xff0c;传入…

深入了解计算机网络中的路由协议与性能优化

在计算机网络中&#xff0c;路由协议是决定数据如何从源节点到达目标节点的关键组成部分。不同的路由协议各有特点&#xff0c;如何根据实际需求选择合适的协议&#xff0c;并对网络性能进行优化&#xff0c;是每个网络管理员需要面临的重要课题。 本篇文章将深入探讨计算机网…

通过视觉语言模型蒸馏进行 3D 形状零件分割

大家读完觉得有帮助记得关注和点赞&#xff01;&#xff01;&#xff01;对应英文要求比较高&#xff0c;特此说明&#xff01; Abstract This paper proposes a cross-modal distillation framework, PartDistill, which transfers 2D knowledge from vision-language models …

Apple Vision Pro 距离视网膜显示还有多远

本文介绍了视网膜屏幕的概念和人眼视敏度极限,以及头戴显示设备在视场角和角分辨率之间的权衡设计。文章还提到了苹果公司的新产品Apple Vision Pro的设计规范和视觉效果。 Retina display 是苹果公司针对其高分辨率屏幕技术的一种营销术语。这个术语最早由乔布斯在 2010 年 6…

微服务学习-快速搭建

1. 速通版 1.1. git clone 拉取项目代码&#xff0c;导入 idea 中 git clone icoolkj-microservices-code: 致力于搭建微服务架构平台 1.2. git checkout v1.0.1版本 链接地址&#xff1a;icoolkj-microservices-code 标签 - Gitee.com 2. 项目服务结构 3. 实现重点步骤 …

美最高法维持TikTok禁令,不卖就禁或有转机,TikTok直播专线助力企业在挑战中前行

一、TikTok 面临的危机与转机 最近&#xff0c;TikTok 在美国的命运可谓是波谲云诡。当地时间 1 月 17 日&#xff0c;美国联邦最高法院裁定 TikTok “不卖就禁” 的法律不违宪&#xff0c;这就意味着该法案将于 1 月 19 日生效 &#xff0c;TikTok 似乎已被逼至悬崖边缘。然而…

编写Wireshark的Lua脚本详解及示例解析

编写Wireshark的Lua脚本详解及示例解析 编写Wireshark Lua脚本的基本步骤SMGP.lua脚本解析脚本解析要点总结Wireshark是一个强大的网络协议分析工具,支持通过Lua脚本扩展其功能,以解析自定义或复杂的协议。下面将详细介绍如何编写Wireshark的Lua脚本,并通过解析一个具体的SM…

【20】Word:小许-质量管理-论文❗

目录 题目​ NO1.2.3.4.5 NO6.7 NO8 NO9 NO10.11 题目 NO1.2.3.4.5 另存为“Word.docx”文件在考生文件夹下&#xff0c;F12Fn是另存为的作用布局→页面设置对话框→纸张&#xff1a;大小A4→页边距&#xff1a;上下左右不连续ctrl选择除表格外的所有内容→开始→字体对…

【软件开发过程管理规范】需求管理,需求分析,设计开发管理,测试管理(Word)

一、需求管理规程 1 简介 2 过程总体描述 2.1 过程概述 2.2 过程流程图 3 过程元素描述 3.1 准备阶段 3.2 需求调研 3.3 需求分析 软件开发人员及用户往往容易忽略信息沟通&#xff0c;这导致软件开发出来后不能很好地满足用户的需要&#xff0c;从而造成返工。而返工不仅在技术…

RabbitMQ-消息可靠性以及延迟消息

目录 消息丢失 一、发送者的可靠性 1.1 生产者重试机制 1.2 生产者确认机制 1.3 实现生产者确认 &#xff08;1&#xff09;开启生产者确认 &#xff08;2&#xff09;定义ReturnCallback &#xff08;3&#xff09;定义ConfirmCallback 二、MQ的持久化 2.1 数据持久…