【动态规划】Island

175 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第23天,点击查看活动详情

[IOI2008] Island

题目描述

你准备浏览一个公园,该公园由 NN 个岛屿组成,当地管理部门从每个岛屿 ii 出发向另外一个岛屿建了一座长度为 LiL_i 的桥,不过桥是可以双向行走的。同时,每对岛屿之间都有一艘专用的往来两岛之间的渡船。相对于乘船而言,你更喜欢步行。你希望经过的桥的总长度尽可能长,但受到以下的限制:

  • 可以自行挑选一个岛开始游览。
  • 任何一个岛都不能游览一次以上。
  • 无论任何时间,你都可以由当前所在的岛 SS 去另一个从未到过的岛 DD。从 SSDD 有如下方法:
    • 步行:仅当两个岛之间有一座桥时才有可能。对于这种情况,桥的长度会累加到你步行的总距离中。
    • 渡船:你可以选择这种方法,仅当没有任何桥和以前使用过的渡船的组合可以由 SS 走到 DD (当检查是否可到达时,你应该考虑所有的路径,包括经过你曾游览过的那些岛)。

注意,你不必游览所有的岛,也可能无法走完所有的桥。

请你编写一个程序,给定 NN 座桥以及它们的长度,按照上述的规则,计算你可以走过的桥的长度之和的最大值。

输入格式

第一行包含 NN 个整数,即公园内岛屿的数目。

随后的 NN 行每一行用来表示一个岛。第 ii 行由两个以单空格分隔的整数,表示由岛 ii 筑的桥。第一个整数表示桥另一端的岛,第二个整数表示该桥的长度 LiL_i。你可以假设对于每座桥,其端点总是位于不同的岛上。

输出格式

仅包含一个整数,即可能的最大步行距离。

样例 #1

样例输入 #1

7
3 8
7 2
4 2
1 4
1 9
3 4
2 3

样例输出 #1

24

提示

样例解释

样例 N=7N=7 座桥,分别为 (13),(27),(34),(41),(51),(63)(1-3), (2-7), (3-4), (4-1), (5-1), (6-3) 以及 (72)(7-2)。注意连接岛 22 与岛 77 之间有两座不同的桥。

其中一个可以取得最大的步行距离的方法如下:

  • 由岛 55 开始。
  • 步行长度为 99 的桥到岛 11
  • 步行长度为 88 的桥到岛 33
  • 步行长度为 44 的桥到岛 66
  • 搭渡船由岛 66 到岛 77
  • 步行长度为 33 的桥到岛 22

最后,你到达岛 22,而你的总步行距离为 9+8+4+3=249+8+4+3=24

只有岛 44 没有去。注意,上述游览结束时,你不能再游览这个岛。更准确地说:

  • 你不可以步行去游览,因为没有桥连接岛 22 (你现在的岛) 与岛 44
  • 你不可以搭渡船去游览,因为你可由当前所在的岛 22 到达岛 44。一个方法是:走 (27)(2-7) 桥,再搭你曾搭过的渡船由岛 77 去岛 66,然后走 (63)(6-3) 桥,最后走 (34)(3-4) 桥。

数据范围

对于 100%100\% 的数据,2N106,1Li1082\leqslant N\leqslant 10^6,1\leqslant L_i\leqslant 10^8

#include<iostream>
#include<cstdio>
using namespace std;
const int maxn=1e6+10; 
const int maxm=maxn<<1;	//无向图存边需要2倍的空间 
typedef long long ll;
struct Edge{	//链式前向星存图 
	int to,w,nxt;
}edge[maxm];
int head[maxn],cnt;
inline void Init(){	//初始化邻接表 
	for(int i=0;i<maxn;i++) head[i]=-1;
	for(int i=0;i<maxm;i++) edge[i].nxt=-1;
	cnt=1;
}
inline void addedge(int u,int v,int w){	//加入边 
	edge[cnt]={v,w,head[u]};
	head[u]=cnt++;
}
inline int read(){	//快读 
	int z=0; char c=getchar();
	while(c<'0'||c>'9') c=getchar();
	while(c>='0'&&c<='9'){
		z=(z<<3)+(z<<1)+(c^'0'); c=getchar();
	}
	return z;
}
int n;
bool vis[maxn];
int lp[maxn],size;	//loop存环,size最终为环的大小 
int fa[maxn];
int dfn[maxn],idx;
void get_loop(int u){
	dfn[u]=++idx;	//时间戳 
	for(int i=head[u];~i;i=edge[i].nxt){
		int v=edge[i].to;
		if(v==fa[u]) continue;
		if(dfn[v]){
			if(dfn[v]<dfn[u]) continue;	//注意:第一次遇到环时先不存 
			lp[++size]=v;				//在dfs的回归过程中再次遇到该环时再记录 
			for(;v!=u;v=fa[v]) lp[++size]=fa[v];	//记录环 
		}
		else fa[v]=u,get_loop(v);	//继续dfs子节点 
	}
}
ll d[maxn],ansd;	//d[i]表示从i出发走向以 i 为根的子树,能到达的最远距离 
void dp(int u){	//dp求树的直径 
	vis[u]=true;
	for(int i=head[u];~i;i=edge[i].nxt){
		int v=edge[i].to,w=edge[i].w;
		if(vis[v]) continue;
		dp(v);
		ansd=max(ansd,d[u]+d[v]+w);	//用经过点u的最长链更新ansd 
		//最长链即各个 d[v]+w 的最大值和次大值的和,
		//最后一次更新时的 d[u] 一定是次大值,d[v]+w 为最大值 
		d[u]=max(d[u],d[v]+w);	//d[u] 应为所有 d[v]+w 中的最大值 
	}
}
ll s[maxn<<1];	//s为前缀和数组 
int q[maxn<<1],l,r;	//单调队列 
inline ll solve(int p){
	idx=size=0; get_loop(p);
	ll len1=0,len2=0; 
	lp[0]=lp[size];
	for(int i=1;i<=size;i++) vis[lp[i]]=true;
	for(int i=1;i<=size;i++){
		ansd=0; dp(lp[i]);
		len1=max(len1,ansd);
	}//计算出情况一的答案 len1 
	if(size==2){	//单独处理由两个点构成的环的情况 
	    for(int i=head[lp[1]];~i;i=edge[i].nxt){
	        if(edge[i].to==lp[2])
	            len2=max(len2,d[lp[1]]+d[lp[2]]+edge[i].w);
	    }
	    return max(len1,len2);
	}
	for(int i=1;i<=size;i++){
		int lpw;
		for(int j=head[lp[i]];~j;j=edge[j].nxt)
			if(edge[j].to==lp[i-1]){
				lpw=edge[j].w; break;
			}//找到边 (lp[i-1],lp[i])
		s[i]=s[i-1]+lpw;	//计算前缀和 
	}
	for(int i=1;i<size;i++) s[size+i]=s[size]+s[i];	//复制一倍 
	l=r=1; q[1]=0;
	for(int i=1;i<size<<1;i++){	//单调队列计算情况二的答案 len2 
		while(l<=r&&q[l]<=i-size) l++;
		//判断队头的决策是否超出size的范围,超出则出队 
		len2=max(len2,d[lp[q[l]%size]]+d[lp[i%size]]+s[i]-s[q[l]]);
		//此时队头即为使 d[lp[i%size]-s[i] 最大的 i,并更新答案 
		while(l<=r&&s[q[r]]-d[lp[q[r]%size]]>=s[i]-d[lp[i%size]]) r--;
		//若队尾的值没有当前的 i 更优,出队 
		q[++r]=i;	//将 i 入队 
	}
	return max(len1,len2);	//最终答案为两种情况中较大的 
}
int main(){
	n=read();
	Init();
	for(int i=1;i<=n;i++){
		int j=read(),l=read();
		addedge(i,j,l); addedge(j,i,l);
	}//读入图 
	ll ans=0;
	for(int i=1;i<=n;i++)
		if(!vis[i]) ans+=solve(i);	//计算每个连通块(基环树)的直径并累加 
	printf("%lld\n",ans);	//输出答案 
	return 0;
}