題目
地圖是一棵無根樹,pty從某個點出發,每次可以往某一條邊走,“逃離”定義爲可以在不遇到怪(點或邊上相遇)的情況下,到達葉子節點。
若干個葉子節點上一開始可以放若干個怪,每次這些怪都可以往某一邊走。
問對於每個點,最少要放多少怪才能保證抓住pty。
實際上可以做
思考歷程
只知道暴力怎麼做,但是沒有部分分,所以也沒有去打。
考慮固定起點之後的答案:
將起點作爲根;
對於每個葉子,找到它和起點之間的中點(如果在邊上就取兒子)。這意味着如果選了這個葉子,那麼這個點的子樹都可以被覆蓋。
選擇最少的葉子節點,讓所有的葉子節點都被覆蓋。
於是在每個葉子和起點的中點上打標記,貪心地取。
正解
正解是個很NB的轉化。
題解將上面“葉子和起點的中點”,定義爲“控制點”。
選擇的葉子節點個數,等於選擇的控制點個數,等於位於控制點的子樹個數。
可以發現貪心地選擇後,覆蓋的點相當於將所有控制點的子樹合併起來,那麼答案就是連通塊個數。
用一種NB的方法表示子樹個數:
顯然,對於一棵子樹而言,
移項得
於是我們對每個位於控制點下方(或控制點本身)的點,求的和,就得到了子樹個數,也就是答案。
然後考慮根節點變化的情況。
設當前根節點爲:
對於某個點,先預處理出離最近的葉子節點(不一定要子樹內)到的距離
對答案有貢獻,當且僅當
直接點分治,假如和分屬的兩個不同子樹,那麼對有貢獻就要滿足。直接樹狀數組解決之,。
其實還有更加優秀的做法。貢獻分子樹和子樹的補集計算,子樹隨便搞,子樹的補集就換根,拿個數據結構(好像樹狀數組就夠了)隨便維護一下。。
當然由於換根看上去不如點分治好寫,所以我沒有寫。
代碼
點分治。
using namespace std;
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cassert>
#define N 70010
#define INF 1000000000
int n;
struct EDGE{
int to;
EDGE *las;
bool bz;
} e[N*2];
int ne;
EDGE *last[N];
#define rev(ei) (e+(int((ei)-e)^1))
int deg[N];
int mnd[N];
void bfs(){
memset(mnd,255,sizeof mnd);
static int q[N];
int tail=0;
for (int i=1;i<=n;++i)
if (deg[i]==1){
mnd[i]=0;
q[++tail]=i;
}
for (int i=1;i<=tail;++i){
int x=q[i];
for (EDGE *ei=last[x];ei;ei=ei->las)
if (mnd[ei->to]==-1){
mnd[ei->to]=mnd[x]+1;
q[++tail]=ei->to;
}
}
}
int siz[N],all;
void getsiz(int x,int fa){
siz[x]=1;
for (EDGE *ei=last[x];ei;ei=ei->las)
if (ei->to!=fa && ei->bz==0){
getsiz(ei->to,x);
siz[x]+=siz[ei->to];
}
}
int getG(int x,int fa){
bool is=all-siz[x]<=all>>1;
for (EDGE *ei=last[x];ei;ei=ei->las)
if (ei->to!=fa && ei->bz==0){
int t=getG(ei->to,x);
if (t)
return t;
is&=siz[ei->to]<=all>>1;
}
return is?x:0;
}
int dep[N];
void init(int x,int fa){
for (EDGE *ei=last[x];ei;ei=ei->las)
if (ei->to!=fa && ei->bz==0){
dep[ei->to]=dep[x]+1;
init(ei->to,x);
}
}
int lis[N],k;
int t[N];
void clear(int n){memset(t,0,sizeof(int)*(n+2));}
void add(int x,int c){++x;for (;x-1<=all;x+=x&-x) t[x]+=c;}
int query(int x){++x;int r=0;for (;x;x-=x&-x) r+=t[x];return r;}
int ans[N];
void calc(int x,int fa){
if (deg[x]!=1)
ans[x]+=query(dep[x]);
for (EDGE *ei=last[x];ei;ei=ei->las)
if (ei->to!=fa && ei->bz==0)
calc(ei->to,x);
}
void insert(int x,int fa){
add(max(mnd[x]-dep[x],0),2-deg[x]);
for (EDGE *ei=last[x];ei;ei=ei->las)
if (ei->to!=fa && ei->bz==0)
insert(ei->to,x);
}
void divide(int x){
getsiz(x,0);
all=siz[x],x=getG(x,0);
dep[x]=0,init(x,0);
k=0;
for (EDGE *ei=last[x];ei;ei=ei->las)
if (ei->bz==0)
lis[++k]=ei->to;
clear(all);
for (int i=1;i<=k;++i){
int y=lis[i];
calc(y,x);
insert(y,x);
}
if (deg[x]!=1)
ans[x]+=query(dep[x]);
clear(all);
for (int i=k;i>=1;--i){
int y=lis[i];
add(max(mnd[x]-dep[x],0),2-deg[x]);
calc(y,x);
add(max(mnd[x]-dep[x],0),-(2-deg[x]));
insert(y,x);
}
for (EDGE *ei=last[x];ei;ei=ei->las)
if (ei->bz==0){
ei->bz=rev(ei)->bz=1;
divide(ei->to);
}
}
int main(){
scanf("%d",&n);
for (int i=1;i<n;++i){
int u,v;
scanf("%d%d",&u,&v);
e[ne]={v,last[u],0};
last[u]=e+ne++;
e[ne]={u,last[v],0};
last[v]=e+ne++;
deg[u]++,deg[v]++;
}
bfs();
for (int i=1;i<=n;++i)
if (deg[i]==1)
ans[i]=1;
divide(1);
for (int i=1;i<=n;++i)
printf("%d\n",ans[i]);
return 0;
}
總結
真是個神仙的轉化思想……
我感覺我可以稱其爲“度數-子樹反演”。
感覺這種神仙東西要靠積累啊,考場真的很難想……