zoukankan      html  css  js  c++  java
  • [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;
    }
    

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

  • 相关阅读:
    数据分析普遍存在的方法及理论有哪些?-数据分析师
    R语言 我要如何开始R语言_数据分析师
    R语言 我要如何开始R语言_数据分析师
    Root(hdu5777+扩展欧几里得+原根)
    CF#256(Div.2) A. Rewards
    HDU-4031-Attack(树状数组)
    POJ 3253 Fence Repair(哈夫曼树)
    Python的Django框架中的Context使用
    C++中面向对象的理解
    统计输入的单词中有几个长度大于n的,n是自己指定的,用函数对象实现
  • 原文地址:https://www.cnblogs.com/Rain142857/p/11708387.html
Copyright © 2011-2022 走看看