zoukankan      html  css  js  c++  java
  • [题解]CF1527D MEX Tree

    题目链接

    #1.0 基本思路

    首先需要理解这个 "mex" 的含义,我们需要找出所有的路径使得 (i) 是编号最小的未出现的点,那么也就意味着 ([0,i-1]) 应当全部包含在我们要找的路径中。那么我们便可以尝试维护这样的路径:路径两端 (x, yin[0,i-1])([0,i-1]) 包含在路径经过的点集中,然后计算有多少条路经可以不经过 (i)(询问)。最后再尝试将 (i)​ 加入当前维护的路径(修改)。

    不妨先将整棵树的根节点设为 (0)

    #2.0 分类讨论

    下文中会反复运用 LCA 来进行判断,这里采用 st 表 (O(nlog n)) 预处理,(O(1))​ 询问的方法。​

    • siz[i] 表示以 (i)​ 为根的子树大小。
    • sp 为非零端点所在 (0) 的对应儿子的子树大小

    本题涉及到的情况比较多,这里会一一讨论。

    #2.1 询问

    先来看询问的部分,整体分为 (3)​ 种情况:

    • (i) 在当前路径中
    • (i) 是当前路径端点的子孙
    • (i) 不属于上面的任意一种情况

    这里再拎出来两种比较特殊的情况:(mex=0)(mex=1.)

    (mex=0)​ 时,每次选择的路径应当是都在同一 (0)​ 的子树内的两个点为端点;当 (mex=1)​​ 时,可行的路径数为在去掉 (1)​ 的子树后的点集中任意选取两个点产生的路径条数,减去不经过 (0) 的路径数量(即在仅在 (0)​ 的子节点的子树内的路径),需要特判是 (1) 的祖先的子树的情况,采用 LCA 判断。计算代码如下:

    inline void pre_ans() {
        ans[1] = (siz[0] - siz[1]) * (siz[0] - siz[1] - 1) / 2;
        for (int i = head[0]; i; i = e[i].nxt) {
            ans[0] += (1ll * siz[e[i].v] * (siz[e[i].v] - 1)) / 2;
            int lca = LCA(1, e[i].v);
            if (lca == e[i].v) ans[1] -= ((siz[e[i].v] - siz[1])
                                        * (siz[e[i].v] - siz[1] - 1) / 2);
            else ans[1] -= siz[e[i].v] * (siz[e[i].v] - 1) / 2;
        }
    }
    

    解决上面的两种特殊情况后,我们再来看将所有情况分为两种情况:

    • 路径一端是零
    • 路径两端都不是零

    先来看路径一端是零(两端分别为 (x,0))的情况:

    • (i)​ 在当前路径中

      (LCA(x,i)=i),这种情况答案一定为 (0)

    • (i) 是当前路径端点的子孙

      1. (i)​ 是不为零的一端的端点的子孙。那么答案应当是 (siz[x] - siz[i]) * (siz[0] - sp)。这种情况的判断条件 (LCA(x,i)=x)
      2. (i)​ 是不与非零端点所在 (0)​ 的对应儿子的子树中。那么答案是 (siz[0] - sp - siz[i]) * siz[x]。这种情况的判断条件 (LCA(x, i)=0)​;
    • (i) 不属于上面的任意一种情况

      那么也就意味着 (i) 是当前路径上的一个分叉,答案即为 (siz[0] - sp) * siz[x]

    再来看路径两端 (x,y) 都不是零的情况:

    • (i) 在当前路径中

      (LCA(x,i)=i)(LCA(y,i)=i)​,那么答案为零;

    • (i) 是当前路径端点的子孙

      答案为对应端点子树大小减去 siz[i] 再乘上另一端点子树大小;

    • (i)​ 不属于上面的任意一种情况

      答案为 siz[x] * siz[y]

    以上,便是查询操作的所有内容。以下是代码:

    inline ll GetAns(int k){
        int lca1 = LCA(k, lp), lca2 = LCA(k, rp);
        ll res1, res2;
        if (rp) { //这里在插入时会保证为 0 的一端一定是 rp
            res1 = siz[lp]; res2 = siz[rp];
            if (lca1 == k || lca2 == k) return 0;
            /*i 在当前路径上*/
            else if (lca1 == lp) res1 -= siz[k];
            else if (lca2 == rp) res2 -= siz[k];
            /*两种不同的情况*/
        } else {
            res1 = siz[lp]; res2 = siz[rp] - sp;
            if (lca1 == k || lca2 == k) return 0;
            else if (lca1 == lp) res1 -= siz[k];
            else if (lca1 == 0) res2 -= siz[k];
        }
        return res1 * res2;
    }
    

    #2.2 修改

    现在来尝试将 (i) 加入当前所维护的路径。

    仍旧分为两种大情况:

    • 至少一端为 (0)
    • 两端都不为 (0)

    若两端都不为 (0),那么有以下三种情况:

    • 在路径上。即 (LCA(x, i)=i)(LCA(y,i)=i),那么说明 (i) 已经在路径上。
    • 是其中一端的儿子,即 (LCA(x,i)=x)(LCA(y,i)=y),将对应的端点设置为 (i) 即可;
    • 不属于上面的两种情况,即是当前路径的分叉,那么显然不可能将 ([1,i]) 通过一条路径相连,那么后面的所有答案数都应当是 (0)​。

    若存在至少一端 (y=0),有以下几种情况:

    • 另一端也是 (0),那么令其中一端变为 (i)
    • (LCA(x,i)=i),已经在路径上;
    • (LCA(x,i)=i),更新 (x=i)​;
    • 不属于上面的情况但 (LCA(x,i) e0),那么意味着分叉了,插入失败;
    • (x e0)(LCA(x,i)=0),那么更新 (y=0)
    inline bool add(int x) {
        int lf = LCA(lp, x), rf = LCA(rp, x);
        if (rp) {
            if (lf == lp) {lp = x; return true;}
            else if (lf == x) return true;
            if (rf == rp) {rp = x; return true;}
            else if (rf == x) return true;
        } else {
            if (lf && (lf != lp && lf != x)) return false;
            else if (lf == lp) {lp = x; return true;}
            else if (lf == x) return true;
            else if (lf) return false;
            else {rp = x; return true;}
        }
        return false;
    }
    

    #3.0 完整代码

    有许多数组可以不 memset() 就不 memset();比如本题中的 dep[]siz[]

    const int N = 600010;
    const int INF = 0x3fffffff;
    
    struct Edge {
        int u, v;
        int nxt;
    };
    Edge e[N << 1];
    
    int n, head[N], cnt = 1, st[N << 1], scnt, lp, rp;
    int mn[N << 1][30], dep[N], w[N], rt, t, lg2[N], f1, f2;
    ll ans[N], sp, siz[N];
    
    inline void clear() { //清空
        cnt = 1, scnt = lp = rp = 0;
        mset(head, 0); dep[0] = 0;
    }
    
    inline void add(const int &u, const int &v) {
        e[cnt].u = u, e[cnt].v = v;
        e[cnt].nxt = head[u], head[u] = cnt ++;
    }
    
    inline int LCA(int a, int b) { //O(1) 获得 LCA
        int l = w[a], r = w[b];
        if (r < l) swap(l, r);
        int k = lg2[r - l + 1];
        if (dep[mn[l][k]] < dep[mn[r - (1 << k) + 1][k]])
            return mn[l][k];
        else return mn[r - (1 << k) + 1][k];
    }
    
    void dfs(int x, int fa) { //计算 siz[],dep[] 等数组
        st[++ scnt] = x, w[x] = scnt;
        siz[x] = 1, dep[x] = dep[fa] + 1;
        for (int i = head[x]; i; i = e[i].nxt) {
            if (e[i].v == fa) continue;
            dfs(e[i].v, x); st[++ scnt] = x;
            siz[x] += siz[e[i].v];
        }
    }
    
    inline void get_st() { //计算 st 表,用于 O(1) LCA
        lg2[1] = 0;
        for (int i = 2; i <= scnt; ++ i) lg2[i] = lg2[i >> 1] + 1;
        for (int i = 1; i <= scnt; ++ i) mn[i][0] = st[i];
        for (int j = 1; j <= lg2[scnt]; ++ j)
          for (int i = 1; i + (1 << j) - 1 <= scnt; ++ i)
            if (dep[mn[i][j - 1]] < dep[mn[i + (1 << (j - 1))][j - 1]])
              mn[i][j] = mn[i][j - 1];
            else mn[i][j] = mn[i + (1 << (j - 1))][j - 1];
    }
    
    inline void pre_ans() { //预处理 0 和 1 的答案
        ans[1] = (siz[0] - siz[1]) * (siz[0] - siz[1] - 1) / 2;
        for (int i = head[0]; i; i = e[i].nxt) {
            ans[0] += (1ll * siz[e[i].v] * (siz[e[i].v] - 1)) / 2;
            int lca = LCA(1, e[i].v);
            if (lca == e[i].v) ans[1] -= ((siz[e[i].v] - siz[1])
                                        * (siz[e[i].v] - siz[1] - 1) / 2);
            else ans[1] -= siz[e[i].v] * (siz[e[i].v] - 1) / 2;
        }
    }
    
    inline bool add(int x) { //尝试将点加入路径
        int lf = LCA(lp, x), rf = LCA(rp, x);
        if (rp) {
            if (lf == lp) {lp = x; return true;}
            else if (lf == x) return true;
            if (rf == rp) {rp = x; return true;}
            else if (rf == x) return true;
        } else {
            if (lf && (lf != lp && lf != x)) return false;
            else if (lf == lp) {lp = x; return true;}
            else if (lf == x) return true;
            else if (lf) return false;
            else {rp = x; return true;}
        }
        return false;
    }
    
    inline ll GetAns(int k){ //计算每个点的答案
        int lca1 = LCA(k, lp), lca2 = LCA(k, rp);
        ll res1, res2;
        if (rp) {
            res1 = siz[lp]; res2 = siz[rp];
            if (lca1 == k || lca2 == k) return 0;
            else if (lca1 == lp) res1 -= siz[k];
            else if (lca2 == rp) res2 -= siz[k];
        } else {
            res1 = siz[lp]; res2 = siz[rp] - sp;
            if (lca1 == k || lca2 == k) return 0;
            else if (lca1 == lp) res1 -= siz[k];
            else if (lca1 == 0) res2 -= siz[k];
        }
        return res1 * res2;
    }
    
    int main(){
        scanf("%d", &t);
        while (t --) {
            scanf("%d", &n); clear();
            for (int i = 1; i < n; ++ i) {
                int u, v; scanf("%d%d", &u, &v);
                add(u, v); add(v, u);
            }
            dfs(rt = 0, 0); get_st(); pre_ans();
            /*进行所需要的预处理*/
            for (int i = head[0]; i; i = e[i].nxt)
              if (LCA(e[i].v, 1) == e[i].v) {
                  sp = siz[e[i].v]; break;
              } //计算 sp
            for (int i = 2; i <= n; ++ i) {
                if (!add(i - 1)) break; 
                //尝试加入 i-1,不行就退出
                if (i == n) {ans[i] = 1; break;}
                /*如果 i == n,那么显然只存在一条路径
                贯穿整个树(其实是一条链)*/
                ans[i] = GetAns(i);
            }
            for (int i = 0; i <= n; ++ i) {
                printf("%lld ", ans[i]);
                ans[i] = 0;
            }
            printf("
    ");
        }
        return 0;
    }
    
  • 相关阅读:
    Qt单元测试工具 QTestlib(QVERIFY, QFETCH, QCOMPARE等)
    再续FPGA初心,京微齐力脱胎京微雅格重新起航(700万元天使轮,泰有基金领投,水木基金、臻云创投、泰科源跟投。数千万元Pre-A轮融资,领投方为海康基金)
    为何放弃 C++ 的工作(开发慢,难度高。完全不适应互联网的快速迭代的思想)
    c++ try catch 问题(没有throw就会被删除,加上/EHa这样就不会被删除)
    西方教育骗局,终于明白精英和普通人的残酷差别!(该校流传着一个数字——4。即“4小时睡眠、4杯咖啡、GPA4.0”——要想获得满分为4分的成绩,每天只睡4个小时,困了就喝4大杯咖啡)
    分析Net 内存对象
    Dynamic Routing Based On Redis
    词云
    vim与sublime
    ASP.NET Core 2.0 源代码
  • 原文地址:https://www.cnblogs.com/Dfkuaid-210/p/CF1527D.html
Copyright © 2011-2022 走看看