• 关于一些信息学数论问题例题的讨论


      大概就是写一些数论水题的题解?

      目录

      不定期更新。


    一.[AHOI2005]约数研究 洛谷oj P1403

      可能需要事先学习的算法:

        埃氏筛法(素数筛)

      题意很容易理解。很明显这是一道真正的水题,适合初学者理解筛法的思想。

      30分暴力做法

        对于一个数$i(i∈[1,n])$ ,枚举所有$[1,i)$之间的正整数$j$,判断$j$是否是$i$的约数,如果是,计数器$result$就加上1。

        复杂度是$O(n^2)$,不是很有讨论价值,写了一下代码。

    #include <cstdio>
    using namespace std;
    
    int n;
    int result;
    
    int main(){
        scanf("%d",&n);
        for (int i=1;i<=n;i++)    //枚举 1~n 所有数 
            for (int j=1;j<=i;j++)    //一个个判断是否是i的约数,如果是,则计数加1 
                if (i%j==0)    result+=1;
        printf("%d",result);
        return 0;
    }
    View Code

      100分算法(暴力筛法):

        或许可以看成暴力做法的优化,但是如果学过筛法,那就直接是筛法的思想了。

        我试着用优化暴力的思路解释一下我的算法。上面的做法是先抓一个数$i(i∈[1,n])$,然后再一个个找它们的约数的。我们可以换个思路,也抓一个数$i(i∈[1,n])$,然后一个个找它的倍数(倍数小于$n$),找到一个倍数,计数器$result$就加上1。

        熟悉筛法的同学应该能一眼AC吧(毕竟是普及组水题)。

        复杂度应该是$O(nsqrt{n})$。

    #include <cstdio>
    using namespace std;
    
    int n;
    int result;
    
    int main(){
        scanf("%d",&n);
        result+=n;    //1可以是所有数的约数 
        for (int i=2;i<=n;i++)
            for (int j=1;i*j<=n;j++)    //枚举倍数 i*j 
                ++result;
        printf("%d",result);
        return 0;
    }

        基于这种想法其实还可以优化。我们很容易发现,第二重循环其实是不必要的,因为对于一个数$i$,$[1,n]$里它的倍数一定有且仅有$frac{n}{i}$个(向下取整)。那么我们就可以扔掉第二重循环了。

    #include <cstdio>
    using namespace std;
    
    int n;
    int result;
    
    int main(){
        scanf("%d",&n);
        for (int i=1;i<=n;i++)
            result+=(n/i);
        printf("%d",result);
        return 0;
    }

        是不是已经挺不错的了?但是洛谷上有神犇给出了下一种复杂度更加优秀的算法。

      100分算法(非常优秀):Kelin的题解

        大概意思是说,$f(i)=frac{n}{i}$,但是因为除法要向下取整,所以有一些数可以当成同一个数来跳过。

        打个比方,对于$n=60$时,不管$i=13$或$i=14$或$i=15$,$frac{n}{i}$的结果都是一样的,因为$int$整型要向下取整。所以可以把它们放在一起算,差不多就是这种思想。

        时间复杂度$O(2sqrt{n})$。我测了一下,可能因为数据比较水,我写的算法$36ms$跑完,这种算法$26ms$跑完,还是十分优秀的。

        代码在上面链接里有,我就不写了。

     

    二.最大公约数和最小公倍数问题  洛谷oj P1029

      必须预先学习的算法:

        欧几里得算法(GCD)(辗转相除法)

      这是一道数论入门好题。在做之前要熟悉$gcd$(即最大公约数)。

      这题我觉得不太可能有靠谱的部分分写法(毕竟比较水),我就直接讲正解了。

      大家都知道怎么求最大公约数$gcd(P,Q)$,也许有人会问是不是也有专门求最小公倍数$lcm(P,Q)$的算法?不需要。最小公倍数$lcm(P,Q)$可以通过最大公约数$gcd(P,Q)$得到。

      引理:两个正整数$P$,$Q$的最小公倍数为$P*Q/gcd(P,Q)$。

      证明:

        记$P=gcd(P,Q)*p_{1}$

          $Q=gcd(P,Q)*p_{2}$,且$gcd(p_{1},p_{2})=1$  //即$p_{1}$和$p_{2}$互质

          $lcm(P,Q)=gcd(P,Q)*p_{1}*p_{2}$

              $=gcd(P,Q)*p_{1}*gcd(P,Q)*p_{2}/gcd(P,Q)$

              $=P*Q/gcd(P,Q)$

        得证。

       肯定有人不喜欢读证明,那我举个例子好了。假设存在两个数,$P=2160$,$Q=4032$。根据唯一分解定理,可得:

        $P=2160=2^4*3^3*5$

        $Q=4032=2^6*3^2*7$

      可以看出来,这时候$gcd(P,Q)=2^4*3^2=144$,那么,$P$和$Q$可以这样改写:

        $P=gcd(P,Q)*3^1*5$

        $Q=gcd(P,Q)*2^2*7$

      很明显,$3^1*5$或$2^2*7$互质,因为如果它们不互质,它们的最大公约数完全可以变成$gcd(P,Q)$的一个因子。

      又因为$lcm(P,Q)=2^6*3^3*5*7$,$lcm(P,Q)$具有$P$和$Q$的所有因子,则:

        $lcm(P,Q)=gcd(P,Q)*2^2*3^1*5*7$

             $=(gcd(P,Q)*3^1*5)*(gcd(P,Q)*2^2*7)/gcd(P,Q)$

             $=P*Q/gcd(P,Q)$

      就可以根据$lcm(P,Q)=P*Q/gcd(P,Q)$求解了。理解力好的同学应该可以直接理解这个结论。

      在知道$lcm(P,Q)=P*Q/gcd(P,Q)$后,再来看这道题。在这道题里,$x$是最大公约数$gcd(P,Q)$,而$y$是最小公倍数$lcm(P,Q)$。

      我们不妨设$P=x*p_{1}$,$Q=x*p_{2}$($p_{1}$和$p_{2}$互质)。

      所以我们可以写出下面的推导

        $y=P*Q/x$

          $=(x*p_{1})*(x*p_{2})/x$

          $=p_{1}*p_{2}*x$

        则$frac{y}{x}=p_{1}*p_{2}$

      是不是发现了什么?题目要求输出的答案是$P$和$Q$,而$P=x*p_{1}$,$Q=x*p_{2}$,且$x$是已知的。要想知道$P$、$Q$的所有可能性,只需要枚举出$p_{1}$和$p_{2}$的所有可能性就好了。

      怎么枚举出$p_{1}$和$p_{2}$?我们已经知道$frac{y}{x}=p_{1}*p_{2}$了,$for$一遍就好了。

      代码:

    #include <cstdio>
    using namespace std;
    const int maxn=100000;
    
    int x,y;
    int result;
    
    inline int gcd(int a,int b){
        return b?gcd(b,a%b):a;
    }
    
    int main(){
        scanf("%d%d",&x,&y);
        if (y%x)    printf("0");    //如果y不能整除x,不存在解
        else{
            int n=y/x;
            for (int i=1;i<=n;i++)
                if (n%i==0){
                    if (gcd(i,n/i)==1)
                        result+=1;
                }
            printf("%d",result);
        }
        return 0;
    }

     三.又是毕业季I  洛谷oj P1372

      需要预先学习的算法:

    感觉不需要预先学习算法?可能需要一点对质数的理解。

      一道大家都很高兴做的水题,可以增强对质数的理解。

      题意大概就是要在$1~n$中找到$k$个最大公倍数最大的数。

      很容易想到,假如$k$个数存在最大公倍数$gcd$,则$k=gcd*m$,$m$一定是正整数。简单地说,就是这些被选中的$k$个数要么就是$gcd$,要么就是$gcd$的倍数。

      因为$k$和$n$已经确定,如何让$gcd$最大?我们很容易想到,$gcd=frac{n}{k}$,注意,这里的除法需要向下取整

      代码就更简单了,复杂度$O(1)$不需要分析了。

    #include <cstdio>
    using namespace std;
    
    int n,k;
    
    int main(){
        scanf("%d%d",&n,&k);
        printf("%d",n/k);
        return 0;
    } 

     

    四.倒水  洛谷oj P1582

    需要一点对二进制的理解

      玩过《2048》这款游戏吗?我觉得和这道题很像。

      很明显,所有瓶子的状态可以压成一个二进制数。

      比如这个二进制数:

        $1100101$

      表示的是,经过处理后,有$64$升水、$32$升水、$4$升水、$1$升水的瓶子各一个。

      为什么可以这么压?假设有$X$升水的瓶子$Y$个,很显然,X一定是$2$的几次方(我们先表示成$X=2^i$),而如果$Y$大等于$2$,则$Y$一定可以倒进更多水的瓶子里(两瓶$X$就可以变成一瓶$2X$)。因为倒水次数不限制,所以贪心的想法是,初始状态每个瓶子里的水越多越好,这样瓶子就少了。

      所以很明显,如果某升水的瓶子数量大于等于$2$,一定可以把多余的水往上倒。初始状态处理到最后,就是二进制数了。而二进制数逢二进一的原则保证了无论多少个1升的瓶子都可以自动处理成最少的瓶数。

      所以,当有$a$个1升的瓶子时,处理完后最少的瓶数就是$a$转换成二进制后的$1$的个数。比如这个二进制数$1100101$最少的瓶数就是$4$。

      想明白了这一点,这题就很好写了。接下去怎么处理就很容易了,题意翻译过来就是求大等于$N$的最小数,使这个数含有的$1$的个数不大于$K$。然后的结果输出这个最小数减去$N$。

      90分暴力算法(最后一个点过不了):

    #include <cstdio>
    using namespace std;
    
    int n,k;
    long long result;
    
    inline int count(long long x){    //数瓶子的个数
        int cnt=0;
        while (x){
            if (x&1)    cnt+=1;
            x>>=1;
        } 
        return cnt;
    }
    
    int main(){
        scanf("%d%d",&n,&k);
        for (result=0;count(result+n)>k;result++);
        printf("%lld",result);
        return 0;    
    } 

      100分算法

        很早以前A的,忘记是怎么优化的了,具体可以直接看代码。如果有时间我再回来补充详细解释。

        记得开longlong,不开还是90分.

    #include <cstdio>
    using namespace std;
    
    long long n,k;
    long long cnt;
    long long result;
    
    inline long long count(long long x){    //数瓶子的个数
        long long cnt=0;
        while (x){
            if (x&1)    cnt+=1;
            x>>=1;
        } 
        return cnt;
    }
    
    int main(){
        scanf("%d%d",&n,&k);
        cnt=count(n),result=n;
        for (int i=0;cnt>k;i++)
            if ((result>>i)&1){
                result-=(1<<i);
                i+=1;
                while (true)
                    if ((result>>i)&1){
                        result+=(1<<i);
                        break;
                    }
                    else i++;
                cnt=count(result);
            }
        printf("%lld",result-n);
        return 0;    
    } 

    五.阶乘问题  洛谷oj P1134

      调了快一个小时才调出来……给跪了。

      题意很清楚了。

      28分骗分算法(皮一下)

        打表可知 可以详细证明,阶乘的尾数只可能是$2$、$4$、$6$、$8$。所以随便抓个数骗分/滑稽

    #include <cstdio>
    using namespace std;
    
    int main(){
        printf("2");
        return 0;
    }

      100分算法

        我们很快可以想到,每次都取最后一个有效数字来乘下一个数,这样复杂度$O(N)$是可以过的。

        但是,在调试的过程中,会发现取最后一个有效数字是不行的,为什么我就不解释了。差不多要模10的7次方或8次方才能过。

        和上一道题目一样,记得开longlong。

    #include <cstdio>
    using namespace std;
    
    int n;
    long long result=1;
    
    int main(){
        scanf("%d",&n);
        for (int i=1;i<=n;i++){
            result*=i;
            while (result%10==0)    result/=10; 
            result%=100000000;
        }
        printf("%d",result%10);
        return 0;
    }

     

     

      

     

  • 相关阅读:
    论 IntStream 和 for 循环的速度
    单链线性表的基本操作--创建,插入,删除,查看,打印
    Android中的异步处理方式
    Kotlin 集合变换与序列
    Kotlin Lazy延迟初始化
    协程及Kotlin协程
    Java 注解
    Android 事件传递机制进阶
    Java 异常
    Java 多线程及线程间通信(Synchronized和Volatile关键字)
  • 原文地址:https://www.cnblogs.com/awakening-orz/p/10938459.html
Copyright © 2020-2023  润新知