听课笔记 - 2026 重庆 - 06 & 07【Prüfer】【二项式反演】【Min-Max】【范德蒙德卷积】

· · 算法·理论

orz _Diu_。

二项式反演

\sum_{i=0}^n\binom{n}{i}(-1)^i=[n=0]

就是把贡献全部消掉。当 n=0 的时候只能特殊为 1,否则一定能消掉。这是一个定义容斥系数的过程。常用于 DP 的就是:

f_i=\sum_{j=1}^{\infty} \binom{j}{i}g_j \\ g_i=\sum_{j=0}^n(-1)^{n-i}\binom{n}{i}f_i \\ g_j=\sum_{j=i}^{\infty} \binom{j}{i}(-1)^{j-i}f_j

\small \color{52C41A}[容斥] ARC102C(E) Stop. Otherwise...

我们对每一个 p 单独计算贡献,那么不能同时选择 x 和 p-x。你发现你只关系匹配组数(一定不会重复),考虑插板法。全部选择是 \binom{n+k-1}{k-1}。我们在不合法匹配中选择一组,总选择就是 \binom{t}{1}\binom{n+k-1-\red 2}{k-1}。这个减去就是你选择的组也要减去才能选择。

然后我们回到要求的部分,也就是有 0 组不合法的选择骰子方案。我们考虑直接容斥,得出 ans=\sum_{j=0}^t(-1)^j\binom{t}{j}\binom{n-2j+k-1}{k-1}。

这个做法的本质是:不断减去不要的贡献,再加回多减的部分算出精确贡献。

::::success[code] 注意 t 与题面中的定义不同,为合法配对数,要重新计算,具体见代码。

#include<bits/stdc++.h>
using namespace std;

#define ll long long
const int Math_Limit=1e5+10;
const ll P=998244353;
namespace fish_math{    
    ll fac[Math_Limit], inv[Math_Limit], fiv[Math_Limit];
    ll qpow(ll a,ll b){
        ll res=1;
        for(;b;b>>=1,a=(a*a)%P) if(b&1) res=(res*a)%P;
        return res;
    }   
    ll Inv(ll x){ return qpow(x,P-2); }
    void Init_Math(int lmt){
        fac[0]=1, fiv[0]=1;
        for(int i=1;i<=lmt;i++)
            fac[i]=(fac[i-1]*i)%P,
            inv[i]=Inv(i),
            fiv[i]=(fiv[i-1]*inv[i])%P;
        return;
    }
    ll C(int n,int m){
        if(n<0||m<0||n<m) return 0;
        return fac[n]*fiv[m]%P*fiv[n-m]%P;
    }
}

using namespace fish_math;
ll k,n,ans;
int main(){
    Init_Math(1e5);
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
    cin>>k>>n;
    for(ll i=2;i<=2*k;i++){
        int t=min(k,i/2)-max(1ll,i-k)+1;
        ans=0;
        for(int j=0,op=1;j<=t;j++,op=-op)
            ans=(ans+op*C(t,j)%P*C(n-2*j+k-1,k-1)%P+P)%P;
        cout<<ans<<"\n";
    }
    return 0;
}

::::

\small \color{52C41A}[容斥] \small \color{3498DB}[反演] P6076 染色问题

为了避免冲突,颜色数为 c。

你考虑设一个容斥结果为 f_{i,j,k} 为 i 行不合法,j 列不合法有 k 种颜色不合法的选择数。容斥的式子就是选取 i 行 j 列 k 色,剩下的必须合法,就是剩余格子随机选择。也就有:

f_{i,j,k}=\binom{n}{i}\binom{m}{j}\binom{c}{k}(c-k+1)^{((n-i)(m-j))}。

注意这个时候 f_{0,0,0} 不是直接答案,因为这个只是保证不合法,并不能保证其他都是合法的,这里涂色格子考虑了不涂色。

所以这个时候我们要用二项式反演来算答案,也就是答案为:

ans=\sum_{i=0}^n\sum_{j=0}^m\sum_{k=0}^c(-1)^{i+j+k}f_{i,j,k}

这个其实是三次二项式反演的形式。

::::success[code]

#include<bits/stdc++.h>
using namespace std;

#define ll long long
const int Math_Limit=1e5+10;
const ll P=1e9+7;
namespace fish_math{    
    ll fac[Math_Limit], inv[Math_Limit], fiv[Math_Limit];
    ll qpow(ll a,ll b){
        ll res=1;
        for(;b;b>>=1,a=(a*a)%P) if(b&1) res=(res*a)%P;
        return res;
    }   
    ll Inv(ll x){ return qpow(x,P-2); }
    void Init_Math(int lmt){
        fac[0]=1, fiv[0]=1;
        for(int i=1;i<=lmt;i++)
            fac[i]=(fac[i-1]*i)%P,
            inv[i]=Inv(i),
            fiv[i]=(fiv[i-1]*inv[i])%P;
        return;
    }
    ll C(int n,int m){
        if(n<0||m<0||n<m) return 0;
        return fac[n]*fiv[m]%P*fiv[n-m]%P;
    }
}

using namespace fish_math;
ll ans,n,m,c;
int main(){
    Init_Math(400);
    cin>>n>>m>>c;
    for(int i=0;i<=n;i++) for(int j=0;j<=m;j++) for(int k=0;k<=c;k++){
        int op=((i+j+k)%2)?-1:1;
        ans=(ans+op*C(n,i)*C(m,j)%P*C(c,k)%P*qpow(c-k+1,(n-i)*(m-j)%P)%P+P)%P;
    }
    cout<<ans;
    return 0;
}

::::

\small \color{52C41A}[容斥] QOJ15623 Charity Raffle / P14686

太难了但为什么是蓝题???原来是容斥做法太难了。我们被要求求出一个有限制的数列个数,限制是长度、值域以及和限制。然后有一个本题主要难点:第一个最大值后要有 \max 或 \max-1 存在。

正难则反,考虑容斥。这里就考虑最大值唯一且后面的数都 \le \max-2。通过插板法求出满足条件的全序列个数:

f(n,m,k)=\sum_{i=0}^n(-1)^i\binom{n}{i}\binom{k-im-i+n-1}{n-1}

考虑如何列出反部分的答案,也就可以直接枚举最大值和位置,得:

\sum_{i=0}^m\sum_{j=1}^n\sum_{s=0}^{k-i} f(n-j,i-2,s)f(j-1,i-1,k-i-s)

展开这玩意然后再整理能得出一坨,可以去看题解。最后得出答案为 f(n,m,k)-f(n,m-1,k-1)。

::::success[code]

#include<bits/stdc++.h>
using namespace std;

#define ll long long
ll Test;
const int Math_Limit=5e6+10;
const ll P=998244353;
namespace fish_math{    
    ll fac[Math_Limit], inv[Math_Limit], fiv[Math_Limit];
    ll qpow(ll a,ll b){
        ll res=1;
        for(;b;b>>=1,a=(a*a)%P) if(b&1) res=(res*a)%P;
        return res;
    }   
    ll Inv(ll x){ return qpow(x,P-2); }
    void Init_Math(int lmt){
        fac[0]=1, fiv[0]=1;
        for(int i=1;i<=lmt;i++)
            fac[i]=(fac[i-1]*i)%P,
            inv[i]=Inv(i),
            fiv[i]=(fiv[i-1]*inv[i])%P;
        return;
    }
    ll C(ll n,ll m){
        if(n<0||m<0||n<m) return 0;
        return fac[n]*fiv[m]%P*fiv[n-m]%P;
    }
}

using namespace fish_math;

ll n,m,k;
ll f(ll n,ll m,ll k){
    ll ans=0;
    for(int i=0;i<=n;i++){
        int op=(i%2)?-1:1;  
        ans=(ans+op*C(n,i)*C(k-m*i-i+n-1,n-1)%P+P)%P;
    }
    return ans;
}
int main(){
    Init_Math(5e6);
    cin>>n>>k>>m;
    cout<<(f(n,m,k)-f(n,m-1,k-1)+P)%P;
    return 0;
}

::::

\min-\max 容斥

也就是求一个集合的最大值,基本形式:见学习笔记。

证明:对于 S 中每一个数 x 设为 k 大,考虑何时作为 \min(T):当且仅当 T 中所有元素都比 x 大。也就是有贡献:

\sum_{i=0}^{k-1}(-1)^i\binom{k-1}{i}=[k=1]

这个式子就是说在比 x 大的数中容斥来算贡献。这样 \max(S) 就转为了 \min(S)。

好像没什么用??但是这个在期望下生效,也就是 E(\max(S))=\sum_{T\in S,T\neq \emptyset} (-1)^{|T|-1}E(\min(T))。

于是就可以做下一道题了。

\small \color{52C41A}[容斥] QOJ15299 Vertex Cover

设 t_i 为 i 点第一次被染黑的时间。设每条边是 u_i,v_i 那么就要求 E(\max(S)),S=\set{\min(t_{u_i},t_{v_i})}。

使用 \min-\max 容斥,得:

E(\max(S))=\sum_{T\in S,T\neq \empty} (-1)^{|T|-1}E(\min(T))

合并 \min 得 \min(T)=\min(t_x|(x=u_i \vee x=v_i),i\in T) 这么个抽象东西,变为求 T 有关点第一个被染黑的期望时间。这个时候就把问题转化为了一个点问题。

设 k=|T|,我们要求覆盖的期望时间。我们使用 trick:第一次出发局面期望时间为每个时刻没被出发的概率和。骗你的其实是第一次合出 Super 的概率为之前每个 Att 没有合出的概率和。显然有一个时刻没有被触发的概率为 (\frac{n-k}{n})^i,也就是每次都 \sout{99\%}。容易知道和就是:

E(\min(T))=\sum_{i\ge 0} (\frac{n-k}{n})^i=\frac{n}{k}

然后考虑如何求解 E(\max(S)),这一步直接枚举显然是不行的,但是可以考虑树上背包。f_{u,i,0/1} 为 u 子树内选择 i 以及 u 的选择情况。

显然有转移:

\begin{aligned} f'_{u,i,k}f_{v,j,l} &\to f_{u,i+j,k} \\ -f'_{u,i,k}f_{v,j,l} &\to f_{u,i+j+2-k-l,1} \end{aligned}

::::success[code]

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N=5e3+10;
const ll P=998244353;
ll f[N][N][2],g[N][2];
ll qpow(ll a,ll b){
    ll res=1;
    for(;b;b>>=1,a=(a*a)%P) if(b&1) res=(res*a)%P;
    return res;
}   
int n,sz[N]; vector<int>e[N];
void dfs(int p,int fa){
    sz[p]=1, f[p][0][0]=1;
    for(auto v:e[p])if(v^fa){
        dfs(v,p);
        sz[p]+=sz[v];
        for(int i=0;i<=sz[p];i++) g[i][0]=g[i][1]=0;
        for(int i=0;i<=sz[p]-sz[v];i++){
            for(int j=0;j<=sz[v];j++){
                (g[i+j][0]+=f[p][i][0]*(f[v][j][0]+f[v][j][1])%P)%=P;
                (g[i+j][1]+=f[p][i][1]*(f[v][j][0]))%=P;
                (g[i+j+1][1]-=(f[p][i][1]*f[v][j][0]%P+f[p][i][0]*f[v][j][1]%P)%P-P)%=P;    
                (g[i+j+2][1]-=(f[p][i][0]*f[v][j][0])%P-P)%=P;
            }
        }
        for(int i=0;i<=sz[p];i++) 
            f[p][i][0]=g[i][0],
            f[p][i][1]=g[i][1];
    }   
    return;
}
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    cin>>n;
    for(int i=1;i<n;i++){
        int u,v;
        cin>>u>>v;
        e[u].push_back(v),
        e[v].push_back(u);
    }
    dfs(1,0);
    ll ans=0;
    for(int i=2;i<=n;i++)
        ans=(ans+(f[1][i][0]+f[1][i][1])%P*qpow(i,P-2)%P)%P;
    cout<<(P-ans*n%P)%P ;
    return 0;
}

::::

\small \color{52C41A}[容斥] P7247 教材运输

同样是一道 \min-\max。设 S 为代价集合,答案就是 E(\max(S))。然后我们转化为 E(\min(T)) 为走到 T 中一点的期望代价。然后直接 DP。

设 f_{T,u} 为从 u 第一次标记到 T 任意节点的期望代价,有转移:

nf_{T,u}=\sum_{v=1}^n f_{T,v}+d_{u,v}\ (u\notin T)

你发现这个东西循环了。所以考虑列方程。

先设等式右侧有 w_u=\sum_{v=1}^n d_{u,v}。我们列出 n-m 个 nf_{T,u}-\sum_{v=1}^n f_{T,v}=w_u 等式,其余均为 0。我们加和 s=\sum_{v=1}^n f_{T,v},发现答案为 E(\min(T))=f_{T,1}=\frac{s+w_1}{m}。最后求出所有 w_u 可以求出答案为:

\begin{aligned} ans&=\sum_{T}=(-1)^{|T|-1}E(\min(T)) \\ &=\sum_{T}(-1)^{|T|-1}(\frac{w_1}{n}+\frac{\sum_{u\notin T}w_u}{n|T|}) \\ &=\sum_{m=1}^{n-1}(-1)^{m-1}(\frac{w_1}{n}\binom{n-1}{m}+\frac{1}{nm}\binom{n-2}{m}\sum_{u=2}^nw_u) \end{aligned}

不像人。

::::success[code] 乐子卡牛魔常呢。

#include<bits/stdc++.h>
using namespace std;

#define ll long long
const int Math_Limit=1e6+10;
const ll P=998244353;
namespace fish_math{    
    ll fac[Math_Limit], inv[Math_Limit], fiv[Math_Limit];
    ll qpow(ll a,ll b){
        ll res=1;
        for(;b;b>>=1,a=(a*a)%P) if(b&1) res=(res*a)%P;
        return res;
    }   
    ll Inv(ll x){ return qpow(x,P-2); }
    void Init_Math(int lmt){
        fac[0]=1, fiv[0]=1;
        for(int i=1;i<=lmt;i++)
            fac[i]=(fac[i-1]*i)%P,
            inv[i]=Inv(i),
            fiv[i]=(fiv[i-1]*inv[i])%P;
        return;
    }
    ll C(ll n,ll m){
        if(n<0||m<0||n<m) return 0;
        return fac[n]*fiv[m]%P*fiv[n-m]%P;
    }
}

template<typename type>
inline void read(type &x){
    x=0;bool flag(0);char ch=getchar();
    while(!isdigit(ch))flag=ch=='-',ch=getchar();
    while(isdigit(ch))x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
    flag?x=-x:0;return;
}
template<typename type>
inline void write(type x){
    x<0?x=-x,putchar('-'):0;static short Stack[50],top(0);
    do Stack[++top]=x%10,x/=10; while(x);
    while(top) putchar(Stack[top--]|48);
}


using namespace fish_math;
const int N=1e6+10;
int n;
ll a[N],sz[N],su[N],W1,Ws,dis[N];
vector<pair<int,ll>>e[N];
void dfs(int p,int fa){
    sz[p]=1, su[p]=a[p];
    for(auto v:e[p])if(v.first^fa){
        dfs(v.first,p);
        sz[p]+=sz[v.first];
        su[p]=(su[p]+su[v.first])%P;
    }
    return;
}
void dfs2(int p,int fa){
    W1=(W1+(dis[p]*a[p])%P)%P;
    for(auto v:e[p])if(v.first^fa){
        dis[v.first]=(dis[p]+v.second)%P;
        dfs2(v.first,p);
        Ws=(Ws+v.second*((sz[v.first]*(su[1]-su[v.first]+P)%P+su[v.first]*(sz[1]-sz[v.first]+P)%P)%P))%P;
    }
    return;
}
int main(){
    read(n);
    Init_Math(n);
    for(int i=1;i<=n;i++)
        read(a[i]);
    for(int i=1;i<n;i++){
        int u,v,w;
        read(u),read(v),read(w);
        e[u].push_back({v,w});
        e[v].push_back({u,w});
    }
    dfs(1,0); dfs2(1,0);
    Ws=(Ws-W1+P)%P;
    ll ans=0;
    for(int m=1;m<n;m++){
        int op=(m%2)?1:-1;
        ans=(ans+(op+P)%P*(C(n-1,m)*W1%P+inv[m]*((C(n-1,m)*W1%P+C(n-2,m)*Ws%P)%P)%P)%P+P)%P;
    }
    write(ans*Inv(n)%P);
    return 0;
}

::::

底下这个做法的实现难度差不多但是好想,就不给代码了。

然后老师还要讲一个简单的做法,就是设 f_{i,0/1} 为 i 个未标记点,0 为根节点,1 表示随机选点计算完成的代价,有转移:

$y$:从随机一个点走回根的期望代价。 $z$:从一个非根点走到另一个非根点的代价。 $$ f_{i,0}=\frac{i}{n-1}f_{i=1,1}+\frac{n-1-i}{n-1}f_{i,1}+x\\ f_{i,1}=\frac{i}{n-1}f_{i-1,1}+\frac{1}{n-1}f_{i,0}+\frac{n-2-i}{n-1} f_{i,1}+\frac{1}{n-1}y+\frac{n-2}{n-1}z $$ ### $\small \color{52C41A}[容斥]$ QOJ3275 小明的树 / P8990 点边容斥也是容斥。对于一颗树,点数等于边数加一。我们要统计连通块,那么考虑统计没点亮的点构成的连通块,没点亮的两头减去两头都没被点亮的边数为 $1$。 考虑如何维护。可以用线段树维护每个事件的状况。维护 $d_i$ 为暗点连通块个数。$l_i$ 为亮点连通块个数。然后线段树维护两个值的最小值,操作的本质就是区间加。 正如不知道上面那题怎么评的蓝,这题是怎么评的黑的。 ::::success[code] ```cpp #include<bits/stdc++.h> using namespace std; #define ll long long const int N=5e5+10; int n,m,p[N],q[N],u[N],v[N]; ll su[N<<2]; int mn[N<<2],mc[N<<2],tc[N<<2],ts[N<<2]; void pushup(int p){ mn[p]=min(mn[p<<1],mn[p<<1|1]); su[p]=mc[p]=0; if(mn[p]==mn[p<<1]) mc[p]+=mc[p<<1], su[p]+=su[p<<1]; if(mn[p]==mn[p<<1|1]) mc[p]+=mc[p<<1|1], su[p]+=su[p<<1|1]; return; } void pushdown(int p){ if(tc[p]){ tc[p<<1]+=tc[p]; tc[p<<1|1]+=tc[p]; mn[p<<1]+=tc[p]; mn[p<<1|1]+=tc[p]; tc[p]=0; } if(ts[p]){ ts[p<<1]+=ts[p]; ts[p<<1|1]+=ts[p]; su[p<<1]+=(ll)mc[p<<1]*ts[p]; su[p<<1|1]+=(ll)mc[p<<1|1]*ts[p]; ts[p]=0; } return; } void build(int p,int l,int r){ if(l==r){ mc[p]=1; mn[p]=n-l+(n==l); return; } int mid=(l+r)>>1; build(p<<1,l,mid); build(p<<1|1,mid+1,r); pushup(p); return; } void update(int p,int l,int r,int ql,int qr,int ux,int uy){ if(ql<0||qr>n||ql>qr) return; if(ql<=l&&r<=qr){ tc[p]+=ux; ts[p]+=uy; mn[p]+=ux; su[p]+=(ll)mc[p]*uy; return; } pushdown(p); int mid=(l+r)>>1; if(ql<=mid) update(p<<1,l,mid,ql,qr,ux,uy); if(qr>mid) update(p<<1|1,mid+1,r,ql,qr,ux,uy); pushup(p); return; } void upd(int l,int r,int k){ l=q[l], r=q[r]; if(l>r) swap(l,r); update(1,1,n,1,l-1,-k,0); update(1,1,n,l,r-1,0,k); return; } int main(){ ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); cin>>n>>m; q[1]=n; for(int i=1;i<n;i++) cin>>u[i]>>v[i]; for(int i=1;i<n;i++) cin>>p[i], q[p[i]]=i; p[n]=1; build(1,1,n); for(int i=1;i<n;i++) upd(u[i],v[i],1); cout<<su[1]<<"\n"; while(m--){ int u1,v1,u2,v2; cin>>u1>>v1>>u2>>v2; upd(u1,v1,-1); upd(u2,v2,1); cout<<su[1]<<"\n"; } return 0; } ``` :::: ### $\small \color{52C41A}[容斥]$ QOJ1838 Intellectual Implementation 困难容斥吧我觉得。首先我们提出一个叫做**网格容斥**的东西:被这个矩形包含的**格子数**减去**相邻两个网格图的数量**加上**田子网格数量**为 $1$。这是和一位呢?$nm-(n-1)m-n(m-1)+(n-1)(m-1)=1$。 然后我们考虑如何查询。我们用上述容斥式子来转化得出询问**有交矩形面积和**。我们可以通过统计 $\binom{k}{2}$ 和 $\binom{k}{3}$。扫描线维护即可,但是真的很难写。 ### $\small \color{3498DB}[反演]$ P6478 游戏 问题:$m$ 组匹配 $k$ 各组祖先后代的方案数 $g_k$。然后设 $f_k$ 为 $k$ 个匹配是合法的。你发现这两个式子符合二项式反演,所以可以通过 $f$ 来求 $g$。设 $dp_{u,i}$ 为 $u$ 处理完了 $i$ 组完成匹配的情况。 对于转移直接树上背包转移,然后答案就是 $f_k=dp_{1,k} \binom{n}{k}(n-k)!$,注意有匹配可以乱选。复杂度 $\mathcal O(n^2)$。 ### $\small \color{52C41A}[容斥]$ 反射容斥 P3266 骗我呢 有两条拦着问路径数的题。考虑将军饮马把折线转正成直线。在每一次碰到直线的时候都在字符串 $S$ 后加一个字符 `A` 或 `B`,然后就可以根据 `A` 和 `B` 来容斥了,这就是反射容斥。 ::::success[code] ```cpp #include<bits/stdc++.h> using namespace std; #define fi first #define se second #define ll long long const int Math_Limit=5e6+10; const ll P=1e9+7; namespace fish_math{ ll fac[Math_Limit], inv[Math_Limit], fiv[Math_Limit]; ll qpow(ll a,ll b){ ll res=1; for(;b;b>>=1,a=(a*a)%P) if(b&1) res=(res*a)%P; return res; } ll Inv(ll x){ return qpow(x,P-2); } void Init_Math(int lmt){ fac[0]=1, fiv[0]=1; for(int i=1;i<=lmt;i++) fac[i]=(fac[i-1]*i)%P, inv[i]=Inv(i), fiv[i]=(fiv[i-1]*inv[i])%P; return; } ll C(ll n,ll m){ n+=m; if(n<0||m<0||n<m) return 0; return fac[n]*fiv[m]%P*fiv[n-m]%P; } } using namespace fish_math; int n,m; pair<int,int>dot; void flip(int x){ dot={dot.se-x,dot.fi+x}; } int main(){ Init_Math(5e6); ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); cin>>n>>m; int x=1, y=-m-2; dot={n+m+1,n}; ll ans=C(dot.fi,dot.se); int op=-1; while(dot.fi>=0&&dot.se>=0){ flip(x); (ans+=op*C(dot.fi,dot.se)+P)%=P, op*=-1; flip(y); (ans+=op*C(dot.fi,dot.se)+P)%=P, op*=-1; } dot={n+m+1,n}; op=-1; while(dot.fi>=0&&dot.se>=0){ flip(y); (ans+=op*C(dot.fi,dot.se)+P)%=P, op*=-1; flip(x); (ans+=op*C(dot.fi,dot.se)+P)%=P, op*=-1; } cout<<ans; return 0; } ``` :::: ### Prüfer 序列 可以将带标号的树转化为一个唯一的整数序列,就是 Prüfer 序列。 做法:每次选择一个**编号最小的叶子节点删去**,记录它链接到的节点,最后只会剩下 $2$ 个节点。复杂度 $\mathcal O(n\log n)$。或者也有指针做法能做到 $\mathcal O(n)$。 这个序列有一定有点为 $n$ 以及每个节点出现的次数为度数 $-1$,也就是不含叶子。 同理,我们可以用 Prüfer 序列重建树。每次选一个最小编号的度数为 $1$ 的点与枚举到的 Prüfer 序列的点连接,并同时减去度。最后会剩下两个度数为 $1$ 的点,一个为 $n$。可以用堆 $\mathcal O(n\log n)$。 这个东西有一个有用的推论:$n$ 点无向图,$k$ 个大小为 $a_1,\dots,a_k$ 的连通块,添加 $k-1$ 条边使得整张图联通的方案是 $n^{k-2}\prod_{i=1}^ka_i$。 ### P2290 树的计数 根据 Prüfer 的意义计算出答案为: $$ \frac{(n-2)!}{\prod_{i=1}^n (d_i-1)!} $$ 注意计算的时候要用高精度,为什么不带取模呢。 ::::success[code] ```cpp #include<bits/stdc++.h> using namespace std; #define ll long long const int N=155; int n,d[N],s; int h[N],box[N][N],su[N]; int main(){ cin>>n; for(int i=1;i<=n;i++) cin>>d[i], s+=d[i]-1; if(s!=n-2) return cout<<0,0; if(n==1) return cout<<(d[1]==0?1:0),0; if(n==2) return cout<<((d[1]==1&&d[2]==1)?1:0),0; for(int i=2;i<=n;i++){ int c=i; for(int j=2;j*j<=c;j++)if(c%j==0) while(c%j==0) box[i][j]++, c/=j; if(c) box[i][c]++; } for(int i=1;i<=n-2;i++) for(int j=2;j<=150;j++) su[j]+=box[i][j]; for(int i=1;i<=n;i++) for(int j=1;j<=d[i]-1;j++) for(int k=2;k<=150;k++) su[k]-=box[j][k]; ll mul=1; for(int i=2;i<=150;i++){ for(int j=1;j<=su[i];j++) mul*=i; } cout<<mul; return 0; } ``` :::: ### 范德蒙德卷积 ~~NOIP 2025 T2。~~ 我们有: $$ \sum_i\binom{i}{n}\binom{k-i}{m}=\binom{k+1}{n+m+1} $$ 这个东西的组合意义是从 $0$ 到 $k$ 中选择一个 $i$ 必选前面选 $n$ 个后面选 $m$ 个,也可以直接选择。 ### $\small \color{FFC116}[组合]$ CF785D Anton and School - 2 范德蒙德卷积的模板题。问合法的子序列个数。我们考虑类似范德蒙德卷积一样选择一个中点来计算。考虑用最后一个 `(` 的位置,整个合法的有 $a$ 个 `(`,右边有 $b$ 个 `)`,那么有: $$ \sum_{i=0}^{\min(a-1,b-1)}\binom{a-1}{i}\binom{b}{i+1} $$ 我们修改一下: $$ \sum_{i=0}^{\min(a-1,b-1)}\binom{a-1}{a-1-i}\binom{b}{i+1} $$ 这个使用的是组合数的对称性。 然后就得出答案: $$ \binom{a+b-1}{a} $$ ::::success[code] ```cpp #include<bits/stdc++.h> using namespace std; #define ll long long const int Math_Limit=2e5+10; const ll P=1e9+7; namespace fish_math{ ll fac[Math_Limit], inv[Math_Limit], fiv[Math_Limit]; ll qpow(ll a,ll b){ ll res=1; for(;b;b>>=1,a=(a*a)%P) if(b&1) res=(res*a)%P; return res; } ll Inv(ll x){ return qpow(x,P-2); } void Init_Math(int lmt){ fac[0]=1, fiv[0]=1; for(int i=1;i<=lmt;i++) fac[i]=(fac[i-1]*i)%P, inv[i]=Inv(i), fiv[i]=(fiv[i-1]*inv[i])%P; return; } ll C(ll n,ll m){ if(n<0||m<0||n<m) return 0; return fac[n]*fiv[m]%P*fiv[n-m]%P; } } using namespace fish_math; int n,sr,l,r; ll ans; string s; int main(){ ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); Init_Math(2e5); cin>>s; n=s.size(); s=" "+s; for(int i=1;i<=n;i++) if(s[i]==')') ++sr; for(int i=1;i<=n;i++){ l+=(s[i]=='('), r+=(s[i]==')'); if(s[i]=='(')ans=(ans+C(l+sr-r-1,l))%P; } cout<<ans; return 0; } ``` :::: ### QOJ15036 Tree and Subgraph Problem 这是一个树上的连通块计数,我们选择一个基准防止记重。直接选择会有一条路径记录计算重复。我们使用颜色来考虑。首先我们枚举 $u$ 讨论另一个连通块在哪里。 - 在子树:统计子节点选择点集的方案数,且去除点集后再选择一个包含 $u$ 的点集方案数。 - 否则:统计 $u$ 子树内选择包含 $u$ 的点集的方案数,并在子树外选择一颗子树的方案数。 可以使用各种数据结构来维护。 ### $\small \color{FFC116}[组合]$ CF708E Student's Camp 风只能摧毁左侧和右侧。那么我们显然可以直接列出一行在 $k$ 天消失 $i$ 个的概率: $$ \binom{k}{i}p^i(1-p)^{k-i} $$ 也就是可以选择一些天来消失。最后一定会成为一些区间。之后我们尝试 DP,可以考虑设 $f_{i,j}$ 为 $i$ 行右端点 $r$,$0$ 到 $i$ 连通的概率。 转移枚举左端点,可以求不相交的方案并减去。此时判断 $r'<l$ 或 $l'<r$ 即可判断方案。 转移如下: $$ f_{i,r}=\sum_{l=1}^r p_{l-1}p_{m-r}\left(\sum_{j=1}^m f_{i-1,j}-\sum_{j=1}^{l-1}f_{i-1,j}-\sum_{j=1}^{m-r} f_{i-1,j}\right) $$ 后面两个减号就是所有符合条件的值减去了两个断开的方案。 然后考虑用前缀和维护 $sf_{j}=\sum_{i=1}^j f_{i-1,j}$。这个式子就会变为: $$ f_{i,r}=\sum_{l=1}^r p_{l-1}p_{m-r}(sf_{m}-sf_{l-1}-sf_{m-r}) $$ 然后我们拆项得: $$ p_{m-r}\left(\left(sf_m-sf_{m-r}\right)\sum_{l=1}^rp_{l-1}-\sum_{l=1}^rsf_{l-1}p_{l-1}\right) $$ 这个还是可以前缀和优化。$sp_l=\sum_{i=1}^l p_i$。$ss_l=\sum_{i=1}^l sf_{l}p_l$。 最后我们有答案: $$ f_{i,r}=p_{m-r}((sf_m-sf_{m-r})sp_{r-1}-ss_{r-1}) $$ ::::success[code] ```cpp #include<bits/stdc++.h> using namespace std; const int N=1550; #define ll long long const int Math_Limit=1e5+10; const ll P=1e9+7; namespace fish_math{ ll fac[Math_Limit], inv[Math_Limit], fiv[Math_Limit]; ll qpow(ll a,ll b){ ll res=1; for(;b;b>>=1,a=(a*a)%P) if(b&1) res=(res*a)%P; return res; } ll Inv(ll x){ return qpow(x,P-2); } void Init_Math(int lmt){ fac[0]=1, fiv[0]=1; for(int i=1;i<=lmt;i++) fac[i]=(fac[i-1]*i)%P, inv[i]=Inv(i), fiv[i]=(fiv[i-1]*inv[i])%P; return; } ll C(ll n,ll m){ if(n<0||m<0||n<m) return 0; return fac[n]*fiv[m]%P*fiv[n-m]%P; } } using namespace fish_math; ll p1,k,a,b,n,m; ll p[N],sp[N],f[N][N],sf[N],ss[N],su; int main(){ ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); Init_Math(1e5); cin>>n>>m>>a>>b>>k; p1=Inv(b)*a%P; for(ll i=0;i<=min(k,m);i++) p[i]=C(k,i)*qpow(p1,i)%P*qpow((1-p1+P)%P,k-i)%P; for(ll i=0;i<=m;i++) sp[i]=((i>0?sp[i-1]:0)+p[i])%P; f[0][m]=1; for(ll i=1;i<=n;i++){ for(ll j=1;j<=m;j++) sf[j]=(sf[j-1]+f[i-1][j])%P, ss[j]=(ss[j-1]+p[j]*sf[j]%P)%P; for(ll j=1;j<=m;j++) f[i][j]=p[m-j]*(((sf[m]-sf[m-j]+P)%P)*sp[j-1]%P-ss[j-1]+P)%P; } for(int j=1;j<=m;j++) su=(su+f[n][j])%P; cout<<su; return 0; } ``` :::: ### $\small \color{52C41A}[容斥]$ LOJ3399 Communication Network 这个题是给你一棵树,然后要你求出所有的生成树与原树边集的并集大小 $x$ 的 $x2^x$ 之和。这个值看起来很奇怪,我们也不会做。 我们可以使用子集容斥的方法,也就是 $f(S)=\sum_{T\in S}\sum_{T'\in T}(-1)^{T-T'}f(T')$。这个可以用二项式定理来证明。 考虑如何使用这个东西。首先有 $f(S)=|S|2^{|S|}$。 那么就有: $$ \begin{aligned} 原式&=\sum_{S_2}f(S_1\cap S_2) \\ &= \sum_{S_2}\sum_{S\in(S_1\cap S_2)} \sum_{T\in S} (-1)^{|S|-|T|}f(T) \\ &= \sum_{S\in S_1}\sum_{T\in S}(-1)^{|S|-|T|}f(T)(\sum_{S\in S_2}1) \\ &=\sum_{S\in S_1}(-1)^{|S|}|S|\sum_{i=0}^{|S|-1}(-2)^{i+1}\binom{|S|-1}{i}(\sum_{S\in S_2}1) \\ &=\sum_{S\in S_1}(-1)^{|S|}(-2)|S|\sum_{i=0}^{|S|-1}(-2)^{i}\binom{|S|-1}{i}(\sum_{S\in S_2}1) \\ &=\sum_{S\in S_1}2|S|(\sum_{S\in S_2}1) \end{aligned} $$ 有结论:$r$ 个连通块,每个大 $a_i$,总生成树个为 $n^{r-2}\prod^r_{i=1}r_i$,答案就转化为 $2\frac{1}{n^2}\prod_{i=1}^r na_i\sum_{S\in S_1}|S|

这个东西的组合意义就是划分树,每个连通块选择点产生 n 的贡献,求所有选法的贡献之和。然后 DP 设 f_{u,0/1,0/1} 为 u 子树是否选点以及是否贡献,转移是容易的。

这里还用到了组合数的吸收公式:

m\binom{n}{m}=n\binom{n-1}{m-1}

证明是容易的,直接大力展开即可。

::::success[code]

#include<bits/stdc++.h>
using namespace std;
const int N=2e6+10;
#define ll long long
#define rep(i) for(int (i)=0;(i)<3;(i)++)
const ll P=998244353;   
ll qpow(ll a,ll b){
    ll res=1;
    for(;b;b>>=1,a=(a*a)%P) if(b&1) res=(res*a)%P;
    return res;
}   
int n;
ll f[N][2][2],g[2][2];
vector<int>e[N];
void dfs(int p,int fa){
    f[p][0][0]=1;
    f[p][1][0]=n; // Choosen, times n
    for(auto v:e[p])if(v^fa){
        dfs(v,p);
        rep(i) rep(j) g[i][j]=0;
        rep(x) rep(y) rep(i) rep(j){
            if(i+x>1||j+y>1) continue;
            (g[i+x][j+y]+=f[p][x][y]*f[v][i][j]%P)%=P;
        }
        rep(x) rep(i){
            if(i+x>1) continue;
            (g[i+x][1]+=f[p][x][0]*f[v][i][0]%P)%=P;
        }
        rep(x) rep(y) rep(j){
            if(j+y>1) continue;
            (g[x][j+y]+=f[p][x][y]*f[v][1][j]%P)%=P;
        }
        rep(i) rep(j) f[p][i][j]=g[i][j];
    }
    return;
}
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    cin>>n;
    for(int i=1;i<n;i++){
        int u,v;
        cin>>u>>v;
        e[u].push_back(v);
        e[v].push_back(u);
    }
    dfs(1,0);
    cout<<2*qpow(n,P-3)%P*f[1][1][1]%P;
    return 0;
}

::::

### $\small \color{FFC116}[组合]$ QOJ15446 Count Triangular Sequences 这个题就是直接计数比较困难所以考虑差分计数。原始序列 $\set{a_i}$,然后我们差分考虑是 $d_i=a_i-a_{i-1}-a_{i-2}$。这样我们就有 $a_i=\sum_{j=1}^n fib_j\times d_{i+1-j}$(打表观察)。最后直接背包制作即可。 ::::success[code] ```cpp #include<bits/stdc++.h> using namespace std; const int N=2e5+10,M=30; #define ll long long const int Math_Limit=2e5+10; const ll P=1e9+7; namespace fish_math{ ll fac[Math_Limit], inv[Math_Limit], fiv[Math_Limit]; ll qpow(ll a,ll b){ ll res=1; for(;b;b>>=1,a=(a*a)%P) if(b&1) res=(res*a)%P; return res; } ll Inv(ll x){ return qpow(x,P-2); } void Init_Math(int lmt){ fac[0]=1, fiv[0]=1; for(int i=1;i<=lmt;i++) fac[i]=(fac[i-1]*i)%P, inv[i]=Inv(i), fiv[i]=(fiv[i-1]*inv[i])%P; return; } ll C(ll n,ll m){ if(n<0||m<0||n<m) return 0; return fac[n]*fiv[m]%P*fiv[n-m]%P; } } using namespace fish_math; int n,m; ll ans,f[N],dp[M][N]; int main(){ ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); Init_Math(2e5); cin>>n; f[1]=1; for(int i=2;;i++){ f[i]=f[i-1]+f[i-2]; if(f[i]>n){ m=i; break; } } dp[1][0]=1; for(int i=1;i<=n;i++) ans=(ans+qpow(n,i))%P; for(int i=1;i<=min(n,m);i++){ for(int j=0;j<=n;j++){ (dp[i+1][j]+=dp[i][j])%=P; if(j+f[i]>n) continue; (dp[i][j+f[i]]+=dp[i][j])%=P; if(j+f[i]+f[i-1]<=n) ans=(ans-fac[i]*dp[i][j]%P+P)%P; else ans=(ans-inv[2]*fac[i]%P*dp[i][j]%P+P)%P; } } cout<<ans; return 0; } ``` ::::