noip2018 day1 T3 赛道修建题解

题目
知识点: 二分、树、贪心
讲解:
对于这一题在考场上是一脸懵逼,先理解一下题目吧。
给你一个数要求你从这个树上抠出m条链,要求:最小的链最长
这个题目最大的困难是怎么抠?
首先想到的就是二分,为什么?最小的链最长不就是二分的经典句子嘛。
所以我们考虑二分最小链的长度,可是还是那个问题怎么抠?
我们在没思路的时候我们可以看看可不可以拿部分分,说不定打着打着就有思路了 (博主本人)

根据数据我们可以分成三类,一类是bi=ai+1,这个其实就是一条链,还有ai=1,这个是本题最恶心的菊花图,还有分枝不超过3,这个其实就是正解的弱化版,我就是打这个然后想出正解的。
我们先考虑链的时候怎么办?
其实我们可以二分,我们二分最小链长度mid,从头开始(根据贪心有选肯定比没选优)放入链,一旦超过mid则清空长度并且数量加1(意味着我们已经找到一条符合条件的链了)。
伪代码:

bool check(int mid)
{
	num=0,much=0;//num为目前链的长度,much为目前已经抠到much条了 
	for(int i=1;图还没遍历完;i=v)//v是i的儿子
	{
		num+=u;//u是这条边的边权
		if(num>=mid)
			num=0,much++; 
	}
	if(much>=m)//我们要抠m条,判断数否可以抠到 
		return true;
	else return false;
}
int l=0,r=inf,ans=0;//inf为极大值
while(l<=r)
{
	int mid=(l+r)>>1;
	if(check(mid))
	{
		ans=mid;
		l=mid+1;
	}
	r=mid-1;
}
printf("%d",ans);

接下来是菊花图:
菊花图也就是所有的点有且仅和1这个点相连,因为长得像菊花,所以叫菊花图。
我们可以发现,根据题目,在菊花图上符合条件的链要么由本身组成,要么由两条链组成,所以根据贪心,我们先把所有的边权按从大到小排序,设边权数组为a,然后我们再找到m条链(根据贪心大链配小链,两个比一个优),所以我们可以得出答案为a[1]+a[2 * m]、a[2]+a[2 * m-1] ······ 中最小的,所以我们可以得出菊花图的解法。
代码:

bool cmp1(int x,int y)//从大到小排 
{
	return x>y;
}
ans=inf;//先给答案付个极大值 
sort(a+1,a+n,cmp1);//排序 
for(int i=1;i<=m;i++)
	ans=min(ans,a[i]+a[2*m-i+1]);//找答案 

然后是分枝不超过三:
这句话的意思就是树上的任意一个节点它的儿子最多两个,这意味着什么? (不知道)
我们分析一下性质,对于一棵树中的任意一个节点(把个体先分析)我们可以知道抠链要么是直接在内部两个儿子通过父亲相连拼接成一条链,要么就是通过父亲到达祖先和祖先拼接(根据贪心我们可以知道,如果有很多链都需要和祖先连接,那么我们选的一定是最长的一条)。
我们先考虑内部连接:
我们直接开个数组存儿子中需要和祖先连接的最长链,然后再两两连接看是不是可以满足大于等于mid,如果可以根据贪心一定是要刚刚好,即与之配对的一定是可以配对中最小的,这样我们才可以尽可能地留下大的与祖先配对。
考虑与祖先连接:
我们直接开一个数组dp,dp[i]表示i这个节点中要与祖先配对的不符合条件(条件就是两两内部拼接不成功的那些链)的链中最长的,在后面直接调用就好了。
总结:
这样我们就考虑完了,因为儿子最多两个,所以内部拼接时要么这两个连接,要么选择最大的去和祖先连接。
那么对于不是最多两个儿子的解呢?
我们从小到大把儿子中要与祖先配对的链长度排序(就是dp数组里的东西),从最小的链去找到最小的可以和它匹配的链(这个也是二分),然后如果没有边可以和它匹配或者只剩一条边了我们就把它存入dp中与祖先匹配(注意要比较选最大)。
这样这一题就解决了。
可是测时我们发现有几个点TLE,正常,这就是我说的这一题的坑点,菊花图!
我们这个的做法是O(n ^ 2 log n)的,(n表示一个节点儿子的个数),所以对于儿子的分布越平均越快,可菊花图就是最不平均的,怎么办呢?
其实我们直接特判一下,如果是菊花图直接用上面说的对于菊花图的做法,其他的用我们的正解就好了。
注意:
要用不定长数组vector来存儿子中要与祖先匹配的链的长度,否则会MLE(爆内存)!
代码:

#include
#include
#include
#include
#include
using namespace std;
int n,m,head[100100],Next[100100],ver[100100],edge[100100],size=0,much=0,dp[50100],a[100100];
inline int read()//快读 
{
	char ch;
	while(ch=getchar(),ch<'0' || ch>'9');
	int num=ch-'0';
	while(ch=getchar(),ch>='0' && ch<='9')
		num=(num<<3)+(num<<1)+ch-'0';
	return num;
}
bool cmp(int x,int y)//从小到大排序 
{
	return x<y;
}
void add(int x,int y,int u)//边表尝龟操作 
{
	size++;
	Next[size]=head[x];
	head[x]=size;
	ver[size]=y;
	edge[size]=u;
	return ;
}
inline void dfs(int x,int fa,int need)
{
	vector <int> q;//用不定常数组 
	for(int i=head[x];i;i=Next[i])//遍历一下 
	{
		int y=ver[i],u=edge[i];
		if(y==fa)
			continue;
		dfs(y,x,need);
		q.push_back(dp[y]+u);//把儿子需要和祖先匹配的链存下来,记得加上这条边的权值 
	}
	sort(q.begin(),q.end(),cmp);//排序 
	while(q.size() && q[q.size()-1]>=need)//从大到小判断是否有不用和别人匹配就满足条件的 
	{
		much++;
		q.pop_back();//删除 
	}
	while(q.size()>=2)//有两条链即以上的 
	{
		if(q[0]+q[q.size()-1]>=need)//判断是否可以合并,如果连和最大值合并都没有用,那肯定不可以 
		{
			int l=0,r=q.size(),ans=-1;
			while(l<=r)//二分找最小的 
			{
				int mid=(l+r)>>1;
				if(q[mid]+q[0]>=need && mid!=0)
					ans=mid,r=mid-1;
				else l=mid+1;
			}
			much++;
			q.erase(q.begin()+ans);//删除 
			q.erase(q.begin());//删除 
		}
		if(q.size() && q[0]+q[q.size()-1]<need)//如果最大值都无法与它匹配,直接存入dp数组 
		{
			dp[x]=max(dp[x],q[0]);//比较大小 
			q.erase(q.begin());//删除 
		}
	}
	if(q.size())//还有一条边的话 
	{
		dp[x]=max(dp[x],q[q.size()-1]);//直接存入dp 
		q.pop_back();//删除 
	}
}
inline bool check(int mid)//check函数,inline不用管它,可有可没有,只是加了会稍微快点,这道题不影响 
{
	for(int i=1;i<=n;i++)//初始化 
		dp[i]=0;
	much=0;//初始化 
	dfs(1,0,mid);//爆搜整棵树 
	if(much>=m)//判断是否可以抠出m条链 
		return true;
	else return false;
}
bool cmp1(int x,int y)//从大到小排序 
{
	return x>y;
}
int main()
{
	int l=0,r,ans=0,sum=0,bj=1;//l,r表示二分边界,ans是答案,sum是所有边权的和,我们通过它计算极大值,bj判断是否是菊花图 
	n=read();
	m=read();
	for(int i=1;i<n;i++)
	{
		int x,y,u;
		x=read();
		y=read();
		u=read();
		a[i]=u;//如果是菊花图我们要用到所有边权所以记下来 
		if(x!=1)//判断是否是菊花图 
			bj=0;
		sum+=u;//计算sum 
		add(x,y,u);
		add(y,x,u);
	}
	r=sum/m+1;//计算极大值并且赋给r 
	if(bj)//是菊花图 
	{
		ans=sum;//先给答案付个极大值 
		sort(a+1,a+n,cmp1);//排序 
		for(int i=1;i<=m;i++)
			ans=min(ans,a[i]+a[2*m-i+1]);//找答案 
	}
	else //否则 
	{
		while(l<=r)//二分 
		{
			int mid=(l+r)>>1;
			if(check(mid))//check,二分尝龟 
			{
				ans=mid;//记录答案 
				l=mid+1;//调整边界 
			}
			else r=mid-1;
		}
	}
	printf("%d",ans);//输出答案 
	return 0;
}

这一题因为有不定长数组比较难调试,很可能会越界,所以要时刻注意数组大小。
如果有看不懂的欢迎留言。

你可能感兴趣的:(题目,图论,树上操作)