洛谷P4381 [IOI2008]Island

版权声明:虽然本蒟蒻很菜,但各位dalao转载请注明出处谢谢。 https://blog.csdn.net/xuxiayang/article/details/85011336

大意

n n 个点 n n 条双向边
对于 x x y y
假设有边则你可以选择走路加经验或开挂没经验
假设每边则你可以选呢开挂没经验

问在每个点都不重复经过的情况下的最高经验


思路

依题得
这是一个基环树森林,开挂其实就是从这个基环树到另一棵基环树,所以答案就是所有基环树的直径和。

怎么求基环树的直径呢?我们假设它只是一棵树,很明显可以直接 d p dp ,首先我们强行求出原来树的直径,由于这是一棵基环树,所以我还有 n n 种在环上的走法,对于这 n n 中走法,可表示为

A n s t = m a x { d i s i d i s j + a i + a j } [ j > i m ] Ans_t=max\{dis_i-dis_j+a_i+a_j\}[j>i-m]

可以发现这就是维护区间长度小于 m m 的最大和,用单调队列维护即可


代码

#include<queue>
#include<cstdio>
#include<cctype>
#include<algorithm>
#define ri register int
using namespace std;int n,m,block,l[1000010],tot,unb[1000010],rd[1000010];
//l为邻接表数组
//unb为每个节点隶属的联通块
//rd为每个节点的入度
struct node{int next,to,w;}e[2000010];//边
inline void add(ri u,ri v,ri w){e[++tot]=(node){l[u],v,w};l[u]=tot;return;}
long long ans,f[1000010],dis[1000010],d[1000010],a[1000010];
//f为树的直径的f数组
//dis为树上第i个节点到第1个节点的距离
//di表示第i棵基环树的答案,当然不卡数组也行
//a为环中每个节点的f用于单调队列
bool vis[1000010];//标记每个联通块是否走过
inline char Getchar()
{
    static char buf[100000],*p1=buf+100000,*pend=buf+100000;
    if(p1==pend)
	{
        p1=buf; pend=buf+fread(buf,1,100000,stdin);
        if (pend==p1) return -1;
    }
    return *p1++;
}
inline long long read()
{
	char c;int d=1;long long f=0;
	while(c=Getchar(),!isdigit(c))if(c==45)d=-1;f=(f<<3)+(f<<1)+c-48;
	while(c=Getchar(),isdigit(c)) f=(f<<3)+(f<<1)+c-48;
	return d*f;
}
inline void write(register long long x)
{
	if(x<0)write(45),x=-x;
	if(x>9)write(x/10);
	putchar(x%10+48);
	return;
}//以上为读写优化,卡到了洛谷最优解Page 1
inline void dfs(ri x)
{
	unb[x]=block;//标记联通块
	for(ri i=l[x];i;i=e[i].next)
	{
		int y=e[i].to;
		if(!unb[y])	dfs(y);//搜索
	}
	return;
}
inline void topsort()//拓扑排序
{
	queue<int>q;
	int y,w;
	for(ri i=1;i<=n;i++) if(!(--rd[i])) q.push(i);
	//因为是无向联通图,每个节点都有>=1的入度,同时-1下面的判断就会方便很多
	while(q.size())
	{
		int x=q.front();q.pop();
		for(ri i=l[x];i;i=e[i].next)
		if(rd[y=e[i].to])
		{
			w=e[i].w;
			if(!--rd[y]) q.push(y);
			d[unb[x]]=max(d[unb[x]],f[x]+f[y]+w);
			f[y]=max(f[y],f[x]+w);
			//因为topsort相当于一个宽搜,所以我们可以“顺便”求直径
		}
	}
	return;
}
inline void dp(ri t,ri x)
{
	int m=0,y=x,i;//m表示环上节点个数,y表示当前环上点
	do
	{
		a[++m]=f[y];rd[y]=0;//放入a数组,并标记已删除防止重复计算
		for(i=l[y];i;i=e[i].next)
		if(rd[e[i].to])//若拓扑排序结束后入度不为0则其为环上的点
		{
			dis[m+1]=dis[m]+e[i].w;y=e[i].to;//接上
			break;
		}
	}while(i);
	if(m==2)//这里是重边情况,要特判
	{
		int w=0;
		for(ri i=l[y];i;i=e[i].next)
		if(e[i].to==x) w=max(w,e[i].w);//找到那两条边,取个最大值
		d[t]=max(d[t],f[x]+f[y]+w);//保存
		return;//退出
	}
	for(ri i=l[y];i;i=e[i].next)//否则就接上
	if(e[i].to==x)
	{
		dis[m+1]=dis[m]+e[i].w;
		break;
	}
	for(ri i=1;i<=m;i++) a[i+m]=a[i],dis[i+m]=dis[m+1]+dis[i];//破环为链
	deque<int>q;q.push_back(1);
	for(ri i=2;i<(m<<1);i++)
	{
		while(q.size()&&i-q.front()>=m) q.pop_front();//长度不小于m就弹出,因为我们不可重复走,所以不能连续m个
		if(q.size()) d[t]=max(d[t],dis[i]-dis[q.front()]+a[i]+a[q.front()]);//保存
		while(q.size()&&a[q.back()]-dis[q.back()]<=a[i]-dis[i]) q.pop_back();//保持队列单调性
		q.push_back(i);//放入
	}
	return;
}
signed main()
{
	n=read();
	for(ri i=1,x,y;i<=n;i++)
	{
		rd[i]++;rd[x=read()]++;//入度++
		y=read();add(i,x,y);add(x,i,y);//连边
	}
	for(ri i=1;i<=n;i++) if(!unb[i]) block++,dfs(i);//标记联通块
	topsort();//拓扑排序
	for(ri i=1;i<=n;i++)
	if(rd[i]&&!vis[unb[i]])//是环中点且该联通块没被处理过
	{
		vis[unb[i]]=true;//标记
		dp(unb[i],i);//dp
		ans+=d[unb[i]];//加上答案
	}
	write(ans);//输出
}

猜你喜欢

转载自blog.csdn.net/xuxiayang/article/details/85011336