【做题】51Nod1766树上的最远点对——直径&线段树

原文链接 https://www.cnblogs.com/cly-none/p/9890837.html

题意:给出一棵大小为\(n\)的树,边有边权。\(m\)次询问,每次给出两个标号区间\([a,b]\)\([c,d]\),求\(\max {dis(i,j) \ | \ a \leq i \leq b, \, c \leq j \leq d }\)
\(n,m \leq 10^5\)

本题主要是对直径性质的运用。

先考虑这样一个结论。

对于两个点集\(A\)\(B\),如果\(A\)的最远点对是\((a,b)\)\(B\)的最远点对是\((c,d)\),那么,点集\(A \bigcup B\)的最远点对的两个点一定是\(a,b,c,d\)中的两个。

至于证明,可以考虑直径的性质:到任何一个结点的最远点一定是两个直径端点中的一个。

注:一个点集的最远点对可以通过构建虚树转化为直径。

那么,考虑\(A\)中的一个结点,在\(B\)集合,到它最远的结点一定可以是\(c,d\)中的一个。否则,我们可以反证\((c,d)\)不是\(B\)中的最远点对。那么,在\(A \bigcup B\)中,任何一个结点,到它的最远点一定是\(a,b,c,d\)中的一个。因此,最远点对就一定是\(a,b,c,d\)中的两个。

于是,我们可以用线段树维护区间的最远点对。再根据我们的结论,\([c,d]\)中到\([a,b]\)的任意一点最远的结点一定是\([c,d]\)里最远点对中的一点,因此我们求出\([a,b]\)\([c,d]\)各自的最远点对,就能求出答案了。

时间复杂度\(O(n \log n)\)

#include <bits/stdc++.h>
using namespace std;
#define gc() getchar()
template <typename tp>
inline void read(tp& x) {
  x = 0; char tmp; bool key = 0;
  for (tmp = gc() ; !isdigit(tmp) ; tmp = gc())
    key = (tmp == '-');
  for ( ; isdigit(tmp) ; tmp = gc())
    x = (x << 3) + (x << 1) + tmp - '0';
  if (key) x = -x;
}
const int N = 100010, MP = 19;
struct edge {
  int la,b,v;
} con[N << 1];
int tot,fir[N],n,m;
void add(int from,int to,int val) {
  con[++tot] = (edge) {fir[from],to,val};
  fir[from] = tot;
}
int dep[N],dfn[N << 1],dcnt,mn[N << 1][MP],ln[N << 1],rec[N],dis[N];
int lca(int x,int y) {
  x = rec[x], y = rec[y];
  if (x > y) swap(x,y);
  int len = ln[y - x + 1];
  return dep[dfn[mn[y][len]]] < dep[dfn[mn[x + (1 << len) - 1][len]]] ?
    dfn[mn[y][len]] : dfn[mn[x + (1 << len) - 1][len]];
}
void dfs(int pos,int fa) {
  dep[pos] = dep[fa] + 1;
  dfn[rec[pos] = ++dcnt] = pos;
  for (int i = fir[pos] ; i ; i = con[i].la) {
    if (con[i].b == fa) continue;
    dis[con[i].b] = dis[pos] + con[i].v;
    dfs(con[i].b,pos);
    dfn[++dcnt] = pos;
  }
}
typedef pair<int,int> pii;
pii t[N << 2];
int ask(int x,int y) {
  return dis[x] + dis[y] - 2 * dis[lca(x,y)];
}
void merge(pii& x,pii ls,pii rs) {
  static int rec[4];
  rec[0] = ls.first;
  rec[1] = ls.second;
  rec[2] = rs.first;
  rec[3] = rs.second;
  x = pii(-1,-1);
  int cur = -1, tmp;
  for (int i = 0 ; i < 4 ; ++ i)
    for (int j = i+1 ; j < 4 ; ++ j) {
      if (rec[i] == -1 || rec[j] == -1) continue;
      tmp = ask(rec[i],rec[j]);
      if (tmp > cur) x = pii(rec[i],rec[j]), cur = tmp;
    }
}
void build(int x=1,int lp=1,int rp=n) {
  if (lp == rp)
    return (void) (t[x] = pii(lp,-1));
  int mid = (lp + rp) >> 1;
  build(x<<1,lp,mid);
  build(x<<1|1,mid+1,rp);
  merge(t[x],t[x<<1],t[x<<1|1]);
}
pii query(int l,int r,int x=1,int lp=1,int rp=n) {
  if (l > rp || lp > r) return pii(-1,-1);
  if (lp >= l && rp <= r)
    return t[x];
  int mid = (lp + rp) >> 1;
  pii ret;
  merge(ret,query(l,r,x<<1,lp,mid),query(l,r,x<<1|1,mid+1,rp));
  return ret;
}
int main() {
  int x,y,z,a,b,c,d;
  read(n);
  for (int i = 1 ; i < n ; ++ i) {
    read(x), read(y), read(z);
    add(x,y,z);
    add(y,x,z);
  }
  dfs(1,0);
  for (int i = 1 ; i <= dcnt ; ++ i) {
    mn[i][0] = i;
    for (int j = 1 ; (1 << j) <= i ; ++ j)
      mn[i][j] = dep[dfn[mn[i][j-1]]] < dep[dfn[mn[i - (1 << j >> 1)][j-1]]] ?
                    mn[i][j-1] : mn[i - (1 << j >> 1)][j-1];
  }
  for (int i = 2 ; i <= dcnt ; i <<= 1)
    ++ ln[i];
  for (int i = 2 ; i <= dcnt ; ++ i)
    ln[i] += ln[i-1];
  build();
  read(m);
  for (int i = 1 ; i <= m ; ++ i) {
    read(a), read(b), read(c), read(d);
    pii t1 = query(a,b);
    pii t2 = query(c,d);
    printf("%d\n",max(max(ask(t1.first,t2.first),ask(t1.first,t2.second)),max(ask(t1.second,t2.first),ask(t1.second,t2.second))));
  }
  return 0;
}

小结:直径这个东西的性质还是很丰富的。通过利用点集直径的可合并性,很容易套上一些数据结构。同时,两个点集间的最远点对可以转化为求各自的直径,这就使回答多个集合间的最远点对异常简洁。

posted @ 2018-11-01 18:04 莫名其妙的aaa 阅读(...) 评论(...) 编辑 收藏