虚树学习笔记(洛谷2495 消耗战)

题目链接

因为辣鸡csdn,导致之前快写好的博客没了
QWQ悲伤逆流成河qwqqq
首先虚树,这个东西,我感觉是一种思想,或者是方法,而并不是一个数据结构什么的。
他主要是用来解决:给出一棵树,每次询问选择一些关键点,求一些信息。
这些信息的特点是,许多未选择的点可以通过某种方式剔除而不影响最终结果。
于是就有了建虚树这个算法。我们根据原树的信息重新建树,这棵树中要尽量少地包含非关键节点。 这棵树就叫做虚树。这棵虚树包含任意两个关键节点的LCA。
通常这类题 O ( n m ) O(nm) T T 飞,而 O ( O(\sum 询问点个数) 是能跑得过的QWQ

那我们应该怎么构造虚树呢QWQ我们把所有关键点按照dfs序排好序。
然后,虚树要有一个根。这里一般直接把1号节点设为根。构建虚树的主要过程就是使用一个栈,维护从根开始的一条链。这条链上的所有点的dfs序一定是递增的。
我们把关键点扫描一遍,一边维护这个栈一边连边构建虚树。

具体来说:

1.把根节点放入栈中2.把所有关键点点扫一遍。设当前的节点为 x x ,栈顶(链末端)的节点为 y y 。求出 x x y y l c a lca

此时有2种情况。

1) l c a lca y y 。此时 x x y y 子树内。我们就把 x x 压入栈,相当于直接把 x x 添加到这条链的末尾。

2) x y x,y 分立在 l c a lca 的两个子树中。此时y这个子树中的所有关键点一定都被遍历过了。(原因:设有一关键点a在y子树中,没有被遍历过。则 d f n [ y ] < d f n [ a ] < d f n [ x ] dfn[y]<dfn[a]<dfn[x] 。但是我们是把关键点按照dfs序来排的,a一定在x之前被扫)

因此y子树内的所有关键点都已经被被加入虚树。接下来我们要把 y > l c a y->lca 这一段的点加入虚树。我们设栈顶的节点为 y y ,栈顶的第二个节点为 z z

重复以下操作:

1.若 d f n [ z ] > d f n [ l c a ] dfn[z]>dfn[lca] 直接连边 z > y z->y ,然后把 y y 出栈。

2.若 d f n [ z ] = d f n [ l c a ] dfn[z]=dfn[lca] 这意味着 z z 就是 l c a lca 。直接连边 l c a > y lca->y 。此时子树已构建完毕。

3.若 d f n [ z ] > d f n [ l c a ] dfn[z]>dfn[lca] ,说明 l c a lca y y z z 夹在中间。此时我们必须要把 l c a lca 加入虚树,所以连边 l c a > y lca->y ,然后把 y y 弹出栈,把lca入栈。然后子树构造完毕。

最后把栈里面的元素搞一搞,每次 a d d e d g e ( s [ t o p 1 ] , s [ t o p ] ) addedge(s[top-1],s[top]) 即可感觉配合代码会比较好理解

void solve()
{
// memset(point,0,sizeof(point));
    cnt=0;
    sort(a+1,a+1+k,cmp);
    top=1;
    sta[top]=1;
    for (int i=1;i<=k;i++)
    {
        int l = lca(sta[top],a[i]);
        if (l!=sta[top])
        {
            while (top>1)
            {
                if (dfn[sta[top-1]]>dfn[l])
                {
                    addedge(sta[top-1],sta[top],0);
                    top--;
                }
                else
                {
                    if (dfn[sta[top-1]]==dfn[l])
                    {
                        addedge(sta[top-1],sta[top],0);
                        top--;
                        break;
                    }
                    else
                    {
                      addedge(l,sta[top],0);
                      sta[top]=l;
                      break;
                    }
                }
                
            }
        }
        if (sta[top]!=a[i]) sta[++top]=a[i];
    }
    while (top>1)
    {
        addedge(sta[top-1],sta[top],0);
        top--;
    }
}

那么剩下的主要就是 d p dp 部分了

首先,我们定义 m n [ x ] mn[x] 表示在原树上 1 x 1到x 的路径上边权的最小值, f [ x ] f[x] 表示切割完 x x 子树内所有关键点的最小花费。

对于当前点 x x ,如果他是关键点,那么必须切割这个点到1的路经上的一条边,那么就是 m n [ x ] mn[x] ,否则 f [ x ] = m i n ( m n [ x ] , f [ s o n ] f[x]=min(mn[x],\sum f[son]

上代码

int dp(int x,int flag)
{
    
    int sum=0;
    for (int &i=point[x];i;i=nxt[i])
    {
        int p = to[i];
        sum=sum+dp(p,flag);
    }
    if (tag[x]==flag)
    {
        return mn[x];
    }
    return min(sum,mn[x]);
}

其中有一个要注意的地方就是虚树的时候,因为每次要重新建树,所以要清空 p o i n t point 数组,而由于时间原因,又不能直接 m e m s e t memset ,所以我们只能使用奇妙的手段!自杀式遍历通过取地址,不断修改 p o i n t point

for (int &i=point[x];i;i=nxt[i])

由于死机了三次QWQ
所以暂时没有办法放整个题的代码

猜你喜欢

转载自blog.csdn.net/y752742355/article/details/84563364