【转】树基于点的分治

http://hi.baidu.com/strongoier/item/fe47a4191c18a37c1009b515

【例1】POJ 1741 Tree


题目大意:


给出N(1 <= N <= 10000)个结点的树,求使得路径u -> v长度不超过k的点对(u, v)的个数。


算法分析:


首先我们对这棵树进行点分治,接下来考虑处理以root为根的子树。

记d[x]为x到root的距离,那么我们把子树的所有结点的d拉出来,即转化为了a[x] + a[y] <= k的(x, y)对数,这是可以通过排序后扫一遍解决的。

然后,这样可能出现重复的情况(x, y在root的同一儿子内部),那么我们只需要再对root的每个儿子进行同样的操作,但是是从之前的答案中减去。

时间复杂度O(Nlog^2N)。

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
#include <algorithm>
#include <cstdio>
#include <cstring>
   
using  namespace  std;
   
const  int  MAXN = 11111, MAXM = 22222;
   
bool  mk[MAXM];
int  k, size, cur, tot, ans, a[MAXN], d[MAXN], s[MAXN], f[MAXN],
     h[MAXN], p[MAXM], w[MAXM], nxt[MAXM];
   
inline  void  checkmax( int  &x,  int  y) {
     if  (y > x)
         x = y;
}
   
inline  void  addedge( int  x,  int  y,  int  z) {
     p[tot] = y;
     w[tot] = z;
     nxt[tot] = h[x];
     mk[tot] =  true ;
     h[x] = tot++;
}
   
void  findroot( int  x,  int  pre) {
     s[x] = 1;
     f[x] = 0;
     for  ( int  k = h[x]; ~k; k = nxt[k])
         if  (mk[k] && p[k] != pre) {
             findroot(p[k], x);
             s[x] += s[p[k]];
             checkmax(f[x], s[p[k]]);
         }
     checkmax(f[x], size - s[x]);
     if  (f[x] < f[cur])
         cur = x;
}
   
void  dfs( int  x,  int  pre) {
     s[x] = 1;
     a[++a[0]] = d[x];
     for  ( int  k = h[x]; ~k; k = nxt[k])
         if  (mk[k] && p[k] != pre) {
             d[p[k]] = d[x] + w[k];
             dfs(p[k], x);
             s[x] += s[p[k]];
         }
}
   
inline  int  calc( int  x,  int  init) {
     int  ret = a[0] = 0;
     d[x] = init;
     dfs(x, 0);
     sort(a + 1, a + a[0] + 1);
     for  ( int  l = 1, r = a[0]; l < r; )
         if  (a[l] + a[r] <= k)
             ret += r - l++;
         else
             --r;
     return  ret;
}
   
void  work( int  x) {
     ans += calc(x, 0);
     for  ( int  k = h[x]; ~k; k = nxt[k])
         if  (mk[k]) {
             mk[k ^ 1] =  false ;
             ans -= calc(p[k], w[k]);
             f[0] = size = s[p[k]];
             findroot(p[k], cur = 0);
             work(cur);
         }
}
   
int  main() {
     int  n;
     while  ( scanf ( "%d%d" , &n, &k) && n) {
         tot = ans = 0;
         memset (h, -1,  sizeof  h);
         for  ( int  i = 1; i < n; ++i) {
             int  x, y, z;
             scanf ( "%d%d%d" , &x, &y, &z);
             addedge(x, y, z);
             addedge(y, x, z);
         }
         f[0] = size = n;
         findroot(1, cur = 0);
         work(cur);
         printf ( "%d\n" , ans);
     }
     return  0;
}


【例2】BZOJ 2599 Race


题目大意:


给出N(1 <= N <= 200000)个结点的树,求长度等于K(1 <= K <= 1000000)的路径的最小边数。


算法分析:


首先我们对这棵树进行点分治,接下来考虑处理以root为根的子树。

为了方便,我们把更新答案和更新状态分离开。用d[i]表示长度为i的路径的最小边数。

当我们枚举root的某个儿子时,我们需要利用的是之前的儿子的d和当前路径长度合并从而更新答案。

做完以后,我们在把这个儿子路径上的状态更新到d中,从而在以后更新别人。

时间复杂度O(NlogN)。

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
#include <cstdio>
#include <cstring>
   
const  int  MAXN = 222222, MAXM = 444444, MAXC = 1111111;
   
bool  mk[MAXM];
int  tot, cur, size, ans, now, n, m, v[MAXC], c[MAXC], s[MAXN], f[MAXN],
     d[MAXN], e[MAXN], h[MAXN], p[MAXM], w[MAXM], nxt[MAXM];
   
inline  void  addedge( int  x,  int  y,  int  z) {
     p[tot] = y;
     w[tot] = z;
     nxt[tot] = h[x];
     mk[tot] =  true ;
     h[x] = tot++;
}
   
inline  void  checkmax( int  &x,  int  y) {
     if  (y > x)
         x = y;
}
   
inline  void  checkmin( int  &x,  int  y) {
     if  (y < x)
         x = y;
}
   
void  findroot( int  x,  int  pre) {
     s[x] = 1;
     f[x] = 0;
     for  ( int  k = h[x]; ~k; k = nxt[k])
         if  (mk[k] && p[k] != pre) {
             findroot(p[k], x);
             s[x] += s[p[k]];
             checkmax(f[x], s[p[k]]);
         }
     checkmax(f[x], size - s[x]);
     if  (f[x] < f[cur])
         cur = x;
}
   
void  dfs1( int  x,  int  pre) {
     if  (d[x] > m)
         return ;
     if  (v[m - d[x]] == now)
         checkmin(ans, c[m - d[x]] + e[x]);
     for  ( int  k = h[x]; ~k; k = nxt[k])
         if  (mk[k] && p[k] != pre) {
             d[p[k]] = d[x] + w[k];
             e[p[k]] = e[x] + 1;
             dfs1(p[k], x);
         }
}
   
void  dfs2( int  x,  int  pre) {
     if  (d[x] > m)
         return ;
     if  (v[d[x]] != now) {
         c[d[x]] = e[x];
         v[d[x]] = now;
     }
     else
         checkmin(c[d[x]], e[x]);
     for  ( int  k = h[x]; ~k; k = nxt[k])
         if  (mk[k] && p[k] != pre)
             dfs2(p[k], x);
}
   
void  work( int  x) {
     v[0] = now = x + 1;
     for  ( int  k = h[x]; ~k; k = nxt[k])
         if  (mk[k]) {
             d[p[k]] = w[k];
             e[p[k]] = 1;
             dfs1(p[k], x);
             dfs2(p[k], x);
         }
     findroot(x, n);
     for  ( int  k = h[x]; ~k; k = nxt[k])
         if  (mk[k]) {
             mk[k ^ 1] =  false ;
             f[n] = size = s[p[k]];
             findroot(p[k], cur = n);
             work(cur);
         }
}
   
int  main() {
     scanf ( "%d%d" , &n, &m);
     memset (h, -1,  sizeof  h);
     for  ( int  i = 1; i < n; ++i) {
         int  x, y, z;
         scanf ( "%d%d%d" , &x, &y, &z);
         addedge(x, y, z);
         addedge(y, x, z);
     }
     ans = f[n] = size = n;
     findroot(0, cur = n);
     work(cur);
     printf ( "%d\n" , ans < n ? ans : -1);
     return  0;
}


【例3】SPOJ FTOUR2


题目大意:


给出N(1 <= N <= 200000)个结点的树,其中有M个是黑结点,求包含不超过K个黑点的路径的最长长度。


算法分析:


首先我们对这棵树进行点分治,接下来考虑处理以root为根的子树。

倘若我们仍采用上一题的方法更新答案,由于更新的是一个区间,其状态总量是非常大的。

那么,我们考虑把root的儿子按路径上最多黑结点个数从大到小排序,用b[i](0 <= i <= K)表示过root的包含不超过i个黑结点的路径的最长长度。

设从某个儿子走下去路径最多黑结点个数为R,则我们用a[i](0 <= i <= R)表示从这个儿子走下去路径上有i个黑结点的最长长度。

注意更新的时候需要考虑[0, R]和[K - R, K]相交的情况。

第一步考虑利用之前的b和当前的a更新答案,即a[i] + b[K - i](0 <= i <= R)。

第二步考虑利用当前的a来更新b,我们发现,由于保证了R的单调减,需要更新的b的元素个数最多为R(再多以后的第一步不可能用到),因此我们只要更新b[i](K - R <= i <= K)即可。

分析复杂度,枚举每个儿子时,其R必然小于等于这个儿子的大小,因此对a, b的更新过程必然不超过以root为根的子树的总结点数。

因此,更新的总复杂度为O(NlogN)。此外,每个点最多在一次排序中,其复杂度仍为O(NlogN)。综上,算法整体时间复杂度O(NlogN)。

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
#include <algorithm>
#include <cstdio>
#include <cstring>
   
using  namespace  std;
   
const  int  MAXN = 222222, MAXM = 444444;
   
bool  mk[MAXM], c[MAXN];
int  tot, cur, size, goal, ans, a[MAXN], b[MAXN], t[MAXN], d[MAXN], e[MAXN],
     g[MAXN], s[MAXN], f[MAXN], h[MAXN], p[MAXM], w[MAXM], nxt[MAXM];
   
inline  void  addedge( int  x,  int  y,  int  z) {
     p[tot] = y;
     w[tot] = z;
     mk[tot] =  true ;
     nxt[tot] = h[x];
     h[x] = tot++;
}
   
inline  void  checkmax( int  &x,  int  y) {
     if  (y > x)
         x = y;
}
   
void  findroot( int  x,  int  pre) {
     s[x] = 1;
     g[x] = f[x] = 0;
     for  ( int  k = h[x]; ~k; k = nxt[k])
         if  (mk[k] && p[k] != pre) {
             findroot(p[k], x);
             checkmax(g[x], g[p[k]]);
             s[x] += s[p[k]];
             checkmax(f[x], s[p[k]]);
         }
     g[x] += c[x];
     checkmax(f[x], size - s[x]);
     if  (f[x] < f[cur])
         cur = x;
}
   
bool  cmp( int  x,  int  y) {
     return  g[p[x]] > g[p[y]];
}
   
void  dfs( int  x,  int  pre) {
     if  (e[x] <= goal)
         checkmax(a[e[x]], d[x]);
     for  ( int  k = h[x]; ~k; k = nxt[k])
         if  (mk[k] && p[k] != pre) {
             e[p[k]] = e[x] + c[p[k]];
             d[p[k]] = d[x] + w[k];
             dfs(p[k], x);
         }
}
   
void  work( int  x) {
     findroot(x, t[0] = 0);
     for  ( int  k = h[x]; ~k; k = nxt[k])
         if  (mk[k])
             t[++t[0]] = k;
     sort(t + 1, t + t[0] + 1, cmp);
     for  ( int  i = max(goal - g[x], 0); i <= goal; ++i)
         b[i] = 0;
     goal -= c[x];
     for  ( int  i = 1; i <= t[0]; ++i) {
         int  k = t[i], hi = min(g[p[k]], goal), lo = goal - hi;
         e[p[k]] = c[p[k]];
         d[p[k]] = w[k];
         for  ( int  j = 0; j <= hi; ++j)
             a[j] = 0;
         dfs(p[k], x);
         for  ( int  j = 0; j <= hi; ++j)
             checkmax(ans, b[goal - j] + a[j]);
         int  mx = 0;
         for  ( int  j = 0; j <= hi; ++j) {
             checkmax(mx, a[j]);
             if  (j >= lo)
                 checkmax(b[j], mx);
         }
         for  ( int  j = max(hi + 1, lo); j <= goal; ++j)
             checkmax(b[j], mx);
     }
     goal += c[x];
     for  ( int  k = h[x]; ~k; k = nxt[k])
         if  (mk[k]) {
             mk[k ^ 1] =  false ;
             f[0] = size = s[p[k]];
             findroot(p[k], cur = 0);
             work(cur);
         }
}
   
int  main() {
     int  n, m;
     scanf ( "%d%d%d" , &n, &goal, &m);
     memset (h, -1,  sizeof  h);
     while  (m--) {
         int  x;
         scanf ( "%d" , &x);
         c[x] =  true ;
     }
     for  ( int  i = 1; i < n; ++i) {
         int  x, y, z;
         scanf ( "%d%d%d" , &x, &y, &z);
         addedge(x, y, z);
         addedge(y, x, z);
     }
     f[0] = size = n;
     findroot(1, cur = 0);
     work(cur);
     printf ( "%d\n" , ans);
     return  0;
}


【例4】BZOJ 1758 重建计划


题目大意:


给出N(1 <= N <= 100000)个结点的树,求边数属于[L, U]的路径中路径长度除以边数的最大值。


算法分析:


首先我们对这棵树进行点分治,接下来考虑处理以root为根的子树。

由于求的是比值,我们很容易想到解决分数规划问题的一般思路——二分。

二分以后我们把边重赋权,然后求边数属于[L, U]的最长路径再判断即可。

这里可以借鉴上一题的思路,同样是排序来做,但更新答案不再是直接一路取max,而是使用单调队列来更新这个类似移动窗口的问题。

要注意的是,初值不能全设为负无穷,长度为0的路径是初始存在的(即直接以root为起点走下去)。

由于有二分,时间复杂度O(Nlog^2N)。

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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
#include <algorithm>
#include <cstdio>
#include <cstring>
  
using  namespace  std;
  
const  double  EPS = 1e-4;
const  int  MAXN = 111111, MAXM = 222222;
  
bool  mk[MAXM];
double  ans, l, r, mid, d[MAXN], a[MAXN], b[MAXN];
int  tot, size, cur, low, upp, e[MAXN], g[MAXN], t[MAXN], q[MAXN],
     s[MAXN], f[MAXN], h[MAXN], p[MAXM], w[MAXM], nxt[MAXM];
  
inline  void  addedge( int  x,  int  y,  int  z) {
     p[tot] = y;
     w[tot] = z;
     mk[tot] =  true ;
     nxt[tot] = h[x];
     h[x] = tot++;
}
  
inline  void  checkmax( int  &x,  int  y) {
     if  (y > x)
         x = y;
}
  
inline  void  checkmax( double  &x,  double  y) {
     if  (y > x)
         x = y;
}
  
void  findroot( int  x,  int  pre) {
     s[x] = 1;
     g[x] = f[x] = 0;
     bool  t =  false ;
     for  ( int  k = h[x]; ~k; k = nxt[k])
         if  (mk[k] && p[k] != pre) {
             t =  true ;
             findroot(p[k], x);
             checkmax(g[x], g[p[k]]);
             s[x] += s[p[k]];
             checkmax(f[x], s[p[k]]);
             checkmax(r, w[k]);
             if  (w[k] < l)
                 l = w[k];
         }
     g[x] += t;
     checkmax(f[x], size - s[x]);
     if  (f[x] < f[cur])
         cur = x;
}
  
void  dfs( int  x,  int  pre) {
     if  (e[x] <= upp)
         checkmax(a[e[x]], d[x]);
     for  ( int  k = h[x]; ~k; k = nxt[k])
         if  (mk[k] && p[k] != pre) {
             e[p[k]] = e[x] + 1;
             d[p[k]] = d[x] + w[k] - mid;
             dfs(p[k], x);
         }
}
  
bool  cmp( int  x,  int  y) {
     return  g[p[x]] > g[p[y]];
}
  
void  work( int  x) {
     l = 1e12;
     r = 0;
     findroot(x, t[0] = 0);
     if  (s[x] <= low)
         return ;
     for  ( int  k = h[x]; ~k; k = nxt[k])
         if  (mk[k])
             t[++t[0]] = k;
     sort(t + 1, t + t[0] + 1, cmp);
     while  (r - l > EPS) {
         mid = (l + r) / 2;
         double  mx = -1e12;
         for  ( int  i = max(upp - g[x], 0); i <= upp; ++i)
             b[i] = i <= upp - low ? 0 : -1e12;
         for  ( int  i = 1; i <= t[0]; ++i) {
             int  k = t[i], hi = min(g[p[k]] + 1, upp), lo = upp - hi;
             e[p[k]] = 1;
             d[p[k]] = w[k] - mid;
             for  ( int  j = 0; j <= hi; ++j)
                 a[j] = -1e12;
             dfs(p[k], x);
             for  ( int  j = 0; j <= hi; ++j)
                 checkmax(mx, b[upp - j] + a[j]);
             int  f = 0, t = -1;
             for  ( int  j = 0; j <= hi; ++j) {
                 for  (; f <= t && j - q[f] > upp - low; ++f);
                 for  (; f <= t && a[j] > a[q[t]]; --t);
                 q[++t] = j;
                 if  (f <= t && j >= lo)
                     checkmax(b[j], a[q[f]]);
             }
             for  ( int  j = max(hi + 1, lo); j <= upp; ++j) {
                 for  (; f <= t && j - q[f] > upp - low; ++f);
                 if  (f <= t)
                     checkmax(b[j], a[q[f]]);
             }
         }
         if  (mx > 0)
             l = mid;
         else
             r = mid;
     }
     checkmax(ans, l);
     for  ( int  k = h[x]; ~k; k = nxt[k])
         if  (mk[k]) {
             mk[k ^ 1] =  false ;
             f[0] = size = s[p[k]];
             findroot(p[k], cur = 0);
             work(cur);
         }
}
  
int  main() {
     int  n;
     scanf ( "%d%d%d" , &n, &low, &upp);
     memset (h, -1,  sizeof  h);
     for  ( int  i = 1; i < n; ++i) {
         int  x, y, z;
         scanf ( "%d%d%d" , &x, &y, &z);
         addedge(x, y, z);
         addedge(y, x, z);
     }
     f[0] = size = n;
     findroot(1, cur = 0);
     work(cur);
     printf ( "%.3lf\n" , ans);
     return  0;
}


                
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值