2018-01-15 HDU 5514 Ver.A 数论 GCD 欧拉函数 总结归纳

本文详细解析了一道关于青蛙跳跃覆盖石头的问题,通过数学方法简化问题,并利用欧拉函数求解,最终给出了高效的代码实现。

题目描述:

There are m stones lying on a circle, and n frogs are jumping over them.

The stones are numbered from 0 to m−1 and the frogs are numbered from 1 to n. The i-th frog can jump over exactly ai stones in a single step, which means from stone j mod m to stone (j+ai) mod m (since all stones lie on a circle).

All frogs start their jump at stone 0, then each of them can jump as many steps as he wants. A frog will occupy a stone when he reach it, and he will keep jumping to occupy as much stones as possible. A stone is still considered "occupied" after a frog jumped away.

They would like to know which stones can be occupied by at least one of them. Since there may be too many stones, the frogs only want to know the sum of those stones' identifiers.

数据输入:

There are multiple test cases (no more than 20), and the first line contains an integer t,meaning the total number of test cases.

For each test case, the first line contains two positive integer n and m - the number of frogs and stones respectively (1≤n≤104, 1≤m≤109).

The second line contains n integers a1,a2,⋯,an, where ai denotes step length of the i-th frog (1≤ai≤109).

数据输出:

For each test case, you should print first the identifier of the test case and then the sum of all occupied stones' identifiers.

样例输入:

3

2 12

9 10

3 60

22 33 66

9 96

81 40 48 32 64 16 96 42 72

样例输出:

Case #1: 42

Case #2: 1170

Case #3: 1872

先解释一下题目, 大意是有 m 个石头围成一圈, 并且将这些石头按顺序标记 0 ~ m-1 , 同时还有 n 只青蛙, 并且给出它们的步长(跳长?), 比如给了第三只青蛙的步长是 3 , 那它可以从第 k 块石头跳到第 k+3 块石头. 由于这些石头是围成一圈的, 所以这些青蛙可以一圈一圈跳, 比如步长为 3 的青蛙可以从第 m-1 块石头跳到第 2 块石头. 然后让这些青蛙从第0块石头一起跳足够久, 如果有一块石头是被青蛙跳过的, 那就给这块石头标记一个 "occupied".然后就是问你足够久之后, 被"occupied"的石头的标记的号码之和是多少.

首先是要注意这个石头是围成一圈的, 针对一只特定的步长是 i 的青蛙, 当它在围成一圈的 j 块石头上面跳的时候, 会被它标记的石头号码必定是 k * gcd( i , j ) .

其中 k 是整数, 可以为零, 而且保证 k * gcd( i , j ) < m

比如步长为5的青蛙在7块石头上面跳的话会把所有的石头都"occupied"了, 而步长为 6 的青蛙在 16 块石头上面跳的话只会把偶数标记的石头给跳一遍 (gcd( 6 , 16 ) = 2).

至于是为什么你可以找规律, 也可以找来数论相关的数证明一下, 反正我是不会证......会证这里也写不下.

根据这个规律可以把步长换一下, step = gcd(step,m), 这是第一个关键步骤

然后, 很快就会想到, 比如第一个样例,  12块石头, 新步长分别为 gcd(9,12)=3, gcd(10,12)=2, 那么掐指一算第6块石头会被两只青蛙同时标记, 所以第二个关键步骤是去重.

然后这里就有了一个来自学长的骚操作

制定一个新规则, 就是想办法让这块石头仅被一种步长的青蛙标记---其他步长的青蛙标记了也不行.

那就是找一个必定存在且唯一的跟这个石头标号相关的数字.

比如第 i 块石头, 那这个数字就是 gcd( i , m ), 即第 i 块石头只能被步长为 gcd( i , m ) 的青蛙标记.

这里还要有一个取巧的理解方式, 就是步长为 gcd( i , m ) 的青蛙可以由其因子为步长的青蛙代替.

例如还是第一个样例, 12块石头, 第6块石头只能被步长为6的青蛙标记, 但是你可以假装一只步长为2( 3 也行)的青蛙的步长是6(多跳几下步长就是6了嘛), 称之为"假6步"青蛙.

现在在这个新规则下面, 我们找找看一只假步长固定的青蛙能标记的石头有哪些.

先确定一下所有的石头对应的步长:(第0块石头不用确认, 因此找 1 ~ 11 号石头就行)

假步长为1的青蛙: 1 , 5 , 7 , 11;

假步长为2的青蛙: 2 , 10;

假步长为3的青蛙: 3 , 9;

假步长为4的青蛙: 4 , 8;

假步长为6的青蛙: 6;

假步长为12的青蛙: 这青蛙没什么用, 放一边卖萌就好.

然而实际上,"假1步"青蛙只能由步长确实为1的青蛙替代,而实际步长为1的青蛙能把所有石头都"occupied",直接特判一下序列求和就行.

如果找不到"假1步青蛙",那就往下看.

所以要找的"假k步"青蛙, k是m的所有非1且非本身的因数, 12块石头就要找"假2步","假3步","假4步","假6步"青蛙.

因此针对k步, 遍历所有青蛙的实际步数, 看看有没有青蛙的实际步数是假步数的因子---这意味着这只青蛙可以当"假k步青蛙",所以可以把相关的一一对应的石头的序号加起来了.

比如说我们找到了一只"假k步青蛙", 怎么求序号和?

先想想这个规则是找怎么来的, k=gcd( i ,m), i是石头编号

所以, m/k 和 i/k 是互质的, 惊喜吧! 找比m/k小还和m/k互质的数的和是有公式的! (就是求了所有满足 i/k 的和)

那一只"假k步"青蛙可标记石头标号和就是那个公式乘以k (就是求满足条件的所有 i 的和)

公式:所有比整数X小且和X互质的数的和=(phi(x)*x)/2

所以, 如果我们找到了一只"假k步青蛙",就在res上加(k*phi(m/k)*m/k)/2=(phi(m/k)*m)/2就行.

找完了所有的"假不知道多少步"青蛙,也就加完所有的序号.

代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <iostream>
#include <algorithm>
using namespace std;

long long int _gcd(long long int a,long long int b)
{
    long long int res;
    res=a%b;
    while(res)
    {
        a=b;
        b=res;
        res=a%b;
    }
    return b;
}

long long int phi(long long int n)
{
    long long int res=n,i;
    for(i=2;i*i<=n;i++)
    {
        if(n%i==0)
        {
            res=res-res/i;
            while(n%i==0)
                n=n/i;
        }
    }
    if(n>1)
    {
        res=res-res/n;
    }
    return res;
}

long long int step[10010],fac[10010];

int main()
{
    int T,j,n,case_num,n_fac;
    long long int m,temp,res,i;
    bool all;
    scanf("%d",&T);
    case_num=1;
    while(T--)
    {
        all=false;
        scanf("%d%lld",&n,&m);
        for(i=0;i<n;i++)
        {
            scanf("%lld",&temp);
            step[i]=_gcd(temp,m);//跳了足够多圈数之后的等效步长,并放在step里
            if(step[i]==1)
                all=true;
        }
        if(all==true)//找到了一只实际等效步长就是1的青蛙的特判
        {
            printf("Case #%d: %lld\n",case_num++,((m-1)*m)/2);
            continue;
        }
        sort(step,step+n);
        n=int(unique(step,step+n)-step);//不妨学习一下unique函数的用法
        n_fac=0;
        for(i=2;i*i<=m;i++)
        {//给m分解因数,确定要找哪些假步数的青蛙,例如30块石头就要找假2,3,5,6,10,15步青蛙,并把这些数放在fac里
            if(i*i==m)
            {
                fac[n_fac++]=i;
            }
            else if(m%i==0)
            {
                fac[n_fac++]=i;
                fac[n_fac++]=m/i;
            }
        }
        sort(fac,fac+n_fac);
        res=0;
        for(i=0;i<n_fac;i++)
        {//找"假fac[i]步"青蛙
            for(j=0;j<n;j++)
            {
                if(fac[i]%step[j]==0)//因为假fac[i]步青蛙可以由以fac[i]的因子为实际等效步数的青蛙代替
                //找到了一只"假fac[i]步"青蛙,所以计算完fac[i]步青蛙可标记的石头之后就可以跳出了,不然会重复计算
                {
                    res+=phi(m/fac[i])*m/2;
                    break;
                }
            }
        }
        printf("Case #%d: %lld\n",case_num++,res);
    }
    return 0;
}




评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值