首页 > 其他分享 >CF1884D Counting Rhyme 题解

CF1884D Counting Rhyme 题解

时间:2024-05-17 13:29:37浏览次数:18  
标签:lfloor cnt CF1884D frac 题解 sum Rhyme rfloor 枚举

题目链接:CF 或者 洛谷

给个莫反题解,讲讲常规套路

题目要求满足没有 \(a_k \mid a_i 与 a_k \mid a_j\) 的 \((i,j)\) 的对数,显然即不存在 \(a_k \mid \gcd(a_i,a_j)\)。稍微拓展下,如果不存在整除多个数,那么显然不整除它们的 \(\gcd\) 即可,因为它们的公因数即为满足的最大数,如果为它的因数,则一定满足整除结论。反过来,如果是 \(a_i \mid a_k\) 与 \(a_j \mid a_k\),则应该是 $lcm(a_i,a_j)\mid a_k $,常见结论应该灵活记忆。

那么考虑 \(a_i\le 1e6\),我们可以预处理出 \(cnt[x]\),表示 \(x\) 因子出现的次数。这样一来我们就可以判断 \(\gcd(a_i,a_j)\) 中的因子是否在原数组 \(a\) 当中是否出现过,我们可以用调和级数预处理出 \(1 \sim 1e6\) 中的每个数的因子,然后这样原来的数组中的数就可以进行分解到哈希桶中了,又因为每个数的因子数量大概为开立方级别,所以这部分的总复杂度是对数级别的。(作图网站)

那么我们考虑枚举 \((i,j)\),先写出暴力表达式:

\[ans=\sum_{i=1}^{n}\sum_{j=i+1}^{n}f(\gcd(a_i,a_j)) \]

其中,\(f(x)\) 表示 \(x\) 的因数是否出现过,如果出现过则为 \(1\),否则为 \(0\)。我们考虑枚举 \(x\) 的因数 \(x_i\),则可以写出:

\[f(x)=\prod_{i=1}^{t}[x_i=0] \]

如果 \(x_i=0\),说明它没有这个因数贡献为 \(1\),如果有 \(0\),无论其他因数是否存在,它的贡献都为 \(0\) 了。

这个算法的复杂度大概为:\(O(n^2\ln{n})\)。

直接求没办法变形,我们考虑莫反当中的变为枚举每个 \(\gcd\) 的贡献,考虑每种 \((i,j)\) 在每种 \(\gcd\) 当中的贡献。令 \(d=\gcd(a_i,a_j)\)

\[ans=\sum_{i=1}^{n}\sum_{j=i+1}^{n}\sum_{d=1}^{n}f(d)\times[\gcd(a_i,a_j)=d] \]

对一些莫反不熟的朋友稍微介绍下变换思路,原先我们的思路是枚举 \((i,j)\),然后看 \(f(\gcd(a_i,a_j))\) 的贡献。现在我们枚举 \(\gcd(a_i,a_j)=d\),对于 \(d\) 来说,已经知道它的 \(f(d)\) 值,我们只需要统计有多少 \(f(d)\) 即可,即考虑有多少个 \((i,j)\) 满足 \(\gcd(a_i,a_j)=d\),当然如果不满足的,显然贡献为 \(0\),所以上式就是这么来的了。

这个式子不会变形的可以做做:P2398

常规变形,我们化简为 \(\gcd(a_i,a_j)=1\) 的贡献即:

先将枚举 \(d\) 放在外面:

\[ans=\sum_{d=1}^{n}f(d)\sum_{i=1}^{n}\sum_{j=i+1}^{n}[\gcd(a_i,a_j)=d] \]

然后我们考虑解决 \(ans(i<j)+ans(i>j)+ans(i=j)=ans\),而当 \(i=j\) 时显然有 \(\gcd(a_i,a_j)=a_i\),贡献为 \(0\),所以我们可以得到:
\(ans(i>j)+ans(i<j)=ans\),而这两个偏序的答案是对偶的,所以我们可以得到 \(ans(i<j)=\dfrac{ans}{2}\),这样一来解决了偏序的限制。

\[2\times ans=\sum_{d=1}^{n}f(d)\sum_{i=1}^{n}\sum_{j=1}^{n}[\gcd(a_i,a_j)=d] \]

后面这个式子太常规了:

我们做一个变形,令 \(cnt[x]\) 表示 \(x\) 出现的次数,那么我们的:

\[\sum_{i=1}^{n}\sum_{j=1}^{n}[gcd(a_i,a_j)=d]\ 可以改为枚举\ a_i和\ a_j \ 为\ xd\ 的贡献 \]

\[=\sum_{t_1=1}^{n}\sum_{t_2=1}^{n}[\gcd(t_1,t_2)=d]\times cnt[t_1d]\times cnt[t_2d] \]

改为枚举值域的贡献,则后面的式子则与下标无关了,具体的考虑所有的值域匹对的 \(\gcd=d\) 的数量有多少个,这个我们预处理出桶就可以利用乘法原理计数了。上述式子实际就是枚举 \(a_i=d,2d,3d,...\) 的情况,因为只有这种情况才有可能构造出 \(\gcd(a_i,a_j)=d\),同时也可以根据这个反推出出 \(a_i=t_1d,a_2=t_2d\)。

这个式子变形很常规:P2522

不会变的可以看看 oiwike

考虑 \(d\) 的约数:

\[=\sum_{t_1=1}^{\lfloor \frac{n}{d}\rfloor}\sum_{t_2=1}^{\lfloor \frac{n}{d}\rfloor}[\gcd(t_1,t_2)=1]\times cnt[t_1d]\times cnt[t_2d] \]

这样变虽然枚举复杂度并没有多大变化,但我们可以用莫反了,莫反的常见性质:

\[[\gcd(i,j)=1]=\sum_{d\mid \gcd(i,j)}μ(d) \]

代入:

\[=\sum_{t_1=1}^{\lfloor \frac{n}{d}\rfloor}\sum_{t_2=1}^{\lfloor \frac{n}{d}\rfloor}\sum_{k\mid \gcd(t_1,t_2)}μ(k)\times cnt[t_1d]\times cnt[t_2d] \]

常规的套路,我们改为枚举 \(k\),因为 \(k \mid \gcd(t_1,t_2)\),所以我们直接考虑 \(k\) 的倍数贡献,即 \(\gcd(t_1,t_2)=k,2k,3k....\dfrac{n}{d}k\) 的贡献。(\(k\) 为公因数,所以应该有 \(k \le t_1,t_2\le \lfloor \frac{n}{d}\rfloor\)),即先枚举 \(k\),再枚举 \(k,2k,3k,....\) 的数量为 \(t_1\) 的数量贡献,继续枚举 \(t_2\) 中分别为 \(k,2k,3k....\) 的数量贡献,而最多的 \(i,j\le \lfloor\frac{n}{dk} \rfloor,使得\ ik,jk \le \lfloor\frac{n}{d} \rfloor\)。即 \(t_1=ik,t_2=jk\)

\[=\sum_{k=1}^{\lfloor\frac{n}{d} \rfloor}μ(k)\sum_{i=1}^{\lfloor\frac{n}{dk} \rfloor}\sum_{j=1}^{\lfloor\frac{n}{dk} \rfloor}cnt[ikd]\times cnt[jkd] \]

这玩意后半部分的 \(t_1,t_2\) 属于对偶部分,是完全一样的,可以考虑整理成:

\[\sum_{i=1}^{\lfloor\frac{n}{dk} \rfloor}\sum_{j=1}^{\lfloor\frac{n}{dk} \rfloor}cnt[ikd]\times cnt[jkd]=\sum_{i=1}^{\lfloor\frac{n}{dk} \rfloor}cnt[ikd]\sum_{j=1}^{\lfloor\frac{n}{dk} \rfloor}\times cnt[jkd] \]

\[=(\sum_{t=1}^{\lfloor \frac{n}{dk} \rfloor}cnt[tkd])^2,\ 这样一来就简化枚举复杂度了 \]

原式可以写成:

\[2\times ans=\sum_{d=1}^{n}f(d)\sum_{k=1}^{\lfloor \frac{n}{d}\rfloor} μ(k)(\sum_{t=1}^{\lfloor \frac{n}{dk} \rfloor}cnt[tkd])^2 \]

莫反的重要套路处理:

我们枚举 \(dk\) 来算贡献,令 $m=dk,d=\frac{m}{k} \(,\)k=\frac{m}{d} $则原式的三个部分分别考虑每个 \(dk\) 的贡献,先考虑换元后的式子:

\[\sum_{d=1}^{n}f(d)\sum_{k=1}^{\lfloor \frac{n}{d}\rfloor} μ(k)(\sum_{t=1}^{\lfloor \frac{n}{m} \rfloor}cnt[tm])^2 \]

容易看出 \(m \le n\),继续变换,考虑枚举 \(m\):

\[\sum_{m=1}^{n}\sum_{d=1}^{n}\sum_{k=1}^{\lfloor \frac{n}{d}\rfloor} ([dk=m]\times f(d)μ(k))(\sum_{t=1}^{\lfloor \frac{n}{m} \rfloor}cnt[tm])^2 \]

容易看出只有 \(d\mid m,k \mid m\) 才有贡献,先把 \(d\) 的范围缩小:

\[\sum_{m=1}^{n}\sum_{d \mid m}\sum_{k=1}^{\lfloor \frac{n}{d}\rfloor} ([dk=m]\times f(d)μ(k))(\sum_{t=1}^{\lfloor \frac{n}{m} \rfloor}cnt[tm])^2 \]

已知 \(d\),那么 \(k=\frac{m}{d}\),所以对于一个特定 \(d\),只有一个 \(k\) 可以和它满足 \(dk=m\),不需要枚举 \(k\) 了,同时我们还可以去掉 \([dk=m]\) 的限制了。

\[\sum_{m=1}^{n}\sum_{d \mid m} f(d)μ(k)(\sum_{t=1}^{\lfloor \frac{n}{m} \rfloor}cnt[tm])^2 \]

此时再将 \(k\) 换成 \(\frac{m}{d}\),可得:

\[2\times ans=\sum_{m=1}^{n}[(\sum_{d \mid m} f(d)μ(\frac{m}{d}))\times (\sum_{t=1}^{\lfloor \frac{n}{m} \rfloor}cnt[tm])^2] \]

观察每部分复杂度,预处理 \(f(x)\) 和 $μ(x) $,那么乘号前面式子就可以枚举因数算了,上文提到了这是对数级别。乘号后面式子枚举 \(m\) 为 \(n\),内层枚举为 \(\frac{n}{1},\frac{n}{2},\frac{n}{3},...\) 显然这玩意是调和级数,复杂度得证:\(O(n\ln{n})\)。

大常数核心代码:

constexpr int N = 1e6 + 10;
constexpr int MX = 1e6;
int mu[N], f[N];
bool vis[N];
vector<int> pos[N]; //因数桶
int cnt[N];

inline void init()
{
    mu[1] = 1;
    vector<int> prim;
    forn(i, 2, MX)
    {
        if (!vis[i]) prim.push_back(i), mu[i] = -1;
        for (const ll j : prim)
        {
            if (i * j > MX) break;
            vis[i * j] = true;
            if (i % j == 0) break;
            mu[i * j] = -mu[i];
        }
    }
    forn(i, 1, MX) for (int j = i; j <= MX; j += i) pos[j].push_back(i);
}

int n, a[N];

inline void solve()
{
    cin >> n;
    forn(i, 1, n) cin >> a[i], cnt[a[i]]++;
    forn(i, 1, n)
    {
        bool vis = true; //是否不存在i的因子,不存在为1,存在为0
        for (const int j : pos[i])
        {
            if (cnt[j])
            {
                vis = false;
                break;
            }
        }
        f[i] = vis;
    }
    ll ans = 0;
    forn(m, 1, n)
    {
        ll pre = 0;
        for (const int d : pos[m]) pre += f[d] * mu[m / d];
        ll sum = 0;
        forn(t, 1, n/m) sum += cnt[t * m];
        const ll suf = sum * sum;
        ans += pre * suf;
    }
    cout << ans / 2 << endl;
    //clear
    forn(i, 1, n) f[i] = 0, cnt[a[i]]--;
}

signed int main()
{
    // MyFile
    Spider
    //------------------------------------------------------
    // clock_t start = clock();
    init();
    int test = 1;
    //    read(test);
    cin >> test;
    forn(i, 1, test) solve();
    //    while (cin >> n, n)solve();
    //    while (cin >> test)solve();
    // clock_t end = clock();
    // cerr << "time = " << double(end - start) / CLOCKS_PER_SEC << "s" << endl;
}

我们预处理出所有的因数桶和莫比乌斯函数,然后再进行上述的直接枚举因数求和。

小常数核心代码:

我们观察枚举 \(m\) 的时候如果能事先预处理出前面部分的乘积和后面部分的乘积就可以减小常数。

考虑前面部分在算的时候涉及到了枚举 \(d \mid m\),直接枚举 \(m\) 那得预处理 \(d\) 出来,我们考虑枚举 \(d\) 去算对每个 \(m\) 的贡献,这个很简单,类似调和级数的 \(+i\) 枚举即可。同时我们还可以根据枚举它的倍数的同时,如果当前的这个数是存在的,那么它的倍数显然对应的 \(f[m]\) 都应该为 \(0\),所以一次枚举因数即可求出 \(f\) 和 \(pre\)。

考虑后面部分,我们是需要枚举得到 \(im \le n\) 的所有的 \(cnt[im]\) 之和,它的贡献是针对于当前的 \(m\) 的。即我们枚举 \(m=d\),那么所有的 \(cnt[id]\) 之和即为 \(suf[d]\) 括号内贡献,然后取个平方即可。

constexpr int N = 1e6 + 10;
constexpr int MX = 1e6;
int mu[N], f[N];
bool vis[N];
int cnt[N];

inline void init()
{
    mu[1] = 1;
    vector<int> prim;
    forn(i, 2, MX)
    {
        if (!vis[i]) prim.push_back(i), mu[i] = -1;
        for (const ll j : prim)
        {
            if (i * j > MX) break;
            vis[i * j] = true;
            if (i % j == 0) break;
            mu[i * j] = -mu[i];
        }
    }
}

int n, a[N];
ll pre[N], suf[N];

inline void solve()
{
    cin >> n;
    forn(i, 1, n) f[i] = 1, cin >> a[i], cnt[a[i]]++;
    ll ans = 0;
    forn(d, 1, n)
    {
        //处理出f[d],同时满足 d|m,算出pre[m]
        for (int m = d; m <= n; m += d)
        {
            f[m] &= cnt[d] == 0;
            pre[m] += f[d] * mu[m / d];
            suf[d] += cnt[m];
        }
        suf[d] *= suf[d];
    }
    forn(m, 1, n) ans += pre[m] * suf[m];
    cout << ans / 2 << endl;
    //clear
    forn(i, 1, n) f[i] = pre[i] = suf[i] = 0, cnt[a[i]]--;
}

signed int main()
{
    // MyFile
    Spider
    //------------------------------------------------------
    // clock_t start = clock();
    init();
    int test = 1;
    //    read(test);
    cin >> test;
    forn(i, 1, test) solve();
    //    while (cin >> n, n)solve();
    //    while (cin >> test)solve();
    // clock_t end = clock();
    // cerr << "time = " << double(end - start) / CLOCKS_PER_SEC << "s" << endl;
}

标签:lfloor,cnt,CF1884D,frac,题解,sum,Rhyme,rfloor,枚举
From: https://www.cnblogs.com/Athanasy/p/18197630

相关文章

  • ArchLinux/Manjaro升级到6.9内核后的问题解决
    1.KDEWallet系统配置---个性化---账户详细信息---kde钱包1.需要启用kde钱包子系统,否则无法正常使用记录的账号密码信息2.右下角,调用钱包管理器,修改密码,设置为空密码至此,开机需要输密码连接kdewallet的应用不需要输入即可密码连接2.更新archcn-keyring报GPG错误解决:sudopa......
  • P10466的题解
    (一)出门左转P3369。只需要同时记录原本属于哪一位即可。这里给出Splay做法。(二)AC代码。建议自己打一遍巩固印象。虽然我是直接拉过来的。#include<bits/stdc++.h>#defineintlonglongusingnamespacestd;inlineintread(){intx=0,f=1;charc=getchar();wh......
  • Ollama的常见问题解答(FAQ)
     1如何更新Ollama? 在macOS和Windows上,Ollama会自动检查更新。只需点击任务栏或菜单栏图标,然后选择“重启以更新”来应用新版本。 在Linux上,需要重新运行安装脚本来升级。 2如何查看日志? 查阅特定的故障排查文档来了解如何查看和使用日志。 3我的GPU是否兼容Ollama? ......
  • 题解:CF1954F Unique Strings
    link计数类*3100首次独立过纪念版题解。首先我们考虑一个去重的问题。貌似针对循环同构去重的问题,只能从循环节上入手。那么我们考虑设\(dp(d)\)为最小循环节长度恰好为\(d\)不同方案数个数,则答案为:\[\sum_{d=1}^ndp(d)=\sum_{d|n}\frac{dp(d)}{d}\]这似乎是一条可行......
  • 2024 jscpc B题 Area of the Devil 题解
    题目链接:AreaoftheDevil算不在题目说的区域内的面积,直接算是比较麻烦的,这里给一个朋友直接算画的图,其实画出区域以后也算好算,当然官解提到的容斥去算更好写。一共有五个空余的区域,我们考虑这五个区域怎么计算,图一是直接画出的所有区域的并集,图二则是五角星处于边界情况时,图......
  • [ARC149D] 的题解
    思路很巧妙,首先,很明显,数轴上关于原点对称的一个点对,不论移动了多少次,都任然是对称的。再看眼数据范围,发现其实点分布的比较紧,考虑直接将所有点看做一个整体(数轴上一个线段),然后依次移动。关键的是,若这个整体横跨了原点的话,那么在原点的点就有答案了,对于剩下的部分,设在正半轴、负......
  • Codeforces 1004B Sonya and Exhibition 题解
    题目简述让你构造一个长度为$n$的$01$字符串。一个区间的价值为其中$0$的数量乘以$1$的数量,给出$m$个区间,最大化它们的价值之和。题目分析设区间$[l,r]$中$1$有$x$个,$0$有$y$个,当$x$和$y$最接近的时候,$x\timesy$最大,此结论可以用二次函数进行证明。......
  • AT_arc042_c的题解
    (一)非常妙的DP题,可惜被翻译毁了。题意:你有一堆零食,每个零食有两个值\(a_i\)和\(b_i\)。要求选出集合\(S\),使\(\sum_{i\inS}a_i-\min_{i\inS}a_i\lep\),求最大的\(\sum_{i\inS}b_i\)。一眼DP。先将\(a_i\)从小到大排序,每次循环更新\(dp_0\)的值为\(\max......
  • P10447 最短 Hamilton 路径 题解
    P10447最短Hamilton路径题解题目传送门题意:给定一张有向带权图(\(n\le20\))求点\(0\)到点\(n-1\)的最短哈密顿路径。这是一道状压DP模板题。在状态压缩DP中,我们将一个集合压成一个二进制数。设\(f_{u,i}\)为已经走了集合\(u\)中的节点,且现在在点\(i\)的最短......
  • CF1886E 题解
    CF1886E思路观察发现每个项目只与程序员数量和最小值有关,所以每个项目对应能力值连续的程序员最优。项目数\(m\le20\),状压。设\(dp_{i,s}\)为前\(i\)个程序员匹配的项目状态为\(s\)是否可行,无法接受。交换维度,改为\(dp_s\)表示状态\(s\)能与前缀\([1,i]\)匹配的......