HDU - 5514 Frogs 欧拉函数||容斥定理


题目链接
题意:

有 n 个青蛙,第 i 个青蛙每次只能够跳 ai 步,现在有 m 个石头围成一圈,编号为 0 到 m−1,现在青蛙可以围着这个石头组成的圆跳无限次,每跳一次就会占领这个石头,可以无限占领,现在问你的是这 n 个青蛙占领的石头的编号的总和是多少。

思路:
先说第一种方法:
我们可以发现对于每个ai,他所能经过的石头为 k*gcd(m,ai).但是我们发现比如第一个样例
2 12
9 10 ,gcd分别为2 ,3。
2 经过 0 2 4 6 8 10
3经过 0 3 6 9
这就会有重复的,比如6就会被算两次.这就需要去掉了,这时候就可以想到这个题需要容斥一下了.那么关键是怎么容斥?

一开始我想错了,因为发现很大,我想用map存一下所有的gcd然后暴力枚举来着,再倒着容斥.然后T了.

这个题有个容斥的技巧就是预处理出所有m的因子然后进行容斥,可以发现所有被重复计算的值以及所有的gcd一定是m的因子(重复的一定是几个数的LCM).并且int范围内每个数因子的个数最多1600+。首先对于m的一个因子g,如果这个g满足题意,那么m的其他因子并且是k*g的一定被重复计算了,我们需要去掉.g对答案的贡献为 (mg1)m2 (等差数列求和). 上面说的公式计算在没有重复计算的情况下的,对于重复计算的根据容斥原理,开一个数组num[i]表示m的第i个因子被加到总答案中几次.每个答案都只能被计算一次,多计算的我们就需要减掉多加的,多减的同样. 这样计算公式为: num[i](mg1)m2
另外对于每个gcd(ai,m) 需要将每个gcd的倍数初始置1,并且每次从小到大计算答案时,需要对第i个gcd的所有倍数的num数组进行更新.(容斥原理本质),因为上面说过因子不会很多,所以直接暴力枚举即可.

#include
#define pb push_back
using namespace std;
const int maxn = 1e4 + 5;
typedef long long ll;
int a,num[maxn];
int n,m;
vector<int>g;
int main()
{
    int _;
    cin>>_;
    int ca = 1; 
    while(_--)
    {
        g.clear();
        memset(num,0,sizeof num);
        scanf("%d %d",&n,&m);
        bool flag = 0;
        for(int i = 1;i * i <= m;++i)
        {
            if(m % i == 0)
            {
                g.pb(i);
                if(i != m/i) g.pb(m/i);
            }
        }
        sort(g.begin(),g.end());
        for(int i = 0;i < n;++i)
        {
            scanf("%d",&a);
            a = __gcd(a,m);
            if(a == 1) flag = 1;
            for(int j = 0;j < g.size();++j)
            if(g[j] % a == 0) num[j] = 1;
        }   
        printf("Case #%d: ",ca++);
        if(flag) {
            printf("%lld\n",(ll)m*(m-1)/2);
            continue;
        }
        ll ans = 0;
        for(int i = 0;i < g.size();++i)
        {
            if(num[i] == 0) continue;
            ans += (ll)(m/g[i] - 1) * m/2*num[i];
            for(int j = i + 1;j < g.size();++j)
            {
                if(g[j] % g[i] == 0) num[j] -= num[i];
            }
        }
        printf("%lld\n",ans);
    } 
    return 0;
}

第二种做法就是欧拉函数,我觉得很巧妙。

还是和上面一样,每次都是经过k*gcd(ai,m)的格子,对于重复计算的我们规定,第i块石头只能由gcd = gcd(i,m) 的青蛙经过.(如果不存在这样的青蛙我们假设添加一个这样的青蛙,对结果的求和是没有影响的).
对于样例可以得出如下结论:
2 12
9 10
步数为2 的经过 2 10
步数为3 的经过 3 9
步数为4的经过 4 8
步数为6的经过 6 . 综上就消除了所有重复的情况.

他们每个对答案的贡献:
2*(1 +5),3*(1+3),4*(1+2),6*(1) 可以发现一个神奇的东西,对于每个x,他的贡献为 x*(与 mx 互质的所有数的和).
存在一个结论:
[1,x]中与x互质的所有的数和为 φxx2 ,知道这个结论这个题目就简单了.
预处理m的所有因子,然后记录所有gcd(ai,m)。然后枚举每个m的因子,先判断该石头能否被走过,(即是否有gcd可以整除该因子).如果被走过就按照公式暴力计算就好了.

#include

using namespace std;
typedef long long ll;
const int maxn = 1e5+5;
int a,g[maxn];
int fac[maxn],len;
int n,m;
void init()
{
    len = 0;
    int tmp = m;
    for(int i = 2;i*i <= m;++i)
    {
        if(tmp % i == 0)
        {
            fac[len++] = i;
            if(i != tmp / i)
            fac[len++] = tmp / i;
        }
    }
    return ;
}
int phi(int x) 
{ 
    int res=x,a=x; 
    for(int i=2;i*i<=x;i++) 
    { 
        if(a%i==0) 
        { 
            res-=res/i; 
            while(a%i==0) a/=i; 
        } 
    } 
    if(a>1) 
    res-=res/a; 
    return res; 
}
int main()
{
    int _;
    cin>>_;
    int ca = 1;
    while(_--)
    {
        scanf("%d %d",&n,&m);
        init();
        bool flag = 0;
        for(int i = 0;i < n;++i)
        {
            scanf("%d",&a);
            if(a > m) a %= m;
            g[i] = __gcd(a,m);
            if(g[i] == 1) flag = 1;
        }
        printf("Case #%d: ",ca++);
        if(flag == 1)
        {
            printf("%lld\n",(ll)m*(m-1)/2);
            continue;
        }
        sort(g,g+n);
        n = unique(g,g+n) - g;
        sort(fac,fac+len);
        ll ans = 0;
        for(int i = 0;i < len;++i)
        {
            for(int j = 0;j < n ;++j)
            {
                if(fac[i] % g[j]) continue;
                ans += (ll)m*phi(m/fac[i])/2;
                break;
            }
        } 
        printf("%lld\n",ans);
    }
    return 0;
}

你可能感兴趣的:(数学定理,容斥,莫比乌斯反演)