[NOIP2018TGd1t3] 赛道修建

badge:标签-枚举,二分答案 标签-图论,树论 标签-动态规划 标签-模拟,暴力,搜索 标签-STL
这题完美地诠释了“CCF为了搞你已经越来越丧心病狂”这句话。
先来看一下丧心病狂的数据表:
datarange
出题人把大表格数据范围交错排布,使得要想拿满暴力分55pts,这一道题要写整整3个暴力
这是疯狂的数据分治主程序:

int main()
{
	int i,j,k,x,y,z;
	bool opt1=1,opt2=1,opt3=1,opt4=1;
	scanf("%d%d",&n,&m);
	for(i=1;i<=n-1;i++)
	{
		scanf("%d%d%d",&x,&y,&z);
		if(opt1&&x!=1&&y!=1)opt1=0;
		if(opt2&&y!=x+1)opt2=0;
		degree[x]++;degree[y]++;
		if(opt4&&(degree[x]>3||degree[y]>3))opt4=0;
		addedge(x,y,z);
		addedge(y,x,z);
	}
	if(m>1)opt3=0;
	if(opt1)juhua();
	else if(opt2)lian();
	else if(opt3)diameter();
	else if(opt4)binary();
	else work();
	return 0;
}

那我们一步一步来吧。

首先先看最难的暴力数据:菊花图。(即a_i=1的数据)
一看题面“最小长度最大”便知是二分答案。关键在于二分之后菊花图如何处理check函数。
菊花图中,简单路径最多只包含两条边。所以,我们计算答案的情况也只有两种:一条边自己,两条边组合。
假设我们二分的最小路径长度是num。
可以将所有的边排序,自己一条边就大于等于num的可以直接计入赛道条数。
剩下的边我们用两个指针维护一下,因为是有序的,所以可以一个指针i从剩下的边中最大的开始一个一个往下贪心选,另一个指针j从最小的边倒着枚举找能和i的权值加起来大于等于num的最小权值,把他俩扔进答案,最后看满足条件的路径条数是否够m。
因为当i增大,i这条边的权值会减小,为满足条件j这条边的权值一定会增大,是单调的。所以check函数的复杂度是O(n)。
复杂度O(nlogn)。
代码:

//subtask 1: ai=1
int weight[1000010],weightt[1000010];
bool cmp1(int x,int y){return x>y;}
bool chk1(int num)
{
	int i=1,j,k;
	//printf("checking %d:
",num);
	while(i<=m&&weight[i]>=num)i++;
	//printf("1~%d >num 
",i-1);
	if(weight[i-1]&&i>m)return true;
	if(weight[i]+weight[i+1]<num)return false;
	i--;j=i+1;k=i+2;
	while(k<=n-1&&weight[j]+weight[k]>=num)k++;
	if(k>=n)k=n-1;
	while(i<=m&&j<k)
	{
		while(weight[j]+weight[k]<num)k--;
		if(j<k)
		{
			//printf("%d mathes %d got %d
",j,k,weight[j]+weight[k]);
			i++;j++;k--;
		}
	}
	return i>=m;
}
void juhua()
{
	int i,j,k,ans=0,l,r,mid;
	for(i=1;i<=cnt;i+=2)
	{
		weight[(i+1)/2]=e[i].w;
	}
	sort(weight+1,weight+n,cmp1);
	//printf("seq:");for(i=1;i<=n-1;i++)printf("%d ",weight[i]);printf("
");
	l=0;r=weight[1]*2+1;
	while(l<=r)
	{
		mid=(l+r)>>1;
		if(chk1(mid))l=mid+1,ans=mid;
		else r=mid-1;
	}
	printf("%d
",ans);
	return;
}

令人智熄一连

然后是b_i=a_i+1的数据,不难看出这是一条
照样二分最小赛道长度。但是这次check函数不能对边排序了,因为在链上边的位置是确定的。
题目转化成了:在一个序列中选出m个互不相交的区间,使得这些区间的最小和最大。
从头到尾贪心做一遍即可。
复杂度O(nlogn)。
代码:

//subtask 2: bi=ai+1
bool chk2(int num)
{
	int i,j=0,k,val=0;
	for(i=1;i<=n-1;i++)
	{
		val+=weight[i];
		if(val>=num)
		{
			j++,val=0;
			if(j>=m)return true;
		}
	}
	return j>=m;
}
void lian()
{
	int i,j,k,ans=0,l,r,mid;
	for(i=1;i<=n-1;i++)
	{
		for(j=in[i];j;j=e[j].last)
		if(e[j].v==i+1)break;
		weight[i]=e[j].w;
	}
	//printf("seq:");for(i=1;i<=n-1;i++)printf("%d ",weight[i]);printf("
");
	l=0;r=0x7f7f7f7f;
	while(l<=r)
	{
		mid=(l+r)>>1;
		if(chk2(mid))l=mid+1,ans=mid;
		else r=mid-1;
	}
	printf("%d
",ans);
	return;
}

令人智熄二连

第三个出场的是m=1的情况。这时候我们只需要求出树的最长链,即直径
求树的直径,一种方法是两遍dfs,另一种方法是树形DP。
两遍dfs:先任取一点,dfs记下树上离他最远的结点,再以这个点为起点,找到树上离他最远的结点,记下的两个结点之间的距离就是树的直径长度。
树形DP:dp[i]表示以i结点为根的子树内路径的最大权值,转移的时候记录一个最大值和一个次大值,答案更新于每一次dp中,为max{子树内最大值+次大值},而非最后dp数组里的某一个元素。
复杂度均为O(n)。
采用的是树形DP法,因为码量小qwq:

//subtask 3: diameter
int length;//答案保存在这里
int dec(int cur,int fa)//函数返回的就是上面所说dp[cur]
{
	int i,t,a=0,b=0;//a-最大值,b-次大值
	for(i=in[cur];i;i=e[i].last)
	{
		t=e[i].v;
		if(t==fa)continue;
		b=maxf(b,dec(t,cur)+e[i].w);
		if(b>a)a^=b^=a^=b;
	}
	length=maxf(length,a+b);//答案更新在这里,而不是最后的dp数组
	if(b>a)a^=b^=a^=b;
	return a;
}
void diameter()
{
	length=0;
	dec(1,0);
	printf("%d
",length);
	return;
}

令人智熄三连

到此为止,我们喜提200行代码+55pts的分数,成功智熄

是时候让正解上场了。

正解:二分答案+树形DP(也不完全算DP qwq)
首先观察树。树有一个特性,就是除根结点外,每个结点往头顶上父亲连的边是唯一的。这成为我们状态转移的关键。
正因如此,我们把和结点cur连的边分成两类:和儿子连的边,和父亲连的边。
为描述方便,我们定义从一个结点出发,在每一个子树内只经过其根结点和一个儿子结点的链为半链
经过cur的赛道可以是两条从cur出发的半链+结点cur组成,我们称这是第一类路径;也可以是一条从cur出发的半链+一条以父亲结束的的链+结点cur组成,我们称这是第二类路径;还可以是一条单独的半链,称为第三类路径
既然我们要树形DP,就得考虑由结点向上贡献。
而结点cur能往上贡献的,只有在子树内选完第一类和第三类路径之后,剩下的最长半链。
我们记dp[cur]表示以cur为根的子树内,选完第一类和第三类路径之后剩下的最长半链长度+cur和父亲之间的距离。也就是cur子树在自己用完之后能向他的父亲贡献多少长度
怎么实现转移呢?
我们把cur结点所有儿子的dp值排序。为了尽可能节省距离,单个一条半链就能满足长度要求的,直接选走。剩下的,有点类似菊花图式的处理,找两条半链组成第一类路径,也把这些统统选走。最后,剩下的半链里,最长的半链加上cur和父亲的距离,贡献给父亲。
每选走一条路径,合法的赛道数就+1。
check函数的树形DP就写完了。
二分答案不用说了吧~
代码:

//subtask 4: binary(can also be used in subtask5 just without multisets but I made it out here)
int dp[100010];
int container[100010],tail;
int goal;
int VIS[100010];
void DFS(int cur,int fa,int div)//当前结点,父亲,二分的最小赛道长度
{
	int i,j,t,lim,l,r,mid,ans;
	for(i=in[cur];i;i=e[i].last)
	{
		t=e[i].v;
		if(t==fa)continue;
		dp[t]=e[i].w;
		DFS(t,cur,div);
	}
	tail=0;
	for(i=in[cur];i;i=e[i].last)
	{
		t=e[i].v;
		if(t==fa)continue;
		container[++tail]=dp[t];
	}
	sort(container+1,container+1+tail);//存到container里排序
	for(i=tail;i>=1;i--)
	{
		if(container[i]>=div)goal--;//goal是目标赛道数
		else break;
	}
	lim=i;
	for(i=1;i<=lim;i++)
	{
		if(VIS[i]!=cur)//减少memset次数,节省时间。VIS[]记录的是结点编号,这样每个结点dfs到的点都是专属这个点的。
		{
			ans=lim+1;
			l=i+1;r=lim;
			while(l<=r)//类似菊花处理,这里需要再来一次二分,找到能和i这条半链组成第一类路径的最小半链
			{
				mid=(l+r)>>1;
				if(container[i]+container[mid]>=div)
				r=mid-1,ans=mid;
				else l=mid+1;
			}
			while(VIS[ans]==cur&&ans<=lim)ans++;
			if(ans>=lim+1)continue;
			VIS[i]=VIS[ans]=cur;
			goal--;
		}
	}
	for(i=lim;i>=0;i--)if(VIS[i]!=cur){dp[cur]+=container[i];break;}
	return;
}
bool chk3(int num,int rt)
{
	memset(VIS,0,sizeof(VIS));
	goal=m;
	DFS(rt,0,num);
	return goal<=0;
}
void binary()
{
	int i,j,k,l,r,mid,ans,root;
	root=rand()%n+1;//随机选根,不过没啥用
	ans=0,l=0,r=0x7f7f7f7f;
	while(l<=r)
	{
		mid=(l+r)>>1;
		if(chk3(mid,root))l=mid+1,ans=mid;
		else r=mid-1;
	}
	printf("%d
",ans);
	return;
}

当然你也看到了,这里check函数又出现了一次二分。所以复杂度是O(nlog^2n)。
令人智熄四连

这就结束了?
当然不。
你要是看过我最一开始的数据分治main函数,会发现我写了五个。
主要是我在题解里发现了一种神奇的用multiset的做法。
思路一样。
代码:

//subtask 5: regular(sol4 + multiset = a faster program)
int target;
multiset<int> s[100010];
multiset<int>::iterator it;
int dfs(int cur,int fa,int lim)
{
	s[cur].clear();
	int i,t,val,ans;
	for(i=in[cur];i;i=e[i].last)
	{
		t=e[i].v;
		if(t==fa)continue;
		val=dfs(t,cur,lim)+e[i].w;
		if(val>=lim)target++;//第三类路径直接选走
		else s[cur].insert(val);//否则加入multiset待命
	}
	ans=0;
	while(!s[cur].empty())
	{
		if(s[cur].size()==1)
		{
			return maxf(ans,*s[cur].begin());
		}
		it=s[cur].lower_bound(lim-*s[cur].begin());//lower_bound就是之前那个check函数里的二分,简洁地解决。
		if(it==s[cur].begin()&&s[cur].count(*it)==1)it++;//如果很不幸,这条半链刚好是目标长度的一半,也就是自己和自己组合,那么迭代器++,让他这个最小值和次小值组合。
		if(it==s[cur].end())//如果没有下一个了,就结束吧。
		{
			ans=maxf(ans,*s[cur].begin());
			s[cur].erase(s[cur].find(*s[cur].begin()));
		}
		else//否则就把这两个组合成第一类路径。
		{
			target++;
			s[cur].erase(s[cur].find(*it));
			s[cur].erase(s[cur].find(*s[cur].begin()));
		}
	}
	return ans;
}
bool chk4(int num)
{
	target=0;
	dfs(1,0,num);
	return target>=m;
}
void work()
{
	int l,r,mid,ans=0;
	length=0;dec(1,0);//这里跨subtask调用了前面的直径函数来优化二分边界,这可以让你卡过最后一个测试点,从95变成100!!!
	l=0;r=length;
	while(l<=r)
	{
		mid=(l+r)>>1;
		if(chk4(mid))l=mid+1,ans=mid;
		else r=mid-1;
	}
	printf("%d
",ans);
	return;
}

是不是STL改变了代码?!!
stl大法好。
最后说一句:
二分上界不改成直径,第20个点会TLE!!!
二分上界不改成直径,第20个点会TLE!!!
二分上界不改成直径,第20个点会TLE!!!

NOIP2018d1t3
到此结束。

完整code:

//This is a good practice of BAOLI ability.
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<ctime>
#include<cstdlib>
#include<set>
using namespace std;
int n,m;
int maxf(int x,int y){return x>y?x:y;}
int minf(int x,int y){return x<y?x:y;}
struct edge
{
	int v,last,w;
}e[2000010];
int in[1000010],cnt=0;
int degree[1000010];
void addedge(int x,int y,int z)
{
	e[++cnt].v=y;
	e[cnt].w=z;
	e[cnt].last=in[x];
	in[x]=cnt;
}
//subtask 1: ai=1
int weight[1000010],weightt[1000010];
bool cmp1(int x,int y){return x>y;}
bool chk1(int num)
{
	int i=1,j,k;
	//printf("checking %d:
",num);
	while(i<=m&&weight[i]>=num)i++;
	//printf("1~%d >num 
",i-1);
	if(weight[i-1]&&i>m)return true;
	if(weight[i]+weight[i+1]<num)return false;
	i--;j=i+1;k=i+2;
	while(k<=n-1&&weight[j]+weight[k]>=num)k++;
	if(k>=n)k=n-1;
	while(i<=m&&j<k)
	{
		while(weight[j]+weight[k]<num)k--;
		if(j<k)
		{
			//printf("%d mathes %d got %d
",j,k,weight[j]+weight[k]);
			i++;j++;k--;
		}
	}
	return i>=m;
}
void juhua()
{
	int i,j,k,ans=0,l,r,mid;
	for(i=1;i<=cnt;i+=2)
	{
		weight[(i+1)/2]=e[i].w;
	}
	sort(weight+1,weight+n,cmp1);
	//printf("seq:");for(i=1;i<=n-1;i++)printf("%d ",weight[i]);printf("
");
	l=0;r=weight[1]*2+1;
	while(l<=r)
	{
		mid=(l+r)>>1;
		if(chk1(mid))l=mid+1,ans=mid;
		else r=mid-1;
	}
	printf("%d
",ans);
	return;
}
/*
//testdata:
5 2
1 2 1
1 3 1
1 4 3
1 5 4
ans:4
*/
//subtask 2: bi=ai+1
bool chk2(int num)
{
	int i,j=0,k,val=0;
	for(i=1;i<=n-1;i++)
	{
		val+=weight[i];
		if(val>=num)
		{
			j++,val=0;
			if(j>=m)return true;
		}
	}
	return j>=m;
}
void lian()
{
	int i,j,k,ans=0,l,r,mid;
	for(i=1;i<=n-1;i++)
	{
		for(j=in[i];j;j=e[j].last)
		if(e[j].v==i+1)break;
		weight[i]=e[j].w;
	}
	//printf("seq:");for(i=1;i<=n-1;i++)printf("%d ",weight[i]);printf("
");
	l=0;r=0x7f7f7f7f;
	while(l<=r)
	{
		mid=(l+r)>>1;
		if(chk2(mid))l=mid+1,ans=mid;
		else r=mid-1;
	}
	printf("%d
",ans);
	return;
}
/*
//testdata:
5 2
1 2 1
2 3 2
3 4 3
4 5 4
ans:4
*/
//subtask 3: diameter
int length;
int dec(int cur,int fa)
{
	int i,t,a=0,b=0;
	for(i=in[cur];i;i=e[i].last)
	{
		t=e[i].v;
		if(t==fa)continue;
		b=maxf(b,dec(t,cur)+e[i].w);
		if(b>a)a^=b^=a^=b;
	}
	length=maxf(length,a+b);
	if(b>a)a^=b^=a^=b;
	return a;
}
void diameter()
{
	length=0;
	dec(1,0);
	printf("%d
",length);
	return;
}
/*
//testdata:
7 1
1 2 1
2 3 3
2 4 2
1 5 4
5 6 1
6 7 2
ans:11
*/
//subtask 4: binary(can also be used in subtask5 just without multisets but I made it out here)
int dp[100010];
int container[100010],tail;
int goal;
int VIS[100010];
void DFS(int cur,int fa,int div)
{
	int i,j,t,lim,l,r,mid,ans;
	for(i=in[cur];i;i=e[i].last)
	{
		t=e[i].v;
		if(t==fa)continue;
		dp[t]=e[i].w;
		DFS(t,cur,div);
	}
	tail=0;
	for(i=in[cur];i;i=e[i].last)
	{
		t=e[i].v;
		if(t==fa)continue;
		container[++tail]=dp[t];
	}
	sort(container+1,container+1+tail);
	for(i=tail;i>=1;i--)
	{
		if(container[i]>=div)goal--;
		else break;
	}
	lim=i;
	for(i=1;i<=lim;i++)
	{
		if(VIS[i]!=cur)
		{
			ans=lim+1;
			l=i+1;r=lim;
			while(l<=r)
			{
				mid=(l+r)>>1;
				if(container[i]+container[mid]>=div)
				r=mid-1,ans=mid;
				else l=mid+1;
			}
			while(VIS[ans]==cur&&ans<=lim)ans++;
			if(ans>=lim+1)continue;
			VIS[i]=VIS[ans]=cur;
			goal--;
		}
	}
	for(i=lim;i>=0;i--)if(VIS[i]!=cur){dp[cur]+=container[i];break;}
	return;
}
bool chk3(int num,int rt)
{
	memset(VIS,0,sizeof(VIS));
	goal=m;
	DFS(rt,0,num);
	return goal<=0;
}
void binary()
{
	int i,j,k,l,r,mid,ans,root;
	root=rand()%n+1;
	ans=0,l=0,r=0x7f7f7f7f;
	while(l<=r)
	{
		mid=(l+r)>>1;
		if(chk3(mid,root))l=mid+1,ans=mid;
		else r=mid-1;
	}
	printf("%d
",ans);
	return;
}
/*
//testdata:
7 1 
1 2 10 
1 3 5 
2 4 9 
2 5 8 
3 6 6 
3 7 7
ans:31
*/
//subtask 5: regular(sol4 + multiset = a faster program)
int target;
multiset<int> s[100010];
multiset<int>::iterator it;
int dfs(int cur,int fa,int lim)
{
	s[cur].clear();
	int i,t,val,ans;
	for(i=in[cur];i;i=e[i].last)
	{
		t=e[i].v;
		if(t==fa)continue;
		val=dfs(t,cur,lim)+e[i].w;
		if(val>=lim)target++;
		else s[cur].insert(val);
	}
	ans=0;
	while(!s[cur].empty())
	{
		if(s[cur].size()==1)
		{
			return maxf(ans,*s[cur].begin());
		}
		it=s[cur].lower_bound(lim-*s[cur].begin());
		if(it==s[cur].begin()&&s[cur].count(*it)==1)it++;
		if(it==s[cur].end())
		{
			ans=maxf(ans,*s[cur].begin());
			s[cur].erase(s[cur].find(*s[cur].begin()));
		}
		else
		{
			target++;
			s[cur].erase(s[cur].find(*it));
			s[cur].erase(s[cur].find(*s[cur].begin()));
		}
	}
	return ans;
}
bool chk4(int num)
{
	target=0;
	dfs(1,0,num);
	return target>=m;
}
void work()
{
	int l,r,mid,ans=0;
	length=0;dec(1,0);
	l=0;r=length;
	while(l<=r)
	{
		mid=(l+r)>>1;
		if(chk4(mid))l=mid+1,ans=mid;
		else r=mid-1;
	}
	printf("%d
",ans);
	return;
}
/*
//testdata:
9 3 
1 2 6 
2 3 3 
3 4 5 
4 5 10 
6 2 4 
7 2 9 
8 4 7 
9 4 4
ans:15
*/
int main()
{
	int i,j,k,x,y,z;
	bool opt1=1,opt2=1,opt3=1,opt4=1;
	scanf("%d%d",&n,&m);
	for(i=1;i<=n-1;i++)
	{
		scanf("%d%d%d",&x,&y,&z);
		if(opt1&&x!=1&&y!=1)opt1=0;
		if(opt2&&y!=x+1)opt2=0;
		degree[x]++;degree[y]++;
		if(opt4&&(degree[x]>3||degree[y]>3))opt4=0;
		addedge(x,y,z);
		addedge(y,x,z);
	}
	if(m>1)opt3=0;
	if(opt1)juhua();
	else if(opt2)lian();
	else if(opt3)diameter();
	else if(opt4)binary();
	else work();
	return 0;
}

这题基本把树上暴力都搞了一遍,不足的是没有生成树,树上倍增,线段树,树剖

原文地址:https://www.cnblogs.com/Rain142857/p/11708387.html