[ABC236Ex] Distinct Multiples 题解

前言

非常好容斥题,使我的笔旋转。

感觉这道题又让我学到了不少。

确实是没有想过可以把容斥系数都 DP 进去这种方式。

思路

显然正着做不太好做,反着正好有事一堆 ai=aj 的限制,说不定会好一点。

故我们考虑容斥。

首先最暴力的一个容斥就是我们对于每一个集合 S,里面储存的是这个点对 (i,j) 是否会被钦定为 ai=aj。(相当于说全集就是 n×(n1)/2 个点对)对于每一个 S 的贡献就是 (1)|S|×mlcmsi。(其中,si 表示将这些点对当作边连起来之后,构成的连通块。)

复杂度 O(2n×(n1)/2×logm),显然不可过。

我们发现,其实我们本质上在乎的是这个连通块的分配情况,和它对应的容斥系数。

故我们定义 dps1,s2,s3,...sk 表示由这些连通块所构成的答案。

假设 Ei 为可以构成 si 这个连通块的所有边集。

dps1,s2,...sk=i=1kmlcmsi×((1)|Ei|)

观察到对于一个块的任意两个点其实都是可以连边的,故这个 (1)|Ei| 只与 si 的大小有关。

定义 gi,0/1 表示大小为 i 的连通块,可以被大小为偶数/奇数的边集所构成的方案数。

(1)|Ei|=g|si|,0g|si|,1

对于 gn,0/1 的转移,我们仍然考虑一个容斥转移:

gn,0=(Cn×(n1)/20+Cn×(n1)/22+Cn×(n1)/24+...)i=1n2Cn1i1×((gi,0×(Ci×(i1)/20+Ci×(i1)/22+Ci×(i1)/24+...)+(gi,1×(Ci×(i1)/21+Ci×(i1)/23+Ci×(i1)/25+...))gn1,0×(n1)

简单理解一下,就是算出用偶数条边的总方案,减去那些不连通的。枚举 i 表示 n 所在连通块的大小,剩下的随便连边,特判一下 i=n1,因为这个时候外部只有 1 个点,只能是 gn1,0×(C(n1,n2)=(n1)),因为外面没办法连边了。

观察到对于 n>1(Cn×(n1)/20+Cn×(n1)/22+Cn×(n1)/24+...)=(Cn×(n1)/21+Cn×(n1)/23+Cn×(n1)/25+...)=2n×(n1)/21

故我们简化一下式子。

gn,0=2n×(n1)/21i=1n2Cn1i1×(gi,0+gi,1)×2i×(i1)/21gn1,0×(n1)

同理:

gn,1=2n×(n1)/21i=1n2Cn1i1×(gi,0+gi,1)×2i×(i1)/21gn1,1×(n1)

但其实,我们更想知道的是 gi,0gi,1 发现 gi,0gi,1=(i1)×(gi,1gi,0)

观察到边界值为 g1,0=1,g1,1=0

求一个简单的通项,gn,0gn,1=(1)n1×(n1)!

我们考虑把这个东西带回原来的 dp 式子,于是有:

dps1,s2,...sk=i=1kmlcmsi×(1)|si|1×(|si|1)!

为了方便转移,我们定义 fS 表示所有 s1+s2+...+sk=Sdps1,s2,...sk 之和。

一个简单的转移就有了:fS=lowbit(S)SSfSS×mlcmS×(1)|S|1×(|S|1)!

这个感觉是比较直观的,这里就不解释了。

初始值的话就是 f=1

终于是大功告成了,答案显然是 f1,2,...,n,为了弄懂这个题我也是煞费苦心啊。

时间复杂度 O(2n×logm+3n)。因为 lcm 的存在。

代码

#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int mod = 998244353;
#define maxn 17
int n;
ll m, a[maxn];
ll Lcm[1 << 16], flg[1 << 16];
ll Gcd(ll x, ll y)
{
    if(x > y) swap(x, y);
    if(x == 0) return y;
    return Gcd(x, y % x);
}
ll fact[maxn];
ll dp[1 << 16];
int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; ++i) cin >> a[i], Lcm[(1 << i - 1)] = a[i];
    fact[0] = 1;
    for (int i = 1; i <= n; ++i) fact[i] = fact[i - 1] * i % mod;
    for (int i = 1; i < (1 << n); ++i)
    {
        if(__builtin_popcount(i) == 1) continue;
        int j = __lg(i);
        if(flg[i - (1 << j)])
        {
            flg[i] = true;
            continue;
        }
        ll gcd = Gcd(Lcm[i - (1 << j)], a[j + 1]);
        if(a[j + 1] * __int128(1) * Lcm[i - (1 << j)] / gcd > m) flg[i] = true;
        else Lcm[i] = Lcm[i - (1 << j)] / gcd * a[j + 1];
    }
    dp[0] = 1;
    for (int i = 1; i < (1 << n); ++i)
    {
        // cout << i << endl;
        int I = i - (i & -i), k = i & -i;
        for (int j = I; ; j = (j - 1) & I)
        {
            if(flg[j + k])
            {
                if(!j) break;
                continue;
            }
            if(Lcm[j + k] > m)
            {
                if(!j) break;
                continue;
            }
            dp[i] += dp[i - j - k] * ((m / Lcm[j + k]) % mod) % mod * fact[__builtin_popcount(j + k) - 1] % mod * ((__builtin_popcount(j + k) & 1) ? 1 : -1);
            dp[i] %= mod, dp[i] += mod, dp[i] %= mod;
            if(!j) break;
        }
    }
    cout << dp[(1 << n) - 1] << endl;
    return 0;
}

后记

人说,
林深时见鹿,
海蓝时见鲸,
梦醒时见你。
可实际,
林深时雾起,
海蓝时浪涌,
梦醒时也许未见鹿,
未见鲸,亦未见你。
但是,
鹿踏雾而来,
鲸随浪儿起,
你没回头,又怎知我没来过。

posted @   Saltyfish6  阅读(23)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
Document
点击右上角即可分享
微信分享提示