数据结构与算法:二项式定理和二项式反演

前言

感觉这个虽然挺有用的,但目前几乎没见过,可能还是太菜了......

一、二项式定理

1.二项式定理及组合恒等式

首先,二项式定理 就是

时,就是著名的杨辉三角,即 。所以,根据杨辉三角的规律,就可以得到 组合恒等式:。有了这个关系,就可以通过类似 dp 的方法递推组合数了。这样就可以在无法求阶乘的情况下,每次 O(1) 查询组合数了。

这里就不多说证明了,可以直接从组合意义上来理解。

2.杨辉三角

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

/*   /\_/\
*   (= ._.)
*   / >  \>
*/

/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/

#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
#define endl '\n'
typedef long long ll;
typedef long double ld;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};

void solve()
{
    int n;
    cin>>n;

    vector<vector<ll>>C(n,vector<ll>(n));

    for(int i=0;i<n;i++)
    {
        C[i][0]=1;
    }

    for(int i=1;i<n;i++)
    {
        for(int j=1;j<i;j++)
        {
            C[i][j]=C[i-1][j]+C[i-1][j-1];
        }
        C[i][i]=1;
    }

    for(int i=0;i<n;i++)
    {
        for(int j=0;j<=i;j++)
        {
            cout<<C[i][j]<<" ";
        }
        cout<<endl;
    }
}

void init()
{
}

signed main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    int t=1;
    //cin>>t;
    init();
    while(t--)
    {
        solve();    
    }
    return 0;
}

就是组合数递推的板子题,没啥好说的。

3.计算系数

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

/*   /\_/\
*   (= ._.)
*   / >  \>
*/

/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/

#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
#define endl '\n'
typedef long long ll;
typedef long double ld;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};

template<class T>
constexpr T power(T a, ll b) {
    T res = 1;
    for (; b != 0; b /= 2, a *= a) {
        if (b & 1) {
            res *= a;
        }
    }
    return res;
}

template<int M>
struct ModInt {
public:
    constexpr ModInt() : x(0) {}

    template<typename T>
    constexpr ModInt(T x_) {
        T v = x_ % M;
        if (v < 0) {
            v += M;
        }
        x = v;
    }
 
    constexpr int val() const {
        return x;
    }

    constexpr ModInt &operator++() & {
        x++;
        if (x == M) {
            x = 0;
        }
        return *this;
    }

    constexpr ModInt operator++(int) & {
        ModInt res = *this;
        ++(*this);
        return res;
    }

    constexpr ModInt &operator--() & {
        if (x == 0) {
            x = M - 1;
        } else {
            x--;
        }
        return *this;
    }

    constexpr ModInt operator--(int) & {
        ModInt res = *this;
        --(*this);
        return res;
    }
 
    constexpr ModInt operator-() const {
        ModInt res;
        res.x = (x == 0 ? 0 : M - x);
        return res;
    }
 
    constexpr ModInt inv() const {
        return power(*this, M - 2);
    }
 
    constexpr ModInt &operator*=(const ModInt &rhs) &{
        x = ll(x) * rhs.val() % M;
        return *this;
    }
 
    constexpr ModInt &operator+=(const ModInt &rhs) &{
        x += rhs.val();
        if (x >= M) {
            x -= M;
        }
        return *this;
    }
 
    constexpr ModInt &operator-=(const ModInt &rhs) &{
        x -= rhs.val();
        if (x < 0) {
            x += M;
        }
        return *this;
    }
 
    constexpr ModInt &operator/=(const ModInt &rhs) &{
        return *this *= rhs.inv();
    }
 
    friend constexpr ModInt operator*(ModInt lhs, const ModInt &rhs) {
        lhs *= rhs;
        return lhs;
    }
 
    friend constexpr ModInt operator+(ModInt lhs, const ModInt &rhs) {
        lhs += rhs;
        return lhs;
    }
 
    friend constexpr ModInt operator-(ModInt lhs, const ModInt &rhs) {
        lhs -= rhs;
        return lhs;
    }
 
    friend constexpr ModInt operator/(ModInt lhs, const ModInt &rhs) {
        lhs /= rhs;
        return lhs;
    }
 
    friend constexpr bool operator==(ModInt lhs, const ModInt &rhs) {
        return lhs.val() == rhs.val();
    }
    
    friend constexpr bool operator<(ModInt lhs, const ModInt &rhs) {
        return lhs.val() < rhs.val();
    }
    
    friend constexpr bool operator>(ModInt lhs, const ModInt &rhs) {
        return lhs.val() > rhs.val();
    }
    
    friend constexpr bool operator<=(ModInt lhs, const ModInt &rhs) {
        return lhs.val() <= rhs.val();
    }
    
    friend constexpr bool operator>=(ModInt lhs, const ModInt &rhs) {
        return lhs.val() >= rhs.val();
    }
    
    friend constexpr bool operator!=(ModInt lhs, const ModInt &rhs) {
        return lhs.val() != rhs.val();
    }
 
    friend constexpr std::istream &operator>>(std::istream &is, ModInt &a) {
        ll i;
        is >> i;
        a = i;
        return is;
    }
 
    friend constexpr std::ostream &operator<<(std::ostream &os, const ModInt &a) {
        return os << a.val();
    }
 
private:
    int x;
};

template<int M, typename T = ModInt<M>>
struct Comb {
    vector<T> fac;
    vector<T> inv;
 
    Comb(int n) {
        fac.assign(n, 1);
        for(int i=1;i<n;i++)
        {
            fac[i]=fac[i-1]*i;
        }
        inv.assign(n, 1);
        inv[n-1]=fac[n-1].inv();
        for(int i=n-2;i>=0;i--)
        {
            inv[i]=inv[i+1]*(i+1);
        }
    }
 
    template<std::signed_integral U>
    T P(U n, U m) {
        if(n<m)
        {
            return 0;
        }
        return fac[n] * inv[n - m];
    }
 
    template<std::signed_integral U>
    T C(U n, U m) {
        if(n<m||m<0)
        {
            return 0;
        }
        return fac[n] * inv[n - m] * inv[m];
    }
};
 
//power函数切记强转成 Z !!!!!
constexpr int M = 10007;
using Z = ModInt<M>;

constexpr int N = 1000+5;
Comb<M>comb(N);

template<std::signed_integral U>
Z P(U n, U m) {
    return comb.P(n, m);
}
 
template<std::signed_integral U>
Z C(U n, U m) {
    return comb.C(n, m);
}

void solve()
{
    ll a,b,k,n,m;
    cin>>a>>b>>k>>n>>m;

    cout<<power((Z)a,n)*power((Z)b,m)*C(k,n)<<endl;
}

void init()
{
}

signed main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    int t=1;
    //cin>>t;
    init();
    while(t--)
    {
        solve();    
    }
    return 0;
}

这个其实就是二项式定理一个简单的应用。

回顾高中知识,可以想到该变量的系数既包含二项式系数,又包含自己的系数。那么对于 x 的 n 次方项,其自己的系数就是 a 的 n 次方,对于 b 也同理。所以就只需要用乘法快速幂计算出 a 的 n 次方和 b 的 m 次方,再乘以二项式系数即可。注意模数很小,所以组合数不能开太大,否则取模就成 0 了。

4.组合数问题

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

/*   /\_/\
*   (= ._.)
*   / >  \>
*/

/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/

#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
#define endl '\n'
typedef long long ll;
typedef long double ld;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};

const int N=2000;

void solve()
{
    int q,k;
    cin>>q>>k;

    vector<vector<int>>C(N+1,vector<int>(N+1));

    for(int i=0;i<=N;i++)
    {
        C[i][0]=1;
        for(int j=1;j<=i;j++)
        {
            C[i][j]=(C[i-1][j]+C[i-1][j-1])%k;
        }
    }

    for(int i=0;i<=N;i++)
    {
        for(int j=0;j<=i;j++)
        {
            C[i][j]=(C[i][j]==0);
        }
    }

    vector<vector<int>>pre(N+1,vector<int>(N+1));
    for(int i=1;i<=N;i++)
    {
        for(int j=1;j<=N;j++)
        {
            pre[i][j]=pre[i-1][j]+pre[i][j-1]-pre[i-1][j-1]+C[i][j];
        }
    }

    int n,m;
    while(q--)
    {
        cin>>n>>m;

        cout<<pre[n][m]<<endl;
    }
}

void init()
{
}

signed main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    int t=1;
    //cin>>t;
    init();
    while(t--)
    {
        solve();    
    }
    return 0;
}

因为 n 和 m 很大,直接求会溢出。所以对于这种整除问题,还是考虑转化成同余问题。那么就是构建每个组合数模 k 的结果,然后还是根据组合恒等式去转移即可。对于多次查询,可以通过对合法的个数求一个二维前缀和解决。

5.单调数组对的数目 II

逆天打表找规律......

cpp 复制代码
typedef long long ll;

template<class T>
constexpr T power(T a, ll b) {
    T res = 1;
    for (; b != 0; b /= 2, a *= a) {
        if (b & 1) {
            res *= a;
        }
    }
    return res;
}

template<int M>
struct ModInt {
public:
    constexpr ModInt() : x(0) {}

    template<typename T>
    constexpr ModInt(T x_) {
        T v = x_ % M;
        if (v < 0) {
            v += M;
        }
        x = v;
    }
 
    constexpr int val() const {
        return x;
    }

    constexpr ModInt &operator++() & {
        x++;
        if (x == M) {
            x = 0;
        }
        return *this;
    }

    constexpr ModInt operator++(int) & {
        ModInt res = *this;
        ++(*this);
        return res;
    }

    constexpr ModInt &operator--() & {
        if (x == 0) {
            x = M - 1;
        } else {
            x--;
        }
        return *this;
    }

    constexpr ModInt operator--(int) & {
        ModInt res = *this;
        --(*this);
        return res;
    }
 
    constexpr ModInt operator-() const {
        ModInt res;
        res.x = (x == 0 ? 0 : M - x);
        return res;
    }
 
    constexpr ModInt inv() const {
        return power(*this, M - 2);
    }
 
    constexpr ModInt &operator*=(const ModInt &rhs) &{
        x = ll(x) * rhs.val() % M;
        return *this;
    }
 
    constexpr ModInt &operator+=(const ModInt &rhs) &{
        x += rhs.val();
        if (x >= M) {
            x -= M;
        }
        return *this;
    }
 
    constexpr ModInt &operator-=(const ModInt &rhs) &{
        x -= rhs.val();
        if (x < 0) {
            x += M;
        }
        return *this;
    }
 
    constexpr ModInt &operator/=(const ModInt &rhs) &{
        return *this *= rhs.inv();
    }
 
    friend constexpr ModInt operator*(ModInt lhs, const ModInt &rhs) {
        lhs *= rhs;
        return lhs;
    }
 
    friend constexpr ModInt operator+(ModInt lhs, const ModInt &rhs) {
        lhs += rhs;
        return lhs;
    }
 
    friend constexpr ModInt operator-(ModInt lhs, const ModInt &rhs) {
        lhs -= rhs;
        return lhs;
    }
 
    friend constexpr ModInt operator/(ModInt lhs, const ModInt &rhs) {
        lhs /= rhs;
        return lhs;
    }
 
    friend constexpr bool operator==(ModInt lhs, const ModInt &rhs) {
        return lhs.val() == rhs.val();
    }
    
    friend constexpr bool operator<(ModInt lhs, const ModInt &rhs) {
        return lhs.val() < rhs.val();
    }
    
    friend constexpr bool operator>(ModInt lhs, const ModInt &rhs) {
        return lhs.val() > rhs.val();
    }
    
    friend constexpr bool operator<=(ModInt lhs, const ModInt &rhs) {
        return lhs.val() <= rhs.val();
    }
    
    friend constexpr bool operator>=(ModInt lhs, const ModInt &rhs) {
        return lhs.val() >= rhs.val();
    }
    
    friend constexpr bool operator!=(ModInt lhs, const ModInt &rhs) {
        return lhs.val() != rhs.val();
    }
 
    friend constexpr std::istream &operator>>(std::istream &is, ModInt &a) {
        ll i;
        is >> i;
        a = i;
        return is;
    }
 
    friend constexpr std::ostream &operator<<(std::ostream &os, const ModInt &a) {
        return os << a.val();
    }
 
private:
    int x;
};

template<int M, typename T = ModInt<M>>
struct Comb {
    vector<T> fac;
    vector<T> inv;
 
    Comb(int n) {
        fac.assign(n, 1);
        for(int i=1;i<n;i++)
        {
            fac[i]=fac[i-1]*i;
        }
        inv.assign(n, 1);
        inv[n-1]=fac[n-1].inv();
        for(int i=n-2;i>=0;i--)
        {
            inv[i]=inv[i+1]*(i+1);
        }
    }
 
    template<std::signed_integral U>
    T P(U n, U m) {
        if(n<m)
        {
            return 0;
        }
        return fac[n] * inv[n - m];
    }
 
    template<std::signed_integral U>
    T C(U n, U m) {
        if(n<m||m<0)
        {
            return 0;
        }
        return fac[n] * inv[n - m] * inv[m];
    }
};
 
//power函数切记强转成 Z !!!!!
constexpr int M = 1e9+7;
using Z = ModInt<M>;

constexpr int N = 2e5+5;
Comb<M>comb(N);

template<std::signed_integral U>
Z P(U n, U m) {
    return comb.P(n, m);
}
 
template<std::signed_integral U>
Z C(U n, U m) {
    return comb.C(n, m);
}

class Solution {
public:
    int countOfPairs(vector<int>&a) {
        int n=a.size();

        int k=a[0]+1;
        for(int i=1;i<n;i++)
        {
            if(a[i-1]>a[i])
            {
                k-=a[i-1]-a[i];
            }
        }

        if(k<=0)
        {
            return 0;
        }
        return C(k+n-1,n).val();
    }
};

将数对 (i,j) 认为分给第一个数组 i,分给第二个数组 j。假如只有一个数 5,那么其必然是可以分成 (1,4),(2,3),(3,2),(4,1) 四种。那么如果再来一个 5,那么对于当前这个 5,其还是可以分成以上四种。之后,在结合的时候,若第一次选择了 (4,1) 这种分法,那么之后就只能也选择 (4,1) 了,否则就会违规,所以有 1 种选法。若选择了 (3,2) 这种分法,那么之后就只能选择 (4,1) 和 (3,2) 了,所以 2 种选法。所以若选择 (2,3) 就有 3 种,选择 (1,4) 就有 4 种,所以一共 10 种。那么此时如果再来一个 5,选择 (4,1) 就还是 1 种,选择 (3,2) 是上一次选 (4,1) 的 1 种加上 (3,2) 的 2 种,一共 3 种。画一下图手玩可以发现,此时一共有 20 种。可以发现,若数组中只有一种数 v,数组长度为 n,那么单个数就有 k=v-1 种方案。那么把杨辉三角画出来,可以发现总的方法数就是 种。

若数字不同,首先,若第一个数是 6,那么其是有 5 种方案的。之后,若第二个数大于 6,那么不管其有几种方案,有效的只有 5 种。举个例子,若第二个数是 8,那么其 7 种方案中,(1,7) 和 (2,6) 是不会产生变化的。因为 (1,7) 不管和前一个数 6 的哪个方案结合都会违规,(2,6) 也是同理。所以,第二个数 8 是完全可以看作 6 的。

而若第二个数是 4 比第一个数 6 小,对于 (1,3),(2,2),(3,1) 这三种方案,第一个数 6 的 (4,2) 和 (5,1) 这两种方案就都不能用了。所以,若后一个数小于前一个数,此时有效方案数就要减去两数的差值。注意,这种减小不是只对上一个数有效,而是会让所有前缀的合法方案都减小。

所以,在对整个数组求出最终的合法方案数 k 后,只需要再用一次组合数公式即可。注意力扣里是可以分配 0 的,那么就只需要把单个数的方案改成 k=v+1 即可,其他的跟着再推一遍可以发现是不变的。

完全不是人......

二、二项式反演

这是给人学的吗......

首先先说一下反演,对于式子 ,其反演就是 。当一个很难求,而另一个很好求时,就可以通过当前反演式推出来。

1.四种形式

第一种形式,

第二种形式,

第三种形式,

第四种形式,

再说一个组合恒等式 ,这个可以通过组合意义求解。对于左侧,就是先从 j 个人里选出 i 个人,再从 i 个人里选 n 个,这个可以看作两轮比赛进行选拔。对于右侧,就可以看作先从 j 个人里内定出 n 个最终获选的人,再从剩下 j-n 个人里选出 j-i 个人第一轮淘汰掉。

2.信封问题

上来第一个题就不简单......

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

/*   /\_/\
*   (= ._.)
*   / >  \>
*/

/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/

#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
#define endl '\n'
typedef long long ll;
typedef long double ld;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};

//动态规划解
void solve1()
{
    int n;
    cin>>n;

    if(n==1)
    {
        cout<<0<<endl;
        return ;
    }

    vector<ll>dp(n+1);

    dp[1]=0;
    dp[2]=1;

    for(int i=3;i<=n;i++)
    {
        dp[i]=1ll*(i-1)*(dp[i-1]+dp[i-2]);
    }
    cout<<dp[n]<<endl;
}

//二项式反演解
void solve2()
{
    int n;
    cin>>n;

    ll fac=1;
    for(int i=1;i<=n;i++)
    {
        fac*=i;
    }

    ll ans=fac;
    ll cur=1;
    for(int i=1;i<=n;i++)
    {
        cur*=i;
        ans+=(i%2?-1:1)*fac/cur;
    }
    cout<<ans<<endl;
}

void init()
{
}

signed main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    int t=1;
    //cin>>t;
    init();
    while(t--)
    {
        solve1();    
    }
    return 0;
}

首先,这个题求的就是 n 个数的错排方案数,就是 1~n 都不在自己位置上的排列方案数。

先说 dp 的方法,考虑定义 dp[i] 为 i 个人的错排。那么对于当前人数 i,首先,肯定可以选定一个人,从这个人开始往其他 i-1 个人送信,那么就是 i-1 种不同的方案。对于每一种方案,若送往的那个人送回去了,那么之后就是剩下 i-2 个人送信,即 dp[i-2]。否则,因为此时对于这两个人,可以发现也是需要一封信送过来,一封信送出去。所以就可以将这两个人看作一个人,所以之后依赖的就是 dp[i-1]。

对于二项式反演解,设 n 个人错排的方案数为 f(n),那么如果发现这个不好求,就可以考虑进行转化。考虑定义 g(n) 为 n 个人的全排列,那么方案数显然就是 n 的阶乘。之后,考虑将全排列的所有方案,按照正好 i 个人不在自己位置划分。此时,f(n) 的定义就是指定 n 个指定的人,让他们不在自己位置的方案数。所以,对于正好 i 个人不在自己位置的方案数,就是先从 n 个人里选 i 个人,再乘以 f(i),即正好 i 个指定的人不在自己位置的方案数。所以就有 ,那么就可以通过第二种反演式求了。所以,将 g(n) 代入化简,有 ,换元就可以得到

3.集合计数

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

/*   /\_/\
*   (= ._.)
*   / >  \>
*/

/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/

#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
#define endl '\n'
typedef long long ll;
typedef long double ld;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};

template<class T>
constexpr T power(T a, ll b) {
    T res = 1;
    for (; b != 0; b /= 2, a *= a) {
        if (b & 1) {
            res *= a;
        }
    }
    return res;
}

template<int M>
struct ModInt {
public:
    constexpr ModInt() : x(0) {}

    template<typename T>
    constexpr ModInt(T x_) {
        T v = x_ % M;
        if (v < 0) {
            v += M;
        }
        x = v;
    }
 
    constexpr int val() const {
        return x;
    }

    constexpr ModInt &operator++() & {
        x++;
        if (x == M) {
            x = 0;
        }
        return *this;
    }

    constexpr ModInt operator++(int) & {
        ModInt res = *this;
        ++(*this);
        return res;
    }

    constexpr ModInt &operator--() & {
        if (x == 0) {
            x = M - 1;
        } else {
            x--;
        }
        return *this;
    }

    constexpr ModInt operator--(int) & {
        ModInt res = *this;
        --(*this);
        return res;
    }
 
    constexpr ModInt operator-() const {
        ModInt res;
        res.x = (x == 0 ? 0 : M - x);
        return res;
    }
 
    constexpr ModInt inv() const {
        return power(*this, M - 2);
    }
 
    constexpr ModInt &operator*=(const ModInt &rhs) &{
        x = ll(x) * rhs.val() % M;
        return *this;
    }
 
    constexpr ModInt &operator+=(const ModInt &rhs) &{
        x += rhs.val();
        if (x >= M) {
            x -= M;
        }
        return *this;
    }
 
    constexpr ModInt &operator-=(const ModInt &rhs) &{
        x -= rhs.val();
        if (x < 0) {
            x += M;
        }
        return *this;
    }
 
    constexpr ModInt &operator/=(const ModInt &rhs) &{
        return *this *= rhs.inv();
    }
 
    friend constexpr ModInt operator*(ModInt lhs, const ModInt &rhs) {
        lhs *= rhs;
        return lhs;
    }
 
    friend constexpr ModInt operator+(ModInt lhs, const ModInt &rhs) {
        lhs += rhs;
        return lhs;
    }
 
    friend constexpr ModInt operator-(ModInt lhs, const ModInt &rhs) {
        lhs -= rhs;
        return lhs;
    }
 
    friend constexpr ModInt operator/(ModInt lhs, const ModInt &rhs) {
        lhs /= rhs;
        return lhs;
    }
 
    friend constexpr bool operator==(ModInt lhs, const ModInt &rhs) {
        return lhs.val() == rhs.val();
    }
    
    friend constexpr bool operator<(ModInt lhs, const ModInt &rhs) {
        return lhs.val() < rhs.val();
    }
    
    friend constexpr bool operator>(ModInt lhs, const ModInt &rhs) {
        return lhs.val() > rhs.val();
    }
    
    friend constexpr bool operator<=(ModInt lhs, const ModInt &rhs) {
        return lhs.val() <= rhs.val();
    }
    
    friend constexpr bool operator>=(ModInt lhs, const ModInt &rhs) {
        return lhs.val() >= rhs.val();
    }
    
    friend constexpr bool operator!=(ModInt lhs, const ModInt &rhs) {
        return lhs.val() != rhs.val();
    }
 
    friend constexpr std::istream &operator>>(std::istream &is, ModInt &a) {
        ll i;
        is >> i;
        a = i;
        return is;
    }
 
    friend constexpr std::ostream &operator<<(std::ostream &os, const ModInt &a) {
        return os << a.val();
    }
 
private:
    int x;
};

template<int M, typename T = ModInt<M>>
struct Comb {
    vector<T> fac;
    vector<T> inv;
 
    Comb(int n) {
        fac.assign(n, 1);
        for(int i=1;i<n;i++)
        {
            fac[i]=fac[i-1]*i;
        }
        inv.assign(n, 1);
        inv[n-1]=fac[n-1].inv();
        for(int i=n-2;i>=0;i--)
        {
            inv[i]=inv[i+1]*(i+1);
        }
    }
 
    template<std::signed_integral U>
    T P(U n, U m) {
        if(n<m)
        {
            return 0;
        }
        return fac[n] * inv[n - m];
    }
 
    template<std::signed_integral U>
    T C(U n, U m) {
        if(n<m||m<0)
        {
            return 0;
        }
        return fac[n] * inv[n - m] * inv[m];
    }
};
 
//power函数切记强转成 Z !!!!!
constexpr int M = 1e9+7;
using Z = ModInt<M>;

constexpr int N = 1e6+5;
Comb<M>comb(N);

template<std::signed_integral U>
Z P(U n, U m) {
    return comb.P(n, m);
}
 
template<std::signed_integral U>
Z C(U n, U m) {
    return comb.C(n, m);
}

void solve()
{
    int n,k;
    cin>>n>>k;

    vector<Z>g(n+1);
    g[n]=2;
    for(int i=n-1;i>=0;i--)
    {
        g[i]=g[i+1]*g[i+1];
    }
    for(int i=0;i<=n;i++)
    {
        g[i]=(g[i]-1)*C(n,i);
    }

    Z ans=0;
    for(int i=k;i<=n;i++)
    {
        Z sign=(i-k)%2?-1:1;

        ans+=sign*C(i,k)*g[i];
    }
    cout<<ans<<endl;
}

void init()
{
}

signed main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    int t=1;
    //cin>>t;
    init();
    while(t--)
    {
        solve();    
    }
    return 0;
}

考虑定义 g(i) 为对于任选 i 个元素的所有方案,交集至少包含每次选择的这 i 个元素的总方案数。注意,这里认为一开始选择的元素不同方案就不同。那么首先,从 n 个元素里选 i 个,方案数就是 。对于每种方案,考虑让这 i 个元素往其他元素构成的集合里插。所以对于剩下的 n-i 个数,可以形成 个集合。又因为每个集合都可以选择要和不要,所以一共就有 种方案。最后,注意需要把空集的情况减去,因为没法往里插入。

对于式子 ,由于指数同余不能直接取模(其实可以费马小定理直接求的,不懂为啥左神这里不讲),所以考虑别的方法。观察式子,可以发现在从后往前求的过程中,前面这部分每次就是上一项的平方,那么就可以递推解决了。

那么之后考虑定义 f(i) 为交集正好 i 个元素的方案数。举个例子,对于 g(3),f(5) 表示交集正好五个元素的方案数,那么其从这五个元素里任选三个,都可以给 g(3) 产生一次贡献。所以就有 ,所以就有 ,那么就可以使用反演形式四了。

所以就有一个二项式反演的 Trick:指定 k 个且至少的问题 和 恰好 k 个的问题可以通过二项式反演的第四种形式相互转化。所以在遇到恰好 k 个的计数问题时,直接求比较困难,那么就可以考虑先求出指定 k 个且至少的问题,然后二项式反演。

4.分特产

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

/*   /\_/\
*   (= ._.)
*   / >  \>
*/

/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/

#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
#define endl '\n'
typedef long long ll;
typedef long double ld;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};

template<class T>
constexpr T power(T a, ll b) {
    T res = 1;
    for (; b != 0; b /= 2, a *= a) {
        if (b & 1) {
            res *= a;
        }
    }
    return res;
}

template<int M>
struct ModInt {
public:
    constexpr ModInt() : x(0) {}

    template<typename T>
    constexpr ModInt(T x_) {
        T v = x_ % M;
        if (v < 0) {
            v += M;
        }
        x = v;
    }
 
    constexpr int val() const {
        return x;
    }

    constexpr ModInt &operator++() & {
        x++;
        if (x == M) {
            x = 0;
        }
        return *this;
    }

    constexpr ModInt operator++(int) & {
        ModInt res = *this;
        ++(*this);
        return res;
    }

    constexpr ModInt &operator--() & {
        if (x == 0) {
            x = M - 1;
        } else {
            x--;
        }
        return *this;
    }

    constexpr ModInt operator--(int) & {
        ModInt res = *this;
        --(*this);
        return res;
    }
 
    constexpr ModInt operator-() const {
        ModInt res;
        res.x = (x == 0 ? 0 : M - x);
        return res;
    }
 
    constexpr ModInt inv() const {
        return power(*this, M - 2);
    }
 
    constexpr ModInt &operator*=(const ModInt &rhs) &{
        x = ll(x) * rhs.val() % M;
        return *this;
    }
 
    constexpr ModInt &operator+=(const ModInt &rhs) &{
        x += rhs.val();
        if (x >= M) {
            x -= M;
        }
        return *this;
    }
 
    constexpr ModInt &operator-=(const ModInt &rhs) &{
        x -= rhs.val();
        if (x < 0) {
            x += M;
        }
        return *this;
    }
 
    constexpr ModInt &operator/=(const ModInt &rhs) &{
        return *this *= rhs.inv();
    }
 
    friend constexpr ModInt operator*(ModInt lhs, const ModInt &rhs) {
        lhs *= rhs;
        return lhs;
    }
 
    friend constexpr ModInt operator+(ModInt lhs, const ModInt &rhs) {
        lhs += rhs;
        return lhs;
    }
 
    friend constexpr ModInt operator-(ModInt lhs, const ModInt &rhs) {
        lhs -= rhs;
        return lhs;
    }
 
    friend constexpr ModInt operator/(ModInt lhs, const ModInt &rhs) {
        lhs /= rhs;
        return lhs;
    }
 
    friend constexpr bool operator==(ModInt lhs, const ModInt &rhs) {
        return lhs.val() == rhs.val();
    }
    
    friend constexpr bool operator<(ModInt lhs, const ModInt &rhs) {
        return lhs.val() < rhs.val();
    }
    
    friend constexpr bool operator>(ModInt lhs, const ModInt &rhs) {
        return lhs.val() > rhs.val();
    }
    
    friend constexpr bool operator<=(ModInt lhs, const ModInt &rhs) {
        return lhs.val() <= rhs.val();
    }
    
    friend constexpr bool operator>=(ModInt lhs, const ModInt &rhs) {
        return lhs.val() >= rhs.val();
    }
    
    friend constexpr bool operator!=(ModInt lhs, const ModInt &rhs) {
        return lhs.val() != rhs.val();
    }
 
    friend constexpr std::istream &operator>>(std::istream &is, ModInt &a) {
        ll i;
        is >> i;
        a = i;
        return is;
    }
 
    friend constexpr std::ostream &operator<<(std::ostream &os, const ModInt &a) {
        return os << a.val();
    }
 
private:
    int x;
};

template<int M, typename T = ModInt<M>>
struct Comb {
    vector<T> fac;
    vector<T> inv;
 
    Comb(int n) {
        fac.assign(n, 1);
        for(int i=1;i<n;i++)
        {
            fac[i]=fac[i-1]*i;
        }
        inv.assign(n, 1);
        inv[n-1]=fac[n-1].inv();
        for(int i=n-2;i>=0;i--)
        {
            inv[i]=inv[i+1]*(i+1);
        }
    }
 
    template<std::signed_integral U>
    T P(U n, U m) {
        if(n<m)
        {
            return 0;
        }
        return fac[n] * inv[n - m];
    }
 
    template<std::signed_integral U>
    T C(U n, U m) {
        if(n<m||m<0)
        {
            return 0;
        }
        return fac[n] * inv[n - m] * inv[m];
    }
};
 
//power函数切记强转成 Z !!!!!
constexpr int M = 1e9+7;
using Z = ModInt<M>;

constexpr int N = 2e5+5;
Comb<M>comb(N);

template<std::signed_integral U>
Z P(U n, U m) {
    return comb.P(n, m);
}
 
template<std::signed_integral U>
Z C(U n, U m) {
    return comb.C(n, m);
}

void solve()
{
    int n,m;
    cin>>n>>m;
    vector<int>a(m+1);
    for(int i=1;i<=m;i++)
    {
        cin>>a[i];
    }

    vector<Z>g(n+1);
    for(int i=0;i<n;i++)
    {
        g[i]=C(n,i);
        for(int j=1;j<=m;j++)
        {
            g[i]*=C(a[j]+n-i-1,n-i-1);
        }
    }
    g[n]=0;

    Z ans=0;
    for(int i=0;i<=n;i++)
    {
        int sign=i%2?-1:1;

        ans+=sign*g[i];
    }
    cout<<ans<<endl;
}

void init()
{
}

signed main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    int t=1;
    //cin>>t;
    init();
    while(t--)
    {
        solve();    
    }
    return 0;
}

还有由上个题得到启发,还是定义 g(i) 为一定要分完所有特产,任选 i 个人不获得特产的所有方案中,最终没获得特产的人中至少包含每次选择的这 i 个人的总方案数。那么再定义 f(i) 为一定分完所有特产,正好 i 个人没有特产的方案数。那么可以发现两者还是存在反演形式四的关系,所以答案 f(0) 就可以直接从 g(i) 得到。

对于 g(i),首先从 n 个人里任选 i 个人,方案数就是 。那么之后就是只在剩下的 n-i 个人里考虑,那么问题就变成了对于第 i 号特产,将 a[i] 个相同的物品分给 n-i 个人的方案数。那么这个就可以用隔板法解决,方案数 ,所以连乘即可。

5.已经没有什么好害怕的了

已经没有什么好害怕的了......

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

/*   /\_/\
*   (= ._.)
*   / >  \>
*/

/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/

#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
#define endl '\n'
typedef long long ll;
typedef long double ld;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};

template<class T>
constexpr T power(T a, ll b) {
    T res = 1;
    for (; b != 0; b /= 2, a *= a) {
        if (b & 1) {
            res *= a;
        }
    }
    return res;
}

template<int M>
struct ModInt {
public:
    constexpr ModInt() : x(0) {}

    template<typename T>
    constexpr ModInt(T x_) {
        T v = x_ % M;
        if (v < 0) {
            v += M;
        }
        x = v;
    }
 
    constexpr int val() const {
        return x;
    }

    constexpr ModInt &operator++() & {
        x++;
        if (x == M) {
            x = 0;
        }
        return *this;
    }

    constexpr ModInt operator++(int) & {
        ModInt res = *this;
        ++(*this);
        return res;
    }

    constexpr ModInt &operator--() & {
        if (x == 0) {
            x = M - 1;
        } else {
            x--;
        }
        return *this;
    }

    constexpr ModInt operator--(int) & {
        ModInt res = *this;
        --(*this);
        return res;
    }
 
    constexpr ModInt operator-() const {
        ModInt res;
        res.x = (x == 0 ? 0 : M - x);
        return res;
    }
 
    constexpr ModInt inv() const {
        return power(*this, M - 2);
    }
 
    constexpr ModInt &operator*=(const ModInt &rhs) &{
        x = ll(x) * rhs.val() % M;
        return *this;
    }
 
    constexpr ModInt &operator+=(const ModInt &rhs) &{
        x += rhs.val();
        if (x >= M) {
            x -= M;
        }
        return *this;
    }
 
    constexpr ModInt &operator-=(const ModInt &rhs) &{
        x -= rhs.val();
        if (x < 0) {
            x += M;
        }
        return *this;
    }
 
    constexpr ModInt &operator/=(const ModInt &rhs) &{
        return *this *= rhs.inv();
    }
 
    friend constexpr ModInt operator*(ModInt lhs, const ModInt &rhs) {
        lhs *= rhs;
        return lhs;
    }
 
    friend constexpr ModInt operator+(ModInt lhs, const ModInt &rhs) {
        lhs += rhs;
        return lhs;
    }
 
    friend constexpr ModInt operator-(ModInt lhs, const ModInt &rhs) {
        lhs -= rhs;
        return lhs;
    }
 
    friend constexpr ModInt operator/(ModInt lhs, const ModInt &rhs) {
        lhs /= rhs;
        return lhs;
    }
 
    friend constexpr bool operator==(ModInt lhs, const ModInt &rhs) {
        return lhs.val() == rhs.val();
    }
    
    friend constexpr bool operator<(ModInt lhs, const ModInt &rhs) {
        return lhs.val() < rhs.val();
    }
    
    friend constexpr bool operator>(ModInt lhs, const ModInt &rhs) {
        return lhs.val() > rhs.val();
    }
    
    friend constexpr bool operator<=(ModInt lhs, const ModInt &rhs) {
        return lhs.val() <= rhs.val();
    }
    
    friend constexpr bool operator>=(ModInt lhs, const ModInt &rhs) {
        return lhs.val() >= rhs.val();
    }
    
    friend constexpr bool operator!=(ModInt lhs, const ModInt &rhs) {
        return lhs.val() != rhs.val();
    }
 
    friend constexpr std::istream &operator>>(std::istream &is, ModInt &a) {
        ll i;
        is >> i;
        a = i;
        return is;
    }
 
    friend constexpr std::ostream &operator<<(std::ostream &os, const ModInt &a) {
        return os << a.val();
    }
 
private:
    int x;
};

template<int M, typename T = ModInt<M>>
struct Comb {
    vector<T> fac;
    vector<T> inv;
 
    Comb(int n) {
        fac.assign(n, 1);
        for(int i=1;i<n;i++)
        {
            fac[i]=fac[i-1]*i;
        }
        inv.assign(n, 1);
        inv[n-1]=fac[n-1].inv();
        for(int i=n-2;i>=0;i--)
        {
            inv[i]=inv[i+1]*(i+1);
        }
    }
 
    template<std::signed_integral U>
    T P(U n, U m) {
        if(n<m)
        {
            return 0;
        }
        return fac[n] * inv[n - m];
    }
 
    template<std::signed_integral U>
    T C(U n, U m) {
        if(n<m||m<0)
        {
            return 0;
        }
        return fac[n] * inv[n - m] * inv[m];
    }
};
 
//power函数切记强转成 Z !!!!!
constexpr int M = 1e9+9;
using Z = ModInt<M>;

constexpr int N = 2e5+5;
Comb<M>comb(N);

template<std::signed_integral U>
Z P(U n, U m) {
    return comb.P(n, m);
}
 
template<std::signed_integral U>
Z C(U n, U m) {
    return comb.C(n, m);
}

void solve()
{
    int n,k;
    cin>>n>>k;
    vector<ll>a(n+1);
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
    }
    vector<ll>b(n+1);
    for(int i=1;i<=n;i++)
    {
        cin>>b[i];
    }

    if((n+k)%2)
    {
        cout<<0<<endl;
        return ;
    }

    k=(n+k)/2;

    sort(a.begin()+1,a.end());
    sort(b.begin()+1,b.end());

    vector<Z>cnts(n+1);
    for(int i=1,j=0;i<=n;i++)
    {
        while(j+1<=n&&b[j+1]<a[i])
        {
            j++;
        }

        cnts[i]=j;
    }

    vector<vector<Z>>dp(n+1,vector<Z>(n+1));
    dp[0][0]=1;
    for(int i=1;i<=n;i++)
    {
        dp[i][0]=dp[i-1][0];
        for(int j=1;j<=i;j++)
        {
            dp[i][j]=dp[i-1][j]+dp[i-1][j-1]*(cnts[i]-(j-1));
        }
    }

    vector<Z>g(n+1);
    for(int i=0;i<=n;i++)
    {
        g[i]=dp[n][i]*comb.fac[n-i];
    }

    Z ans=0;
    for(int i=k;i<=n;i++)
    {
        int sign=(i-k)%2?-1:1;
        ans+=sign*C(i,k)*g[i];
    }
    cout<<ans<<endl;
}

void init()
{
}

signed main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    int t=1;
    //cin>>t;
    init();
    while(t--)
    {
        solve();    
    }
    return 0;
}

首先,设糖果大的有 a 对,药片大的有 b 对,那么就有关系 ,所以相加就可以得到 。所以,如果 n 和 k 加起来是奇数,那必然是没有方案的。因为如果 n 是偶数 k 是奇数,那么糖果和药片必然全偶或者全奇,那么就必然不可能差值是个奇数。奇偶性相反时类似。那么问题就转化为了,要求糖果大的对数一定为 的配对数。

那么对于这种恰好的问题,还是考虑转化成指定且至少的问题,然后二项式反演。所以还是定义 g(i) 为任意指定 i 个糖果,最终至少保证选的这 i 个糖果形成糖果大的配对的总方案数。那么再定义 f(i) 为恰好 i 个糖果形成糖果大的配对的总方案数。

对于 g(i),考虑定义 dp[i][j] 为前 i 个糖果中,指定 j 个糖果,必须形成糖果大的对的方案数。那么首先考虑对两个数组排序,之后就可以用双指针找出比每个位置的糖果小的药片数量 cnts[i]。那么对于当前第 i 个糖果,若不指定,那么依赖的就是 dp[i-1][j]。若指定,那么依赖的就是 dp[i-1][j-1]。此时由于之前的糖果占用了比当前糖果小的 j-1 个药片,所以要乘上 cnts[i]-(j-1)。所以就能发现,dp[n][i] 是不关心没指定的情况的,而 g(i) 是关心没指定的情况的。也就是说,对于剩下的糖果,g(i) 认为其他糖果不同的配对属于不同的方案。所以剩下 n-i 个糖果和药片就可以看作一个全排列问题,那么就需要在 dp[n][i] 的基础上,再乘上 n-i 的阶乘。

6.游戏

这个反演形式四这么常用。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

/*   /\_/\
*   (= ._.)
*   / >  \>
*/

/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/

#define endl '\n'
#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
typedef long long ll;
typedef long double ld;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};

template<class T>
constexpr T power(T a, ll b) {
    T res = 1;
    for (; b != 0; b /= 2, a *= a) {
        if (b & 1) {
            res *= a;
        }
    }
    return res;
}

template<int M>
struct ModInt {
public:
    constexpr ModInt() : x(0) {}

    template<typename T>
    constexpr ModInt(T x_) {
        T v = x_ % M;
        if (v < 0) {
            v += M;
        }
        x = v;
    }
 
    constexpr int val() const {
        return x;
    }

    constexpr ModInt &operator++() & {
        x++;
        if (x == M) {
            x = 0;
        }
        return *this;
    }

    constexpr ModInt operator++(int) & {
        ModInt res = *this;
        ++(*this);
        return res;
    }

    constexpr ModInt &operator--() & {
        if (x == 0) {
            x = M - 1;
        } else {
            x--;
        }
        return *this;
    }

    constexpr ModInt operator--(int) & {
        ModInt res = *this;
        --(*this);
        return res;
    }
 
    constexpr ModInt operator-() const {
        ModInt res;
        res.x = (x == 0 ? 0 : M - x);
        return res;
    }
 
    constexpr ModInt inv() const {
        return power(*this, M - 2);
    }
 
    constexpr ModInt &operator*=(const ModInt &rhs) &{
        x = ll(x) * rhs.val() % M;
        return *this;
    }
 
    constexpr ModInt &operator+=(const ModInt &rhs) &{
        x += rhs.val();
        if (x >= M) {
            x -= M;
        }
        return *this;
    }
 
    constexpr ModInt &operator-=(const ModInt &rhs) &{
        x -= rhs.val();
        if (x < 0) {
            x += M;
        }
        return *this;
    }
 
    constexpr ModInt &operator/=(const ModInt &rhs) &{
        return *this *= rhs.inv();
    }
 
    friend constexpr ModInt operator*(ModInt lhs, const ModInt &rhs) {
        lhs *= rhs;
        return lhs;
    }
 
    friend constexpr ModInt operator+(ModInt lhs, const ModInt &rhs) {
        lhs += rhs;
        return lhs;
    }
 
    friend constexpr ModInt operator-(ModInt lhs, const ModInt &rhs) {
        lhs -= rhs;
        return lhs;
    }
 
    friend constexpr ModInt operator/(ModInt lhs, const ModInt &rhs) {
        lhs /= rhs;
        return lhs;
    }
 
    friend constexpr bool operator==(ModInt lhs, const ModInt &rhs) {
        return lhs.val() == rhs.val();
    }
    
    friend constexpr bool operator<(ModInt lhs, const ModInt &rhs) {
        return lhs.val() < rhs.val();
    }
    
    friend constexpr bool operator>(ModInt lhs, const ModInt &rhs) {
        return lhs.val() > rhs.val();
    }
    
    friend constexpr bool operator<=(ModInt lhs, const ModInt &rhs) {
        return lhs.val() <= rhs.val();
    }
    
    friend constexpr bool operator>=(ModInt lhs, const ModInt &rhs) {
        return lhs.val() >= rhs.val();
    }
    
    friend constexpr bool operator!=(ModInt lhs, const ModInt &rhs) {
        return lhs.val() != rhs.val();
    }
 
    friend constexpr std::istream &operator>>(std::istream &is, ModInt &a) {
        ll i;
        is >> i;
        a = i;
        return is;
    }
 
    friend constexpr std::ostream &operator<<(std::ostream &os, const ModInt &a) {
        return os << a.val();
    }
 
private:
    int x;
};

template<int M, typename T = ModInt<M>>
struct Comb {
    vector<T> fac;
    vector<T> inv;
 
    Comb(int n) {
        fac.assign(n, 1);
        for(int i=1;i<n;i++)
        {
            fac[i]=fac[i-1]*i;
        }
        inv.assign(n, 1);
        inv[n-1]=fac[n-1].inv();
        for(int i=n-2;i>=0;i--)
        {
            inv[i]=inv[i+1]*(i+1);
        }
    }
 
    template<std::signed_integral U>
    T P(U n, U m) {
        if(n<m)
        {
            return 0;
        }
        return fac[n] * inv[n - m];
    }
 
    template<std::signed_integral U>
    T C(U n, U m) {
        if(n<m||m<0)
        {
            return 0;
        }
        return fac[n] * inv[n - m] * inv[m];
    }
};
 
//power函数切记强转成 Z !!!!!
constexpr int M = 998244353;
using Z = ModInt<M>;

constexpr int N = 2e5+5;
Comb<M>comb(N);

template<std::signed_integral U>
Z P(U n, U m) {
    return comb.P(n, m);
}
 
template<std::signed_integral U>
Z C(U n, U m) {
    return comb.C(n, m);
}

void solve()
{
    int n;
    cin>>n;
    int m=n/2;
    string s;
    cin>>s;
    s=" "+s;
    vector<vector<int>>adj(n+1);
    for(int i=1,u,v;i<=n-1;i++)
    {
        cin>>u>>v;
        adj[u].push_back(v);
        adj[v].push_back(u);
    }

    vector<vector<Z>>dp(n+1,vector<Z>(m+1));
    vector<int>sz(n+1);
    vector<array<int,2>>belong(n+1);

    auto dfs=[&](auto &&self,int u,int fa)->void
    {
        int cur=s[u]-'0';
        sz[u]=1;
        belong[u][cur]=1;
        dp[u][0]=1;

        //not contain u
        for(auto v:adj[u])
        {
            if(v!=fa)
            {
                self(self,v,u);

                vector<Z>ndp(m+1);
                for(int a=0;a<=min(sz[u]/2,m);a++)
                {
                    for(int b=0;b<=min(sz[v]/2,m-a);b++)
                    {
                        ndp[a+b]+=dp[u][a]*dp[v][b];
                    }
                }

                sz[u]+=sz[v];
                belong[u][0]+=belong[v][0];
                belong[u][1]+=belong[v][1];
                dp[u]=ndp;
            }
        }

        int cnt=belong[u][cur^1];
        vector<Z>ndp(m+1);
        ndp[0]=dp[u][0];
        for(int i=1;i<=min(cnt,m);i++)
        {
            ndp[i]=dp[u][i]+dp[u][i-1]*(cnt-(i-1));
        }
        dp[u]=ndp;
    };

    dfs(dfs,1,0);

    vector<Z>g(m+1);
    for(int i=0;i<=m;i++)
    {
        g[i]=dp[1][i]*comb.fac[m-i];
    }

    vector<Z>f(m+1);
    for(int k=0;k<=m;k++)
    {
        for(int i=k;i<=m;i++)
        {
            int sign=(i-k)%2?-1:1;
            f[k]+=sign*C(i,k)*g[i];
        }
    }

    for(int k=0;k<=m;k++)
    {
        cout<<f[k]<<endl;
    }
}

void init()
{
}

signed main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    int t=1;
    //cin>>t;
    init();
    while(t--)
    {
        solve();    
    }
    return 0;
}

对于恰好 k 个的问题,还是转化成指定 k 个且至少。所以定义 g(i) 为指定 i 个回合,保证至少指定的这 i 个回合非平局的方案数,再定义 f(i) 为正好 i 个回合非平局的方案数。

对于树上问题,还是考虑分析当前节点 u 和孩子节点 v 的关系,然后看能否 dp。所以考虑定义 dp[u][i] 为在 u 的子树内考虑,选出 i 对节点非平局,要求选出的对里节点都不同的方案数。那么不难发现 g(i) 可以从 dp[1][i] 中得到,又因为 dp[1][i] 只关注选出的方法数,不关注剩下的,所以要把剩下 m-i 个能形成的方案数乘进去。因为这个可以看作固定一个选择的顺序,然后另一个自由排列,所以方案数就是全排列,即 m-i 的阶乘。

对于这个树型 dp,是一个类似背包的过程。若当前节点 u 不参与,那么对于当前孩子 v,若之前考虑过的孩子中选了 a 对,当前孩子中选了 b 对,那么就能在原来 dp[u][a+b] 的基础上,加上两者的乘积。

重点是复杂度的证明。乍一看这样好像是 O(n^3) 的,但实际上是 O(n^2) 的。因为每次循环的上限是 sz[u] 的,那么就可以看作,每来到一个孩子 v,枚举所有之前的遍历过的子树内的所有节点,再枚举当前孩子子树内的所有节点。在这个过程中,可以看作每次产生一个点对 (x,y),而且是不会重复出现的,因为之后就都合并成一个子树了。那么由于一共有 O(n^2) 个点对,所以这个过程的复杂度其实是 O(n^2) 的。

若当前节点 u 必须参与,那么依赖的肯定就是不包含 u 的情况下,产生 i-1 对的方案数。在这个基础上,若当前 u 内有 a 个属于甲的节点,有 b 个属于乙的节点,其中 u 属于甲。那么因为之前有 i-1 个乙的节点被占用了,所以就只能选剩下 b-(i-1) 个节点了,所以再乘上 b-(i-1) 即可。

总结

太难了......

END

相关推荐
nianniannnn2 小时前
力扣104.二叉树的最大深度 110. 平衡二叉树
算法·leetcode·深度优先
_深海凉_2 小时前
LeetCode热题100-只出现一次的数字
算法·leetcode·职场和发展
yashuk2 小时前
C语言 vs. C++ ,哪个更适合初学者?
c语言·c++·面向对象编程·初学者·学习路径
-许平安-2 小时前
MCP项目笔记十(客户端 MCPClient)
c++·笔记·ai·raii·mcp·pluginapi·plugin system
一只旭宝3 小时前
【C++ 入门精讲2】函数重载、默认参数、函数指针、volatile | 手写笔记(附完整代码)
c++·笔记
nianniannnn3 小时前
力扣206.反转链表 92.反转链表II
算法·leetcode·链表
澈2073 小时前
哈希表实战:从原理到手写实现
算法·哈希算法
旖-旎3 小时前
哈希表(存在重复元素||)(4)
数据结构·c++·算法·leetcode·哈希算法·散列表
Run_Teenage3 小时前
Linux:认识信号,理解信号的产生和处理
linux·运维·算法