NOIP后的第一次更新嗯。

Description

  如果某个无向连通图的任意一条边至多只出现在一条简单回路(simple cycle)里,我们就称这张图为仙人掌图(cactus)。所谓简单回路就是指在图上不重复经过任何一个顶点的回路。

  [BZOJ]1023 cactus仙人掌图(SHOI2008)-LMLPHP

  举例来说,上面的第一个例子是一张仙人图,而第二个不是——注意到它有三条简单回路:(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,你的任务是求出给定的仙人图的直径。

Input

  输入的第一行包括两个整数n和m。其中n代表顶点个数,我们约定图中的顶点将从1到n编号。接下来一共有m行。代表m条路径。每行的开始有一个整数k,代表在这条路径上的顶点个数。接下来是k个1到n之间的整数,分别对应了一个顶点,相邻的顶点表示存在一条连接这两个顶点的边。一条路径上可能通过一个顶点好几次,比如对于第一个样例,第一条路径从3经过8,又从8返回到了3。

Output

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

Sample Input

  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

Sample Output

  8

HINT

  1≤n≤50000,0≤m≤10000,2≤k≤1000。

  保证所有的边都会出现在某条路径上,而且不会重复出现在两条路径上,或者在一条路径上出现两次。

  如图,6号点和12号点的最短路径长度为8,所以这张图的直径为8。

   [BZOJ]1023 cactus仙人掌图(SHOI2008)-LMLPHP

Solution

  关于业界毒瘤仙人掌的初探。

  毕竟仙人掌的原型还是树,所以我们想办法把仙人掌转化成树来做。

  最直接的想法是缩环,变成树形DP,然后自己在环内瞎搞搞来更新答案。

  事实证明,这种方法是可行的。例如样例,我们可以通过缩点的方式把仙人掌转化成如下的树:

    [BZOJ]1023 cactus仙人掌图(SHOI2008)-LMLPHP

  这样树上的每一个点都是一个环,环与环之间根据原图的构造连上0或1的边,这是最简单最无脑的建图方式。

  现在我们考虑怎么在环内瞎搞:

    [BZOJ]1023 cactus仙人掌图(SHOI2008)-LMLPHP

  假设一个这样的环,其中环上一些节点有通向儿子的边,红色节点上有通向父亲的边。

  考虑最普通的树上DP求直径,我们很自然地对于每个环维护所有后辈中,距离红色节点最远的点的距离,以便向上更新答案。

  这样我们就知道了这个环上所有拥有通向儿子的边的节点离它的所有后辈的最远距离,这样直接加上在环上离红色节点的距离就可以更新DP值。

  当然,这个环上没有拥有通向儿子的边的节点也可以更新DP值,注意一下即可。

  但是按照树形DP求直径,以该点为lca的直径是最大的两个儿子的DP值之和。如果放在一个环上该怎么求呢?

  这个做法同样很经典,对于环,我们一般的做法是拆环成链,再把链复制一份接到后面来做,例如旋转卡壳的做法。

  所以对于这个问题,我们同样只要把被拆环之后的链扫一遍,用单调队列维护每个点之前的最大值来更新就好了。

  这也大概就是最笨的算法的整个流程。

  缩环建图虽然简单,但写起来确实复杂。但基于以上思想,我们依然可以对代码进行优化。

  在缩环建图的时候,我们实际上已经找出了仙人掌图的dfs树,然后对于每条返祖边,我们建立一个环。

  这条返祖边是名副其实的返祖边,一定会返回到该点的祖先。也就是说,图中的环一定对应的是dfs树上一条由上到下的链。

    [BZOJ]1023 cactus仙人掌图(SHOI2008)-LMLPHP

  而且根据仙人掌图的性质,这些链不会相交。

  再仔细想想,这些链中深度最小的点,不就是我们刚刚所说的环内的红色节点么?

  所以我们在对一个环进行DP的时候,只要把得到的信息丢到环中深度最小的点里就可以了。

  然后对于非环边x→y(也就是图中的桥),直接更新f[x]=max(f[x],f[y]+1)即可。

  还有就是处理顺序的问题。例如样例,我们在遍历到10号节点后,必须先往11号节点走而不是15号节点。

  因为在15号节点对于9号节点的更新有赖于13号节点对10号节点的更新。

  剩下就是代码实现的问题了。这样的代码会比之前介绍的的方法简洁很多。

  时间复杂度O(n+mk)。第一种方法写得太丑就不发了。

#include <cstdio>
#include <algorithm>
#include <cstring>
#define MN 100005
#define MM 5000005
using namespace std;
struct edge{int nex,to;}e[MM];
bool u[MN],ih[MN];
int hr[MN],dep[MN],lto[MN],dw[MN],bac[MN],f[MN],sk[MN],q[MN];
int pin,tp,hd,tl,ans,n,m,p,pre; inline int read()
{
int n=,f=; char c=getchar();
while (c<'' || c>'') {if(c=='-')f=-; c=getchar();}
while (c>='' && c<='') {n=n*+c-''; c=getchar();}
return n*f;
} inline void ins(int x,int y) {e[++pin]=(edge){hr[x],y}; hr[x]=pin;}
inline int dis(int x,int y) {return x<=y/?x:y-x;} void dp(int x,int depth)
{
register int i;
u[x]=true; dw[depth]=x;
for (i=hr[x];i;i=e[i].nex)
{
if (u[e[i].to]||e[i].to==lto[x]) continue;
dp(e[i].to,depth+);
if (!ih[e[i].to]) ans=max(ans,f[x]+f[e[i].to]+),f[x]=max(f[x],f[e[i].to]+);
}
if (lto[x]) dp(lto[x],depth+);
if (bac[x])
{
tp=;
for (i=dep[bac[x]];i<=dep[x];++i) sk[tp++]=f[dw[i]];
for (i=;i<tp;++i) sk[tp+i]=sk[i];
tp<<=; hd=; tl=;
for (i=;i<tp;++i)
{
if (i<(tp>>)) f[bac[x]]=max(f[bac[x]],sk[i]+dis(i,tp>>));
for (;hd<=tl&&i-q[hd]>(tp>>);++hd);
if (hd<=tl) ans=max(ans,sk[i]+sk[q[hd]]+i-q[hd]);
for (;hd<=tl&&sk[i]-i>=sk[q[tl]]-q[tl];--tl);
q[++tl]=i;
}
}
ans=max(ans,f[x]);
} void dfs(int x,int fat,int depth)
{
register int i,j;
u[x]=true; dep[x]=depth; dw[depth]=x;
for (i=hr[x];i;i=e[i].nex)
{
if ((i^)==fat) continue;
if (u[e[i].to])
{
for (j=dep[e[i].to]+;j<dep[x];++j) lto[dw[j]]=dw[j+];
for (j=dep[e[i].to]+;j<=dep[x];++j) ih[dw[j]]=true;
if (dep[x]>dep[e[i].to]) bac[x]=e[i].to;
}
else dfs(e[i].to,i,depth+);
}
} int main()
{
register int x,i;
n=read(); m=read(); pin=;
while (m--)
for (p=read()-,pre=read();p;--p)
x=read(),ins(x,pre),ins(pre,x),pre=x;
memset(u,,sizeof(u)); dfs(,,);
memset(u,,sizeof(u)); dp(,);
printf("%d\n",ans);
}

Last Word

  顺带一提,基环外向树也是通过把环取出来转化成树上问题的,而基环外向树也是一种特殊的仙人掌。

  基环外向树在BZOJ第一页同样有一道裸题:BZOJ1040

  所以仙人掌什么的大概就是这种模型了吗?

05-11 13:17