badge:
这题完美地诠释了“CCF为了搞你已经越来越丧心病狂”这句话。
先来看一下丧心病狂的数据表:
出题人把大表格数据范围交错排布,使得要想拿满暴力分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;
}
这题基本把树上暴力都搞了一遍,不足的是没有生成树,树上倍增,线段树,树剖。