传送门
先转换一下题目所给的图:
给一个n个点n条边的图,不一定连通,但是不连通的地方可以用没有权值的虚边连接起来,既然可以任意连虚边那么我们就可以把原图中的一个个块分开处理。
然后分析这是什么图,因为一定有n个点n条边所以假如两个点之间没有边那么某一个块中一定会多出一条边来满足数量有n条。所以原图中每个连通块的边数=点数,刚好与基环树的性质契合,所以原图=基环树森林。
那么答案就是对于将每个基环树的直径加起来,我们分别求出每一个基环树的直径。那么,问题转换为,求一个基环树的直径。
基环树的直径定义为:基环树中最长的简单路径的长度(简单路径指不重复经过任何点或边的路径)。 那么基环树的直径只可能有两种情况
1、不经过环(在环上的某一点的子树中)
2、经过了环(某一段在环上)
那么对于一个基环树首先一次dfs把环搞出来。
对于第一种情况,我们对于环上的每一个点x求出他的子树的直径 d s o n [ x ] d_{son}[x] dson[x],那么第一种情况下的直径 D 1 D_1 D1
D 1 = m a x { d s o n [ x ] } D_1=max\lbrace d_{son}[x] \rbrace D1=max{dson[x]}
对于第二种情况,首先对于环上的一点 x x x,求出他的子树中最深的点和与根节点距离 d e p [ x ] dep[x] dep[x],然后第二种情况下的直径 D 2 D_2 D2
D 2 = m a x { d e p [ i ] + d e p [ j ] + d i s t ( i , j ) } ( i , j ∈ 环 , d i s t ( i , j ) 表 示 i , j 在 环 上 的 最 大 距 离 ) D_2=max\lbrace dep[i]+dep[j]+dist(i,j) \rbrace(i,j \in 环,dist(i,j)表示i,j在环上的最大距离) D2=max{dep[i]+dep[j]+dist(i,j)}(i,j∈环,dist(i,j)表示i,j在环上的最大距离)
如何求?首先环上的距离有两条,顺时针和逆时针,所以我们把环展开,然后复制一份接在后面,对于边权求一个前缀和,前缀和之差顺逆时针都可以求出,顺时针相当于是 s u m [ i ] − s u m [ j ] sum[i]-sum[j] sum[i]−sum[j],逆时针相当于 l o o p l e n − ( s u m [ i ] − s u m [ j ] ) loop_{len}-(sum[i]-sum[j]) looplen−(sum[i]−sum[j])。
又因为 d i s t ( i , j ) = ( s u m [ j ] − s u m [ i ] ) dist(i,j)=(sum[j]-sum[i]) dist(i,j)=(sum[j]−sum[i]),要求每个i找到最大的 m a x { d e p [ j ] − s u m [ j ] } max\lbrace dep[j]-sum[j]\rbrace max{dep[j]−sum[j]} ,下标和 m a x { d e p [ j ] − s u m [ j ] } max\lbrace dep[j]-sum[j]\rbrace max{dep[j]−sum[j]}都有单调性,那么用单调队列维护 m a x { d e p [ j ] − s u m [ j ] } max\lbrace dep[j]-sum[j]\rbrace max{dep[j]−sum[j]}即可。
代码来自wisdom,有详细注释。
//BZOJ1791
#include
#include
#include
#include
#include
#include
using namespace std;
typedef long long ll;
const int N=1e6+10;
inline int read()
{
int x=0,f=1; char ch=getchar();
while(ch<'0' || ch>'9'){if(ch=='-')f=-1; ch=getchar();}
while(ch>='0' && ch<='9'){x=x*10+ch-'0'; ch=getchar();}
return x*f;
}
struct edge
{
int x,y,c,next;
}a[N*2]; int len,last[N];
int n;
struct node
{
int w,to;
}fa[N],loop[N];
void ins(int x,int y,int c)
{
a[++len].x=x;a[len].y=y;a[len].c=c;
a[len].next=last[x];last[x]=len;
}
int vis[N],id=0,cnt=0;
void get_loop(int x)//扣环
{
vis[x]=++id;
for(int k=last[x];k;k=a[k].next)
{
int y=a[k].y;
if(y==fa[x].to) continue;
if(vis[y])//如果访问到一个点之前被访问过那么久形成了一个环
{
if(vis[y]<vis[x]) continue;//按照一个方向获取环(vis天然形成了一个方向,模拟可得) 这样的loop得到的环是连续的
loop[++cnt].to=y;
loop[cnt].w=a[k].c;
for(;y!=x;y=fa[y].to)
loop[++cnt]=fa[y];
}
else
{
fa[y].to=x; fa[y].w=a[k].c;
get_loop(y);
}
}
}
ll mx=0,sum[N*2],dep[N*2];
int v[N],now,pos;
void dfs(int x,int Fa,ll w)
{
if(w>=mx)
{
mx=w;
pos=x;
}
for(int k=last[x];k;k=a[k].next)
{
int y=a[k].y;
if(y!=Fa && (!v[y] || y==now)) //不访问到环上的点 但是第二次的时候可以访问到now
dfs(y,x,w+a[k].c);
}
}
ll f(int i){return dep[i]-sum[i];}
int list[N*2],head,tail;
int main()
{
memset(last,0,sizeof(last)); len=0;
n=read();
for(int i=1;i<=n;i++)
{
int x=read(),w=read();
ins(i,x,w); ins(x,i,w);
}
ll ans=0;
sum[0]=dep[0]=0;
for(int i=1;i<=n;i++)
{
if(!vis[i])//处理每棵基环树
{
cnt=0; id=0; ll t=0;
get_loop(i); //loop记录环上的点信息 to为点编号 w为点与环上另一个点之间的边的权值
for(int j=1;j<=cnt;j++) v[loop[j].to]=1;//标记环上的点
for(int j=1;j<=cnt;j++)//cnt环的长度
{
mx=0; now=-1; //now当前从环上的哪个点下去 mx子树的直径
dfs(loop[j].to,0,0);
dep[j]=mx; //第一次dfs后找到最深的点
now=loop[j].to;
dfs(pos,0,0);//对子树求一次直径(两次dfs)
t=max(t,mx);//第一种情况:基环树的直径在子树里
}
for(int j=1;j<=cnt;j++) dep[j+cnt]=dep[j];
head=1,tail=0; //第二种情况:基环树的直径经过了环
for(int j=1;j<=2*cnt;j++)
{//答案 dist(i,j)+dep[j]+dep[i] ( dist(i,j)=(sum[j]-sum[i]) )
if(j<=cnt) sum[j]=sum[j-1]+loop[j].w;
else sum[j]=sum[j-1]+loop[j-cnt].w;
if(head<=tail) t=max(t, f(list[head])+sum[j]+dep[j]);
//要求每个j找到最大的f(k) ,那么用单调队列维护f(i)=max{dep[i]-sum[i]}
while(head<=tail && f(list[tail])<=f(j)) tail--;
list[++tail]=j;
while(head<=tail && list[head]<=j-cnt+1) head++;
}
ans+=t;
}
}
printf("%lld\n",ans);
return 0;
}