cactus仙人掌图

题目描述

如果某个无向连通图的任意一条边至多只出现在一条简单回路(simple cycle)里,我们就称这张图为仙人掌图(cactus)。所谓简单回路就是指在图上不重复经过任何一个顶点的回路。 SHOI2008 cactus仙人掌图 和 UOJ87 mx的仙人掌_结点 举例来说,上面的第一个例子是一张仙人图,而第二个不是——注意到它有三条简单回路:(4,3,2,1,6,5,4)、(7,8,9,10,2,3,7)以及(4,3,7,8,9,10,2,1,6,5,4),而(2,3)同时出现在前两个的简单回路里。另外,第三张图也不是仙人图,因为它并不是连通图。显然,仙人图上的每条边,或者是这张仙人图的桥(bridge),或者在且仅在一个简单回路里,两者必居其一。定义在图上两点之间的距离为这两点之间最短路径的距离。定义一个图的直径为这张图相距最远的两个点的距离。现在我们假定仙人图的每条边的权值都是1,你的任务是求出给定的仙人图的直径

输入格式

输入的第一行包括两个整数n和m(1≤n≤50000以及0≤m≤10000)。其中n代表顶点个数,我们约定图中的顶点将从1到n编号。接下来一共有m行。代表m条路径。每行的开始有一个整数k(2≤k≤1000),代表在这条路径上的顶点个数。接下来是k个1到n之间的整数,分别对应了一个顶点,相邻的顶点表示存在一条连接这两个顶点的边。一条路径上可能通过一个顶点好几次,比如对于第一个样例,第一条路径从3经过8,又从8返回到了3,但是我们保证所有的边都会出现在某条路径上,而且不会重复出现在两条路径上,或者在一条路径上出现两次。

输出格式

只需输出一个数,这个数表示仙人图的直径长度。

输入输出样例

输入 #1 复制
15 3
9 1 2 3 4 5 6 7 8 3
7 2 9 10 11 12 13 10
5 2 14 9 15 10
输出 #1 复制
8
输入 #2 复制
10 1
10 1 2 3 4 5 6 7 8 9 10
输出 #2 复制
9

说明/提示

SHOI2008 cactus仙人掌图 和 UOJ87 mx的仙人掌_虚树_02 对第一个样例的说明:如图,6号点和12号点的最短路径长度为8,所以这张图的直径为8。

【注意】使用Pascal语言的选手请注意:你的程序在处理大数据的时候可能会出现栈溢出。 如果需要调整栈空间的大小,可以在程序的开头填加一句:{$M 5000000},其中5000000即指代栈空间的大小,请根据自己的程序选择适当的数值。

题解

参见王逸松论文《仙人掌相关算法及其应用》。

1.2 仙人掌的边数

对于一棵n个结点的仙人掌,我们考虑它的一棵生成树,有n − 1条边。剩下的边均为非树边,每条非树边对应了生成树上的一条路径,并形成了一个简单环。由于每一条边最多属于一个简单环,任意两条非树边对应的路径的边的交集为空,即这些路径不重叠。因此最多有n − 1条这样的路径,所以n个结点的仙人掌最多有2n − 2条边,最少有n − 1条边。

1.3.1 父亲

类比树上结点的父亲,我们定义仙人掌上结点的父亲和环的父亲。

如果一个结点u到仙人掌的根之间的所有简单路径的第一条边相同,那么就类似树上的情况,u的父亲为u到根的一条简单路径上第二个点,否则u的父亲为u到根的一条简单路径上第一条边所在的环。

1.3.2 儿子

类比树上结点的儿子,我们定义仙人掌上结点的儿子和环的儿子。

一个结点的儿子可能是个结点,也可能是个环。

一个环的儿子为环上除去环的父亲以外的所有结点。

1.3.3 父亲结点和母亲结点

对于一个在环上的结点,我们定义它的父亲结点和母亲结点分别为环上与它相邻的两个结点。(请注意区分父亲和父亲结点)

从环的任意一个儿子出发,沿着父亲结点和母亲结点就能遍历整个环。

1.4 如何遍历一棵仙人掌

类似遍历一棵树的过程,我们从根开始dfs,假设当前在结点x,接下来要访问结点y。

如果y还没访问过,就跟树上的情况一样处理,只要将y的父亲设置为x即可。

如果y已经访问过,且第一次访问y的时间早于第一次访问x的时间,那么说明我们发现了一个环,这个环的父亲为y,儿子为x到y路径上除去y的所有点。我们遍历这个环,并设置环上所有点的父亲结点和母亲结点就行了。

如果y已经访问过,且第一次访问y的时间晚于第一次访问x的时间,那么说明y在一个已经访问过的环上,可以无视这种情况。

仙人掌上的DP

类比树上的DP,我们可以在仙人掌上进行DP。

从仙人掌的根开始DP,假设当前DP到结点u,我们先处理结点u上的信息,然后枚举u的每一个儿子和以u为父亲的每一个环的每个儿子进行DP。

考虑树形DP。我们可以对每个x求出以x为根的子树的最大深度,然后用每个结点的不同儿子的最大深度+次大深度更新答案即可。

对于仙人掌上的情况,可以类比树形DP。我们先dfs一遍弄清仙人掌的结构,然后对于每个结点x,求出子仙人掌x的最大深度。(子仙人掌x的定义是,删掉根到x的所有简单路径上的边之后,x所在的连通块)

对于每个结点,用它的不同儿子的最大深度+次大深度来更新答案。

对于每个环,还要用环上的每对结点的最大深度的和加上这对结点的最短路长度来更新答案。不妨枚举其中一个结点,那么最短路是顺时针走的另一个结点,一定是环上的一个区间,而且这个区间的另一个端点随着枚举的结点顺时针移动而顺时针移动。这是个两个端点都单调移动的RMQ问题,可以用单调队列来维护。


实际上,仙人掌问题可以采用更为现代的科技:圆方树。

圆点是原来的节点,方点环所对应的点,他们的拓扑关系其实就是父亲儿子的定义。

考虑转化与化归的思想,多叉树→二叉树→多叉树,先抽象再还原。这里的仙人掌→圆方树→仙人掌也是一样的。与左儿子-右兄弟表示法一样,圆方树对应着更新的拓扑关系。

把圆方树建出来做就行了。

时间复杂度\(O(n)\).

co int N=100000+10;
int n;
// originial tree
vector<int> e[N];
int pos[N],dfn;
// round square tree
int cir,len[N];
int fa[N],dep[N],to[N],nx[N];
int f[N],ans;

void tarjan(int x,int fa){
    static int st[N],top;
    st[++top]=x;
    pos[x]=++dfn;
    for(unsigned i=0;i<e[x].size();++i){
        int y=e[x][i];
        if(y==fa) continue;
        if(!pos[y]){
            ::fa[y]=x,dep[y]=dep[x]+1;
            tarjan(y,x);
        }
        else if(pos[y]<pos[x]){
            len[++cir]=dep[x]-dep[y]+1;
            ::fa[n+cir]=y,nx[n+cir]=to[y],to[y]=n+cir;
            for(int j=top;st[j]!=y;--j)
                ::fa[st[j]]=n+cir,nx[st[j]]=to[n+cir],to[n+cir]=st[j];
        }
    }
    for(unsigned i=0;i<e[x].size();++i){
        int y=e[x][i];
        if(::fa[y]!=x) continue;
        nx[y]=to[x],to[x]=y;
    }
    --top;
}
void dp(int x){
    if(x<=n){ // round
        for(int y=to[x];y;y=nx[y]){
            dp(y);
            ans=max(ans,f[x]+f[y]+1);
            f[x]=max(f[x],f[y]+1);
        }
        return;
    }
    // square
    for(int y=to[x];y;y=nx[y]) dp(y);
    static int tmp[N],tot;
    tot=0;
    for(int y=to[x];y;y=nx[y])
        tmp[++tot]=y; // notice the order
    assert(tot+1==len[x-n]);
    copy(tmp+1,tmp+tot+1,tmp+tot+1);
    static int qu[N],l,r;
    l=1,r=0;
    for(int i=1;i<=tot<<1;++i){
        while(l<=r&&i-qu[l]>(tot+1)>>1) ++l; // tot+1 -> ::fa[x]
        if(l<=r) ans=max(ans,f[tmp[i]]+f[tmp[qu[l]]]+i-qu[l]);
        while(l<=r&&f[tmp[i]]-i>f[tmp[qu[r]]]-qu[r]) --r;
        qu[++r]=i;
    }
    for(int y=to[x];y;y=nx[y])
        f[x]=max(f[x],f[y]+min(dep[y]-dep[::fa[x]],len[x-n]-(dep[y]-dep[::fa[x]]))-1); // correspondence with f
}
int main(){
    read(n);
    for(int m=read<int>();m--;){
        int k=read<int>()-1;
        for(int x=read<int>(),y;k--;x=y){
            read(y);
            e[x].push_back(y),e[y].push_back(x);
        }
    }
    tarjan(1,0);
    dp(1);
    printf("%d\n",ans);
    return 0;
}
#87. mx的仙人掌

如果一个无向连通图的任意一条边最多属于一个简单环,我们就称之为仙人掌。所谓简单环即不经过重复的结点的环。

SHOI2008 cactus仙人掌图 和 UOJ87 mx的仙人掌_动态规划_03

现给定一棵仙人掌,每条边有一个正整数权值,每次给 $k$ 个点(可以存在相同点),问从它们中选出两个点(可以相同),它们之间最短路的最大值是多少。

输入格式

第一行两个非负整数 $n, m$,表示仙人掌的点数和边数。

接下来 $m$ 行,每行三个正整数 $v, u, w$ $(1 \le v, u \le n)$,表示 $v$ 与 $u$ 之间有一条边权为 $w$ 的无向边。点从 $1$ 开始编号。

保证输入的图是一棵仙人掌,保证没有自环,但可能有重边。

接下来一行一个非负整数 $Q$,表示询问个数。

接下来 $Q$ 行每行第一个数是正整数 $\mathrm{cnt}$ 表示点数,接下来 $\mathrm{cnt}$ 个数表示给定的点。

输出格式

对每个询问输出一个数,表示该询问对应的最大值。

样例一

input

10 14
10 7 1
3 8 7
1 6 9
7 2 10
8 9 9
1 7 1
8 5 2
4 5 4
1 7 4
2 9 8
9 3 3
8 4 2
1 6 5
7 9 10
6
2 9 5
2 8 10
3 8 7 6
2 6 4
3 3 4 2
1 10

output

11
20
25
27
19
0

explanation

前五个询问的答案路径分别为(如果有重边则显然走较短的边):

$9 \rightarrow 8 \rightarrow 5$

$8 \rightarrow 9 \rightarrow 7 \rightarrow 10$

$8 \rightarrow 9 \rightarrow 7 \rightarrow 1 \rightarrow 6$

$4 \rightarrow 8 \rightarrow 9 \rightarrow 7 \rightarrow 1 \rightarrow 6$

$2 \rightarrow 9 \rightarrow 8 \rightarrow 4$

最后一个询问的答案显然是$0$。

限制与约定

边权不超过 $2 ^ {31} - 1$。

$\mathrm{tot}$ 表示询问的总点数。

对于 5% 的数据,$n, \mathrm{tot} \le 7$。

对于 10% 的数据,$n, \mathrm{tot} \le 5000$。

对于另外 10% 的数据,$Q \le 10$,其中存在 $Q \le 2$ 的数据。

对于另外 30% 的数据,$m = n - 1$。

对于 100% 的数据,$n, \mathrm{tot} \le 300000$。

此外为了照顾被卡常数的同学,本题存在过渡数据。

时间限制:$5\texttt{s}$

空间限制:$512\texttt{MB}$

题解

看题面,猜想有虚仙人掌这种算法,的确有。

用虚圆方树还原虚仙人掌即可。当然不用把虚仙人掌建出来,因为反正DP是在圆方树上进行的,所以直接在虚圆方树上DP即可。

把圆点连向方点的边权设为圆点到方点的父亲的最短距离。用树上倍增来维护,因为不仅要求lca和距离,还需要在方点DP时找到圆点在环上的祖先来做单调队列DP。

学到了:unique的自定义比较函数是判断相等的,而不是判断小于的。

时间复杂度\(O(n\log n)\)


吐槽:有重边,我没看到……询问点有重点,我没看到……于是写了一天。

仙人掌有重边是正常的,以后写题的时候要注意处理方法。这道题我选择去掉重边,但是不去掉相当于一个环也是可以处理的。

co int N=(int)6e5+10;

int n;
vector<pair<int,int> > e[N];
int pos[N],dfn;
int cir;LL len[N];
int fa[N];LL dfs_dep[N];
int nx[N],to[N];LL we[N];

void tarjan(int x,int fa){
	static int st[N],top;
	st[++top]=x;
	pos[x]=++dfn;
	for(unsigned i=0;i<e[x].size();++i){
		int y=e[x][i].first,w=e[x][i].second;
		if(y==fa) continue;
		if(!pos[y]){
			::fa[y]=x,dfs_dep[y]=dfs_dep[x]+w;
			tarjan(y,x);
		}
		else if(pos[y]<pos[x]){
			len[++cir]=dfs_dep[x]-dfs_dep[y]+w;
			::fa[cir]=y;
			nx[cir]=to[y],to[y]=cir;
			for(int j=top;st[j]!=y;--j){
				::fa[st[j]]=cir;
				nx[st[j]]=to[cir],to[cir]=st[j];
				we[st[j]]=min(dfs_dep[st[j]]-dfs_dep[y],len[cir]-(dfs_dep[st[j]]-dfs_dep[y]));
			}
		}
	}
	for(unsigned i=0;i<e[x].size();++i){
		int y=e[x][i].first;
		if(::fa[y]!=x) continue;
		nx[y]=to[x],to[x]=y,we[y]=e[x][i].second;
	}
	--top;
}

LL dis[N];int dep[N];
int anc[N][20],lg[N];

void pretreat(int x){
	pos[x]=++dfn;
	anc[x][0]=fa[x];
	for(int i=1;i<=lg[cir];++i) anc[x][i]=anc[anc[x][i-1]][i-1];
	for(int y=to[x];y;y=nx[y]){
		dis[y]=dis[x]+we[y],dep[y]=dep[x]+1;
		pretreat(y);
	}
}
int lca(int x,int y){
	if(dep[x]<dep[y]) swap(x,y);
	while(dep[x]>dep[y]) x=anc[x][lg[dep[x]-dep[y]]];
	if(x==y) return x;
	for(int i=lg[dep[x]];i>=0;--i)
		if(anc[x][i]!=anc[y][i]) x=anc[x][i],y=anc[y][i];
	return anc[x][0];
}
il LL get_dis(int x,int y){
	int lca=::lca(x,y);
	return dis[x]+dis[y]-2*dis[lca];
}
int reach(int y,int x){
	while(dep[y]>dep[x]+1) y=anc[y][lg[dep[y]-dep[x]-1]];
	return y;
}

namespace T{ // vitual tree
	int cnt,a[N];
	int nx[N],to[N];LL we[N];
	LL f[N],ans;
	
	struct node {LL f,dep;};
	il bool operator<(co node&a,co node&b){
		return a.dep<b.dep;
	}
	void dp(int x){
		if(x<=n){
			for(int y=to[x];y;y=nx[y]){
				dp(y);
				ans=max(ans,f[x]+f[y]+we[y]);
				f[x]=max(f[x],f[y]+we[y]);
			}
			return;
		}
		for(int y=to[x];y;y=nx[y]) dp(y);
		static node p[N<<1];
		int tot=0;
		for(int y=to[x];y;y=nx[y]){
			int z=reach(y,x);
			p[++tot].f=f[y]+get_dis(z,y),p[tot].dep=dfs_dep[z];
		}
		sort(p+1,p+tot+1);
		for(int i=tot+1;i<=tot<<1;++i)
			p[i]=p[i-tot],p[i].dep+=len[x];
		static int qu[N<<1];
		int l=1,r=0;
		for(int i=1;i<=tot<<1;++i){
			while(l<=r&&p[i].dep-p[qu[l]].dep>len[x]>>1) ++l;
			if(l<=r) ans=max(ans,p[qu[l]].f+p[i].f+p[i].dep-p[qu[l]].dep);
			while(l<=r&&p[i].f-p[i].dep>=p[qu[r]].f-p[qu[r]].dep) --r;
			qu[++r]=i;
		}
		for(int i=1;i<=tot;++i)
			f[x]=max(f[x],p[i].f+min(p[i].dep-dfs_dep[fa[x]],len[x]-(p[i].dep-dfs_dep[fa[x]])));
	}
	
	il bool cmp(int a,int b){
		return pos[a]<pos[b];
	}
	il bool equ(int a,int b){
		return pos[a]==pos[b];
	}
	void solve(){
		read(cnt);
		for(int i=1;i<=cnt;++i) read(a[i]);
		sort(a+1,a+cnt+1,cmp);
		cnt=unique(a+1,a+cnt+1,equ)-a-1; // edit 2: mutivertex
		static int st[N],top;
		st[top=1]=a[1];
		for(int i=2,end=cnt;i<=end;++i){
			int lca=::lca(a[i],st[top]);
			for(;top>1&&pos[st[top-1]]>=pos[lca];--top)
				nx[st[top]]=to[st[top-1]],to[st[top-1]]=st[top],we[st[top]]=get_dis(st[top-1],st[top]);
			if(lca!=st[top]){
				a[++cnt]=lca;
				nx[st[top]]=to[lca],to[lca]=st[top],we[st[top]]=get_dis(lca,st[top]);
				st[top]=lca;
			}
			st[++top]=a[i];
		}
		for(;top>1;--top)
			nx[st[top]]=to[st[top-1]],to[st[top-1]]=st[top],we[st[top]]=get_dis(st[top-1],st[top]);
		dp(st[top]);
		printf("%lld\n",ans);
		for(int i=1;i<=cnt;++i)
			nx[a[i]]=to[a[i]]=we[a[i]]=f[a[i]]=0;
		ans=0;
	}
}

struct edge{int x,y,w;};
il bool operator<(co edge&a,co edge&b){
	return a.x!=b.x?a.x<b.x:(a.y!=b.y?a.y<b.y:a.w<b.w);
}
int main(){
	cir=read(n);int m=read<int>();
	static edge oe[N]; // edit 1: multiedge
	for(int i=1;i<=m;++i){
		read(oe[i].x),read(oe[i].y),read(oe[i].w);
		if(oe[i].x>oe[i].y) swap(oe[i].x,oe[i].y);
	}
	sort(oe+1,oe+m+1);
	for(int i=1;i<=m;++i){
		if(oe[i].x==oe[i-1].x&&oe[i].y==oe[i-1].y) continue;
		e[oe[i].x].push_back(mp(oe[i].y,oe[i].w)),e[oe[i].y].push_back(mp(oe[i].x,oe[i].w));
	}
	tarjan(1,0);
	lg[0]=-1;
	for(int i=1;i<=cir;++i) lg[i]=lg[i>>1]+1;
	dfn=0,pretreat(1);
	for(int q=read<int>();q--;) T::solve();
	return 0;
}