树上分治算法

感觉最近有点累,想好好休息一下。嗯,还是要注意调节自己嘻嘻

题目

poj 1741

题意

  • 给定一棵有n个节点的树,给定一个权值k。
  • 问树上有多少个顶点对,使他们之间的距离不超过k。

题解

  • 由于是树形的结构,不像数列一样直接二等分即可。我们首先要找到树的重心(将该节点删去之后,剩下的子树种的最大子树的节点树最小)。
  • 有如下的三种情况:

  • 第一种递归求解即可;第二种先把子树中所有的节点到根节点的距离算出来,然后放到一个数组中,之后统计;情况三:我们压入了一个0距离的节点,因此可以视为情况二。但是在情况而统计的时候,有一部分是不符合题意的,因此要减掉。

挑战程序设计里面说是为了减去与情况一重复的情况.更进一步的说:是相同子树下避免不符合条件的点对的统计,而且这些点对也可能和情况一中的重合(仅仅是点对的重复,他们之间的距离是变化的,因为后面还必须要经过树根),但情况一中符合条件的点对不一定在这里面统计到。
下面举样例来说明:

当以1为重心的时候,当分治完所有的节点后,ans = 1,(3–5满足条件)。
当运行到这段代码时,统计1–3–5子树的时候

1
enumerate_path(v, s, G[s][i].length, tds);

tds里面保存了1, 2,然后执行减去的是3<——>1<—–>5(此时统计的还是3–5点对)
因为后面会重复的统计这种情形,并且这种情形也是不符合条件的。

AC代码

  • 计算子树的大小的时候,要把计算函数放在分治函数中,不能放在主函数中。T成傻逼
  • 注意双指针统计符合条件的点对的思想。先统计,把自身重复的剔除(-1),最后除以2.
  • 建立树的时候都是从0开始的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
#include <cstdio>
#include <cstring>
#include <vector>
#include <cmath>
#include <climits>
#include <algorithm>
using namespace std;
int n, k;
const int maxn = 1e4+10;
int ans;
struct Edge{
int to;
int length;
};
vector<Edge> G[maxn];
int sub_tree_size[maxn];//以该节点为根节点的子树的定点数
bool centroid[maxn];//标记是否已经为中心
int cal_subtree_number(int u, int p){
int c = 1;
for(int i=0; i<G[u].size(); i++){
int v = G[u][i].to;
if(v == p || centroid[v]) continue;
c += cal_subtree_number(v, u);
}
sub_tree_size[u] = c;
return c;
}
//返回的是一个pair类型的变量,第一个变量是最大子树的节点个数,第二个是重心的序号
//函数的三个参数是:①dfs的当前坐标 ②父亲节点 ③节点总数,这个是不变的(n)
pair<int, int> find_centroid(int u, int p, int tot_number){
int m = 0, number = 1;
pair<int, int> res = make_pair(INT_MAX, -1);//初始化一个节点
for(int i=0; i<G[u].size(); i++){
int v = G[u][i].to;
if(v == p || centroid[v])continue;
res = min(res, find_centroid(v, u, tot_number));
number += sub_tree_size[v];
m = max(m, sub_tree_size[v]);//所有的子树的最大值
}
m = max(m, tot_number-number);//除去子树和本身的剩余节点的个数,可以看我书上画的图理解
res = min(res, make_pair(m, u));
return res;
}
//以u为中心节点,子树的所有的节点到中心节点(根节点)的距离
void enumerate_path(int u, int p, int length, vector<int>& ds){
ds.push_back(length);
for(int i=0; i<G[u].size(); i++){
int v = G[u][i].to;
if(centroid[v] || v == p) continue;
enumerate_path(v, u, length+G[u][i].length, ds);
}
return ;
}
int count_pair(vector<int>& ds){
int ans1 = 0;
sort(ds.begin(), ds.end());
int j = ds.size();
for(int i=0; i<ds.size(); i++){//以i为一条边基准,进行遍历计数
while(j>0&&ds[i]+ds[j-1]>k) j--;
if(j>i) ans1 += (j-1);
else ans1 += j;
}
return ans1/2;
}
void sub_problem(int u){
cal_subtree_number(u, -1);//一定要注意在子问题中计算子树的大小,不要放在主函数中,T成傻子。
int s = find_centroid(u, -1, sub_tree_size[u]).second;
centroid[s] = true;
//printf("%d\n", s);
//情况一:统计的节点在以s为重心的子树中
for(int i=0; i<G[s].size(); i++){
int v = G[s][i].to;
if(centroid[v]) continue;
sub_problem(v);
}
vector<int> ds;
ds.clear();
ds.push_back(0);//以s为根节点的情况
for(int i=0; i<G[s].size(); i++){
vector<int> tds;
tds.clear();
int v = G[s][i].to;
if(centroid[v])continue;
enumerate_path(v, s, G[s][i].length, tds);
ans -= count_pair(tds);//情况二中避免后面的重复,所以要减掉。
ds.insert(ds.end(), tds.begin(), tds.end());
}
ans += count_pair(ds);//统计情况二。这里面会有不符合条件的,但是前面已经减掉了。
centroid[s] = false;
}
void solve(int s){
ans = 0;
memset(centroid, false, sizeof(centroid));
sub_problem(s);
printf("%d\n", ans);
}
int main()
{
while(~scanf("%d%d", &n, &k)&&(n+k)){
int a, b, length;
for(int i=0; i<n; i++){
G[i].clear();
}
for(int i=0; i<n-1; i++){
scanf("%d%d%d", &a, &b, &length);
Edge temp;
temp.to = b-1, temp.length = length;
G[a-1].push_back(temp);
temp.to = a-1;
G[b-1].push_back(temp);
}
solve(a-1);
}
return 0;
}

未解决的问题

文章目录
  1. 1. 题目
  2. 2. 题意
  3. 3. 题解
  4. 4. AC代码
  5. 5. 未解决的问题
{{ live2d() }}