1 条题解
-
0
自动搬运
来自洛谷,原作者为

zyc2003
El Psy Congroo搬运于
2025-08-24 22:23:49,当前版本为作者最后更新于2021-07-05 10:18:32,作者可能在搬运后再次修改,您可在原文处查看最新版自动搬运只会搬运当前题目点赞数最高的题解,您可前往洛谷题解查看更多
以下是正文
这一切 , 都是 命运 石之门的选择 !
(中二一下 , 咳咳咳
本文摘要
- 简要介绍了 (容斥) 的做法 , 详细介绍了 (dp 方程的推导 , 深度的离散化 (虚树)) 和 (整体dp , 线段树合并) 的解法 .
- 由于我看各路大佬题解的线段树合并看了老半天没看懂 , 所以这一篇的线段树合并会讲的比较详细 , 可能更加适合像我一样没那么熟练链线段树合并的同学 .
- 同时 , 对于 dp 方程的推出有自然的想法 , 并非跳跃式的直接给出状态设计 . (不过对大佬来说确实一点都不跳跃
- 希望大家看得愉快 .
题意简述
题目中已经给出了形式化的描述 , 这里不再复述 . 为了下文叙述方便 , 我们把将一条边赋值为 说成是染了色 , 把题目给出的带限制的链有时候简称为一个限制 .
: 容斥大法好 !
看到题目后 , 我一开始心态是崩的 : 我怎么只有 的枚举算法 ? 完了呀 ... 但是你冷静一下 , 前面 的 都很小 - 也就是给定的树链 很少 , 有什么用呢 ?
题目要求的是 : 所有给定的树链 , 其链上至少有 条边被染色的方案数 . 我对着它冥思苦想 ... 没什么思路 , 不妨取问题的补集 , 也就是逆向思维 ? 可以等价于任意染色/不染色的方案数 , 减去 : 至少存在一条给定树链 , 其链上没有边被染色的方案数 .
到了这里 , 就可以使用我们的容斥大法 . 枚举所有 的子集 , 求出这个子集中所有被给定树链覆盖的边都不能染色的染色方案数 , 再乘以容斥系数 即可 . 该算法正确是因为 , 当 时 , 我们加上了任意染色的方案数 : 此时所有合法的和不合法的方案都在里面 ; 当 时 , 我们减去了 第 条链 , 第 条链 , , 第 条链上没有任何一条边被染色的方案数 ; 但是这些链是会相交的 , 我们多减去了一些东西 , 所以我们还需要加上第 条链和第 条链均没有任何一条边被染色的方案数 ... 归纳地证明了容斥的正确性 . 写成式子 , 就是 :
其中 是题目给定的树链的集合 , 表示 集合内的树链求交之后 , 覆盖的边数 .
而我们发现 , 覆盖一条链的操作可以用树链剖分维护 , 所以总复杂度为 : , 也可以优化到 (不过没什么用 , 得分没有变化 ... 所以我 的美梦破灭了
: dp 大法好 !
实际上容斥的做法 , 对于参加 NOI 的选手可能是小菜一碟的 . 那么更进一步的做法呢 ? 求方案数嘛 , 怎么想都和 dp 脱不了关系 . 而树上 dp 的设计肯定和子树有关 , 不妨先设计一个最 naive 的方程 :
表示 , 所有树链限制 , 其两个端点都在 子树内的限制都已经被满足 , 而 在 子树内 , 在 子树外的限制没有/有 被全部满足 , 的方案数 . 这是我们最初的想法 .
但是这是不可转移的 , 因为我们把所有链的信息浓缩在 中 , 以至于我们从子节点 向父节点 转移时 , 我们无法确定边 染色/不染色会造成的影响 . 主要影响是 , 当某个限制 满足 , 且 在 的子树内时 , 从 向 的转移就出了问题 : 你不懂何时需要覆盖 何时不需要 , 因为你不懂 这个限制是否已经在 的子树内得到满足 !
那我们尝试维护这个信息如何 ? 但是注意到 , 转移到 , 又会转移到 的父亲 ... 这可能要求我们维护一个和深度有关的信息 . 一端 在 的子树外 , 一端 在 的子树内 , 那么显然所有 都是 的祖先 ... 等等 , 所有 都是 的祖先 ? 在看看题目的要求 : 所有给定链上至少有 条边被染色 . 那么这就意味着 , 我们考虑所有尚未被满足的 , 且 在 子树外 , 在 子树内 , 那么能使得它们合法的染色只可能是将 中的某条边染色 ; 而最终情况下 , 必须让所有限制都合法 , 所以我们必须所有给 中最深的 中的某条边染色 ; 染色了 , 所有限制一并满足 ; 如果不是给 上的某条边染色 , 那么 这个限制将永远得不到满足 . 这就是其他题解中所说的 key observation .
所以 , 我们的 dp 方程新鲜出炉 : 设 表示 为根的子树中 , 两端点都在 子树内的限制已经被满足 , 而对于尚未被满足的 , 且 在 子树外 , 在 子树内 , 最深的 的深度为 .
现在来考虑转移 . 首先 , 如果对于一个 , 题目给出了多个限制 , 那么最深的那个 是有用的 . 设 (ancestor , 祖先) , 当 时 , 说明没有限制 , 或者也可以认为是设置了虚点 , 该限制为 . 那么 , 对于 , 我们有 , 因为对于限制 , 无论你怎么给 子树内的边染色 , 都不可能满足 ; 而 也不能取到 (显然) .
好 . 接下来推导 的转移 . 当处理到子节点 的时候 , 的值如何变化 :
对 染色
那么所有端点 在 内的限制全部得到满足 . 所以贡献为
$$f_{x,i}\times \sum_{\mathrm{dep}_{anc_y} \leq j < \mathrm{dep}_y} f_{y,j} $$噢 , 为了方便 , 不妨设 为 的前缀和 : , 由于 的部分都为 , 所以这个定义是合法的 .
所以贡献为 :
对 不染色
那么 , 如果 中最深的限制是 的 , 那么合并后的子树的最深的限制仍然是 . 所以 , 贡献为 :
但如果是 的 , 那么最深的限制可以有更浅的限制转移而来 , 贡献为 :
整合起来就是 :
$$f_{x,i}\leftarrow f_{x,i}\times g_{y,\mathrm{dep_y}-1}+f_{x,i}\times g_{y,i}+g_{x,i-1}\times f_{y,i},\mathrm{dep}_{anc_x} \leq i < \mathrm{dep}_x $$这样就有了 的做法 . 但是注意到 , 有用的点 , 也就是作为限制的点 是 级别的 . 所以 , 第二维 : 深度是可以离散化的 . 离散化后的第二维是 级别的 , 所有时空复杂度均为 , 可以拿到 的好成绩 . 我对于这部分分有实现 , 下面是离散化这部分的代码 :
int main() { n=read(); for(int i=1,x,y;i<n;++i) x=read(),y=read(),add(x,y),add(y,x); dfs(1,0); m=read();dep[0]=0; for(int i=1;i<=m;++i) { int u=read(),v=read(); anc[v]=dep[u] > dep[anc[v]] ? u : anc[v]; } for(int i=1;i<=n;++i) if(anc[i]) rk[++cnt]=dep[anc[i]]; sort(rk+1,rk+1+cnt),cnt=unique(rk+1,rk+1+cnt)-rk-1; rk[++cnt]=n; for(int i=1;i<=n;++i) dep[i]=lower_bound(rk+1,rk+1+cnt,dep[i])-rk; treedp(1,0); Writes(f[1][0]); return 0; }Q : 那么 数组怎么开 ?
A : 当然是用 vector 啦 !
Q : 你为什么不用虚树 ? 你这样写 , 两个点会爆空间啊 ! ...
A : 我忘记虚树了 ... 用虚树的话 , 时空复杂度变为 , 足以解决 的问题 .
: 整体 dp - 线段树合并 !
好吧 , 这部分我无法自然地想出 qaq ... 我们对式子进行一些变化 :
$$f_{x,i}\leftarrow f_{x,i}\times (g_{y,\mathrm{dep_y}-1}+ g_{y,i})+f_{y,i}\times g_{x,i-1} $$发现是对 乘上某个数在加上 乘上某个数 , 但是所谓的"这个数"是会变的啊 ?
不要紧 . 我们仍然考虑线段树合并 . 一开始 , 令 . 维护两个全局值 , 来表示 的前缀和 . 前缀和可以以引用的形式 (引用变量 , 或者设为全局变量也可以) , 在进入叶子结点或者 或者 时更新 . 而后在合并 的子树和 的子树时 : 我们发现难点在于当 或者 时 , 应该怎么维护下面的值呢 ?
当我们进入 或者 的结点时 , 看到我们的式子 , 就会发现 : 时 , 说明在该结点内 将不会变化 , 且所有 没有值 , 那么相当于整个子树全部同乘以一个数 : ; 而当 时 , 说明在该结点内 将不会变化 , 且所有 没有值 , 那么相当于整个子树全部同乘以一个数 : ; 而 时 , 显然我们会继续往下合并 , 只需要 pushup 即可 .
而后需要注意 , 对于 , 由于只有 会有值 , 所以处理完子树转移后 , 我们需要将 和 区间赋值为 . (覆盖总是没问题的 , 不覆盖可能会出锅)
综上 , 我们维护区间和和乘法标记即可 (区间赋值为 可以将乘法标记设为 )
#define Lx (T[x].l) #define Rx (T[x].r) #define Ly (T[y].l) #define Ry (T[y].r) #define mid ((l+r)>>1) int newp() {++cnt,T[cnt].mul=1;return cnt;} void pushup(int x) {T[x].v=qmod(T[Lx].v+T[Rx].v);} void pushdown(int x) { if(T[x].mul == 1) return ; T[Lx].mul=1ll*T[Lx].mul*T[x].mul%mod,T[Lx].v=1ll*T[Lx].v*T[x].mul%mod; T[Rx].mul=1ll*T[Rx].mul*T[x].mul%mod,T[Rx].v=1ll*T[Rx].v*T[x].mul%mod; T[x].mul=1; } void insert(int x,int l,int r,int p) { if(l == r) {T[x].v=1;return ;} if(p <= mid) Lx=newp(),insert(Lx,l,mid,p); else Rx=newp(),insert(Rx,mid+1,r,p); pushup(x); } int merge(int x,int y,int l,int r,int &sumx,int &sumy) { if(!x and !y) return 0; if(!x) { sumy=qmod(sumy+T[y].v); T[y].mul=1ll*T[y].mul*sumx%mod,T[y].v=1ll*T[y].v*sumx%mod; return y; } if(!y) { sumx=qmod(sumx+T[x].v); T[x].mul=1ll*T[x].mul*sumy%mod,T[x].v=1ll*T[x].v*sumy%mod; return x; } if(l == r) { sumx=qmod(sumx+T[x].v); T[x].v=qmod(1ll*T[x].v*sumy%mod+1ll*T[y].v*sumx%mod); sumy=qmod(sumy+T[y].v); return x; } pushdown(x),pushdown(y); Lx=merge(Lx,Ly,l,mid,sumx,sumy); Rx=merge(Rx,Ry,mid+1,r,sumx,sumy); pushup(x); return x; } void cover(int x,int l,int r,int ql,int qr) { if(!x) return ; if(ql <= l and r <= qr) {T[x].mul=0,T[x].v=0;return ;} pushdown(x); if(ql <= mid) cover(Lx,l,mid,ql,qr); if(qr > mid) cover(Rx,mid+1,r,ql,qr); pushup(x); } int query(int x,int l,int r,int p) { if(!x) return 0; if(l == r) return T[x].v; pushdown(x); return p<=mid?query(Lx,l,mid,p):query(Rx,mid+1,r,p); } void treedp(int x,int fa) { root[x]=newp(); insert(root[x],0,maxdep,dep[anc[x]]); for(int i=head[x];i;i=nxt[i]) { int y=ver[i]; if(y == fa) continue; treedp(y,x); int sumx=0,sumy=T[root[y]].v;//g_{y,dep_y-1} root[x]=merge(root[x],root[y],0,maxdep,sumx,sumy); } cover(root[x],0,maxdep,dep[x],maxdep); }我从开始看这道题 , 然后写部分分 , 写正解 , 写题解 ... 一共断断续续地经过了 天 , 完结撒花 !
- 1
信息
- ID
- 5849
- 时间
- 2000ms
- 内存
- 1024MiB
- 难度
- 7
- 标签
- 递交数
- 0
- 已通过
- 0
- 上传者