第1.5版
關於樹的重心
- 有配圖
- 有文字講解
關於Godfather
- 有AC代碼
- 文字說明
關於centroid
- 本人蒟蒻這晚上只寫了55pts(以后會有AC代碼的)
- 新增20pts二叉樹
- 手把手教你分析時間復雜度
- 考場寫暴力得省一心得
- 有了樹的重心的性質拓展
序
閑登小閣看新晴
樹的重心
在樹的問題中,會遇到一些題目。時問你樹的重心的,不會就涼了,就像CSP-S2019
定義
度娘的定義是這樣的 : 找到一個點,其所有的子樹中最大的子樹節點數最少,那么這個點就是這棵樹的重心,刪去重心后,生成的多棵樹盡可能平衡。
性質
- 樹中所有點到某個點的距離和中,到重心的距離和是最小的,如果有兩個重心,他們的距離和一樣。
- 把兩棵樹通過一條邊相連,新的樹的重心在原來兩棵樹重心的連線上。
- 一棵樹添加或者刪除一個節點,樹的重心最多只移動一條邊的位置。
- 一棵樹最多有兩個重心,且相鄰。
- 事實上,有個性質度娘沒說,在centroid中提到了

Godfather
這道題
我們可以用樹形dp來解決這個問題令\(d_i\)表示你把他轉為有根樹之后以\(i\)為根的子樹所包含節點個數
采用鄰接表存邊,我們能可以十分簡單的解決此問題
我們判斷一個點是否樹的重心的方法是去掉這個點所有子樹的最大連通塊最小
事實上,這些連通塊就是,這個節點上面的部分和以這個點所有兒子為根的子樹的最大值
這是AC代碼
#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
#include <algorithm>
using namespace std;
const int Maxn=50005;
int n,d[Maxn],h[Maxn],cnt,u,v,a[Maxn],num,min1;
struct Edge{
int fr,to,lac;
void insert(int x,int y){
fr=x;
to=y;
lac=h[x];
h[x]=cnt++;
}
}edge[Maxn*2];
int read(){
int x=0;
char ch=getchar();
while(ch<'0'||ch>'9') ch=getchar();
while(ch<='9'&&ch>='0'){
x=(x<<1)+(x<<3)+(ch-'0');
ch=getchar();
}
return x;
}
void dfs(int u,int fa){
//樹形dp
int max1=0;d[u]=1;
//就是他的尺寸
for(int i=h[u];i!=-1;i=edge[i].lac){
if(edge[i].to==fa) continue;
dfs(edge[i].to,u);
d[u]+=d[edge[i].to];
//加上子樹
max1=max(max1,d[edge[i].to]);
}
max1=max(max1,n-d[u]);
//得出最大連通塊
if(max1<min1){
min1=max1;
num=1;
a[num]=u;
}
else if(max1==min1) a[++num]=u;
return ;
}
int main(){
//freopen("MRK.in","r",stdin);
n=read();
memset(h,-1,sizeof h);
for(int i=1;i<n;i++){
u=read();v=read();
edge[cnt].insert(u,v);
edge[cnt].insert(v,u);
}
//建樹
min1=n+1;
dfs(1,-1);
sort(a+1,a+1+num);
for(int i=1;i<=num;i++) printf("%d ",a[i]);
return 0;
}
Q:為什么存答案的的數組要開到很大?樹的重心不最多兩個嗎
A: 你一開始時求出的不一定是樹的重心,也就可能會很多,開太小會RE
Q:為什么答案要進行排序?
A: 樹形dp ,不保證字典序最小
Q: min1 有什么用?為什么要給n+1
A: min1是記錄最大連通塊的最小點數,剛開始要給最小值
這里我們給出第五個性質
- 對於一個大小為 n 的樹與任意一個樹中結點 c,稱 c 是該樹的重心當且僅當在樹中刪去 ccc 及與它關聯的邊后,分裂出的所有子樹的大小均不超過 $[\frac {n} {2}] $(其中 $[\frac {n} {2}] $是下取整函數)。對於包含至少一個結點的樹,它的重心只可能有 1 或 2 個。
於是我們在41至46行進行優化,避免多次記錄無用信息
if(max1<=min1) a[++num]=u;
這會快一些
CSP-S 2019 D2T3 樹的重心
這題十分的坑,以至於本蒟蒻在考場上只寫出了鏈的特判。。。靠着我逆天的暴力。。
我們能對於40pts的小數據做一個簡單的分析
話說時間復雜度是 \(O(VE)\),為啥呢對於隔斷每個邊,會跑一個O(V)的樹的重心
- 建圖
- 枚舉每次斷邊
- 對於每次斷邊分出的兩個圖分別跑樹的重心
- 這里寫要注意的是n他不是本來的n,而應該先跑一邊bfs ,跑出分出的樹的大小,注意分開后的樹
- 統計答案
在考慮鏈的15pts
- 建圖
- 跑一個鏈,做一個nxt[],表示后驅,做好映射
- 然后枚舉每次斷的邊,找到節點中間的節點,即為重心
再想想二叉樹的20pts
事實上,對於二叉樹,有這么幾個性質
-
刪除葉子節點連邊,根會成為重心之一。
-
刪除某個除根節點外的節點連邊時,這個點會成為重心之一。
-
刪除根的左子樹某條邊時,根的右兒子會成為重心之一,對稱同理。
正解
- 待補充
#include <iostream>
#include <bits/stdc++.h>
#include<queue>
using namespace std;
const int Maxn=262144;
int n,d[Maxn],t,h[Maxn],cnt,u,v,a[Maxn],num,min1,nown;
long long ans;
typedef long long ll;
bool vis[Maxn];
struct Node{
int fr,to,lac;
bool can;
void insert(int x,int y){fr=x;to=y;lac=h[x];h[x]=cnt++;}
}edge[2*Maxn];
int read(){
int x=0;
char ch=getchar();
while(ch<'0'||ch>'9') ch=getchar();
while(ch<='9'&&ch>='0'){
x=(x<<1)+(x<<3)+(ch-'0');
ch=getchar();
}
return x;
}
void dfs(int u,int fa){
//樹形dp
int max1=0;d[u]=1;
//就是他的尺寸
for(int i=h[u];i!=-1;i=edge[i].lac){
if(edge[i].to==fa||edge[i].can) continue;//斷了,不能走
dfs(edge[i].to,u);
d[u]+=d[edge[i].to];
//加上子樹
max1=max(max1,d[edge[i].to]);
}
max1=max(max1,nown-d[u]);
//得出最大連通塊
if(max1<=min1) a[++num]=u;
return ;
}
void bfs(int u){
//從源節點開始
nown=0;memset(vis,0,sizeof vis);
queue<int> q;
q.push(u);vis[u]=1;
while(!q.empty()){
nown++;
int fr=q.front();
q.pop();
for(int i=h[fr];i!=-1;i=edge[i].lac){
if(vis[edge[i].to]||edge[i].can) continue;
q.push(edge[i].to);
vis[edge[i].to]=1;
}
}
}
int main() {
freopen("centroid12.in","r",stdin);
t=read();
while(t--){
n=read();
if(n==49991){
ans=0;cnt=0;
memset(vis,0,sizeof vis);
memset(d,0,sizeof d);
memset(h,-1,sizeof h);
for(int i=1;i<n;i++){
u=read();v=read();
edge[cnt].insert(u,v);
edge[cnt].insert(v,u);
d[u]++,d[v]++;
}
for(int i=1;i<+n;i++)
if(d[i]==1){
a[1]=i;
break;
}
vis[a[1]]=1;
int i=1;
while(i<n){
int v=-1;
for(int q=h[a[i]];q!=-1;q=edge[q].lac){
if(!vis[edge[q].to]){
v=edge[q].to;
vis[edge[q].to]=1;
break;
}
}
i++;
a[i]=v;
}
for(int k=1;k<n;k++){
if(!((1+k)&1)){
ans+=a[(1+k)/2];
}
else {
ans+=(a[(1+k)/2]+a[(1+k)/2+1]);
}
if(!((n+k+1)&1)){
ans+=a[(n+k+1)/2];
}
else {
ans+=(a[(n+k+1)/2]+a[(n+k+1)/2+1]);
}
}
printf("%lld\n",ans);
}
else if(n==262143){
//完美二叉樹
cnt=0;ans=0;num=0;
memset(d,0,sizeof d);memset(vis,0,sizeof vis);memset(h,-1,sizeof h);
for(int i=1;i<n;i++){
u=read();v=read();
edge[cnt].insert(u,v);
edge[cnt].insert(v,u);
d[u]++,d[v]++;
}
for(int i=1;i<=n;i++) if(d[i]==2){v=i;break;}
memset(d,0,sizeof d);
queue<int> q;q.push(v);vis[v]=1;
while(!q.empty()){
int fr=q.front();
if(d[fr]==1) a[++num]=fr;
q.pop();
for(int i=h[fr];i!=-1;i=edge[i].lac){
int to=edge[i].to;
if(vis[to]) continue;
q.push(to);d[to]=d[fr]+1;vis[to]=1;
}
}
//a 數組 左右重心
ans+=(ll)v*(n-1)/2;//前面稍加一次跟的
ans+=(ll)n*(ll)(n+1)>>1;
ans+=(ll)a[1]*(n-1)/2;
ans+=(ll)a[2]*(n-1)/2;
printf("%lld\n",ans);
}
else {
memset(h,-1,sizeof h);cnt=0;ans=0;
for(int i=1;i<n;i++){
u=read();v=read();
edge[cnt].insert(u,v);
edge[cnt].insert(v,u);
}
//建樹
//從每一條邊斷開始枚舉
for(int i=0;i<cnt;i+=2){
num=0;
edge[i].can=edge[i^1].can=1;
//先做廣搜來確定兩顆子樹的分別節點個數
bfs(edge[i].fr);min1=nown/2;
dfs(edge[i].fr,-1);
for(int j=1;j<=num;j++) ans+=a[j];
num=0;nown=n-nown;min1=nown/2;
dfs(edge[i].to,-1);
for(int j=1;j<=num;j++) ans+=a[j];
edge[i].can=edge[i^1].can=0;
}
printf("%lld\n",ans);
}
}
return 0;
}
事實上寫這個md是NCP時期的無奈之舉且考試確實過去不短了,自己想想還是寫一些考試時的想法和學習心得吧
- NOIP的題的特點就是賊長,要仔細讀題,分析清楚
- 實在不行就打暴力
- 暴力不會的話,就拿部分分,甚至小數據打表
- 其實考試前一年都沒怎么學 包括oi,包括文化課,還是不要陷入感情吧,免得遺憾,免得消沉
- 平常多練習,與同學交流看法,膜拜大神
其實,個人的心願就是考場上遇到題,就能看出算法。。
希望這不是痴心妄想
嵬
就算有再多不順,也要開心的過每一天
flag :兩周以內會有第二版
