HDU 1695 数论 容斥 欧拉函数 || 莫比乌斯反演

题目链接

思路:
想了两个小时才想出容斥的思路…(真是太弱了

AC之后搜题解发现是一个莫比乌斯的入门题,准备最近抽时间学习一下AC之后再来补上莫比乌斯的思路(占坑(更新见下

容斥:
此题可以等价为:
从【1,n】中选出一个 X
从【1,m】中选出一个 Y
问有多少对不重复的 X,Y 满足 GCD(X,Y) == K?

首先 我们可以从上述问题中提取出以下条件:
1 .$ 1 <= X <= n$ 且 X = a * k
2. $ 1 <= Y <= m$ 且 Y = b * k
3. g c d ( a , b ) = = 1 gcd(a,b) == 1 gcd(a,b)==1

故我们可以枚举 a ,然后对于每一个 a 去查询 有多少个b能与其配对
1 &lt; = b &lt; = m / k 1&lt;=b &lt;= m/k 1<=b<=m/k
故问题便转化成了经典的区间容斥问题:

在【1,m/k】中 有多少个数b 与已知的数a 互质。

此时我们发现会有重复的情况,举个例子
对于【1,5】中,我们发现:
当a = 2时,b = 1,3,5
当a = 3时,b = 1,2,4,5
此时 a = 2 ,b =3 和 a = 3,b = 2构成了重复

去重的方法很简单,我们只需要保证每次计算的(a,b)都满足 a < b
故我们需要减掉【1,m/k】中与a互质又小于a的b的数量。
很显然,这就是求 a的欧拉函数。
最后再加上一个1即可,因为 1是与任何a都互质的,但每个数都没有算上他,故最后应该加上一个1.

故 容斥 + 欧拉函数 可解。
代码:

#include<cstdio>
#include<cstdlib>
#include<algorithm>
#include<cstring>
using namespace std;
typedef long long ll;

const int A = 1e6 + 10;
int a[A];

ll solve(ll m,ll x){
    ll euler = x;
    int tot = 0;
    for(ll i=2 ;i*i<=x ;i++){
        if(x % i == 0){
            euler = euler / i * (i-1);
            a[tot++] = i;
            while(x%i == 0) x/=i;
        }
    }
    if(x>1){
        euler = euler / x * (x-1);
        a[tot++] = x;
    }

    ll res = 0;
    for(ll i=1 ;i<(1<<tot) ;i++){
        int num = 0;
        for(int j=i ;j!=0 ;j>>=1) num += j&1;

        ll lcm = 1;
        for(int j=0 ;j<tot ;j++){
            if((i>>j) & 1){
                lcm *= a[j];
                if(lcm > m) break;
            }
        }
        if(num & 1) res += m/lcm;
        else        res -= m/lcm;
    }
    return m - res - euler;
}

int main(){
    //freopen("input","r",stdin);
    int T,_=1;
    scanf("%d",&T);
    while(T--){
        ll x,y,c,d,k;
        scanf("%I64d%I64d%I64d%I64d%I64d",&x,&y,&c,&d,&k);
        if(d < y) swap(d,y);
        if(k == 0 || k>y || k>d){
            printf("Case %d: 0\n",_++);
            continue;
        }

        ll ans = 0;
        for(ll i=k ;i<=y ;i+=k){
            ans += solve(d/k,i/k);
        }
        printf("Case %d: %I64d\n",_++,ans+1);
    }
    return 0;
}


莫比乌斯反演思路:
参考文档:https://wenku.baidu.com/view/fbec9c63ba1aa8114431d9ac.html?from=search(图片转侵删)


莫比乌斯反演常用的两大公式:
这里写图片描述

这里写图片描述


而此题我们需要用到的是第一种形式。
我自己的理解的话莫比乌斯反演类似于矩阵的相似,当一个函数 f ( x ) f(x) f(x)很难根据定义直接求解时,我们可以求出与其具有一定相似关系的另一个函数 F ( x ) F(x) F(x),如倍数和或约数和(其转换关系分别对应于上述两张图)

根据题意,题目可以等价转化为:
已知 x ∈ [ 1 , n ] 和 y ∈ [ 1 , m ] 求 g c d ( x , y ) = = 1 的 数 量 x \in [1,n] 和 y \in[1,m] 求 gcd(x,y)==1的数量 x[1,n]y[1,m]gcd(x,y)==1
我们假设:
f ( i ) f(i) f(i) g c d ( x , y ) = = i gcd(x,y) == i gcd(x,y)==i的个数
F ( i ) F(i) F(i)为 $ gcd(x,y) == (i的倍数) 的 个 数 。 则 可 推 出 两 个 函 数 满 足 第 一 种 形 式 的 等 价 关 系 , 即 可 得 : 因 为 由 的个数。 则可推出 两个函数满足第一种形式的等价关系,即可得: 因为由 F(i)$的定义可以得出:
对于gcd(x,y) == (i的倍数), 则x,y一定也是i的倍数
而在【1,n】中i的倍数有[n/i],【1,m】中i的倍数有[m/i] ([x]表示对x向下取整)
故由排列组合知识 : F [ i ] = [ n / i ] ∗ [ m / i ] F[i] = [n/i] * [m/i] F[i]=[n/i][m/i]

又由莫比乌斯变换,得出:
这里写图片描述

这样就可以在O(n)的复杂度下得到答案了。

对于此题还有一个去重的细节:举例
对于区间区间【1,3】 和区间【1,5】
重复的gcd有 (1,1),(1,2),(1,3),(2,3) 与 (1,1),(2,1),(3,1),(3,2)
显然重复都是由于其具有重合的区间【1,3】,所以我们只需要再算一下
x,y分别属于【1,3】和【1,3】区间的答案,减去这一部分答案的1/2即可。


但对于该题的升级版:BZOJ 2301
这样的复杂度是不能解决问题的,因为涉及除法,可以分块并维护莫比乌斯函数的前缀和进行优化,详细讲解见参考文档:

代码:HDU 1695

#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long ll;

const int A = 1e5 + 10;
bool vis[A];
int pri[A],mu[A],tot;
ll sum[A];

void init(){
    mu[1] = 1;
    vis[0] = vis[1] = 1;
    tot = 0;
    for(int i=2 ;i<A ;i++){
        if(vis[i] == 0){
            pri[++tot] = i;
            mu[i] = -1;
        }
        for(int j=1 ;j<=tot && pri[j]*i<A ;j++){
            vis[i*pri[j]] = 1;
            if(i%pri[j] == 0){
                mu[i*pri[j]] = 0;
                break;
            }
            mu[i*pri[j]] = -mu[i];
        }
    }
    sum[0] = 0;
    for(int i=1 ;i<A ;i++){
        sum[i] = sum[i-1] + mu[i];
    }
}

int main(){
    init();
    int T,_=1;
    scanf("%d",&T);
    while(T--){
        int a,b,c,d,k;
        scanf("%d%d%d%d%d",&a,&b,&c,&d,&k);
        if(k == 0){
            printf("Case %d: 0\n",_++);
            continue;
        }

        int n = b/k,m = d/k;
        if(n>m) swap(n,m);
        int last;
        ll sum1,sum2;
        sum1 = sum2 = 0;
        for(int i=1 ;i<=n ;i=last+1){
            last = min(n/(n/i),m/(m/i));
            sum1 += (sum[last] - sum[i-1])*(n/i)*(m/i);
        }
        for(int i=1 ;i<=n ;i=last+1){
            last = n/(n/i);
            sum2 += (sum[last] - sum[i-1])*(n/i)*(n/i);
        }
        printf("Case %d: %I64d\n",_++,sum1 - sum2/2);
    }
    return 0;
}

BZOJ 2301:

/**************************************************************
    Problem: 2301
    User: WuBaizhe
    Language: C++
    Result: Accepted
    Time:10828 ms
    Memory:1652 kb
****************************************************************/
 
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<algorithm>
using namespace std;
typedef long long ll;
 
const int A = 5e4 + 10;
bool vis[A];
int pri[A],mu[A],tot,k;
ll sum[A];
 
void init(){
    tot = 0;
    vis[0] = vis[1] = 1;
    mu[1] = 1;
    for(int i=2 ;i<A ;i++){
        if(vis[i] == 0){
            pri[++tot] = i;
            mu[i] = -1;
        }
        for(int j=1 ;j<=tot&&pri[j]*i<A ;j++){
            vis[i*pri[j]] = 1;
            if(i%pri[j] == 0){
                mu[i*pri[j]] = 0;
                break;
            }
            mu[i*pri[j]] = -mu[i];
        }
    }
    sum[0] = 0;
    for(int i=1 ;i<A ;i++){
        sum[i] = sum[i-1] + mu[i];
    }
}
 
ll solve(int x,int y){
    int n = x/k,m = y/k;
 
    if(n > m) swap(n,m);
    ll sum1 = 0,sum2 = 0;
    int last;
    for(int i=1 ;i<=n ;i=last+1){
        last = min(n/(n/i),m/(m/i));
        sum1 += (sum[last] - sum[i-1])*(n/i)*(m/i);
    }
    return sum1;
}
 
int main(){
    init();
    int T;
    scanf("%d",&T);
    while(T--){
        int a,b,c,d;
        scanf("%d%d%d%d%d",&a,&b,&c,&d,&k);
        //printf("test: %lld\n",solve(b,d));
        ll ans = solve(b,d) - solve(a-1,d) - solve(b,c-1) + solve(a-1,c-1);
        printf("%lld\n",ans);
    }
    return 0;
}

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值