【洛谷P6773】命运
题目
题目链接:https://www.luogu.com.cn/problem/P6773
提示:我们在题目描述的最后一段提供了一份简要的、形式化描述的题面。
在遥远的未来,物理学家终于发现了时间和因果的自然规律。即使在一个人出生前,我们也可以通过理论分析知晓他或她人生的一些信息,换言之,物理学允许我们从一定程度上“预言”一个人的“命运”。
简单来说,一个人的命运是一棵由时间点构成的有根树 \(T\):树的根结点代表着出生,而叶结点代表着死亡。每个非叶结点 \(u\) 都有一个或多个孩子 \(v_1, v_2,\dots , v_{c_u}\),表示这个人在 \(u\) 所代表的时间点做出的 \(c_u\) 个不同的选择可以导向的不同的可能性。形式化的,一个选择就是树上的一条边 \((u, v_i)\),其中 \(u\) 是 \(v_i\) 的父结点。
一个人的一生是从出生(即根结点)到死亡(即某一个叶子结点)的一条不经过重复结点的路径,这条路径上任何一个包含至少一条边的子路径都是这个人的一段人生经历,而他或她以所有可能的方式度过一生,从而拥有的所有人生经历,都被称为潜在的人生经历。换言之,所有潜在的人生经历就是所有 \(u\) 到 \(v\) 的路径,满足 \(u, v \in T\),\(u \neq v\),并且 \(u\) 是 \(v\) 的祖先。在数学上,这样一个潜在的人生经历被记作有序对 \((u, v)\),树 \(T\) 所有潜在的人生经历的集合记作 \(\mathcal P_T\)。
物理理论不仅允许我们观测代表命运的树,还能让我们分析一些潜在的人生经历是否是“重要”的。一个人所作出的每一个选择——即树上的每一条边——都可能是重要或不重要的。一段潜在的人生经历被称为重要的,当且仅当其对应的路径上存在一条边是重要的。我们可以观测到一些潜在的人生经历是重要的:换言之,我们可以观测得到一个集合 \(\mathcal Q \subseteq \mathcal P_T\),满足其中的所有潜在的人生经历 \((u, v) \in \mathcal Q\) 都是重要的。
树 \(T\) 的形态早已被计算确定,集合 \(\mathcal Q\) 也早已被观测得到,一个人命运的不确定性已经大大降低了。但不确定性仍然是巨大的——来计算一下吧,对于给定的树 \(T\) 和集合 \(\mathcal Q\),存在多少种不同的方案确定每条边是否是重要的,使之满足所观测到的 \(\mathcal Q\) 所对应的限制:即对于任意 \((u, v) \in \mathcal Q\),都存在一条 \(u\) 到 \(v\) 路径上的边被确定为重要的。
形式化的:给定一棵树 \(T = (V, E)\) 和点对集合 \(\mathcal Q \subseteq V \times V\) ,满足对于所有 \((u, v) \in \mathcal Q\),都有 \(u \neq v\),并且 \(u\) 是 \(v\) 在树 \(T\) 上的祖先。其中 \(V\) 和 \(E\) 分别代表树 \(T\) 的结点集和边集。求有多少个不同的函数 \(f\) : \(E \to \{0, 1\}\)(将每条边 \(e \in E\) 的 \(f(e)\) 值置为 \(0\) 或 \(1\)),满足对于任何 \((u, v) \in \mathcal Q\),都存在 \(u\) 到 \(v\) 路径上的一条边 \(e\) 使得 \(f(e) = 1\)。由于答案可能非常大,你只需要输出结果对 \(998,244,353\)(一个素数)取模的结果。
思路
假设我们已经确定点 \(x\) 子树内所有边的权值,剩余没有被 \(1\) “覆盖” 且 \(y\in \operatorname{sub(x)}\)的点对 \((x,y)\) 中,\(x\) 深度最深的为 \(d\),那么我们只要知道这个 \(d\) 就恶意进行转移。
设 \(f[x][i]\) 表示点 \(x\) 的子树内,没有被覆盖的点对中祖先的最大深度为 \(i\) 的方案数。
考虑加入 \(x\) 的一个儿子 \(y\) 为根的子树时,分类讨论这个 \(i\) 由哪一边得到的:
- 如果 \(x\) 到 \(y\) 的边填 \(1\),那么 \(f[x][i]=f[x][i]\times \sum^{dep[x]}_{j=0}f[y][j]\)
- 如果 \(x\) 到 \(y\) 的边填 \(0\),那么 \(f[x][i]=f[x][i]\times \sum^{i}_{j=0} f[y][j]+f[y][i]\times \sum^{i-1}_{j=0} f[x][j]\)
所以有
当加入一个新的约束时,假设这个约束的深度为 \(d\),那么直接让 \(f[x][d]=\sum^{i=d}_{dep[x]}f[x][i]\),然后将 \(f[x][d+1\sim dep[x]]\) 全部赋值为 \(0\) 即可。
直接上树形 dp,时间复杂度 \(O(n^3)\);加上前缀和优化即可做到 \(O(n^2)\)。
我们把点 \(x\) 对应的每一个 dp 值 \(f[x][i]\) 放到一棵线段树上,那么将 \(y\) 的线段树合并到 \(x\) 的线段树时:
- 若一个位置 \([l,r](r>l)\) 只有一棵线段树有,那么容易发现方程变为了乘上前缀的系数。
- 若到达一个叶子节点时两个线段树都有,那么方程就是单点的修改。具体的,依然要求出前缀和。
这时一个“点 \(i\) 要合并另一棵线段树前缀 \(i\) 的和”的形式,我们在线段树合并的时候记录一下前缀和即可。
时空复杂度 \(O(n \log n)\)。
代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=500010,MOD=998244353,LG=40;
int n,m,tot,maxd[N],head[N],dep[N],rt[N];
ll sumx,sumy;
int read()
{
int d=0; char ch=getchar();
while (!isdigit(ch)) ch=getchar();
while (isdigit(ch)) d=(d<<3)+(d<<1)+ch-48,ch=getchar();
return d;
}
struct edge
{
int next,to;
}e[N*2];
void add(int from,int to)
{
e[++tot].to=to;
e[tot].next=head[from];
head[from]=tot;
}
void dfs1(int x,int fa)
{
dep[x]=dep[fa]+1;
for (int i=head[x];~i;i=e[i].next)
if (e[i].to!=fa) dfs1(e[i].to,x);
}
struct SegTree
{
int tot,lc[N*LG],rc[N*LG];
ll f[N*LG],mul[N*LG];
bool lazy[N*LG];
void pushdown(int x)
{
if (lazy[x])
{
f[lc[x]]=f[rc[x]]=lazy[x]=0;
lazy[lc[x]]=lazy[rc[x]]=1;
}
if (mul[x]>1)
{
f[lc[x]]=f[lc[x]]*mul[x]%MOD;
f[rc[x]]=f[rc[x]]*mul[x]%MOD;
mul[lc[x]]=mul[lc[x]]*mul[x]%MOD;
mul[rc[x]]=mul[rc[x]]*mul[x]%MOD;
mul[x]=1;
}
}
void pushup(int x)
{
f[x]=(f[lc[x]]+f[rc[x]])%MOD;
}
int update(int x,int l,int r,int p,ll v)
{
if (!x) x=++tot,mul[x]=1;
pushdown(x);
if (l==p && r==p)
{
f[x]=(f[x]+v)%MOD;
return x;
}
int mid=(l+r)>>1;
if (p<=mid) lc[x]=update(lc[x],l,mid,p,v);
else rc[x]=update(rc[x],mid+1,r,p,v);
pushup(x);
return x;
}
ll query(int x,int l,int r,int ql,int qr)
{
if (!x) return 0;
pushdown(x);
if (l==ql && r==qr) return f[x];
int mid=(l+r)>>1;
if (qr<=mid) return query(lc[x],l,mid,ql,qr);
if (ql>mid) return query(rc[x],mid+1,r,ql,qr);
return (query(lc[x],l,mid,ql,mid)+query(rc[x],mid+1,r,mid+1,qr))%MOD;
}
void clear(int x,int l,int r,int ql,int qr)
{
if (!x) return;
pushdown(x);
if (l==ql && r==qr)
{
lazy[x]=1; f[x]=0;
return;
}
int mid=(l+r)>>1;
if (qr<=mid) clear(lc[x],l,mid,ql,qr);
else if (ql>mid) clear(rc[x],mid+1,r,ql,qr);
else clear(lc[x],l,mid,ql,mid),clear(rc[x],mid+1,r,mid+1,qr);
pushup(x);
}
int merge(int x,int y,int l,int r)
{
if (!x && !y) return 0;
pushdown(x); pushdown(y);
if (x && !y)
{
sumx=(sumx+f[x])%MOD;
f[x]=f[x]*sumy%MOD; mul[x]=mul[x]*sumy%MOD;
return x;
}
if (y && !x)
{
sumy=(sumy+f[y])%MOD;
f[y]=f[y]*sumx%MOD; mul[y]=mul[y]*sumx%MOD;
return y;
}
if (l==r)
{
ll fx=f[x];
sumy=(sumy+f[y])%MOD;
f[x]=(f[x]*sumy+f[y]*sumx)%MOD;
sumx=(sumx+fx)%MOD;
return x;
}
int mid=(l+r)>>1;
lc[x]=merge(lc[x],lc[y],l,mid);
rc[x]=merge(rc[x],rc[y],mid+1,r);
f[x]=(f[lc[x]]+f[rc[x]])%MOD;
return x;
}
}seg;
void dfs2(int x,int fa)
{
rt[x]=seg.update(rt[x],0,n,0,1);
for (int i=head[x];~i;i=e[i].next)
{
int v=e[i].to;
if (v!=fa)
{
dfs2(v,x);
sumx=0; sumy=seg.query(rt[v],0,n,0,dep[x]);
rt[x]=seg.merge(rt[x],rt[v],0,n);
}
}
if (maxd[x])
{
ll s=seg.query(rt[x],0,n,0,maxd[x]);
seg.clear(rt[x],0,n,0,maxd[x]);
rt[x]=seg.update(rt[x],0,n,maxd[x],s);
}
}
int main()
{
memset(head,-1,sizeof(head));
n=read();
for (int i=1,x,y;i<n;i++)
{
x=read(); y=read();
add(x,y); add(y,x);
}
dfs1(1,0);
m=read();
for (int i=1,x,y;i<=m;i++)
{
x=read(); y=read();
maxd[y]=max(maxd[y],dep[x]);
}
dfs2(1,0);
printf("%lld\n",seg.query(rt[1],0,n,0,0));
return 0;
}