【HNOI2014】世界树(worldtree) (虚树详解)

2 篇文章 0 订阅
2 篇文章 0 订阅

题目大意

原题又臭又长,总之简介来说的话就是
给定一棵树,有若干个询问,每次给定m个点,每个点都被这m个点中最近(距离相同,编号小的近)的点管辖。问m个点分别管几个点。
n<=300000,q<=300000,∑m<=300000。

The Solution

我们先来对题目进行分析,很明显,我们要往树上来想,那么怎么想呢?第一想法肯定是暴力啊!!可是,给定m个点,询问它分别管辖几个点,会有很多中状态,且都是可变的,这TM怎么暴力?
或许可能可以YY到处理它lca及自己的状态,然后再在树上贪心之类的~囧~
好主意估计能行,水点分吧,可这也很GG啊

好吧口胡了这么久,接下来切入主题

正解是虚树树形DP

那么问题来了

什么是虚树呢?

(这里引用了别人的一些资料与图,供大家学习。)

假设如下的一棵树

这里写图片描述

蓝色的是询问点。红色点就会在虚树上。

这里写图片描述

我们用一个栈来维护虚树的“当前这一坨东西”…例如我们在栈中加入了18。然后接下来打算加入一个16。

我们现在发现这个栈顶的lca,也就是2,是有用的,那么我们现在就要把18弹掉,换成2,然后再扔进去一个16。

接下来我们要加一个20,那么它与栈顶的lca为2。我们就考虑16,16是没用的,把它弹掉,然后看见2,正好就是lca,就保留。

类似这样我们可以发现开始把所有询问点加入虚树后,我们把询问点按dfs序排个序,这时栈里面应该维护一个奇怪的玩意,先计算一个新加的点与栈顶的lca,然后如果一个栈里的东西一直都“没用”,也就是深度比这个lca来得大,就一直弹出栈顶,最后如果栈顶不是lca,就把lca加入栈,并且加入虚树,然后再加入这个点。

这样我们就可以求出虚树啦,同时我们也可以得到虚树上每一个点的父亲节点。

接下来我们考虑如何求出虚树上每个点被哪个点控制。我们可以用一个 pair<int,int> 来存它到控制点的距离和控制点,然后我们用每一个点更新它的父亲,再用每一个父亲更新它的孩子。注意到因为所有虚树上的点的lca也在虚树上,所以对于虚树上某一个点和某一个控制点,第一遍必然会更新到lca,第二遍必然会更新到这个点,所以这个做法是正确的。

然后我们要考虑虚树以外的点要怎么维护。

比如对于这样一个小树,红色的点在虚树上。

这里写图片描述

我们考虑虚树上的一条父子边,对应实际的树上就会是一坨东西。比如这棵树上2-5就对应4和6。

具体地说,对于一条父子边(f,x),f=fa[x],那么对应实际树上的就是f在x方向的这棵子树除掉x这棵子树。

例如2-5就表示4这棵子树除掉5这棵子树,就是4和6这两个点。

我们发现树上除了这些父子边,还有一些没有被计入统计的点,需要分别考虑,虚树根往上的都需要单独统计,例如1,还有像图中的3这样也不会被统计到。

注意到跟这些点最近的点必然和和这些点相连的点最近的是同一个点,比如1和3最近的都与2最近的一样。

现在,如果5与2最近的是同一个点,那么4和6必然也是这一个点。

否则我们可以发现,这是与深度有关的。比如2最近的点->2距离为p,5最近的点->5距离为q。

那么与控制5的点距离不超过(p+q+2到5距离)/2的点都应该选择5最近的点,那么深度就要>=dep[5]-(near_dis(2)+near_dis(5)+dis(2,5))/2+near_dis(5)。

只要从5开始往上跳到这个深度,计算一下就可以了。

需要注意的是,如果(p+q+2到5距离)是偶数的话,一定会有一个(些)点到2和5最近的点距离相同,那么这个(些)点应该选编号小的一个。即因为正常情况下这些点会选择下面那个点的控制点,所以如果上面那个点控制点编号小,就要考虑深度++。

所以基本的思路都清楚了,我们只要写一个倍增维护一下lca和往上跳这种东西就可以了。复杂度大概是O((n+q+∑m)logn)的。

参考CODE

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <algorithm>
#include <cstring>
#include <cmath>
#define fo(i,a,b) for (int i=a;i<=b;i++)
#define fd(i,a,b) for (int i=a;i>=b;i--)
#define INF 1 << 30
#define N 600005 

using namespace std;

int read(int &n)
{
    char ch = ' ';
    int q = 0, w = 1;
    for (;(ch != '-') && ((ch < '0') || (ch> '9'));ch = getchar());
    if (ch == '-') w = -1,ch = getchar();
    for (; ch >= '0' && ch <= '9';ch = getchar()) q = q * 10 + ch - 48;
    n = q * w;
    return n;
}

namespace ib {char b[100];}

inline void pint(int x)
{
    if (x == 0)
    {
        putchar(48);
        return;
    }
    if (x < 0) 
    {
        putchar('-');
        x = -x;
    }
    char *s = ib :: b;
    while (x) *(++ s) = x % 10,x /= 10;
    while (s != ib :: b) putchar((* (s --)) + 48);
}

typedef long long ll;

struct Edge
{
    int to,next;
    Edge(void){}
    Edge(int a,int b) : to(a),next(b){}
}E[N];

int tot = 0,n,Q;
int Final[N];

void Link(int x,int y)
{
    E[++ tot] = Edge(y,Final[x]),Final[x] = tot;
    E[++ tot] = Edge(x,Final[y]),Final[y] = tot; 
}

int Dad[N],Up[N][20],Dep[N],M=0,Dfsn[N],C=0,Son[N];

void Dfs_rt(int x)
{
    Dfsn[x] = ++ C;
    Son[x] = 1;
    for (int k = Final[x];k;k = E[k].next)
    {
        if (E[k].to == Dad[x]) continue;
        Dad[E[k].to] = Up[E[k].to][0] = x;
        Dep[E[k].to] = Dep[x] + 1;
        Dfs_rt(E[k].to);
        Son[x] += Son[E[k].to];
    }
}

void Build_rt()
{
    Dfs_rt(1);
    fo(g,1,19)
        fo(i,1,n)
            if (Up[i][g - 1]) Up[i][g] = Up[Up[i][g - 1]][g - 1];
}
jump up (x=fa[x]) until dep[x]=d
int Jump(int x,int d)
{
    fd(i,19,0)
        if (!Up[x][i] || Dep[Up[x][i]] < d);else x = Up[x][i];
    return x;
}

int Lca(int x,int y)
{
    if (Dep[x] > Dep[y]) swap(x,y);
    y = Jump(y,Dep[x]);
    if (x == y) return x;
    fd(i,19,0)
        if (Up[x][i] != Up[y][i]) x = Up[x][i],y = Up[y][i];
    return Dad[x];
}

typedef pair<int,int> P;
bool cmp_Dfsn(int a,int b) { return Dfsn[a] < Dfsn[b];}
int ss[N],vs[N],st[N],vfa[N],emp[N],vfe[N],ans[N],ss_[N],vn,stn = 0,sn;
P Dot[N]; //(dis,controller)
//vs: points in vtree 
void Build_vt()
{
    vn = stn = 0;
    fo(i,1,sn) ss_[i] = ss[i];
    sort(ss + 1,ss + 1 + sn,cmp_Dfsn);
    fo(i,1,sn) 
    {
        vs[++ vn] = ss[i];
        Dot[ss[i]] = P(0,ss[i]);
        ans[ss[i]] = 0;
    }
    fo(i,1,sn)
    {
        int x = ss[i];
        if (! stn) 
        {
            st[++ stn] = x;
            vfa[x] = 0;
            continue;
        }
        int lca = Lca(x,st[stn]);
        for (;Dep[st[stn]] > Dep[lca];stn --)
            if (Dep[st[stn - 1]] <= Dep[lca]) vfa[st[stn]] = lca;
        if (st[stn] != lca)
        {
            vs[++ vn] = lca;
            Dot[lca] = P(INF,0);
            vfa[lca] = st[stn];
            st[++ stn] = lca;
        }
        vfa[x] = lca;
        st[++ stn] = x;
    }
    sort(vs + 1,vs + 1 + vn,cmp_Dfsn);//注意到按dfs序排序是满足儿子一定在父亲的后面的 
    fo(i,1,vn)
    {
        int x = vs[i];
        emp[x] = Son[x];
        if (i > 1) vfe[x] = Dep[x] - Dep[vfa[x]];
    }
    fd(i,vn,2)
    {
        int x = vs[i],
            f = vfa[x];
        Dot[f] = min(P(Dot[x].first + vfe[x],Dot[x].second),Dot[f]);
    }
    fo(i,2,vn)
    {
        int x = vs[i],
            f = vfa[x];
        Dot[x] = min(P(Dot[f].first + vfe[x],Dot[f].second),Dot[x]);
    }
    fo(i,1,vn)
    {
        int x = vs[i],
            f = vfa[x];
           //树根往上的点 
        if (i == 1) 
        {
            ans[Dot[x].second] += n - Son[x];
            continue;
        } 
        int fp = Jump(x,Dep[f] + 1),
            cnt = Son[fp] - Son[x];
        emp[f] -= Son[fp]; //这一棵子树已经处理过了
        if (Dot[f].second == Dot[x].second)
        {
            ans[Dot[x].second] += cnt;
            continue;
        }
        int mid = Dep[x] - (Dot[f].first + Dot[x].first + vfe[x]) / 2 + Dot[x].first;
        if ((Dot[f].first + Dot[x].first + vfe[x]) % 2 == 0 && Dot[f].second < Dot[x].second) mid ++;
        int tmp = Son[Jump(x,mid)] - Son[x];
        ans[Dot[x].second] += tmp;
        ans[Dot[f].second] += cnt - tmp;
    }
    fo(i,1,vn) ans[Dot[vs[i]].second] += emp[vs[i]];
    fo(i,1,sn)
    {
        pint(ans[ss_[i]]);
        putchar(' ');
    }
    putchar(10);
}
int main()
{
    freopen("worldtree.in","r",stdin);
    freopen("worldtree.out","w",stdout);
    read(n); 
    fo(i,1,n - 1)
    {
        int x,y;
        read(x),read(y);
        Link(x,y);
    }
    Build_rt();
    read(Q);
    while (Q --)
    {
        read(sn);
        fo(i,1,sn) read(ss[i]);
        Build_vt(); 
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值