CF1712 E1/E2 LCM Sum (easy/hard version)

Description

给定 \(l\)\(r\),求 \(l\)\(r\) 之间有多少三元组 \((i,j,k)\),满足 \(\operatorname{lcm}(i,j,k) \ge i+j+k\)\(i \lt j\lt k\)

Easy version

共有 \(T (1 \le T \le 5)\) 组询问, $ 1 \le l \le r \le 2 \times 10^5 $。

Hard version

共有 \(T (1 \le T \le 2 \times 10^{5})\) 组询问, $ 1 \le l \le r \le 2 \times 10^5 $。

Solution

观察样例,发现合法的状态太多,不好统计,但正难则反 ,我们考虑统计不合法的三元组,再用总数减去不合法的个数即可。

问题转化为统计 \(\operatorname{lcm}(i,j,k) \lt i+j+k\) 的三元组个数。

由于 \(i \lt j\lt k\),所以 \(i + j + k \le 3k\) ,从而得出 \(\operatorname{lcm}(i,j,k) \lt 3 \times k\)

根据最小公倍数的性质可知,\(k\) 必然是 \(lcm(i,j,k)\) 的约数 ,那么只有两种可能:\(\operatorname{lcm}(i,j,k) = k\)\(\operatorname{lcm}(i,j,k) = 2 \times k\)

先看 \(\operatorname{lcm}(i,j,k) = 2 \times k\) 的情况,通过打小数据暴力的表发现,只有 \((3,4,6)\)\((6,10,15)\) 和它们的整数倍的情况能够满足。

再看另一种情况, \(i\)\(j\)\(k\) 的最小公倍数为 \(k\),说明 \(i\)\(j\) 都是 \(k\) 的约数,这是一个较强的性质。

Easy version:

easy version 下,发现 \(T\) 很小,几乎可以忽略成常数对待,而区间值域也不大,可以从 \(l+2\)\(r\) 枚举 \(k\),对于每个 \(k\) 计算不合法三元组个数:

  • 对于 \(\operatorname{lcm}(i,j,k) = k\) 的贡献,枚举 \(k\)\([l,r]\) 中有多少个约数,设区间内有 \(fac\) 个约数,则贡献为 \(C(fac, 2)\) 种。

  • 对于 \(\operatorname{lcm}(i,j,k) = 2 \times k\) 的贡献,我们直接求是不是上述两种的整数倍即可。

整个区间所有三元组的总数为 \(C(r-l+1,3)\) 个,最后答案即为 $C(r-l+1,3) - $ 所有不合法方案总数。

不要忘记开 long long,以及注计算组合数时不要一下全乘起来,保证不炸 long long。

时间复杂度约为 \(O(TN\sqrt{N})\)

Hard version:

hard version 下,\(T\) 很大,不能直接暴力,考虑优化。

区间三元组总数可以用组合数学轻松求出,对于 \(\operatorname{lcm}(i,j,k) = 2 \times k\) 的贡献也能直接求出(在代码中有注释),

我们主要处理 \(\operatorname{lcm}(i,j,k) = k\) 的情况 。

既然 \(i\)\(j\) 都是 \(k\) 的约数,而且值域不大,所以可以先把 \(1 \sim 200000\) 中每个数除本身的约数处理出来。

为了使约数有序从而方便计算,这里求约数运用了这种方法:

建立一个 vector,外层枚举每个数 \(i\),内层枚举它的整数倍,即 \(2 \times i\)\(3 \times i\),...,一直到超出值域为止,这些数它们都有共同的约数 \(i\),插入它们的约数数组中即可,不难发现现在每个数的约数组内也是有序的。

枚举 \(1 \sim 200000\) 中所有数,把每一个数都视作 \(k\),此时对应的 \(i\) 就是 \(k\) 的约数, \(i \sim k\) 中所有 \(k\) 的约数个数 \(j\) 就是这一个二元组 \((i,k)\) 对答案的贡献。

具体的,若我们现在正在计算 \(x\) 这个数的约数的贡献,设处理到了第 \(i\) 个约数,\(x\) 共有 \(siz_{x}\) 个约数,则这个约数的贡献就是 \(siz_{x} - i - 1\)

现在该问题转化成了:处理多次询问,每次询问当 \(k \le r\) 时计算 \([l,r]\) 区间的答案。这是一个标准的二维数点问题。

可以把所有询问离线下来,按照右端点 \(r\) 排序,外层从 \(1 \sim 200000\) 枚举,建立一棵树状数组,把所有约数的贡献插入树状数组中, 若碰到一个询问的右端点就计算答案,最后按照原询问顺序输出即可。

由于询问也已经有序,我们用一个类似双指针的思想维护现在处理到哪个询问,不断向后移动即可。

\(N\) 为最大的 \(r\),时间复杂度约为 \(O(N\sqrt{N} + T \log N)\)

Code

// by youyou2007 in 2022.
#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <queue>
#include <stack>
#include <map>
#define REP(i, x, y) for(int i = x; i < y; i++)
#define rep(i, x, y) for(int i = x; i <= y; i++)
#define PER(i, x, y) for(int i = x; i > y; i--)
#define per(i, x, y) for(int i = x; i >= y; i--)
#define lc (k << 1)
#define rc (k << 1 | 1)
using namespace std;
const int N = 200005;
int T;
struct node{
    long long l, r, id;
}q[N];
long long ans[N], cnt;
long long tree[N];
vector <int> fac[N];
bool cmp(node x, node y)
{
    if(x.r == y.r) return x.l < y.l;
    else return x.r < y.r;
}
int lowbit(int x)
{
    return x & (-x);
}
void add(int x, long long y)
{
    while(x <= 200000)//注意值域应到 200000
    {
        tree[x] += y;
        x += lowbit(x);
    }
}
long long query(int x)
{
    long long res = 0;
    while(x > 0)
    {
        res += tree[x];
        x -= lowbit(x); 
    }
    return res;
}
int main()
{
    scanf("%d", &T);
    for(int i = 1; i <= T; i++)
    {
        scanf("%lld%lld", &q[i].l, &q[i].r);
        ans[i] = (q[i].r - q[i].l + 1) * (q[i].r - q[i].l) / 2 * (q[i].r - q[i].l - 1) / 3;//计算总的三元组数
        ans[i] -= max((long long)0, q[i].r / 6 - (q[i].l - 1) / 3);//处理 (3, 4, 6) 以及它们的整数倍情况,先看[1, r] 区间有多少个 6 的倍数,再看 [1, l - 1] 区间有多少个 3 的倍数,两者一减则就是 [l, r] 中满足 (3x, 6x) 这样的数,但可能这个区间小到没有这样的数,所以要和 0 取 max
        ans[i] -= max((long long)0, q[i].r / 15 - (q[i].l - 1) / 6);//处理 (6, 10, 12) 以及它们的整数倍情况,道理同上,注意是 l - 1
        q[i].id = i;//记录下询问的编号
    }
    for(int i = 1; i <= 200000; i++)
    {
        for(int j = i * 2; j <= 200000; j += i)//从 i * 2 开始
        {
            fac[j].push_back(i);//计算出每个数除自己之外的约数,正好 vector 也能一并记下约数个数
        }
    }
    sort(q + 1, q + T + 1, cmp);//按照右端点按照询问排序
    int now = 1;
    for(int i = 1; i <= 200000; i++)
    {
        for(int j = 0; j < fac[i].size(); j++)
        {
            add(fac[i][j], fac[i].size() - 1 - j);//把 i 的所有约数插入树状数组中,原因已解释
        }
        while(now <= T && q[now].r <= i)//类似双指针扫描,对于每个询问我们计算答案
        {
            ans[q[now].id] -= query(q[now].r) - query(q[now].l - 1);
            now++;
        }
    }
    for(int i = 1; i <= T; i++)
    {
        printf("%lld\n", ans[i]);
    }
	return 0;

}
posted @ 2022-08-16 15:11  panjx  阅读(171)  评论(0编辑  收藏  举报