COCI2016/2017 #1 D Mag 点分治

COCI2016/2017 #1 D Mag 点分治

​ 题目链接:COCI2016-2017#1


算法: 点分治

证明&结论:

​ 首先这题关于选出来的路径有一个结论:

  • 路径上点权全为 \(1\)
  • 路径长度为 \(2\times l+1\),有且只有一个权值为 \(2\) 的点在 \(l+1\) 这个位置,其他点权都为 \(1\)
  • 如果没有点权为 \(1\) 的点,那么答案就是最小的权值。

​ 首先声明一点,据题意可知,题目中出现的数据都是正整数,所以以下推导中出现的变量均为正整数

​ 我们假设一个点权为 \(x\) 的点连接了两条魔力值分别为 \(\dfrac{a}{b},\dfrac{c}{d}\) 的路径。那么连接完后路径的魔力值就为 \(\dfrac{a\times c\times x}{b+d+1}\)。我们将这个魔力值与 \(\dfrac{a}{b}\) 相比,如果 \(\dfrac{a\times c\times x}{b+d+1}\le\dfrac{a}{b}\) 的话,就有 \(d+1+b\ge c\times x\times b\) 否则肯定直接选 \(\dfrac{a}{b}\) 更优。

​ 再与 \(\dfrac{c}{d}\) 比较,得出相似的一条式子 \(d+1+b\ge a\times x\times d\)。两式左右分别相加,得到 \(2\ge (c\times x-2)\times b+(a\times x-2)\times d\) 。又因为 \(b,d\ge 1\) 所以 \(c\times x,a\times x\le 3\)

​ 但是注意到, \(c\times x,a\times x\) 能取到 \(3\) 是因为原不等式带等号,取 \(3\) 的时候可能会使这个不等式满足等于号而成立。但是我们选答案的范围是整张图,如果取等号的话,为什么不直接取魔力值为 \(\dfrac{a}{b}\) 或者 \(\dfrac{c}{d}\) 作为答案呢?

​ 所以我们尝试把不等式的等号去掉就得到 \(c\times x,a\times x\le2\) 。由于 \(a,c,x\) 均为正整数,那么 \(a,c,x\) 只能为 \(1,2\)。接下来分类讨论

  • \(x=2\) 的情况。

    那么 \(a,c\) 都只能为 \(1\)。那么就有 \(\dfrac{1}{b},\dfrac{1}{d},\dfrac{2}{b+d+1}\)\(3\) 种情况。如果拼起来更优的\(\dfrac{2}{b+d+1}<\dfrac{2}{2\times b}\)\(\dfrac{2}{b+d+1}<\dfrac{2}{2\times d}\)。所以当且仅当 \(b=d\) 时,拼起来会更优,也就是 \(x\) 这个点连接了两条长度一样,点权全为 \(1\) 的链。这对标我们的结论 \(2\)

  • \(x=1\) 的情况。

    首先如果 \(a,c\) 都为 \(1\)。那么拼起来是更优的,对标结论 \(1\)。如果 \(a,c\) 之中有一个是 \(2\) 。就说明拼起来的路径中只有一个点权是 \(2\)。根据我们刚刚推导的情况,一条路径上只有一个点权为 \(2\) 时。当且仅当这个点处于路径中心时拼接起来更优(即结论 \(2\))。

    最后一种情况:\(a,c\) 都为 \(2\)。也就是这条链上有 \(2\) 个权值为 \(2\) 的点的情况。 那么我们设这两个权值为 \(2\) 的点将总长度为 \(b+d+1\) 的路径划分成了 \(3\) 条只包含点权为 \(1\) 的点的链,它们的长度分别为 \(u,v,p\)。那么可能的最小的权值情况就为 \(\dfrac{1}{\max(u,v,p)},\dfrac{4}{b+d+1},\dfrac{2}{u+v+1},\dfrac{2}{v+p+1}\)。直接对比 \(\dfrac{1}{max(u,v,p)},\dfrac{4}{u+v+p+2}\)。 得 \(4\times\max(u,v,p)<u+v+p+2\),当且仅当 \(u,v,p\) 全为 \(1\) 时成立。那么这 \(4\) 个可能的值就全是具体的了。分别是 \(\dfrac{1}{1},\dfrac{4}{5},\dfrac{2}{3},\dfrac{2}{3}\) ,显然 \(\dfrac{2}{3}<\dfrac{4}{5}\),所以拼接起来一定不是最优的。(可以结合下图理解)

​ 综上所述,我们在文章一开始提出的三个结论成立。


解题:

​ 知道了结论,我们就可以根据结论进行求解了,那么我们要找的就是两种链。第一种是点权全是 \(1\) 的。第二种是权值为 \(2\) 的点在链中心的。

​ 找链的工作可以使用树形 dp 来求解。关于树形 dp 的写法网上已经有很多题解了。这里介绍一种点分治的写法。

​ 首先是关于全是 \(1\) 的链的求解,那么我们寻找路径是就只要遍历点权为 \(1\) 的点。然后维护路径最大值与次大值(注意最大值与次大值不能在同一子树内)。局部代码如下:

void Get_dis(int p,int fa,int d)
{
	if(val[p]!=1) return ;//点权不是1不用记录到数组内
	arr[++cnt]=d;
	for(int i=0;i<e[p].size();++i)
	{
		int to=e[p][i];
		if(to==fa||vis[to]) continue;
		Get_dis(to,p,d+1);
	}
}
void solve(int p)
{
	int mx[2]={},tmp=0,id[2];id[0]=id[1]=-1;
	for(int i=0;i<e[p].size();++i)
	{
		int to=e[p][i];
		if(vis[to]) continue;
		cnt=0;Get_dis(to,p,1);tmp=0;
		for(int j=1;j<=cnt;++j)
			tmp=max(tmp,arr[j]);//维护子树最大值
		if(tmp>mx[1])//维护最大值与次大值
			mx[1]=tmp;id[1]=i;
		if(mx[1]>mx[0])
		{
			swap(mx[1],mx[0]);
			swap(id[1],id[0]);
		}
		ans=max(ans,mx[0]+mx[1]+1);//用不在同一子树内的最大值次大值更新答案
	}
}

​ 同时的,Get_dis 这个函数还可以在获取当前解决的点(当前解决子树的重心)点权为 \(2\) 时需要的路径,然后我们维护一下最长的两条相等的路径即可,可以通过以下方式实现。

struct Data
{
	int x,y;//x是分子,y是分母
	bool operator <(const Data&a) const
	{
		return x*1.0/y<(a.x*1.0/a.y);
	}
	void print()
	{
		int d=gcd(x,y);
		printf("%d/%d\n",x/d,y/d);
	}
}res;
void solve_(int p)
{
	int mx=0;
	for(int i=0;i<e[p].size();++i)
	{
		int to=e[p][i];
		if(vis[to]) continue;
		cnt=0;Get_dis(to,p,1);
		for(int j=1;j<=cnt;++j)//因为两个链的长度要相等,所以取较小值更新答案。
			res=min(res,(Data){2,min(mx,arr[j])*2+1});
		for(int j=1;j<=cnt;++j)//维护一下已经遍历到的最大值
			mx=max(mx,arr[j]);
	}
}

​ 但是有一种可能,我们在解决的点点权是 \(1\) 但是我们获取的链的长度里面有点权为 \(2\) 的点,如下图这种情况,\(p\) 是我们目前处理的点(当前处理子树的重心)。

​ 所以我们还得再写一个类似 Get_dis 的函数用来获取带一个点权为 \(2\) 的点的路径。如下:

void Get_dis2(int p,int fa,int d,int d2,int c)
{
	if(val[p]!=1&&val[p]!=2) return ;
	if(c>2) return ;
	dis[++all]={d,d2};//这里的dis数组是data类型,但只是为了存储两个值,可以用pair替代。
	for(int i=0;i<e[p].size();++i)
	{
		int to=e[p][i];
		if(to==fa||vis[to]) continue;
		Get_dis2(to,p,c==2?d:d+1,c==2?d2+1:d2,c*val[p]);
	}
}

​ 至于根据路径求解的代码,由于当前处理的点(处理子树的重心)的点权是 \(1\) 我们可以将它与上面的 solve(处理全是 \(1\) 的链的函数)合并一下。就变成了下面这个代码:

void solve(int p)
{
	int mx[2]={},tmp=0,id[2];id[0]=id[1]=-1;//下面这部分展示过了,可以跳过。
	for(int i=0;i<e[p].size();++i)
	{
		int to=e[p][i];
		if(vis[to]) continue;
		cnt=0;Get_dis(to,p,1);tmp=0;
		for(int j=1;j<=cnt;++j)
			tmp=max(tmp,arr[j]);
		if(tmp>mx[1])
			mx[1]=tmp;id[1]=i;
		if(mx[1]>mx[0])
		{
			swap(mx[1],mx[0]);
			swap(id[1],id[0]);
		}
		ans=max(ans,mx[0]+mx[1]+1);
	}//上面这部分展示过了,可以跳过。
	for(int i=0;i<e[p].size();++i)
	{
		int to=e[p][i];
		if(vis[to]) continue;
		all=0;Get_dis2(to,p,val[to]==1?1:0,0,val[to]);
		for(int j=1;j<=all;++j)
		{
			int d1=dis[j].x,d2=dis[j].y;//一样,由于全为1的链的长度需要相等,取较小值。
			res=min(res,Data{2,min((i==id[0]?mx[1]:mx[0])+1+d1,d2)*2+1});
		}
	}
}

​ 上述问题到这里就解决了,总时间复杂度为 \(O(n\log(n))\)加大常数。

​ 全部代码如下:

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN = 1e6+5;
int n,dp[MAXN],siz[MAXN],Tsiz,root,cnt,arr[MAXN],ans,val[MAXN],all;
bool vis[MAXN];
vector <int> e[MAXN];
int gcd(int x,int y)
{
	if(y==0) return x;
	return gcd(y,x%y);
}
void Get_root(int p,int fa)
{
	dp[p]=0;siz[p]=1;
	for(int i=0;i<e[p].size();++i)
	{
		int to=e[p][i];
		if(to==fa||vis[to]) continue;
		Get_root(to,p);
		siz[p]+=siz[to];
		dp[p]=max(siz[to],dp[p]);
	}
	dp[p]=max(dp[p],Tsiz-siz[p]);
	if(dp[p]<dp[root]) root=p;
}
struct Data
{
	int x,y;//x表示分子,y表示分母
	bool operator <(const Data&a) const
	{
		return x*1.0/y<(a.x*1.0/a.y);
	}
	void print()
	{
		int d=gcd(x,y);
		printf("%d/%d\n",x/d,y/d);
	}
}res,dis[MAXN];
void Get_dis(int p,int fa,int d)
{
	if(val[p]!=1) return ;
	arr[++cnt]=d;
	for(int i=0;i<e[p].size();++i)
	{
		int to=e[p][i];
		if(to==fa||vis[to]) continue;
		Get_dis(to,p,d+1);
	}
}
void Get_dis2(int p,int fa,int d,int d2,int c)
{
	if(val[p]!=1&&val[p]!=2) return ;
	if(c>2) return ;
	dis[++all]={d,d2};
	for(int i=0;i<e[p].size();++i)
	{
		int to=e[p][i];
		if(to==fa||vis[to]) continue;
		Get_dis2(to,p,c==2?d:d+1,c==2?d2+1:d2,c*val[p]);
	}
}
void solve(int p)
{
	int mx[2]={},tmp=0,id[2];id[0]=id[1]=-1;
	for(int i=0;i<e[p].size();++i)
	{
		int to=e[p][i];
		if(vis[to]) continue;
		cnt=0;Get_dis(to,p,1);tmp=0;
		for(int j=1;j<=cnt;++j)
			tmp=max(tmp,arr[j]);
		if(tmp>mx[1])
			mx[1]=tmp;id[1]=i;
		if(mx[1]>mx[0])
		{
			swap(mx[1],mx[0]);
			swap(id[1],id[0]);
		}
		ans=max(ans,mx[0]+mx[1]+1);
	}
	for(int i=0;i<e[p].size();++i)
	{
		int to=e[p][i];
		if(vis[to]) continue;
		all=0;Get_dis2(to,p,val[to]==1?1:0,0,val[to]);
		for(int j=1;j<=all;++j)
		{
			int d1=dis[j].x,d2=dis[j].y;
			res=min(res,Data{2,min((i==id[0]?mx[1]:mx[0])+1+d1,d2)*2+1});
		}
	}
}
void solve_(int p)
{
	int mx=0;
	for(int i=0;i<e[p].size();++i)
	{
		int to=e[p][i];
		if(vis[to]) continue;
		cnt=0;Get_dis(to,p,1);
		for(int j=1;j<=cnt;++j)
			res=min(res,(Data){2,min(mx,arr[j])*2+1});	
		for(int j=1;j<=cnt;++j)
			mx=max(mx,arr[j]);
	}
}
void Divide(int p)
{
	vis[p]=1;
	if(val[p]==1) solve(p);
	if(val[p]==2) solve_(p);
	for(int i=0;i<e[p].size();++i)
	{
		int to=e[p][i];
		if(vis[to]) continue;
		Tsiz=siz[to];root=0;
		Get_root(to,p);
		Divide(root);
	}
}
int main()
{
	scanf("%d",&n);
	for(int i=1;i<n;++i)
	{
		int u,v;
		scanf("%d %d",&u,&v);
		e[u].push_back(v);
		e[v].push_back(u);
	}
	for(int i=1;i<=n;++i)
		scanf("%d",&val[i]);
	dp[root=0]=n+1;Tsiz=n;ans=-1;res.y=1;res.x=1e9;
	Get_root(1,0);
	Divide(root);
	if(ans!=-1)
	{
		if(Data{1,ans}<res)
			printf("1/%d\n",ans);
		else res.print();			
	}
	else
	{
		if(res.x!=1e9)
		{
			res.print();
		}
		else
		{
			ans=1e9;
			for(int i=1;i<=n;++i)
				ans=min(ans,val[i]);
			printf("%d/1\n",ans);			
		}
	}
	return 0;
}

总结:点分治,解决树上路径的一种优秀方法。(虽然在这题并不优秀)

posted @ 2021-08-20 15:17  夜空之星  阅读(65)  评论(0编辑  收藏  举报