岛屿【基环树、单调队列】

[IOI2008] Island

题目描述

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

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

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

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

输入格式

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

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

输出格式

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

样例 #1

样例输入 #1

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

样例输出 #1

24

提示

样例解释

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

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

  • 由岛 5 5 5 开始。
  • 步行长度为 9 9 9 的桥到岛 1 1 1
  • 步行长度为 8 8 8 的桥到岛 3 3 3
  • 步行长度为 4 4 4 的桥到岛 6 6 6
  • 搭渡船由岛 6 6 6 到岛 7 7 7
  • 步行长度为 3 3 3 的桥到岛 2 2 2

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

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

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

数据范围

对于 100 % 100\% 100% 的数据, 2 ⩽ N ⩽ 1 0 6 , 1 ⩽ L i ⩽ 1 0 8 2\leqslant N\leqslant 10^6,1\leqslant L_i\leqslant 10^8 2N106,1Li108

题解

题目大意为给我们 n n n个点, n n n条边的图,可以自己选一个起点出发,每个点至多经过一次,求最多可以走多远。当我们处于一个点时,我们有两种方法到达另一个点:
1.步行:通过桥前往一个未曾到过的点。
2.渡船:通过渡船到达另一个连通块中的任意一个点。

本题特点:通过题目中的 n n n个点 n n n条边,我们可以知道该图是一个基环树森林。

我们先看单独一个基环树,由于渡船的方法不能前往当前连通块中的点,故在一个基环树内部,我们只能采用步行的方式前进。那如何才能在一个基环树内部走的最远呢,答案就是沿着该基环树的直径走。

分析至此,本题的解法有了大致的轮廓,我们先求出每个基环树,在基环树内部求直径,基环树和基环树之间采用渡船的方式连接,答案就是所有基环树的直径之和。

所以现在的问题就变成了如何求基环树的直径。

我们首先通过 d f s dfs dfs将环找出来。
基环树的直径有两种情况:
1.不经过环中的节点。
2.经过环中的节点。

对于情况 1 1 1(不经过环中的节点):
我们依次将环中的所有节点当作根节点,在不经过其他环中节点的前提下进行 树形 D P 树形DP 树形DP求直径即可,同时维护环中每个节点可以到达的最远距离 D [ u ] D[u] D[u],最后求最大值。

对于情况 2 2 2(经过环中的节点):
这种情况一定是取环中的一段再加上端点可以到达的最远距离。我们假设环中的那一段的端点分别为 i i i j j j,则答案就是 s [ j ] − s [ i ] + D [ i ] + D [ j ] s[j]-s[i]+D[i]+D[j] s[j]s[i]+D[i]+D[j]。其中 s [ i ] s[i] s[i] i i i号点在环中的前缀和(此处的 i i i不是环中编号而是全局的编号), D [ i ] D[i] D[i]为从 i i i出发不经过环中其他节点可以到达的最远距离。
如何求:如果我们固定 i i i,就是求使得 s [ j ] + D [ j ] s[j]+D[j] s[j]+D[j]最大的 j j j,我们可以用单调队列进行维护 O ( n ) O(n) O(n)求得。

最后对两种情况求最大值即可,每个基环树的答案累加入全局答案。

找环等部分具体实现方式不唯一,我的代码仅供参考。

代码

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 1e6+10,M = N<<1;
int h[N],e[M],ne[M],idx;
ll w[M];
void add(int a,int b,ll c){
    e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
int st[N];
int n;
int id[N],cnt;
int vec[N];
ll Ans;
int stk[N],top;
ll D[N];
int p[M],q[M];
ll s[M],cha;
void dfs(int u,int B){
    id[u]=B;
    for(int i=h[u];~i;i=ne[i]){
        int j=e[i];
        if(!id[j])dfs(j,B);
    }
}
void get_ring(int u,int edge,bool&flag,ll dis){
    if(st[u]==1){
        int t=1;
        while(stk[t]!=u){
            st[stk[t]]=0;
            t++;
        }
        cha=dis-s[u];
        for(int i=top;i>=t;i--){
            s[stk[i]]-=s[u];
        }
        for(int i=t;i<=top;i++)stk[i-t+1]=stk[i];
        
        flag=true;
        top=top-t+1;
        return;
    }
    s[u]=dis;
    stk[++top]=u;
    st[u]=1;
    for(int i=h[u];~i;i=ne[i]){
        int j=e[i];
        if(edge!=-1&&(i^edge)==1)continue;
        get_ring(j,i,flag,dis+w[i]);
        if(flag==true)return;
    }
    top--;
    st[u]=0;
    
}
void get_ring(int block){
    top=0;
    bool flag=false;
    get_ring(vec[block],-1,flag,0);
}
ll dp(int u,ll&ans){
    D[u]=0;
    st[u]=1;
    for(int i=h[u];~i;i=ne[i]){
        int j=e[i];
        if(st[j])continue;
        dp(j,ans);
        ans=max(ans,D[u]+D[j]+w[i]);
        D[u]=max(D[u],D[j]+w[i]);
    }
}
void count(int block){
    get_ring(block);
    ll ans=0;
    for(int i=1;i<=top;i++){
        ll tmp=0;
        dp(stk[i],tmp);
        ans=max(ans,tmp);
    }
    for(int i=1;i<=top;i++)p[i]=p[i+top]=stk[i];
    int hh=0,tt=0;
    q[tt++]=1;
    for(int i=1;i<=top;i++){
        while(q[tt-1]<i+top-1){
            int id=q[tt-1]+1;
            int j=p[id];
            ll d=D[j]+s[j];
            if(id>top)d+=cha;
            while(hh!=tt){
                int k=p[q[tt-1]];
                ll dd=D[k]+s[k];
                if(q[tt-1]>top)dd+=cha;
                if(dd<=d)tt--;
                else break;
            }
            q[tt++]=id;
        }
        while(q[hh]<=i)hh++;
        int x=p[q[hh]];
        ll tmp=D[p[i]]-s[p[i]]+D[x]+s[x];
        if(i>top)tmp-=cha;
        if(q[hh]>top)tmp+=cha;
        ans=max(ans,tmp);
    }
    Ans+=ans;
}
int main(){
    memset(h,-1,sizeof h);
    scanf("%d",&n);
    for(int i=1;i<=n;i++){
        int j;
        ll c;
        scanf("%d%lld",&j,&c);
        add(i,j,c),add(j,i,c);
    }
    for(int i=1;i<=n;i++)
        if(!id[i]){
            vec[++cnt]=i;
            dfs(i,cnt);
        }
    for(int i=1;i<=cnt;i++){
        count(i);
    }
    printf("%lld",Ans);
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值