前言
由于水平有限,这篇文章比较难懂,并且也有很多不够透彻的地方,如果您有任何的看法,非常感谢您私信指导。
容斥dp
用dp的方法来描述容斥,大概的想法是,把容斥系数分到每一步里去乘。
通常当你有容斥做法,且适配的子集条件较为一般,且数据范围不足通过时考虑使用容斥dp。
确实不太好描述,看题吧。
来源神秘
求长度为 n ≤ 2000 n\leq 2000 n≤2000,值域为 m ≤ 2000 m\leq 2000 m≤2000的序列个数,满足前一个数不是后一个数的非本身的倍数。
设
f
i
,
j
f_{i,j}
fi,j表示前
i
i
i个数,第
i
i
i个数为
j
j
j的合法序列方案数,则:
f
i
,
j
=
∑
m
k
=
1
[
j
=
k
或
j
∤
k
]
f
i
−
1
,
k
f_{i,j}=\underset{k=1}{\overset m\sum}[j=k 或 j\nmid k]f_{i-1,k}
fi,j=k=1∑m[j=k或j∤k]fi−1,k
复杂度
O
(
n
m
2
)
O(nm^2)
O(nm2)
如果我们转化成:
f
i
,
j
=
∑
m
k
=
1
f
i
−
1
,
k
−
∑
m
k
=
1
[
j
≠
k
且
j
∣
k
]
f
i
−
1
,
k
f_{i,j}=\underset{k=1}{\overset m\sum}f_{i-1,k}-\underset{k=1}{\overset m\sum}[j\not=k 且 j\mid k]f_{i-1,k}
fi,j=k=1∑mfi−1,k−k=1∑m[j=k且j∣k]fi−1,k
就可以做到 O ( n m log m ) O(nm\log m) O(nmlogm)
n
,
m
≤
1
0
5
n,m\leq 10^5
n,m≤105:
考虑容斥,如果设
S
i
S_i
Si表示第
i
,
i
+
1
i,i+1
i,i+1个数满足限制的所有方案构成的集合,那么答案为
∣
⋂
S
i
∣
|\bigcap S_i|
∣⋂Si∣,那么其补集为一些数不满足限制,也就是说是倍数关系。如果设
f
(
i
)
f(i)
f(i)表示
i
i
i选出
i
i
i个位置不满足条件的方案数,设
g
(
i
)
g(i)
g(i)表示恰好
n
n
n中有
i
i
i个位置不满足条件的方案数,那么相当于二项式反演的式子(虽然可以直接认为是容斥,但是虽然但是):
f
(
x
)
=
∑
n
i
=
x
(
i
x
)
g
(
i
)
f(x)=\underset{i=x}{\overset n\sum}\begin{pmatrix}i\\x\end{pmatrix}g(i)
f(x)=i=x∑n(ix)g(i)
g
(
x
)
=
∑
n
i
=
x
(
i
x
)
(
−
1
)
i
−
x
f
(
i
)
g(x)=\underset{i=x}{\overset n\sum}\begin{pmatrix}i\\x\end{pmatrix}(-1)^{i-x}f(i)
g(x)=i=x∑n(ix)(−1)i−xf(i)
a
n
s
=
g
(
0
)
=
∑
n
i
=
0
(
−
1
)
i
f
(
i
)
ans=g(0)=\underset{i=0}{\overset n\sum}(-1)^if(i)
ans=g(0)=i=0∑n(−1)if(i)
我们可以认为一个不合法的相邻位置会贡献一个 − 1 -1 −1的容斥系数,具体来说会给它这种方案乘上一个 − 1 -1 −1的系数,对于一种方案来说,它实际上对答案的贡献(加到答案上的值是) ( − 1 ) k (-1)^k (−1)k,其中 k k k表示这种方案中有多少邻居不合法。
然后我们考虑现在的所有位置,存在着两种情况,一种叫做“非法”,一种叫做“不管”,这些东西形成了一些连续段,最终拼成了整个序列。
那么可以将数列拆成 1 1 1个不知道+ x ≥ 0 x\geq 0 x≥0个非法 的若干连续段,相当于编出来一个转移。
那么就可以根据这个东西把序列划分为若干个阶段,然后设出状态。即 f i f_i fi表示以 i i i为结尾的所有容斥系数之和。容易发现 f i f_i fi等于长度为 i i i的合法序列的数量。
那么就会有
f
i
=
∑
j
≥
0
f
i
−
j
⋅
w
j
⋅
h
(
j
)
f_i=\underset{j\geq 0}{\sum}f_{i-j}\cdot w_{j}\cdot h(j)
fi=j≥0∑fi−j⋅wj⋅h(j),相当于枚举最后一个非法段的长度。
那么
h
(
j
)
=
(
−
1
)
j
−
1
h(j)=(-1)^{j-1}
h(j)=(−1)j−1,因为长度为
j
j
j的非法段其实指定了
j
−
1
j-1
j−1个非法位置,以及开头有一个不管的位置。
w j w_j wj表示长度为 j j j的非法段共有多少种,容易发现这种段的长度为 log m \log m logm
这样可以做到 O ( ( n + m ) log m ) O((n+m)\log m) O((n+m)logm)
n
≤
1
0
18
,
m
≤
1
0
6
n\leq 10^{18},m\leq 10^6
n≤1018,m≤106:
转移与前面
log
m
\log m
logm项有关,可以矩阵加速。
O
(
log
3
m
log
n
+
m
poly
log
m
)
O(\log^3m\log n+m \;\text{poly}\log m)
O(log3mlogn+mpolylogm)
n ≤ 1 0 18 , m ≤ 1 0 9 n\leq 10^{18},m\leq 10^9 n≤1018,m≤109:配合各类筛法以及根号分治。
AT_dp_y/CF599C
不能走黑不好做。考虑容斥,设 S i S_i Si表示第 i i i个黑色格子不走的方案数,则 a n s = ∣ ⋂ S i ∣ ans=|\bigcap S_i| ans=∣⋂Si∣,其补集转化为走一些黑色格子。
设 f ( x ) f(x) f(x)表示选出 x x x个黑走的方案数,则 a n s = ∑ n i = 0 ( − 1 ) i f ( i ) ans=\underset{i=0}{\overset n\sum}(-1)^if(i) ans=i=0∑n(−1)if(i)
我们先把黑色格子以 ( x , y ) (x,y) (x,y)升序排列,然后设 f i f_i fi表示必然走第 i i i个黑格子的所有方案的系数和,在这里会发现这实际上相当于以第 i i i个黑格子为终点,其他黑色格子不走的方案数。
那么转移就是
f
i
=
∑
j
<
i
−
(
x
i
−
x
j
+
y
i
−
y
j
x
i
−
x
j
)
f
j
f_i=\underset{j<i}{\sum}-\begin{pmatrix}x_i-x_j+y_i-y_j\\x_i-x_j\end{pmatrix}f_j
fi=j<i∑−(xi−xj+yi−yjxi−xj)fj,表示枚举之前走了哪个黑色格子。
这里的
−
1
-1
−1是因为当前走了一个黑色格子,所以要乘上
−
1
-1
−1的系数。
组合数是表示从上一个黑色格子走到这里的方案数。
这样会发现一个问题是,这样没有办法表示是从左上角出发,到右下角结束的。因此我们强制在左上角和右下角添加一个黑色格子。并且令初值 f 1 = − 1 f_1=-1 f1=−1,表示走了左上角黑色格子的方案容斥系数和。
观察转移的形式,我们会发现 f i f_i fi实际上表示的是走了左上角那个黑色格子,以及第 i i i个黑色格子的方案数,这样就可以做了。
时间复杂度 O ( n 2 ) O(n^2) O(n2)
CF1728G
标算做法是由于每次只添加一盏灯,因此会照亮一个连续段,只需要剩下的灯照亮前后缀即可,预处理前后缀就能算出来。
但是有多项式做法。
考虑容斥是怎么做的,相当于选出一些点强制不能照亮,剩下点随意,这样方案数是好求的。
那么对于一种照亮的情况,我们可以把它划分为 首尾不能照亮+中间随意的连续段,那么设
f
i
f_i
fi表示第
i
i
i个点为结尾的容斥系数和,转移相当于枚举最后一个左右为不能照亮,中间是任意的连续段。(以
i
i
i为结尾,说明
i
i
i不能照亮)
那么 f i = ∑ j < i − w j , i ⋅ f j f_i=\underset{j<i}\sum- w_{j,i}\cdot f_j fi=j<i∑−wj,i⋅fj
w w w是 O ( n m 2 ) O(nm^2) O(nm2)好算的。
这样dp出来相当于强制第一个点和第 i i i个点不能照亮的方案数,所以我们在正负无穷的位置添加两个点,就可以dp出答案。
这样每次询问 w w w只会有 m 2 m^2 m2个位置(在这次询问中)改变。
复杂度为 O ( ( n + q ) m 2 ) O((n+q)m^2) O((n+q)m2)
填数(模拟赛题)
给定 n , m , k n,m,k n,m,k 和 k k k 个区间 ( l i , r i ) (l_i,r_i) (li,ri),求有多少个长为 n n n 的正整数数组 a a a 满足:
- 1 ≤ a i ≤ m 1 \leq a_i \leq m 1≤ai≤m。
- 对于每个给定的区间 ( l i , r i ) (l_i,r_i) (li,ri),存在至少一对 l i ≤ p 0 < p 1 ≤ r i l_i \leq p_0 < p_1 \leq r_i li≤p0<p1≤ri,满足 a p 0 = a p 1 a_{p_0} = a_{p_1} ap0=ap1。
答案对 1 0 9 + 7 10^9 + 7 109+7 取模。 1 ≤ n , m ≤ 1 0 6 1 \leq n,m \leq 10^6 1≤n,m≤106, 1 ≤ k ≤ 2000 1 \leq k \leq 2000 1≤k≤2000。
区间问题一个经典套路是不存在相交区间或者不存在包含区间。
本题中我们发现子集严格强于超集,因此不存在包含区间。我们把包含区间去重之后编一个后效性消除,把区间按照右端点升序排列。
然后我们把 = = =容斥为 ≠ \not= =,相当于强制一些区间当中没有重复的数字,然后用dp的方法把容斥系数乘起来。
设 f i f_i fi表示考虑前 i i i个区间,第 i i i个区间被选为非法的所有情况的容斥系数和,则:
f i = ∑ j < i − f j ⋅ { l i > r j : m l e n i ‾ ⋅ m l i − r j e l s e : ( m − ( r j − l i + 1 ) ) l e n i − ( r j − l i + 1 ) ‾ f_i=\underset{j<i}\sum -f_j\cdot \left\{\begin{matrix} l_i>r_j:m^{\underline{len_i}}\cdot m^{l_i-r_j}\hspace{2.25cm}\\ else:(m-(r_j-l_i+1))^{\underline{len_i-(r_j-l_i+1)}} \end{matrix}\right. fi=j<i∑−fj⋅{li>rj:mleni⋅mli−rjelse:(m−(rj−li+1))leni−(rj−li+1)
强制开头结尾有一个区间即可。
P4099
首先这个题有不容斥的做法,如果设
f
u
f_u
fu表示以
u
u
u为子树的答案会发现没法转移,因为不知道子树内部拓扑序的情况。
如果我们想要把一个儿子加入,必须要知道这个儿子在拓扑序列中的排名,因此必须要记录进状态里,即
f
u
,
i
f_{u,i}
fu,i表示以
u
u
u为子树,
u
u
u在拓扑序列中的顺序为
i
i
i的方案数。然后编一个转移就可以了。
但是这个题也可以容斥(虽然容斥做法好像更难一点…)
具体来说,一条边其实提供了一个限制,即
u
→
v
u\rightarrow v
u→v说明
u
u
u的拓扑序在
v
v
v之前,如果只有内向边是好做的,那我们选择对外向边容斥。
设
S
i
S_i
Si表示满足所有内向边的限制和第
i
i
i条外向边限制的所有序列构成的集合,则答案为
∣
⋂
S
i
∣
|\bigcap S_i|
∣⋂Si∣,那么补集相当于取到一些外向边,然后把它们反转为内向边,剩下一些外向边断开的方案数。
那么如果我们指定一个反转,可以认为那条外向边带有 − 1 -1 −1的容斥系数。
非常容易编出来一个dp,即
f
u
f_u
fu表示
u
u
u子树的答案,转移考虑把
u
u
u的一个儿子合并到
u
u
u子树内部,如果是内向边,就乘上
(
s
i
z
u
+
s
i
z
v
−
1
s
i
z
v
)
\begin{pmatrix}siz_u+siz_v-1\\siz_v\end{pmatrix}
(sizu+sizv−1sizv)合并,表示
u
u
u为最后一个节点,前面
s
i
z
u
+
s
i
z
v
−
1
siz_u+siz_v-1
sizu+sizv−1个位置选出
s
i
z
v
siz_v
sizv个位置放置
v
v
v子树内部的拓扑序列情况。
如果是外向边,那么就考虑两种情况。
一种叫做反转,这时候带有
−
(
s
i
z
u
+
s
i
z
v
−
1
s
i
z
v
)
-\begin{pmatrix}siz_u+siz_v-1\\siz_v\end{pmatrix}
−(sizu+sizv−1sizv)的系数。
另一种叫做断开,这时候带有
(
s
i
z
u
+
s
i
z
v
s
i
z
v
)
\begin{pmatrix}siz_u+siz_v\\siz_v\end{pmatrix}
(sizu+sizvsizv)的系数,因为与
u
u
u没有关系了,
u
u
u不一定是最后一个,因此可以在任意位置插入。
void dfs(int u,int fa) {
f[u]=1;
siz[u]=1;
for(auto&i:a[u]) {
int w=i.first,v=i.second;
if(v==fa) continue;
dfs(v,u);
if(w) f[u]=f[u]*f[v]%M*C[siz[u]+siz[v]-1][siz[v]]%M;
else f[u]=f[u]*f[v]%M*C[siz[u]+siz[v]-1][siz[v]-1]%M;
siz[u]+=siz[v];
}
}
但是会发现这样是错的。
具体错误是,假如说要合并一个子树,并且一种情况经过了断边/反转之后 u u u的连通块大小实际上为 x x x, v v v的连通块大小为 y y y,但是我们在合并内向边的时候强制 u u u在最后一个,即从 s i z u + s i z v − 1 siz_u+siz_v-1 sizu+sizv−1中选,实际上是不对的,我们只需要强制在 x + y x+y x+y个位置中, u u u在最后一个即可。那我们可以修改一下状态,即设 f u , x f_{u,x} fu,x表示目前所在的内向连通块大小为 x x x的方案数,现在再来编一下转移:
- 内向边: f u , x + y ← + f u , x f v , y ( s i z u + s i z v x + y ) ( x + y − 1 y ) f_{u,x+y}\leftarrow +f_{u,x}f_{v,y}\begin{pmatrix}siz_u+siz_v\\x+y\end{pmatrix}\begin{pmatrix}x+y-1\\y\end{pmatrix} fu,x+y←+fu,xfv,y(sizu+sizvx+y)(x+y−1y),表示先选出拓扑序列的一些位置放连通块,然后再连通块内部安排放置顺序,在放连通块的位置内部安排顺序时,必须强制 u u u在最后一个位置。
- 外向边取反: f u , x + y ← − f u , x f v , y ( s i z u + s i z v x + y ) ( x + y − 1 y ) f_{u,x+y}\leftarrow -f_{u,x}f_{v,y}\begin{pmatrix}siz_u+siz_v\\x+y\end{pmatrix}\begin{pmatrix}x+y-1\\y\end{pmatrix} fu,x+y←−fu,xfv,y(sizu+sizvx+y)(x+y−1y)
- 外向边断开: f u , x ← + f u , x f v , y ( s i z u + s i z v s i z v ) f_{u,x}\leftarrow +f_{u,x}f_{v,y}\begin{pmatrix}siz_u+siz_v\\siz_v\end{pmatrix} fu,x←+fu,xfv,y(sizu+sizvsizv)
void dfs(int u,int fa) {
f[u][1]=1;
siz[u]=1;
for(auto&i:a[u]) {
int w=i.first,v=i.second;
if(v==fa) continue;
dfs(v,u);
memset(g,0,sizeof g);
for(int i=1;i<=siz[u];i++)
for(int j=1;j<=siz[v]&&i+j<=n;j++)
if(w)
(g[i+j]+=f[u][i]*f[v][j]%M*C[siz[u]+siz[v]][i+j]%M*C[i+j-1][j]%M)%=M;
else
(g[i]+=f[u][i]*f[v][j]%M*C[siz[u]+siz[v]][siz[v]]%M)%=M,
(g[i+j]+=(M-1)*f[u][i]%M*f[v][j]%M*C[siz[u]+siz[v]][i+j]%M*C[i+j-1][j]%M)%=M;
memcpy(f[u],g,sizeof g);
siz[u]+=siz[v];
}
}
但是这样会发现连样例都过不了。原因是系数乘错了。乘错的原因是,在 f u , x f_{u,x} fu,x中,已经乘上了 ( s i z u x ) \begin{pmatrix}siz_u\\x\end{pmatrix} (sizux)作为大小为 x x x的连通块放在 s i z u siz_u sizu个位置的方案,然后在转移到 f u , x + y f_{u,x+y} fu,x+y时又选了一次,这样就选多了。事实上只需要在 s i z u + s i z v siz_u+siz_v sizu+sizv个位置中选出 s i z v siz_v sizv个位置放新子树的拓扑序信息即可。但是这样就没法限制 v v v在 u u u前了,具有后效性,而增加一维状态记录 u u u在拓扑序中的位置,来消除后效性在时间上是不可能的,因此我们选择推迟后效性。
转移:
- 内向边: f u , x + y ← + f u , x f v , y ( s i z u + s i z v s i z v ) f_{u,x+y}\leftarrow +f_{u,x}f_{v,y}\begin{pmatrix}siz_u+siz_v\\siz_v\end{pmatrix} fu,x+y←+fu,xfv,y(sizu+sizvsizv),表示先选出拓扑序列的一些位置放连通块,然后再连通块内部安排放置顺序,在连通块内部安排顺序时,必须强制 u u u在最后一个位置。
- 外向边取反: f u , x + y ← − f u , x f v , y ( s i z u + s i z v s i z v ) f_{u,x+y}\leftarrow -f_{u,x}f_{v,y}\begin{pmatrix}siz_u+siz_v\\siz_v\end{pmatrix} fu,x+y←−fu,xfv,y(sizu+sizvsizv)
- 外向边断开: f u , x ← + f u , x f v , y ( s i z u + s i z v s i z v ) f_{u,x}\leftarrow +f_{u,x}f_{v,y}\begin{pmatrix}siz_u+siz_v\\siz_v\end{pmatrix} fu,x←+fu,xfv,y(sizu+sizvsizv)
注意是先合并再修改 f f f,具体看代码。
当我们把
u
u
u的所有子树合并完成之后,考虑一个实际上大小为
x
x
x的连通块,其中由于我们并没有限制
u
u
u必须要在其他节点之前,因此答案会多算
x
x
x倍,因此再除掉就可以了:
f
u
,
x
←
f
u
,
x
⋅
1
x
f_{u,x}\leftarrow f_{u,x}\cdot \frac 1x
fu,x←fu,x⋅x1
#include<iostream>
#include<cstring>
#include<vector>
#include<map>
#include<numeric>
using namespace std;
const int N=1e3;
const long long M=1e9+7;
long long C[N+5][N+5];
long long f[N+5][N+5];
long long g[N+5];
long long siz[N+5];
long long inv[N+5];
vector<vector<pair<int,int>>>a;
int n;
void dfs(int u,int fa) {
f[u][1]=1;
siz[u]=1;
for(auto&i:a[u]) {
int w=i.first,v=i.second;
if(v==fa) continue;
dfs(v,u);
memset(g,0,sizeof g);
for(int i=1;i<=siz[u];i++)
for(int j=1;j<=siz[v]&&i+j<=n;j++)
if(w)
(g[i+j]+=f[u][i]*f[v][j]%M*C[siz[u]+siz[v]][siz[v]]%M)%=M;
else {
(g[i]+=f[u][i]*f[v][j]%M*C[siz[u]+siz[v]][siz[v]]%M)%=M;
(g[i+j]+=(M-1)*f[u][i]%M*f[v][j]%M*C[siz[u]+siz[v]][siz[v]]%M)%=M;
}
memcpy(f[u],g,sizeof g);
siz[u]+=siz[v];
}
for(int i=1;i<=siz[u];i++)
(f[u][i]*=inv[i])%=M;
}
int main() {
for(int i=0;i<=N;i++) C[i][i]=C[i][0]=1;
inv[0]=inv[1]=1;
for(int i=2;i<=N;i++) (inv[i]=(M-1)*(M/i)%M*inv[M%i])%=M;
for(int i=1;i<=N;i++)
for(int j=1;j<=N;j++)
(C[i][j]=C[i-1][j-1]+C[i-1][j])%=M;
int T;
cin>>T;
while(T--) {
memset(f,0,sizeof f);
memset(siz,0,sizeof siz);
a.resize(0);
cin>>n;
a.resize(n+5);
for(int i=1,u,v;i<n;i++) {
char c;
cin>>u>>c>>v;
u++;
v++;
if(c=='<') a[u].push_back({0,v}),a[v].push_back({1,u});
else a[u].push_back({1,v}),a[v].push_back({0,u});
}
dfs(1,0);
cout<<accumulate(f[1]+1,f[1]+n+1,0ll)%M<<endl;
}
}
/*
1
4
1 < 0
2 < 0
3 < 0
1
3
0 < 1
0 < 2
1
4
0 < 1
0 < 2
0 < 3
*/
ARC101_C
树上任意一边都没覆盖,容斥为选出一些边不被覆盖,每选出这样一条边,就在这种方案上乘一个 − 1 -1 −1的系数。
考虑一个dp,再合并完所有子树之后,如果我们要断开它与父亲的那条边,那就要求出子树内部随意匹配的方案数,显然与子树连通块的大小有关,因此要记录一维表示子树连通块的大小。
设 f u , i f_{u,i} fu,i表示以 u u u为根的子树目前有大小为 i i i的不知道边构成的连通块的方案数,转移就是考虑把儿子 v v v合并进来:
f u , x + y ← + f u , x f v , y ( x > 0 , y ≥ 0 ) f_{u,x+y}\leftarrow+f_{u,x}f_{v,y}(x>0,y\geq 0) fu,x+y←+fu,xfv,y(x>0,y≥0)
先合并再修改。
如果 u u u和父亲的连边是断开的,那么 u u u父亲边的状态是“非法”,不是“未指定”,因此不知道连通块的大小为 0 0 0,则 f u , 0 f_{u,0} fu,0表示断边的容斥方案数,我们dp求出 f u , x > 0 f_{u,x>0} fu,x>0后考虑求出 f u , 0 f_{u,0} fu,0
那么
f
u
,
0
←
−
f
u
,
i
w
i
f_{u,0}\leftarrow-f_{u,i}w_i
fu,0←−fu,iwi,表示我们指定它向父亲的连边是断开的,因此带有
−
1
-1
−1的系数。
w
i
w_i
wi表示大小为
i
i
i的连通块任意匹配方案数,显然
w
奇数
=
0
w_{奇数}=0
w奇数=0,现在考虑偶数是如何递推的。
我们想要从 w i − 2 w_{i-2} wi−2递推到 w i w_i wi,那么就需要增加一对匹配,可以选择新增一对匹配,这样方案数是 1 1 1,或者选择新增两个点,然后断开原来的 i − 2 2 \frac {i-2}2 2i−2组中的一组匹配,新的两个点和旧的两个点形成两组匹配,这样配对会有 2 2 2种方案,这样方案数是 2 ( i − 2 2 ) = i − 2 2\left(\frac{i-2}2\right)=i-2 2(2i−2)=i−2
因此 w i = ( 1 + i − 2 ) w i − 2 = ( i − 1 ) w i − 2 w_i=(1+i-2)w_{i-2}=(i-1)w_{i-2} wi=(1+i−2)wi−2=(i−1)wi−2
或者得到
w
i
=
∏
j
/
2
j
=
1
(
2
j
−
1
)
w_i=\underset{j=1}{\overset {j/2}\prod}(2j-1)
wi=j=1∏j/2(2j−1),表示每次选出最小的点进行匹配。
初值为
f
u
,
1
=
1
f_{u,1}=1
fu,1=1,答案为
−
f
1
,
0
-f_{1,0}
−f1,0,因为转移的时候假设根节点连向父亲的边断开了,所以乘了一个
−
1
-1
−1的系数,要再乘回来。
#include<iostream>
#include<vector>
#include<cstring>
using namespace std;
const int N=5e3;
const long long M=1e9+7;
vector<int> a[N+5];
long long w[N+5];//i对
long long f[N+5][N+5];
int siz[N+5];
int n;
long long g[N+5];
void dfs(int u,int fa) {
siz[u]=1;
for(auto&v:a[u])
if(v^fa) {
dfs(v,u);
memset(g,0,sizeof g);
for(int x=1; x<=siz[u]; x++)
for(int y=0; y<=siz[v]; y++)
if(x+y<=N)
(g[x+y]+=f[u][x]*f[v][y])%=M;
memcpy(f[u],g,sizeof f[u]);
siz[u]+=siz[v];
}
for(int i=0; i<=n; i++)
(f[u][0]+=(M-1)*f[u][i]%M*w[i]%M)%=M;
}
int main() {
cin>>n;
for(int i=1,u,v; i<n; i++) {
cin>>u>>v;
a[v].push_back(u);
a[u].push_back(v);
}
w[0]=1;
for(int i=2; i<=n; i+=2) w[i]=w[i-2]*(i-1)%M;
for(int i=1; i<=n; i++) f[i][1]=1;
dfs(1,0);
// for(int i=1;i<=n;i++,cout<<endl)
// for(int j=0;j<=n;j++,cout<<' ')
// cout<<f[i][j];
cout<<(M-1)*f[1][0]%M;
}
话说不知道为啥初值设为 − 1 -1 −1也能过,有人知道请您联系我。
有标号连通图计数
求
n
n
n个节点的无向连通图有多少个,节点有标号,编号为
[
1
,
n
]
[1,n]
[1,n]
n
<
=
5000
n<=5000
n<=5000
这个题是朴素容斥,即考虑如何用容斥原理来修正答案。
设
f
i
f_i
fi表示大小为
i
i
i的连通图数量,一个想法是首先连出一棵树,但是不太好搞,考虑补集转换,总边数为
i
(
i
−
1
)
2
\frac{i(i-1)}2
2i(i−1),总可能性为
h
(
i
)
=
2
i
(
i
−
1
)
2
h(i)=2^{\frac {i(i-1)}2}
h(i)=22i(i−1)种,减去不连通的情况即可,那我们枚举
1
1
1号点所在的连通块大小
j
j
j,然后从剩下的
i
−
1
i-1
i−1个点中选出
j
−
1
j-1
j−1个点,情况数量为:
f
j
(
i
−
1
j
−
1
)
f_j \begin{pmatrix}i-1\\ j-1\end{pmatrix}
fj(i−1j−1),剩下
i
−
j
i-j
i−j个点随意连边,转移方程为:
f
i
=
h
(
i
)
−
∑
j
<
i
f
j
(
i
−
1
j
−
1
)
h
(
i
−
j
)
f_i=h(i)-\underset{j<i}\sum f_j \begin{pmatrix}i-1\\ j-1\end{pmatrix}h(i-j)
fi=h(i)−j<i∑fj(i−1j−1)h(i−j)
山东省队集训
给定一张 n ≤ 15 n\leq 15 n≤15个点 m ≤ 200 m\leq 200 m≤200条边的无向图,对于所有 k k k ,请求出保留恰好 k k k条边使得整张图连通的方案数。
容斥,设
f
i
,
S
f_{i,S}
fi,S表示保留了
i
i
i条边,使得
S
S
S连通的方案数。不连通就枚举
1
1
1所在的连通块。
f
i
,
S
=
(
c
n
t
S
i
)
−
∑
1
∈
T
⊊
S
∑
i
j
=
0
f
j
,
T
(
c
n
t
S
−
T
i
−
j
)
f_{i,S}=\begin{pmatrix}cnt_S\\i\end{pmatrix}-\underset{1\in T\subsetneq S}{\sum}\underset{j=0}{\overset i\sum}f_{j,T}\begin{pmatrix}cnt_{S-T}\\i-j\end{pmatrix}
fi,S=(cntSi)−1∈T⊊S∑j=0∑ifj,T(cntS−Ti−j)
c n t S cnt_S cntS表示两端都在 S S S内的边的数量,时间复杂度 O ( 3 n m 2 ) O(3^nm^2) O(3nm2),难以通过。
观察到转移系数只与
c
n
t
S
−
T
cnt_{S-T}
cntS−T有关,把
c
n
t
cnt
cnt相同的
T
T
T一起转移,复杂度
O
(
3
n
m
+
2
n
m
3
)
O(3^nm+2^nm^3)
O(3nm+2nm3),可以通过:
f
i
,
S
=
(
c
n
t
S
i
)
−
∑
i
k
=
0
∑
i
j
=
0
(
k
i
−
j
)
×
g
j
(
S
,
k
)
f_{i,S}=\begin{pmatrix}cnt_S\\i\end{pmatrix}-\underset{k=0}{\overset i\sum}\underset{j=0}{\overset i\sum}\begin{pmatrix}k\\i-j\end{pmatrix}\times g_j(S,k)
fi,S=(cntSi)−k=0∑ij=0∑i(ki−j)×gj(S,k)
其中 g i ( S , k ) = ∑ 1 ∈ T ⊊ S [ c n t S − T = k ] f i , T g_i(S,k)=\underset{1\in T\subsetneq S}\sum[cnt_{S-T}=k]f_{i,T} gi(S,k)=1∈T⊊S∑[cntS−T=k]fi,T
我们枚举一个 S , T , i S,T,i S,T,i,把它贡献到对应的 g g g上即可。
或者把 f i f_i fi看成多项式的形式,那么就是 ( 1 + x ) ? (1+x)^? (1+x)?的形式,那我们令 ( 1 + x ) = y (1+x)=y (1+x)=y,转移就变成了多项式平移,可以 O ( m ) O(m) O(m)完成。
或者还有一种我没看懂的做法:
(来自lyp的PPT)
没有看懂,如果您看懂了的话,请您联系我。
LOJ575
首先想到的是离散dp。
考虑如果我们要往序列后面添加一个数字,那么我们只需要知道原来的序列末尾在序列中的排名,就可以知道我们能添加多少个比它小/大的数字了。
设 f i , j f_{i,j} fi,j表示长度为 i i i的序列值域为 [ 1 , i ] [1,i] [1,i],末尾为 j j j的方案数,如果是小于号,那么 f i , j ( n − j + 1 ) + → f i + 1 , k > j f_{i,j}(n-j+1)+\rightarrow f_{i+1,k>j} fi,j(n−j+1)+→fi+1,k>j,表示我们向 [ 1 , i ] [1,i] [1,i]中插入一个比 j j j大的数字 k k k,把值域变为 [ 1 , i + 1 ] [1,i+1] [1,i+1],大于号同理。
但是这样 O ( n 2 ) O(n^2) O(n2)到顶了。
考虑容斥,我们对大于号容斥,那么一些大于号会变成小于号,另一些会变成不知道。
那么符号序列就可以拼成一个不知道+若干个非法。
f
i
f_i
fi表示dp到了第
i
i
i个数字的系数和,转移就是枚举最后一个上升序列是
(
j
,
i
]
(j,i]
(j,i],同时要求
a
j
=
‘
>
’
a_j=‘>’
aj=‘>’,表示
a
j
a_j
aj被我们容斥为了不知道:
f
i
=
∑
j
<
i
[
a
j
=
‘
>
’
]
(
−
1
)
c
n
t
i
−
1
−
c
n
t
j
f
j
(
i
j
)
f_i=\underset{j<i}\sum [a_j=‘>’](-1)^{cnt_{i-1}-cnt_j}f_j\begin{pmatrix}i\\j\end{pmatrix}
fi=j<i∑[aj=‘>’](−1)cnti−1−cntjfj(ij)
时间复杂度 O ( n 2 ) O(n^2) O(n2)
做分治NTT即可优化到 O ( n log 2 n ) O(n\log^2n) O(nlog2n)
ABC236Ex
如果到图上考虑,相等连边,那么合法方案数就是图全部不连通的方案数。
设 S i S_i Si表示第 i i i条边不存在的方案集,则要求的就是 ∣ ⋂ S i ∣ |\bigcap S_i| ∣⋂Si∣,补集就是指定一些边存在,另一些边不知道。
那么可以对边集容斥,就获得了 O ( 2 n ( n − 1 ) 2 ) O\left(2^{\frac{n(n-1)}2}\right) O(22n(n−1))的做法。
相当于我们把点集 [ n ] [n] [n]划分为若干集合 { S 1 , S 2 , S 3 , . . . , S k } \{S_1,S_2,S_3,...,S_k\} {S1,S2,S3,...,Sk},确保内部连通,外部不知道。
设
f
S
f_S
fS表示考虑
S
S
S内部的容斥系数之和。转移就是枚举最后一个被选出的连通块。
f
S
=
∑
u
∈
T
⊆
S
w
T
⋅
h
′
(
T
)
⋅
f
S
−
T
f_S=\underset{u\in T\subseteq S}\sum w_{T}\cdot h'(T)\cdot f_{S-T}
fS=u∈T⊆S∑wT⋅h′(T)⋅fS−T
那我们只需要知道 h ′ ( T ) = h ( ∣ T ∣ ) h'(T)=h(|T|) h′(T)=h(∣T∣)即可,那就是使得 ∣ T ∣ |T| ∣T∣个点的图连通的所有方案的容斥系数之和。
可以通过一个打表小容斥求出:
n
n
n个点的所有方案的容斥系数和为,枚举实际上选了几条边,得到:
H
(
n
)
=
∑
n
(
n
−
1
)
2
i
=
0
(
n
(
n
−
1
)
2
i
)
(
−
1
)
i
=
[
n
=
1
]
H(n)=\underset{i=0}{\overset{\frac{n(n-1)}2}\sum}\begin{pmatrix}\frac{n(n-1)}2\\i\end{pmatrix}(-1)^i=[n=1]
H(n)=i=0∑2n(n−1)(2n(n−1)i)(−1)i=[n=1]
那么它等于连通+不连通的系数和,如果不连通,那么枚举1所在的连通块大小:
H
(
n
)
=
h
(
n
)
+
∑
n
−
1
i
=
1
(
n
−
1
i
−
1
)
h
(
i
)
H
(
n
−
i
)
=
h
(
n
)
+
∑
n
−
1
i
=
1
(
n
−
1
i
−
1
)
h
(
i
)
[
n
−
1
=
i
]
H(n)=h(n)+\underset{i=1}{\overset {n-1}\sum}\begin{pmatrix}n-1\\i-1\end{pmatrix}h(i)H(n-i)=h(n)+\underset{i=1}{\overset {n-1}\sum}\begin{pmatrix}n-1\\i-1\end{pmatrix}h(i)[n-1=i]
H(n)=h(n)+i=1∑n−1(n−1i−1)h(i)H(n−i)=h(n)+i=1∑n−1(n−1i−1)h(i)[n−1=i]
即: [ n = 1 ] = h ( n ) + ( n − 1 ) h ( n − 1 ) [n=1]=h(n)+(n-1)h(n-1) [n=1]=h(n)+(n−1)h(n−1)
则: h ( n ) = [ n = 1 ] − ( n − 1 ) h ( n − 1 ) h(n)=[n=1]-(n-1)h(n-1) h(n)=[n=1]−(n−1)h(n−1)
就可以做了。
但是话说这个集合划分容斥和斯特林反演什么的有啥关系,不知道,也没有查到。如果有人知道,请您私信告诉我。
后记
作者水平不行,如果您对文中问题有任何见解,非常感谢您的私信指导!