题目大意:
给一棵树。求用最少的链覆盖这棵树(链不能相交),在这个基础上求最长的链最短可以是多少。
n<=10000
题解:
肯定先处理第一问:
答案:$sum_(du[i]-1)/2+1$
证明:
1.对于一个非根的节点x,x的每一个到儿子的边必须被覆盖。
只有两种可能:要么这个链不超过x,要么从x头上进来。
发现,从x头上进来的链有且只有一个。
如果x的儿子数量是偶数,肯定只能把边两两配对。如果奇数,那么剩下这一个就和从x头上下来的链在一起即可。
儿子数du[i]-1,花费:(du[i]-1)/2
2.对于根节点rt
儿子偶数的话,同理,如果儿子是奇数的话,由于rt没有从头上下来的链,只能单独算作一个链。
所以答案是:(du[i]+1)/2,即(du[i]-1)/2+1
证毕。
第二问:
二分答案显然。
判定mid:我们既要满足最长小于等于mid,还要满足贪心方法依然能使得链数最小。
根据刚才第一问的分析,子树x的对于儿子的覆盖情况无非就那么两种。而且x内的覆盖情况对其他子树的影响只有从x上去的那一条链。
而比较麻烦的是x头上下来的链。发现,这个链只有一个。
所以,我们在x子树内合法覆盖、用的链最少的前提下,想头上的链越短越有利。
f[x]表示 ,x头上的链的最短长度。
或者更准确地说,x头上的链进入x子树后,还要延伸多少。
dfs时,
对于x,我们先求出f[son]
然后想办法求f[x]
把f[son]+1计入mem数组,然后从小到大排序。
如果x有奇数个儿子,二分f[x]位置,然后大小配对。
如果x有偶数个儿子,如果可以大小直接配对,完事大吉,f[x]=0,然后回溯。
否则,二分f[x]位置,然后把剩下的最大的f[son]+1独自一条链(这样也可以满足方案数最小的)
继续大小配对判断。
当然,对于根节点,f[x]没有意义了,就检查一下,如果偶数个儿子,直接配对。奇数个,去掉最大的配对。
任何时候,如果f[son]+1>mid,或者f[x]不存在,或者根节点匹配失败,都return false
所以,其实是大二分,然后dfs贪心的时候再二分。
代码:
注意两个mid不要弄混。。。。
#include<bits/stdc++.h> using namespace std; typedef long long ll; const int N=10000+5; int n; struct node{ int nxt,to; }e[2*N]; int hd[N],cnt; void add(int x,int y){ e[++cnt].nxt=hd[x]; e[cnt].to=y; hd[x]=cnt; } int son[N],tot; int mid; int ans1,ans2; int du[N]; bool fl; int f[N]; void dfs(int x,int fa){ //cout<<x<<" "<<fa<<endl; if(!fl) return;//warning!!! for(int i=hd[x];i;i=e[i].nxt){ int y=e[i].to; if(y==fa) continue; dfs(y,x); } tot=0; if(!fl) return;//warning!! for(int i=hd[x];i;i=e[i].nxt){ int y=e[i].to; if(y==fa) continue; son[++tot]=f[y]+1; } sort(son+1,son+tot+1); //cout<<x<<" 's son "<<endl; //for(int i=1;i<=tot;i++) cout<<son[i]<<" ";cout<<endl; if(son[tot]>mid) {fl=false;return;}//warning!!! if(x==1){ bool can=true; if(tot%2==1) tot--; for(int i=1;i<=tot/2;i++){ if(son[i]+son[tot-i+1]>mid){ can=false;break; } } if(!can) fl=false; } else{ if(tot%2==0){ bool can=true; for(int i=1;i<=tot/2;i++){ if(son[i]+son[tot-i+1]>mid){ can=false;break; } } if(can) { f[x]=0;return;//warning!!!!! has returned } } f[x]=-1;//warning!!! int L=1,R=tot; while(L<=R){ int M=(L+R)>>1; int up=tot; if(M==tot) up--; if(tot%2==0) up--; bool can=true; for(int i=1;i<=up;i++){ if(i==M) continue; if(son[i]+son[up]>mid) { can=false;break; } up--; } //cout<<x<<" M "<<M<<" : "<<can<<endl; if(can) f[x]=son[M],R=M-1; else L=M+1; } if(f[x]==-1) { fl=false; } } } bool che(){ fl=true; //cout<<" mid "<<mid<<" ------------------"<<endl; memset(f,0,sizeof f); dfs(1,0); //cout<<" ff "<<endl; //for(int i=1;i<=n;i++){ /// cout<<i<<" : "<<f[i]<<endl; //} return fl; } int main(){ scanf("%d",&n);int x,y; for(int i=1;i<=n-1;i++){ scanf("%d%d",&x,&y);add(x,y);add(y,x); du[x]++,du[y]++; } for(int i=1;i<=n;i++){ ans1+=(du[i]-1)/2; } ans1++; int l=1,r=n-1; while(l<=r){ mid=(l+r)>>1; if(che()){ ans2=mid,r=mid-1; } else l=mid+1; } printf("%d %d",ans1,ans2); return 0; }
总结:
树上贪心肯定和儿子有关系。
观察覆盖x到儿子的边的两种方案。结合二分贪心
$upda:2018.12.29$
NOIP考到了这个题目,几乎是原题,而且没有第一问。。。。
大致的思路是对的。
但是二分里面的二分没有写上,,(反而写了儿子个数平方套set?)
看来写题目不光是思路要有,关键的trick也不能放过
(对了感谢ywy_c_asm神犇,多亏了他给我推荐SZN这个题,否则NOIP就GG了)