• 五一培训 清北学堂 DAY4


    今天上午是钟皓曦老师的讲授,下午是吴耀轩老师出的题给我们NOIP模拟考了一下下(悲催暴零)

    今天的内容——数论

    话说我们可能真的是交了冤枉钱了,和上次清明培训的时候的课件及内容一样(哭。

    整除性

    质数的性质:

    √n判质数:

    按照素数的定义:我们枚举从2到√n,若其中没有一个数是n的因子,则说明n是个质数!

    证明为什么到√n:

    若n有因数,则n可以写成a*b的形式,我们假设a<=b,若两个数都大于√n,则乘起来一定大于n呀,所以一定有个大于等于√n,一个小于等于√n,证毕!

    所以说我们就有了一个直接暴力判质数的方法qwq,时间复杂度O(√n)。

    但是太慢了,判一个或几个数还是可以的,若是判一个大范围内的所以质数,那就要GG了。 

    nlogn判质数(我也不知道叫啥,就这么叫吧):

    我们来想一个问题:如果一个数是质数,那么它的倍数是不是质数?

    显然不是!因为的它的倍数都有该质因子!

    所以这个算法的思路就是:如果我们现在在判断一个数n是否为质数,在判完后我们可以将所求范围内所有它的倍数标记为“不是质数”。

    实际代码也就三行,简单明了qwq,时间复杂度O(nlogn):

    #include<iostream>
    #include<cstdio>
    using namespace std; 
    int not_prime[100001];             //存1~n是否为质数 
    int main()
    {
        int n;
        cin>>n;
        for(int i=2;i<=n;i++)
            for(int j=i+i;j<=n;j+=i)   //将i的所有倍数标记为不是质数
               not_prime[j]=true;
        for(int i=1;i<=n;i++)
        if(not_prime[i]==false) cout<<i<<endl;
        return 0;
    }

    埃拉斯特尼筛法(埃氏筛):

    这个筛法就是上面那个筛法的改进版,我们只将质数的所有倍数标记为不是质数就行了,这样就可以减少一部分数被重复标记。

    证明:若我们将合数的倍数也判为不是质数,根据唯一分解定理:所有的合数都能写成几个质数乘积的形式,也就是说,这个合数已经被比他还小的质因子筛过了,就不用重复再筛一次了。

    调和级数:

    代码很简单,就是在上面判断一下是否为质数就好了:

    #include<iostream>
    #include<cstdio>
    using namespace std; 
    int not_prime[100001];             //存1~n是否为质数 
    int main()
    {
        int n;
        cin>>n;
        for(int i=2;i<=n;i++)
            for(int j=i+i;j<=n;j+=i)   //将i的所有倍数标记为合数 
               if(not_prime[i]==false) //保证只将质数的倍数筛掉 
                  not_prime[j]=true; 
        for(int i=1;i<=n;i++)
        if(not_prime[i]==false) cout<<i<<endl;
        return 0;
    }

    时间复杂度:O(nlog logn)——已经很快了,很接近线性筛

    欧拉筛(线性筛)

    这个是埃氏筛的再改进版qwq,真强啊!

    虽然埃氏筛已经保证大部分的数不被重复筛,但是还有那一小部分呀!

    什么?你不信!

    你看:

    6这个数,它的质因子有2和3,按照埃氏筛的算法,它还是会被2和3标记一次,谁让它俩都是质数呢。

    埃拉斯特尼:tql,这我都没想到%%%

    那又咋办呢?

    欧拉筛!!!

    它的思路是:保证每个数只被它的最小质因数筛掉,这样的话每个数就只被筛了一次,时间复杂度是O(n);

    线性筛能处理积性函数!而其他筛法不能!

    超级简单的代码:

    #include<iostream>
    #include<cstdio>
    using namespace std;
    int gcd(int a,int b)       //求两个点的最大公约数 
    {
        if(b==0) return a;     //0和任何数的最大公约数都是这个数 
        return gcd(b,a%b);     //辗转相除法 
    }
    int main()
    {
        int a,b;
        cin>>a>>b;
        cout<<gcd(a,b)<<endl;      
        return 0;
    }

    C++内自带的求最大公因数的函数:

    __gcd(注意两个下划线)

    若gcd(a,b)=d,则一定可以写出ax+by=d,且有多组解

    所以我们要求一组x和y的值,咋办呢?

    扩展欧几里得算法!exgcd,求不定方程!

           

    模意义下的的除法

    要用到费马小定理:

    如果p是一个质数,而整数a不是p的倍数,则有a^(p-1)≡1(mod p)。

    P是质数

    那么就可以转化成:

    以上亮眼操作是限制于P是质数以上的,难道P不是质数就GG了?

    NONONO!!!

    有个叫欧拉定理的东东!

    2.高精度

     我们都知道:

    Int 和long long都是有范围的:

    所以就算是long long,如果数的范围超过了20位,也无济于事,那咋办呢?说好的能计算一切数的!没错,我们用到了高精度!

    高精度实际是模拟竖式运算,遵循的原则一样:

    各位对其,逐位运算。

    怎么实现呢?

    假设有一个很大的数,我们用数组来存它的每一位,咋存?

    以19260817为例:

    很自然很单纯的想法是一位一位的按顺序像这样存进去:

    但这样就出现了一个问题:运算时各位没有对齐,是按最高位对齐的,显然不行!

    那咋办?反过来存!:

    假设我们又存进去一个123,也反过来存:

    这样的话,它们的各位就对齐啦qwq,没错就是这么简单!

    写代码啦(高进度加法为例):

    A+B Problem

    刚学OI几天的萌新的代码:

    Zhx:只要改两步就AC!

    1.将你的高精度模板贴过来(为什么感觉不大对劲呢);

    2.将int a,b;  改成gaojing a,b;(gaojing是结构体)

    结构体中有一个专门的初始化的函数——构造函数

    #include<iostream>
    #include<cstdlib>
    #include<cstring>
    
    using namespace std;
    
    struct gaojing   
    {
        int z[100010];
        int l;
        
        gaojing()                                 //构造函数 
        {
            l=1;
            memset(z,0,sizeof(z));                //清空 
        }
        
        friend istream& operator>>(istream &cin, gaojing &a)         //一个我永远也不会懂的读入流 
        {
            static char s[100010];
            cin >> s;
            int l=strlen(s);                                         //先以字符串的形式读入,方便计算位数 
            for (int i=0;i<l;i++)
                a.z[i] = s[l-i-1] - '0';                             //转化为int类型的,并倒着存进去 
            a.l = l;                                                 //记录长度 
        
            return cin;                                              //zhx说一定要写return cin(表示不懂qwq) 
        }
        
        friend ostream& operator<<(ostream &cout,const gaojing &a)   //输出流 
        {
            for (int i=a.l-1;i>=0;i--)
                cout << a.z[i];
            
            return cout;
        }
    };
    
    gaojing operator+(const gaojing &a,const gaojing &b)       //重点,高精算法的核心,这里用了一下重载运算符,能进入函数的就是前面定义的两个gaojing类型的数 
    {
        gaojing c;                                             //作为答案 
        
        int l = max(a.l,b.l);                                  //算出答案可能的长度 
        for (int i=0;i<l;i++)
        {
            c.z[i] += a.z[i] + b.z[i];
            c.z[i+1] += c.z[i] / 10;                           //进位 
            c.z[i] = c.z[i] % 10;
        }
        if (c.z[l] != 0) l++;                                  //处理运算后増位的情况 
        c.l = l;
        
        return c;
    }
    
    gaojing operator*(const gaojing &a,const gaojing &b)        //高精度乘法,也是重载运算符,只不过重载*号 
    {
        gaojing c;
        
        for (int i=0;i<a.l;i++)
            for (int j=0;j<b.l;j++)
                c.z[i+j] += a.z[i] * b.z[j];                    //普通模拟竖式运算 
        
        c.l = a.l+b.l;                                          //计算答案可能的位数 
        for (int i=0;i<c.l;i++) 
        {
            c.z[i+1] += c.z[i] / 10;                            //进位 
            c.z[i] = c.z[i] % 10;
        }
        while (c.l>0 && c.z[c.l]==0)                            //去除前导零 
            c.l--;
        c.l ++;
        
        return c;
    }
    
    
    int main()
    {
        gaojing a,b;
        cin >> a >> b;
        a+b;
        cout << a*b << endl;
    }

    3.进制转换

    二进制以10为例:

    第一个问题:

    将一个十进制的x转化成一个k进制的数

    用短除法!!!

    例如我们将55转化成一个三进制的数:

    然后将所有得到的余数从下往上写:

    就得到了答案: 2001(3进制)

    注意一个小细节:2001不能读作两千零一(因为这默认是十进制),只能读作二零零一

    第二个问题:

    将一个k进制的数转化成一个十进制的数

    我们可以将这个数写成一个k位的数(因为是k进制):

    那么转化后的十进制就是:

    不理解?举个例子!

    将一个三进制的2001转化成十进制:

    常用的一些进制:

    二进制,八进制,十进制,十六进制

    如果你不想定义一个十进制的数,你就可以在它前面加一个0(表示二进制),例如:

    Int a=1001     实际a为1001

    Int a=01001    实际a为9

    十六进制中要用x乘来表示计算十六进制;

    十六进制每一位的取值是0~15,那么我们用字母来表示10~15:

    A—10

    B—11

    C—12

    D—13

    E—14

    F—15

    ……………………考试的下午qwq(爆零)……………………

    好难呀,一题都不会qwq,老师讲了之后还是只会思路~

    这个题一看就是LCA模板吧(再说上面都写了),考试的时候真后悔没有把他搞懂qwq,不过现在懂啦!

    再看一下LCA的原理及实现:

    但是这样好像很慢,尤其是len层数特别大的时候,那怎么呢?我们在用倍增法:

    这个实际上是用了二分的思想吧,说下思路:

    第一步和之前一样,也是将x和y跳到同一层上;

    然后我们用一个grand[x][i]数组来表示编号为x的结点向上跳了2^i层后的结点编号,那么grand[x][0]就是x的父亲结点对吧(因为2^0是1,那么意思就是x向上跳了一层)

    那么对于一般的结点,都有grand[x][i]=grand[grand[x][i-1]][i-1]

    正常学生:这……跨度有点大吧!

    没错,这确实跨度有点大,老师刚开始就这么讲还不仔细解释一番,听不懂怪我喽!

    但是,既然你都已经看过来了,我肯定会仔细滴讲给你听啦:

    先考虑一下这个问题:2^i=2^(i-1+1)=2^[(i-1)+1]=2^(i-1)*2=2^(i-1)+2^(i-1)      别说你看不懂这个,这不是初中学的嘛?

    换句话说,你直接往上跳2^i层和先跳2^(i-1)层再跳2^(i-1)层是一样的

    那么我们分别将两种方式表达出来,它们是相等的:

    直接跳2^i层: grand[x][i]

    分两步跳: 跳完一次后,此时所在的结点是grand[x][i-1],没错吧;接下来把那一坨式子看做一个整体,如果整体感差的话你可以换元换成a

    那么第二次跳后所在的结点就是:grand[a][i-1],把a换过去就是grand[ grand[x][i-1] ][i-1](换个颜色更直观)

    两个式子做等号,就是上面的式子:grand[x][i]=grand[grand[x][i-1]][i-1]

    有木有感觉突然明白啦?什么,没有。。。。好吧我收回刚才的话qwq

    水一发洛谷LCA模板题解:

    #include<iostream>
    #include<cstdio>
    #include<cstring>
    using namespace std;
    const int maxn=500001;
    int head[2*maxn],to[2*maxn],next[2*maxn],grand[2*maxn][21],dep[maxn];       //注意开两倍大小的数组,因为这个图是无向图 
    //head[i]是存以i结点为起点的最后一条出边的编号 
    //to[i]数组是存第i条边的终点
    //next[i]是存以i结点为起点的所有出边中的倒数第二条出边的编号,其实也就是head[i]的上一条边 
    int n,m,s,edge_sum=0;                  //edge_sum记录边数 
    void add(int x,int y)                  //用链式前向星(链表)建图     
    {
        next[++edge_sum]=head[x];          //根据我们定义的head与next的含义得出 
        head[x]=edge_sum;                  //有新的边加入,则将head[x]内的值更新 
        to[edge_sum]=y;                    //当前边的终点是y 
    }
    void dfs(int v,int deep)
    {
        dep[v]=deep;                       //记录v结点的深度 
        for(int i=head[v];i>0;i=next[i])   //后序访问v的所有出边 
        {
            int u=to[i];                   //u记录当前边的终点,也就是说u是v的儿子 
            if(!dep[u]) dfs(u,deep+1),grand[u][0]=v;  //如果该儿子u的深度没被更新,则更新它,并记录u的父亲是v 
        }
    }
    int lca(int x,int y)
    {
        if(dep[x]<dep[y]) swap(x,y);       //我们让x是深度最大的那个 
        for(int i=20;i>=0;i--)
           if(dep[y]<=dep[x]-(1<<i)) x=grand[x][i];     //让x和y跳到同一层上 
        if(x==y) return y;                 //如果跳到同一点上了,说明这个点就是最近公共祖先 
        for(int i=20;i>=0;i--)             //倍增找最近公共祖先 
        {
            if(grand[x][i]!=grand[y][i])   //如果跳不到公共祖先,那就往上跳 
            {
                x=grand[x][i];             //将x和y往上跳 
                y=grand[y][i];              
            }
        }
        return grand[x][0];                //因为我们只要求跳不到同一点就往上跳,所以这样操作之后它们再往上跳一层也就是它们的最近公共祖先了 
    }
    int read()                             //快读 
    {
        char ch=getchar();
        int a=0;
        while(ch<'0'||ch>'9') ch=getchar();
        while(ch>='0'&&ch<='9')
        {
            a=a*10+(ch-'0');
            ch=getchar();
        }
        return a;
    }
    int main()
    {
        memset(head,0,sizeof(head));       //head数组初始化,好像可以去掉 
        n=read(),m=read(),s=read();        //这个题数据那么大,快读优化下 
        for(int i=1;i<n;i++)               //n-1条边 
        {
            int x=read(),y=read();          
            add(x,y);                      //题目中给的是无向图,所以也要反过来建一次       
            add(y,x);                       
        }
        grand[s][0]=s;                     //设起点s的父亲就是自己 
        dfs(s,1);                          //从深度为1的起点开始深搜 
        for(int i=1;(1<<i)<=n;i++)         //利用状态转移方程计算出每个点的grand值 
           for(int j=1;j<=n;j++)
              grand[j][i]=grand[grand[j][i-1]][i-1];
        for(int i=1;i<=m;i++)              //m次询问 
        {
            int x=read(),y=read();
            printf("%d
    ",lca(x,y));
        }
        return 0;
    }

    这道题看来ok了?NO!

    这道题实际是数学题qwq(大雾~

    来,我们一步步分析:

    首先这是一颗满k叉树,那么它满足这个性质:对于任何一个结点(除叶结点)它的儿子都有k个!

    我们求这个树里任意两个结点的最近公共祖先的深度,如果只按每个结点对树的贡献来说,那么这个问题就转化成了任意两个结点有多少个公共祖先!

    以我的想法解释一下(不一定对哈,助于理解嘛):

    你想想,我们找到了结点a和b的最近公共祖先c了,我们现在要求它的深度,那么如果c的上面还有父亲的话,假设为d,那么d一定是a和b的公共祖先(没有最近),这样的话是不是c的深度就要+1了?

    如果d上面还有父亲,那么c又被往下顶了一层,深度又+1……这样看来:这个最近公共祖先c上面有多少个祖先,c的深度就加几,再加上c这个结点,合起来不就是求有多少个公共祖先嘛?

    a和b有一个公共祖先,所以最近公共祖先的深度为1;

    此时c还有父亲,那么c的深度+1,a和b有两个公共祖先,所以c的深度为2;

    往下各位读者自己脑补脑补就好啦!

    我们还可以再将问题进行转化:

    我们已经要求对于任何结点a和b有多少个公共祖先了,那么我们将问题反过来:求任意一个点看看它是多少对结点的公共祖先!

    但是这样暴力好像一定会TLE,我们可以做下优化:

    这样以来,对于每一层的所有结点,我们只有找一个结点看看它是多少对结点的公共祖先就好了。

    对于上面的化简,配合等比数列求和公式效果更佳哦!

    有同学对于∑那一块一脸懵逼,没关系,给你解释一下:

    我们要求的答案是每个点看看是多少对结点的公共祖先,然后求和,所以我们要遍历每一层的结点,当然每一层只要找一个就好啦!

    所以这个∑从1遍历到n,这是所有的层数,然后我们随便找个结点,利用上面的公式,那么这一层上任意一个点都有个符合要求的,这一层一共有K^(i-1)个点(这是满叉树的性质),所以要乘上它;

    但这只是60分的做法(以本蒟蒻的水平理解到这一步已经很不容易了,接下的满分做法直接gg)!!

    还是一个问题:太慢!!!

    这是满分做法,也需要些数学功底嘛,所以我才说是一道数学题qwq:

    附上满分代码:

    #include <bits/stdc++.h>
    
    using namespace std;
    
    const int mod = 998244353;
    typedef long long LL;
    int fpm(int p, int k)
    {
        int res = 1;
        for (p %= mod; k; k >>= 1, p = (LL) p * p % mod)
            if (k & 1) res = (LL) res * p % mod;
        return res;
    }
    int main()
    {
        freopen("lca.in", "r", stdin);
        freopen("lca.out", "w", stdout);
        int n, K; cin >> n >> K;
        int e = fpm(K - 1, mod - 2); //(K - 1) ^ (-1)
        int x = (fpm(K, n) - 1) * (LL) e % mod; //(K ^ n - 1) / (K - 1)
        int ans = (fpm(K, n + 1) + 1) * (LL) x % mod;
        ans = (ans - 2 * n * (LL) fpm(K, n)) % mod;
        ans = ans * (LL) e % mod * (LL) e % mod;
        cout << (ans < 0 ? ans + mod : ans);
    }

     

  • 相关阅读:
    c语言 ,回调函数[个人理解]
    MFC、C++ 、Windows编程高手
    c++, 虚基派生 : 共同基类产生的二义性的解决办法
    c++,命名空间(namespace)
    c++,纯虚函数与抽象类
    c++ ,protected 和 private修饰的构造函数
    c++ 虚析构函数[避免内存泄漏]
    c++,虚函数
    c++,类的组合
    GPU与CPU的区别
  • 原文地址:https://www.cnblogs.com/xcg123/p/10800771.html
Copyright © 2020-2023  润新知