1. 程式人生 > >[BZOJ4784][Zjoi2017]仙人掌(樹形DP)

[BZOJ4784][Zjoi2017]仙人掌(樹形DP)

Address

洛谷P3687
BZOJ4784
UOJ#290
LOJ#2250

Solution

首先,如果原圖不是仙人掌,就直接輸出 0 0
否則對圖進行一遍 DFS ,找出所有的環並去掉,原圖變成森林。答案顯然是每棵樹的答案的乘積。
考慮到在如果在樹上的點對 ( u

, v ) (u,v) 之間連邊,就相當於把 u u v
v
的路徑上的所有邊標記為環邊。
但存在兩個問題:
(1)不能有重邊,也就是說如果樹上已經有邊 ( u , v ) (u,v)
就不能連邊 ( u , v ) (u,v)
(2)每一條邊不一定要全部被標記為環邊。
但撕烤後發現上面這兩個條件可以抵消。
也就是說,如果我們假設可以有重邊,那麼問題再次轉化成選出一些路徑集合 { ( u , v ) u v } \{(u,v)|u\ne v\} 覆蓋樹上所有的邊。
我們可以開始愉快地樹形 DP 了!
f [ u ] f[u] 表示覆蓋 u u 的子樹內所有邊的方案數。
g [ u ] g[u] 表示覆蓋 u u 的所有子樹內所有邊,並選出一條端點為 u u 的路徑向 u u 的父親延伸的方案數。
w [ i ] w[i] 表示將 i i 個元素分到一些集合內,使得每個集合非空且大小不超過 2 2 的方案數。
h [ i ] h[i] 表示將 i i 個元素分到一些集合內,使得每個集合非空且大小不超過 2 2 ,所有方案中大小為 1 1 的集合個數之和。
邊界:
w [ 0 ] = 1 , h [ 0 ] = 0 , f [ ] = 0 , g [ ] = 0 w[0]=1,h[0]=0,f[葉子節點]=0,g[葉子節點]=0
w w 的轉移:
w [ i ] = w [ i 1 ] + w [ i 2 ] × ( i 1 ) w[i]=w[i-1]+w[i-2]\times(i-1)
h h 的轉移:
h [ i ] = h [ i 1 ] + w [ i 1 ] + h [ i 2 ] × ( i 1 ) h[i]=h[i-1]+w[i-1]+h[i-2]\times(i-1)
f f 的轉移也就是列舉 u u 的子節點 v v ,討論兩種情況(一為路徑 ( u , v ) (u,v) 單獨存在,二為路徑 ( u , v ) (u,v) 不單獨存在),然後需要把子節點分組,每個組大小不超過 2 2 ,對於大小為 2 2 的組,在點 u u 把來自兩條處於不同子樹的路徑連線成一條:
f [ u ] = w [ d [ u ] ] v s o n [ u ] ( f [ v ] + g [ v ] ) f[u]=w[d[u]]\prod_{v\in son[u]}(f[v]+g[v])
g g 也差不多:
g [ u ] = h [ d [ u ] ] v s o n [ u ] ( f [ v ] + g [ v ] ) g[u]=h[d[u]]\prod_{v\in son[u]}(f[v]+g[v])
答案為所有森林的根的 f f 之積。

Code

#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define For(i, a, b) for (i = a; i <= b; i++)
#define Edge(u) for (int e = adj[u], v = go[e]; e; e = nxt[e], v = go[e])
#define Tree(u) for (int e = adj[u], v; e; e = nxt[e]) if ((v = go[e]) != fu)
using namespace std;

inline int read()
{
	int res = 0; bool bo = 0; char c;
	while (((c = getchar()) < '0' || c > '9') && c != '-');
	if (c == '-') bo = 1; else res = c - 48;
	while ((c = getchar()) >= '0' && c <= '9')
		res = (res << 3) + (res << 1) + (c - 48);
	return bo ? ~res + 1 : res;
}

const int N = 5e5 + 5, M = N << 2, ZZQ = 998244353;
int n, m, ecnt, nxt[M], adj[N], go[M], f[N], g[N], h[N], w[N], cnt[N],
dfn[N], T, d[N];
bool is, cut[M], vis[N];

void add_edge(int u, int v)
{
	nxt[++ecnt] = adj[u]; adj[u] = ecnt; go[ecnt] = v;
	nxt[++ecnt] = adj[v]; adj[v] = ecnt; go[ecnt] = u;
}

void dfs(int u, int fe)
{
	dfn[u] = ++T;
	cnt[u] = 0;
	Edge(u)
	{
		if (e == (fe ^ 1)) continue;
		if (!dfn[v]) dfs(v, e), cnt[u] += cnt[v];
		else if (dfn[u] < dfn[v]) cnt[u]--;
		else cnt[u]++;
	}
	if (u > 1 && !cnt[u]) cut[fe] = cut[fe ^ 1] = 1;
	if (cnt[u] >= 2) is = 0;
}

int dp(int u, int fu)
{
	d[u] = 0;
	f[u] = 1; vis[u] = 1;
	Tree(u)
	{
		if (!cut[e]) continue;
		d[u]++;
		dp(v, u);
		f[u] = 1ll * f[u] * (f[v] + g[v]) % ZZQ;
	}
	g[u] = 1ll * f[u] * h[d[u]] % ZZQ;
	f[u] = 1ll * f[u] * w[d[u]] % ZZQ;
	return f[u];
}

void work()
{
	int i, x, y, ans = 1;
	n = read(); m = read();
	ecnt = 1; T = 0;
	For (i, 1, n) adj[i] = dfn[i] = 0, vis[i] = 0;
	while (m--) x = read(), y = read(), add_edge(x, y);
	For (i, 2, ecnt) cut[i] = 0;
	is = 1;
	dfs(1, 0);
	if (!is) return (void) puts("0");
	For (i, 1, n) if (!vis[i])
		ans = 1ll * ans * dp(i, 0) % ZZQ;
	printf("%d\n", ans)