题解 P3203 【[HNOI2010]弹飞绵羊】

· · 题解

一道对初学LCT者来说比较有思维难度的题。

题解里的开一个点表示弹飞的做法蒟蒻只能表示想不到qaq,在没看题解的情况下想出了这样的做法:

首先我们可以让每个点弹到的点作为它的父亲,如果弹飞那么就是根。容易发现我们需要维护一个森林。询问操作就是求这个点的深度,而修改操作就是把一个点断开和父亲的连接,接到一个新的点上。说白了就是可以修改父亲的动态深度询问

那么,看到动态维护森林,便可以想到LCT。问题是要求深度,那么我们有了一个很令人讨厌的限制条件:不能换根。这样,我们就不得不用剩下来的几种操作和人类的智慧来完成这道题。

外接表示弹飞的点的做法:渣渣,我就可以换根!

0. 建树

要是所有点都link到它的father,且不说带一个log,不能换根link操作也用不了啊。这里有一个好方法:所有边默认初始虚边,直接连就完事。

建树部分代码:

for (int i=1; i<=n; ++i)
{
    int tmp;
    scanf("%d", &tmp);
    fa[i]=(i+tmp>n)?0:(i+tmp);
    // 要是弹飞father就是0(即是树根)
}

1. 查询操作

这个操作相对简单。LCT的实链剖分有一个好处:可以直接用access操作打通到根节点的路径。比如说,我们的原树长这样:

      2
     / \
    1   5
   / \
  3   4
       \
        6

(假设一开始全是虚边)

然后假如我们要查询节点4的路径,我们先access一下4号点,树变成这样:

       2
     // \
    1   5
   / \\
  3   4
       \
        6

(双杠表示实边,单杠表示虚边)

此时根所在的splay的元素一定是根到查询的点这条链,因此要查询点的rank即是它的深度。把要查询的点splay到根,那么它的左子树的大小就是它的祖先个数(而且它一定没有右子树)。把左子树的size加一,即可得到要查询的点的rank,也就是它的深度。

为了便于理解,这时的辅助树长这样:

      4
    // \
   1    6
 // \
2    3
|
5

可以看到查询的点(4)所有的祖先都在左子树。如果仍有疑虑,可以加一句assert(son[i][1]==0);来验证。

查询操作代码:

int query(int i)
{
    access(i);
    splay(i);
    assert(son[i][1]==0);
    return son[i][0]?siz[son[i][0]]+1:1;
    // 要额外判目前的点已经是根节点的情况,因为0号节点可能有垃圾数值
}

2. 修改操作

这是比较困难的一个操作。

看到断/连,想到的第一个就是cutlink。然而先不说常数,不能换根这一点,就让cutlink成为了不可能。那么,怎么做呢?

要断开查询节点(称为i)和它在原树中的父亲,我们首先要找到它在原树中的父亲。容易想到,首先要让它和父亲在一棵splay里面。所以,先要access(i)。然后,我们可以知道,由于ii的父亲在原树中深度连续,i的父亲在splay中一定是i的前驱。查找前驱是比较基础的splay操作(当然不是普通的二叉搜索树那种从根节点查找前驱的操作),可以先splay(i),然后从i的左儿子一直往右儿子找,直到不存在右儿子为止。找父亲部分代码:

access(i);
splay(i);
// 找父亲
int ifa=son[i][0];
while (son[ifa][1]) ifa=son[ifa][1];

找到父亲之后,为了断边,我们要让i号节点和ifa节点之间存在直接相连的边。因此,先splay(ifa),然后,由于ifai深度连续,ifa的右儿子一定是i,而且i不存在左儿子(其实也不存在右儿子,因为我们accessi)。直接断边,把i连到新父亲即可。修改操作代码:

void modify(int i, int j) // 修改,j是新的父亲
{
    access(i);
    splay(i);
    // find father
    int ifa=son[i][0];
    while (son[ifa][1]) ifa=son[ifa][1];
    splay(ifa);
    assert(ifa==fa[i]);
    son[ifa][1]=0;
    push_up(ifa);
    fa[i]=j;
    return;
}

但是,且慢!如果你用这份代码提交,你将会得到两个TLE(而且是小数据的)。这倒不是常数的原因,而是因为,如果i已经是根,那么ifa就是0,然后我们就会进入splay(0)死循环!解决的方法是加一个ifa是否为0的判断。正确的修改操作代码:

void modify(int i, int j) // 修改,j是新的父亲
{
    access(i);
    splay(i);
    // find father
    int ifa=son[i][0];
    if (ifa)
    {
        while (son[ifa][1]) ifa=son[ifa][1];
        splay(ifa);
        assert(ifa==fa[i]);
        son[ifa][1]=0;
        push_up(ifa);
    }
    fa[i]=j;
    return;
}

好啦,操作都讲完啦qwq,接下来就是完整的AC代码了。顺带提一句,这份代码只有130+行(含空行),比普通LCT码量小了很多,而且常数出奇地小,不加任何刻意卡常,吸一口氧,总时间580+ms,在洛谷居然排在前5页(前几页有大量的巨佬反复提交),可谓是相当令人惊喜了qwq

完整AC code:

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <assert.h>

// by luogu @樱初音斗橡皮

const int N=200000;
const int M=100000;

int n, m;

int fa[N+10], son[N+10][2], siz[N+10];

inline bool nroot(int i) // 不是根
{
    return (son[fa[i]][0]==i)||(son[fa[i]][1]==i);
}

inline bool chk(int i) // 是右儿子
{
    return son[fa[i]][1]==i;
}

inline void push_up(int i) // 上传信息
{
    siz[i]=1;
    if (son[i][0])
        siz[i]+=siz[son[i][0]];
    if (son[i][1])
        siz[i]+=siz[son[i][1]];
    return;
}

inline void spin(int i) // 旋转
{
    int ifa=fa[i], igfa=fa[ifa], dir=chk(i),
        ison=son[i][dir^1], dirfa=chk(fa[i]), nrt=nroot(ifa);
    fa[ison]=ifa;
    son[ifa][dir]=ison;
    fa[ifa]=i;
    son[i][dir^1]=ifa;
    fa[i]=igfa;
    if (nrt)
        son[igfa][dirfa]=i;
    push_up(ifa);
    push_up(i);
    return;
}

void splay(int i) // splay
{
    int ifa=fa[i];
    while (nroot(i))
    {
        if (nroot(ifa))
        {
            if (chk(i)==chk(ifa))
                spin(ifa);
            else
                spin(i);
        }
        spin(i);
        ifa=fa[i];
    }
    return;
}

void access(int i) // access
{
    int j=0;
    while (i)
        splay(i), son[i][1]=j, push_up(i), j=i, i=fa[i];
    return;
}

int query(int i) // 查询
{
    access(i);
    splay(i);
    assert(son[i][1]==0);
    return son[i][0]?siz[son[i][0]]+1:1;
}

void modify(int i, int j) // 修改,j是新的父亲
{
    access(i);
    splay(i);
    // find father
    int ifa=son[i][0];
    if (ifa)
    {
        while (son[ifa][1]) ifa=son[ifa][1];
        splay(ifa);
        assert(ifa==fa[i]);
        son[ifa][1]=0;
        push_up(ifa);
    }
    fa[i]=j;
    return;
}

int main()
{
    scanf("%d", &n);
    for (int i=1; i<=n; ++i)
        fa[i]=0, son[i][0]=son[i][1]=0, siz[i]=1;
    for (int i=1; i<=n; ++i)
    {
        int tmp;
        scanf("%d", &tmp);
        fa[i]=(i+tmp>n)?0:(i+tmp);
    }
    scanf("%d", &m);
    for (int i=1; i<=m; ++i)
    {
        int cmd, j, k;
        scanf("%d%d", &cmd, &j);
        ++j;
        // 题目中的编号为0~n-1,实际的编号要+1便于操作
        switch (cmd)
        {
        case 1:
            printf("%d\n", query(j));
            break;
        case 2:
            scanf("%d", &k);
            modify(j, (j+k>n)?0:(j+k));
            break;
        default:
            printf("stupid yyc\n"); // 滑稽
            // 防止数据出错
            break;
        }
    }
    return 0;
}