树上处理的问题总结
树的直径
也就是树的最长路径的长度
做法
求2次DFS,第一次DFS找到当前距离根节点最远的点(叶节点),然后以这个点出发再做一次DFS,找到最远的点。可以证明这个距离即是树上的最长路径。
//hdu4607 //树的直径,树上最长的链的顶点的个数 #include <cstdio> #include <cstring> #include <string> #include <map> #include <iostream> #include <cmath> #include <vector> #include <set> #include <algorithm> #include <queue> typedef long long ll; #define rep(i,a,b) for (int i = (a); i <= (b); ++i) #define rep(i,a,b) for (int i = (a); i <= (b); ++i) using namespace std; const int N = 200010; int k, node, ans, x, y, n , m, a[N], l[N], f[N], far[N], zzz[N]; vector<int> v[N], s[N]; bool b[N]; void dfs(int k,int cnt){ int mx1, mx2; if (cnt > ans){ ans = cnt; node = k; } b[k] = true; for (auto i:s[k]){ if (!b[i]){ dfs(i, cnt+1); } } } int main() { int T; cin >> T; rep (_,1,T){ cin >> n >> m; rep (i,1,n){ s[i].clear(); } rep (i, 1, n-1){ scanf("%d%d",&x, &y); s[x].push_back(y); s[y].push_back(x); } ans = 0; memset(b,false,sizeof(b)); dfs(1,1); memset(b,false,sizeof(b)); dfs(node,1); cout << ans << endl; // rep(i,1,m){ // scanf("%d",&k); // if (k <= ans) // cout << k-1 << endl; // else // cout << (k-ans)*2+ans-1 << endl; // } } return 0; }
树的重心
为了防止出现一个顶点下链接了一条大长链会导致一些在树上的分治处理可能出现的问题,我们需改变根节点的位置(例如此时把根节点放在中间就比较好)
树的重心有下面几条常见性质:
定义1:找到一个点,其所有的子树中最大的子树节点数最少,那么这个点就是这棵树的重心。
定义2:以这个点为根,那么所有的子树(不算整个树自身)的大小都不超过整个树大小的一半。
性质1:树中所有点到某个点的距离和中,到重心的距离和是最小的;如果有两个重心,那么他们的距离和一样。
性质2:把两个树通过一条边相连得到一个新的树,那么新的树的重心在连接原来两个树的重心的路径上。
性质3:把一个树添加或删除一个叶子,那么它的重心最多只移动一条边的距离。
做法
做一次DFS(两次也可以),求出所有顶点有多少子节点(包括自己),同时计算每个点的balance,是所有子树i中son[i]的最大值或者是父节点的那个森林的点数(也就是n-son[k]),从中取一个最大的。
最终的重心就是所有节点中的bal最小的那个节点。
poj1655:
定义树的balance为删去顶点i之后的森林中顶点个数最大的值。求这个点(重心)与它的balance值.
//poj1655: //定义树的balance为删去顶点i之后的森林中顶点个数最大的值。求这个点(重心)与它的balance值 #include <cstdio> #include <cstring> #include <string> #include <map> #include <iostream> #include <cmath> #include <vector> #include <set> #include <algorithm> #include <queue> typedef long long ll; #define rep(i,a,b) for (int i = (a); i <= (b); ++i) #define rep(i,a,b) for (int i = (a); i <= (b); ++i) using namespace std; const int N = 2000010; int ans, n , m, x, y, son[N], bal[N]; vector<int> v[N]; bool vis[N]; void dfs(int k){ vis[k] = true; int i; //for (auto i : v[k]){ for (int j = 0; j < v[k].size(); ++j){ i = v[k][j]; if (!vis[i]){ dfs(i); son[k]+=son[i]; bal[k] = max(bal[k], son[i]); } } if (n-son[k]>bal[k]) bal[k] = n-son[k]; } int main() { int T; cin >> T; rep (_,1,T){ cin >> n; rep (i,1,n){ v[i].clear(); } memset(bal,0,sizeof(bal)); rep (i, 1, n-1){ scanf("%d%d",&x, &y); v[x].push_back(y); v[y].push_back(x); } ans = 0; memset(vis, false, sizeof(vis)); rep (i,1,n){ son[i] = 1; } dfs(1); memset(vis,false,sizeof(vis)); //dfs2(1,1); int mii = 1; rep (i,2,n){ if (bal[i] < bal[mii]){ mii = i; } } cout << mii << " " << bal[mii] << endl; } return 0; }
点分治
树的点的分治:首先选取一个点将无根树转为有根树,再递归处理每一颗以根结点的儿子为根的子树。
树的点的分治:首先选取一个点将无根树转为有根树,再递归处理每一颗以根结点的儿子为根的子树。
首先我们考虑如何选取点。对于基于点的分治,我们选取一个点,要求将其删去后,结点最多的树的结点个数最小,这个点就是树的重心。
在基于点的分治中每次我们都会将树的结点个数减少一半,因此递归深度最坏是 O(NlogN) 的,在树是一条链的时候达到上界。
poj1741:
楼教主男人八题中的一题,实现起来比看上去感觉要麻烦些。
求树上小于等于K的点对有多少。
每个点对都可以看做是在某个根节点的“领导下”。然后为了退化,我们每次做某个根的时候,都需要找到树的重心进行分治。把所有点到根节点的距离存到一个数组里并进行排序,然后运用经典的O(n)相向搜索进行处理,不过要减去没有经过根节点的,因为这个之后在另一个根节点也计算了。
//poj1655: //定义树的balance为删去顶点i之后的森林中顶点个数最大的值。求这个点(重心)与它的balance值 #include <cstdio> #include <cstring> #include <string> #include <map> #include <iostream> #include <cmath> #include <vector> #include <set> #include <algorithm> #include <queue> typedef long long ll; #define rep(i,a,b) for (int i = (a); i <= (b); ++i) #define rep(i,a,b) for (int i = (a); i <= (b); ++i) using namespace std; const int N = 2010; int ans, n , m, x, y, z, son[N], bal[N], d[N]; vector<int> v[N], dis[N]; bool vis[N]; int mi,mii,cnt; void dfs(int root,int k,int fa){ son[k] = 1; bal[k] = 0; int i, cos; //for (auto i : v[k]){ for (int j = 0; j < v[k].size(); ++j){ i = v[k][j]; cos = dis[k][j]; if (i != fa && !vis[i]){ dfs(root, i, k); son[k]+=son[i]; bal[k] = max(bal[k], son[i]); } } } void dfs2(int root,int k,int fa){ // vis[k] = true; if (son[root]-son[k]>bal[k])//son[root]就是n啦,代表所有的顶点数,减掉之后就相当于删掉自己之后的父节点的那棵子树 bal[k] = son[root]-son[k]; int i; //for (auto i : v[k]){ for (int j = 0; j < v[k].size(); ++j){ i = v[k][j]; if (i != fa && !vis[i]){ dfs2(root, i, k); } } if (bal[k] < mi){ mii = k; mi = bal[k]; } } void calcdis(int k,int cost,int fa){ d[++cnt] = cost; int i, cos; for (int j = 0; j < v[k].size(); ++j){ i = v[k][j]; cos = dis[k][j]; if (i != fa && !vis[i]){ calcdis(i, cost+cos, k); } } } int calc(int root,int cos_prim){ cnt=0; calcdis(root,cos_prim,0); sort(d+1,d+cnt+1); int i = 1; int j = cnt; int tot = 0; while (i < j){ while (d[i]+d[j]>m&&i<j) j--; tot+=j-i; i++; } return tot; } void work(int k){ mi = n+1; dfs(k,k,0); dfs2(k,k,0); ans+=calc(mii, 0); vis[mii] = true; int i,cos; //for (auto i : v[k]){ int root = mii; for (int j = 0; j < v[root].size(); ++j){ i = v[root][j]; cos = dis[root][j]; if (!vis[i]){ ans-=calc(i,cos); } } for (int j = 0; j < v[root].size(); ++j){ i = v[root][j]; if (!vis[i]){ work(i); } } } int main() { while ((scanf("%d%d",&n,&m))!=EOF&&n+m>0){ rep (i,1,n){ v[i].clear(); dis[i].clear(); } rep (i, 1, n-1){ scanf("%d%d%d",&x, &y, &z); v[x].push_back(y); dis[x].push_back(z); v[y].push_back(x); dis[y].push_back(z); } ans = 0; memset(vis, false, sizeof(vis)); work(1); memset(vis,false,sizeof(vis)); cout << ans << endl; } return 0; }
bzoj 2152
熟悉一下树分治。
常见的套路:先找到当前根的子树的重心,计算以这个重心初始、距离为0的结果。然后删掉这个重心。减去和这个重心相连的、初始距离为边的权值的结果,然后再分治这些个点。
//poj1655: //定义树的balance为删去顶点i之后的森林中顶点个数最大的值。求这个点(重心)与它的balance值 #include <cstdio> #include <cstring> #include <string> #include <map> #include <iostream> #include <cmath> #include <vector> #include <set> #include <algorithm> #include <queue> typedef long long ll; #define rep(i,a,b) for (int i = (a); i <= (b); ++i) #define rep(i,a,b) for (int i = (a); i <= (b); ++i) using namespace std; const int N = 2010; int ans, n , m, x, y, z, son[N], bal[N], d[N], cnt[4]; vector<int> v[N], dis[N]; bool vis[N]; int mi,mii; void dfs(int k,int fa){//这个函数其实可以和getroot合并起来,就是为了求出子树的重心的(当我们知道这颗子树的大小的时候就可以融合了,分开更直观) son[k] = 1; bal[k] = 0; int i, cos; //for (auto i : v[k]){ for (int j = 0; j < v[k].size(); ++j){ i = v[k][j]; cos = dis[k][j]; if (i != fa && !vis[i]){ dfs(i, k); son[k]+=son[i]; bal[k] = max(bal[k], son[i]); } } } void getroot(int root,int k,int fa){ // vis[k] = true; if (son[root]-son[k]>bal[k])//son[root]就是n啦,代表所有的顶点数,减掉之后就相当于删掉自己之后的父节点的那棵子树 bal[k] = son[root]-son[k]; int i; //for (auto i : v[k]){ for (int j = 0; j < v[k].size(); ++j){ i = v[k][j]; if (i != fa && !vis[i]){ getroot(root, i, k); } } if (bal[k] < mi){ mii = k; mi = bal[k]; } } void calc(int k,int fa,int dist){ cnt[dist % 3]++; int cos, i; for (int j = 0; j < v[k].size(); ++j){ i = v[k][j]; cos = dis[k][j]; if (!vis[i] && i != fa){ calc(i, k, dist+cos); } } } void work(int k){ memset(cnt,0,sizeof(cnt)); mi = n+1; dfs(k,0); getroot(k,k,0); calc(mii,0,0); //ans += cnt[0]*(cnt[0]-1) + 1 + 2*cnt[1]*cnt[2]; ans+= cnt[0]*cnt[0] + 2*cnt[1]*cnt[2];//计算当前的这个重心上的树的值为3的路径有多少 vis[mii] = true;//然后把这个重心及它相连的边从树上删除掉 int i,cos; //for (auto i : v[k]){ int root = mii; for (int j = 0; j < v[root].size(); ++j){ i = v[root][j]; cos = dis[root][j]; if (!vis[i]){ memset(cnt,0,sizeof(cnt)); calc(i,0,cos); ans-=cnt[0]*cnt[0] + 2*cnt[1]*cnt[2];//减去子树上计算的重复的数 work(i); } } } int gcd(int x,int y){ int t; if (x < y) swap(x, y); while (y != 0){ t = y; y = x % y; x = t; } return x; } int main() { cin >> n; rep (i,1,n){ v[i].clear(); dis[i].clear(); } // memset(bal,0,sizeof(bal)); rep (i, 1, n-1){ scanf("%d%d%d",&x, &y, &z); v[x].push_back(y); dis[x].push_back(z); v[y].push_back(x); dis[y].push_back(z); } ans = 0; memset(vis, false, sizeof(vis)); work(1); int g = gcd(ans, n*n); printf("%d/%d\n",ans/g,n*n/g); return 0; }