CF1884D Counting Rhyme 题解
题目链接: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)\),先写出暴力表达式:
其中,\(f(x)\) 表示 \(x\) 的因数是否出现过,如果出现过则为 \(1\),否则为 \(0\)。我们考虑枚举 \(x\) 的因数 \(x_i\),则可以写出:
如果 \(x_i=0\),说明它没有这个因数贡献为 \(1\),如果有 \(0\),无论其他因数是否存在,它的贡献都为 \(0\) 了。
这个算法的复杂度大概为:\(O(n^2\ln{n})\)。
直接求没办法变形,我们考虑莫反当中的变为枚举每个 \(\gcd\) 的贡献,考虑每种 \((i,j)\) 在每种 \(\gcd\) 当中的贡献。令 \(d=\gcd(a_i,a_j)\)
对一些莫反不熟的朋友稍微介绍下变换思路,原先我们的思路是枚举 \((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(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}\),这样一来解决了偏序的限制。
后面这个式子太常规了:
我们做一个变形,令 \(cnt[x]\) 表示 \(x\) 出现的次数,那么我们的:
改为枚举值域的贡献,则后面的式子则与下标无关了,具体的考虑所有的值域匹对的 \(\gcd=d\) 的数量有多少个,这个我们预处理出桶就可以利用乘法原理计数了。上述式子实际就是枚举 \(a_i=d,2d,3d,...\) 的情况,因为只有这种情况才有可能构造出 \(\gcd(a_i,a_j)=d\),同时也可以根据这个反推出出 \(a_i=t_1d,a_2=t_2d\)。
这个式子变形很常规:P2522
不会变的可以看看 oiwike
考虑 \(d\) 的约数:
这样变虽然枚举复杂度并没有多大变化,但我们可以用莫反了,莫反的常见性质:
代入:
常规的套路,我们改为枚举 \(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\)
这玩意后半部分的 \(t_1,t_2\) 属于对偶部分,是完全一样的,可以考虑整理成:
原式可以写成:
莫反的重要套路处理:
我们枚举 \(dk\) 来算贡献,令 \(m=dk\),\(d=\frac{m}{k}\),$k=\frac{m}{d} $则原式的三个部分分别考虑每个 \(dk\) 的贡献,先考虑换元后的式子:
容易看出 \(m \le n\),继续变换,考虑枚举 \(m\):
容易看出只有 \(d\mid m,k \mid m\) 才有贡献,先把 \(d\) 的范围缩小:
已知 \(d\),那么 \(k=\frac{m}{d}\),所以对于一个特定 \(d\),只有一个 \(k\) 可以和它满足 \(dk=m\),不需要枚举 \(k\) 了,同时我们还可以去掉 \([dk=m]\) 的限制了。
此时再将 \(k\) 换成 \(\frac{m}{d}\),可得:
观察每部分复杂度,预处理 \(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;
}