100130. 【USACO 2018 January Platinum】魚池逃脫Cow at Large

題目

地圖是一棵無根樹,pty從某個點出發,每次可以往某一條邊走,“逃離”定義爲可以在不遇到怪(點或邊上相遇)的情況下,到達葉子節點。
若干個葉子節點上一開始可以放若干個怪,每次這些怪都可以往某一邊走。
問對於每個點,最少要放多少怪才能保證抓住pty。
n70000n \leq 70000
實際上可以做n1000000n\leq 1000000


思考歷程

只知道暴力怎麼做,但是沒有部分分,所以也沒有去打。
考慮固定起點之後的答案:
將起點作爲根;
對於每個葉子,找到它和起點之間的中點(如果在邊上就取兒子)。這意味着如果選了這個葉子,那麼這個點的子樹都可以被覆蓋。
選擇最少的葉子節點,讓所有的葉子節點都被覆蓋。
於是在每個葉子和起點的中點上打標記,貪心地取。
O(n2)O(n^2)


正解

正解是個很NB的轉化。
題解將上面“葉子和起點的中點”,定義爲“控制點”。
選擇的葉子節點個數,等於選擇的控制點個數,等於位於控制點的子樹個數。
可以發現貪心地選擇後,覆蓋的點相當於將所有控制點的子樹合併起來,那麼答案就是連通塊個數。
用一種NB的方法表示子樹個數:
顯然,對於一棵子樹SS而言,vSdegv=2S1\sum_{v\in S}deg_v=2|S|-1
移項得vS2degv=1\sum_{v \in S}{2-deg_v}=1
於是我們對每個位於控制點下方(或控制點本身)的點vv,求2degv2-deg_v的和,就得到了子樹個數,也就是答案。

然後考慮根節點變化的情況。
設當前根節點爲xx
對於某個點yy,先預處理出離yy最近的葉子節點(不一定要子樹內)到yy的距離mndymnd_y
yy對答案有貢獻,當且僅當mndydis(x,y)mnd_y\leq dis(x,y)

直接點分治,假如xxyy分屬rootroot的兩個不同子樹,那麼yyxx有貢獻就要滿足mndydepydepxmnd_y-dep_y\leq dep_x。直接樹狀數組解決之,O(nlg2n)O(n\lg^2n)

其實還有更加優秀的做法。貢獻分子樹和子樹的補集計算,子樹隨便搞,子樹的補集就換根,拿個數據結構(好像樹狀數組就夠了)隨便維護一下。O(nlgn)O(n \lg n)
當然由於換根看上去不如點分治好寫,所以我沒有寫。


代碼

點分治。

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;
}

總結

真是個神仙的轉化思想……
我感覺我可以稱其爲“度數-子樹反演”。
感覺這種神仙東西要靠積累啊,考場真的很難想……

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章