1. 程式人生 > 實用技巧 >【洛谷P6773】命運

【洛谷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]\)
    所以有

\[f[x][i]=f[x][i]\times \sum^{dep[x]}_{j=0}f[y][j]+f[x][i]\times \sum^{i}_{j=0} f[y][j]+f[y][i]\times \sum^{i-1}_{j=0} f[x][j] \]

\[f[x][i]=f[x][i]\times (\sum^{dep[x]}_{j=0}f[y][j]+\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;
}