P4381 [IOI2008] Island 题解
本题是道水题,码量小,请放心食用。
题目
第一眼看上去是不是非常《简单》。
再看一眼,\(syh\) 这个憨憨,说好的 树形\(dp\) 题解呢,这分明是个图嘛
然而你认为我会闲着没事搞这些吗
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX_
好的正片开始
不愧是你IOI,一来就100%
好的 \(N\) 个点,\(N-1\) 条边,是树没错了。
因为每个点都可以到达且只能到一次所以从上到下一个点只能选一条边。
选择最大的边,再子节点选,最后累计答案就可以了。
然后开开心心的AKIOI
样例过不了?
OK样例有问题,下一题
喂,它也没给你说联通呐
好的,那这是最长路,分层图?
好家伙,你可听说过有个东西叫基环树
没听说过?不用慌,这题也不需要。
但这样我们就知道它是个森林了
分析:
对于这一堆森林,树(tu)与树(tu)之间可以互相到达,并且每个节点只能走过一次,所以肯定是对每一颗树求它的最长路径,最后累计起来。
那么如何求一个树的最大方案呢?
基环树,顾名思义有环,只要处理了环,那我们就可以将其缩成一个点。
最后每一个就可以利用树的直径求出这里的最大路径。
此时就有人有疑问了,树的直径怎么一定是最优解?
我只能告诉你,
它不是最优。
那这怎么办呢?只能翻车了?
不,它是一棵树我也让它不是一颗树。
把在直径里面的提出来,剩下的树中继续求直径。
这样一定是最优解,可以自己推出。
难点1:环在哪儿?
难点2:如何基于环求解?
OK这个时候你的所有思路已经整理好了,随时准备AKIOI
1.dfs找环:
随机找根往下搜,并打上 \(vis\) 标记,如果返回到vis即这之前都是环。
这里我们需要记录下一个 \(r_i\) , 其中 \(i\) 表示第几个进入环上的数,而\(r_i\)整体表示一个初始节点编号。
\(s_i\) 表示环的末端到目前 \(i\) 节点的距离,所以 \(s_i=s_{i-1}+dist{_i,_{i-1}}\)其中 \(dist_{i,{i-1}}\)就表示 \(i\) 到 \(i-1\) 的距离。
最后用一个 \(vis2\) 判断是否是遍历过的环上节点。
要注意的是,可能会有双向边,所以判断回路时不能存父亲节点,而是判断是从哪条边过来的。
dfs代码:
bool dfs(int x,int fa)
{
if(vis[x])
{
vis[x]=2,r[++cnp]=x,vis2[x]=1;
return 1;
}
vis[x]=1;
for(int i=h[x];i!=-1;i=a[i].next)
{
if((i^1)!=fa&&dfs(a[i].to,i))
{
if(vis[x]!=2)
{
r[++cnp]=x,vis2[x]=1,s[cnp]=s[cnp-1]+a[i].data;
return 1;
}
else
{
s[st-1]=s[st]-a[i].data;
return 0;
}
}
}
return 0;
}
2.找树上最长方案
对于每一个环上节点作为一个根进行一个树的直径,如果发现是遍历过的环上节点则跳过,取最大值。
void situ1(int x)
{
vis2[x]=1;
for(int i=h[x];i!=-1;i=a[i].next)
{
if(vis2[a[i].to])continue;
situ1(a[i].to);
ans=max(ans,dis[x]+dis[a[i].to]+a[i].data);
dis[x]=max(dis[x],dis[a[i].to]+a[i].data);
}
}
模板应该不用讲吧……
此时此刻有有人要说了,\(syh\) 这个 \(cb\) ,这样不就把环中所有节点废了吗?
欸说到点子上了,现在还有个问题,假设根在环上,那一定是两颗子树的边汇集过来,形成一个直径,为什么是两棵呢?不然你人格分裂呐。
3.处理特殊情况
很明显,我们要找的是在环上的每两个点中,\(d_i+d_j+dist_{i,j}\) 的最大值,而因为我们之前处理过 \(s\) 数组,所以可以改为 \(d_i+d_j+s_i-s_j (i>j)\) 的最大值。那么这一点我们可以枚举。
“我觉得你是个cb,那另一边的环怎么走?”
忍三有个B级卡叫做复制,直接复制大法好,复制两份。那这样就得防止有两个相同的节点或是方向不相同的节点,也就是注意排除即可。
恭喜你获得了65高分,对于我们来说在IOI上拿这点分已经不错了。
此时有人又吼起来了:“咋地,瞧不起咱?”
被迫讲正解
如果单纯枚举的话,复杂度是扛不住的。
那么该如何去抗住这个该死的复杂度呢。
光从 \(d_i+d_j+dist_{i,j}\) 来看,貌似可以用单调队列的氩子。但 \(d_i+d_j+s_i-s_j (i>j)\) 狠狠的甩了你一巴掌。
别人甩你一巴掌怎么办,那就甩回去呗(滑稽)
对于每一个点,都有一个最优解与其匹配。
当一个点与另一个点满足 \(d_j-s_j\leq d_i-s_i (i>j)\)时,\(j\) 所出现的路径被 \(i\) 整体覆盖,所以会排掉,因为呈于一个递增状态,所以当找到第二大的路径时,会自动匹配到第一大路径。所以可以轻松匹配到第一答案。
int situ2(int x)
{
st=cnp+1,ans2=0,ans3=0;
dfs(x,-1);
for(int i=st;i<=cnp;i++)
{
ans=0;
situ1(r[i]);
ans2=max(ans2,ans);
dp[i+cnp-st+1]=dp[i]=dis[r[i]];
s[i+cnp-st+1]=s[i+cnp-st]+s[i]-s[i-1];
}
deque<int>q;
for(int i=st;i<=2*cnp-st+1;i++)
{
while(q.size()&&q.front()<=i-cnp+st-1)q.pop_front();
if(q.size())ans3=max(ans3,dp[i]+dp[q.front()]+s[i]-s[q.front()]);
while(q.size() && dp[q.back()]-s[q.back()]<=dp[i]-s[i])
q.pop_back();
q.push_back(i);
}
return max(ans2,ans3);
}
没啦没啦。
你们最爱的完整代码来了
#include<iostream>
#include<cstdio>
#include<queue>
#include<cstring>
#define N 2000005
#define int long long
using namespace std;
struct node
{
int from,to,data,next;
}a[N];
int n,ans,ans2,ans3,dis[N],vis2[N],h[N],cnt=-1,vis[N],r[N],cnp,s[N],st,dp[N];
void add(int x,int y,int z)
{
a[++cnt].from=x;
a[cnt].to=y;
a[cnt].data=z;
a[cnt].next=h[x];
h[x]=cnt;
}
bool dfs(int x,int fa)
{
if(vis[x])
{
vis[x]=2,r[++cnp]=x,vis2[x]=1;
return 1;
}
vis[x]=1;
for(int i=h[x];i!=-1;i=a[i].next)
{
if((i^1)!=fa&&dfs(a[i].to,i))
{
if(vis[x]!=2)
{
r[++cnp]=x,vis2[x]=1,s[cnp]=s[cnp-1]+a[i].data;
return 1;
}
else
{
s[st-1]=s[st]-a[i].data;
return 0;
}
}
}
return 0;
}
void situ1(int x)
{
vis2[x]=1;
for(int i=h[x];i!=-1;i=a[i].next)
{
if(vis2[a[i].to])continue;
situ1(a[i].to);
ans=max(ans,dis[x]+dis[a[i].to]+a[i].data);
dis[x]=max(dis[x],dis[a[i].to]+a[i].data);
}
}
int situ2(int x)
{
st=cnp+1,ans2=0,ans3=0;
dfs(x,-1);
for(int i=st;i<=cnp;i++)
{
ans=0;
situ1(r[i]);
ans2=max(ans2,ans);
dp[i+cnp-st+1]=dp[i]=dis[r[i]];
s[i+cnp-st+1]=s[i+cnp-st]+s[i]-s[i-1];
}
deque<int>q;
for(int i=st;i<=2*cnp-st+1;i++)
{
while(q.size()&&q.front()<=i-cnp+st-1)q.pop_front();
if(q.size())ans3=max(ans3,dp[i]+dp[q.front()]+s[i]-s[q.front()]);
while(q.size() && dp[q.back()]-s[q.back()]<=dp[i]-s[i])
q.pop_back();
q.push_back(i);
}
return max(ans2,ans3);
}
signed main()
{
memset(h,-1,sizeof(h));
scanf("%lld",&n);
for(int i=1;i<=n;i++)
{
int y,z;
scanf("%lld%lld",&y,&z);
add(i,y,z);
add(y,i,z);
}
int sum;
for(int i=1;i<=n;i++)
{
if(!vis2[i])
{
sum+=situ2(i);
}
}
printf("%lld",sum);
return 0;
}
原来IOI也会出模板题(滑稽)。
建议改题名P4381 [IOI2008] Island (模板【基环树森林直径和问题】)
\(AKIOI\) \(1/6\)
编 \(md\) 不易 , 感谢您的阅读。

浙公网安备 33010602011771号