• CF674E Bear and Destroying Subtrees


    CF674E Bear and Destroying Subtrees

    题目大意

    题目链接

    一棵树。初始时只有一个节点:根节点 (1)

    你需要支持 (q) 次操作。操作可能是如下两种之一:

    • (1 v)。表示添加一个叶子,它的父亲是 (v)
    • (2 v)。表示对以 (v) 为根的子树进行询问。你需要回答:如果子树里每条边以 (frac{1}{2}) 的概率保留(不同边被保留的概率相互独立),这棵子树的期望最大深度是多少?深度定义为到根(子树的根,即 (v))路径上的边数,这些边必须全部被保留。

    数据范围:(1leq qleq 5 imes 10^5)

    本题题解

    先不考虑动态添加叶子。假设已知某棵树的形态,如何回答单次询问?根据期望的定义,可以写出答案为:

    [egin{align} &sum_{i = 1}^{mathrm{maxdep}} icdot P( ext{树的最大深度为 $i$})\ = &sum_{i = 1}^{mathrm{maxdep}} P( ext{树的最大深度为 $geq i$}) end{align} ]

    其中 (P(dots)) 表示一件事发生的概率。

    要让树的最大深度 (geq i),只需要存在任意一个深度为 (i) 的点,使得它到根路径上的边全部被保留即可。这样的情况可能有很多种。具体来说,设树上深度为 (i) 的点有 (k) 个,那它们的任何一个非空子集满足要求,都是合法的情况。所以朴素的想法是,对 (2^k - 1) 种情况做容斥。但这显然太慢了!

    既然 (geq i) 的概率不好求,那么考虑补集转化,求最大深度 $ < i$ 的概率。此时的要求是:所有深度为 (i) 的点,到根路径上的边,都不能被全部保留。换句话说,我们把“(exist)”的问题,转化为了“(forall)”的问题。这就好办多了。考虑树形 DP。设 (f(u, i)) 表示 (u) 的子树,最大深度(定义为到 (u) 的距离,而不是到整棵树的根)$ < i$ 的概率。则有转移:

    [f(u, i) = prod_{vinmathrm{son}(u)} frac{1}{2}(1 + f(v, i - 1)) quad (i > 0) ]

    其中括号里的 (1) 表示 ((u, v)) 这条边不保留时的概率,(f(v, i - 1)) 表示保留时的概率。边界是 (f(u, 0) = 0)

    答案就是:

    [sum_{i = 1}^{mathrm{maxdep}}icdot (f(1, i + 1) - f(1, i)) ]

    设树的节点数为 (n)。则按此方法计算答案的时间复杂度是 (mathcal{O}(n^2)),更准确地说是 (mathcal{O}(ncdot mathrm{maxdep}))

    考虑动态添加叶子。注意到只需要更新它所有祖先的 DP 值。并且距离它为 (l) 的祖先 (a),只有 (f(a, l)) 的值会受到影响,所以添加一个叶子的时间复杂度是 (mathcal{O}(mathrm{maxdep})) 的。

    继续优化,发现当 (i) 较大时,最大深度等于 (i) 的概率较低。也就是说,存在一个阈值 (d),满足当 (i > d) 时,(f(1, i + 1) - f(1, i) < mathrm{eps} = 10^{-6})。此时就没必要计算 (f(u, i + 1)) 了。可以证明这个阈值大约在 (60) 左右。所以每次添加叶子,只需要向上修改 (d = 60) 个祖先的 DP 值。于是我们在 (mathcal{O}(nd)) 的时间复杂度内解决了本题。

    参考代码

    核心片段:

    const int MAXN = 5e5;
    const int D = 60;
    int q;
    int n, fa[MAXN + 5];
    double dp[MAXN + 5][D + 5];
    
    void update(int v) {
    	
    	static int anc[D + 5];
    	int top = 0;
    	
    	int u = v;
    	for (int i = 0; i <= D; ++i) {
    		if (u == 0) break;
    		top = i;
    		anc[i] = u;
    		u = fa[u];
    	}
    	
    	for (int i = top; i >= 2; --i) {
    		int u = anc[i];
    		int son = anc[i - 1];
    		dp[u][i] /= 0.5 * (1 + dp[son][i - 1]); // 把老的 dp[son] 值对父亲的贡献去掉
    	}
    	
    	for (int i = 1; i <= D; ++i) dp[v][i] = 1;
    	
    	for (int i = 1; i <= top; ++i) {
    		int u = anc[i];
    		int son = anc[i - 1];
    		dp[u][i] *= 0.5 * (1 + dp[son][i - 1]);
    	}
    }
    
    double query(int v) {
    	double res = 0;
    	for (int i = 1; i < D; ++i) {
    		res += i * (dp[v][i + 1] - dp[v][i]);
    	}
    	return res;
    }
    
    int main() {
    	n = 1;
    	for (int i = 1; i <= D; ++i) dp[1][i] = 1;
    	
    	read(q);
    	for (int tq = 1; tq <= q; ++tq) {
    		int op, v;
    		read(op); read(v);
    		if (op == 1) {
    			++n;
    			fa[n] = v;
    			update(n);
    		} else {
    			print(query(v));
    		}
    	}
    	return 0;
    }
    
  • 相关阅读:
    DS博客作业02--栈和队列
    DS博客作业01-线性表
    c博客06-结构体&文件
    C博客作业05--2019-指针
    C语言博客作业04——数组
    C语言博客作业03--函数
    面向对象设计大作业第二阶段:图书馆系统
    JAVA程序设计-有理数类的设计
    DS博客作业05--查找
    DS博客作业04--图
  • 原文地址:https://www.cnblogs.com/dysyn1314/p/14315658.html
Copyright © 2020-2023  润新知