zoukankan      html  css  js  c++  java
  • 【学习笔记】快速幂&“快速”乘

    模板:P1226,可以用本文章的快速幂代码通过。

    Update On 2020/3/24:增加了一些说明,可能会更加清晰易懂QwQ


    快速幂

    问题描述:见P1226

    直接做的话,需要进行最多 (2^{31}-1) 次运算,时间复杂度直接爆炸。


    我们发现一个性质:

    [x^2=x imes x ]

    [x^4=x imes x imes x imes x=x^2 imes x^2 ]

    [x^8=x imes x imes x imes x imes x imes x imes x imes x=x^2 imes x^2 imes x^2 imes x^2=x^4 imes x^4 ]

    [... ]

    我们发现,对于 (x^p) ,我们最终可以把她分解成 (p)(x^1) 相乘的形式,而从上面的例子中可以看出,通过把 (x^1) 两两相乘,可以合并成若干个 (x^2),再通过把 (x^2) 两两相乘,可以得到若干个 (x^4) ......以此类推。

    从而我们得到重要结论:可以通过不断把当前指数 (p) 减半(也就是 (dfrac{p}{2}))而得到子问题的答案,然后把子问题的答案合并得到原问题的答案。 这就是快速幂的核心思想。

    Q:如果当前分解的 (p) 为奇数呢?

    [x^3=x^2 imes x ]

    [x^5=x^2 imes x^2 imes x ]

    [... ]

    我们发现,若当前分解的 (p) 为奇数,假设分解当前的 (x^p) 的项数为 (k) ,那么必定有 (k-1) 个同类项和一个非同类项(似乎只能这样表达......)。

    这是因为现在分解的 (p) 为奇数,而 (p ext{mod} 2=1)

    于是我们发现,如果分解的一开始不去管那个“落单”的数(让她保持1次方),那么在当前合并完之后,把那个数再给乘上就好了!

    而之所以一开始就不管她,是为了让最后直接乘上原来的 (x) 就好了,很简单。

    至此,我们便得到了解决办法:先假装没有哪个“不同类”的项,合并玩其他答案后再乘上即可。


    大概的算法流程就是(假设我们要求 (x^p ext{mod} k) ):

    • 如果当前的 (p) 为 0 ,直接返回1。因为任何数的0次方都为1。
    • 如果当前的 (p) 为1,返回 (x) 即可。因为 (x^1=x)
    • 计算出当前指数 (p) 缩小一般之后的结果 (a)
    • 如果当前的 (p) 为偶数,则返回 (a^2) ;否则返回 (a^2 imes x)(因为无法正好分解成两半,那么就假装现分成两半,再把这两半的结果乘上那个“不同类”的项 (x) )。

    可以发现这种快速幂算法的核心思想是不断缩小问题规模,算出这些小问题的答案后再合并。这种思想很重要,再数论中有广泛的涉及(著名的CRT的特解的构造过程就是先构造一个小问题)。

    上述算法可以很优雅的解决指数 (p) 为0的情况(直接返回1)。

    我们采用递归实现,注意在每乘上一步都要取模(随时取模性质)。

    #include <iostream>
    #include <cstdio>
    
    using namespace std;
    
    long long k;
    
    int po(long long x,long long p) {
    	if(p==0) return 1;
    	if(p==1) return x%k;
    	long long a=po(x,p/2); //把问题规模降一半
    	if(p%2==0) return a*a%k;
    	else return a*a%k*x%k; //注意这里要两次取模
    }
    
    int main() {
    	long long b,p;
    	cin>>b>>p>>k;
    	cout<<b<<"^"<<p<<" mod "<<k<<"="<<po(b,p)<<endl;
    	return 0;
    }
    

    然而这份代码交上去只有 88 分。

    这是因为我们自大的认为1这个数很小,不用对 (k) 取模。

    然而其中一组测试点是这样的......

    1 0 1
    

    谔。

    这里直接给了0次方,可以解决;然而又给了模数 (k=1)

    那么我们上面那个程序中,直接返回了1;然而正确答案是 (1^0 ext{mod} 1=0)!!!

    所以......不要怀有侥幸心理......老老实实的取模吧QAQ

    AC代码:

    #include <iostream>
    #include <cstdio>
    
    using namespace std;
    
    long long k;
    
    int po(long long x,long long p) {
    	if(p==0) return 1%k;
    	if(p==1) return x%k;
    	long long a=po(x,p/2);
    	if(p%2==0) return a*a%k;
    	else return a*a%k*x%k;
    }
    
    int main() {
    	long long b,p;
    	cin>>b>>p>>k;
    	cout<<b<<"^"<<p<<" mod "<<k<<"="<<po(b,p)<<endl;
    	return 0;
    }
    

    该算法由于每次都把问题规模缩小一半,故其时间复杂度为 (mathcal{O}(log p))

    猜想:“快速”乘

    如果给定两个不超过long long类型的整数 (a,b) ,要求计算 (a imes b) 的值,怎么办?

    现在已经不是时间快慢的问题了。(a imes b)有可能会爆long long(注意到 (a,b) 有可能会是负数,所以不能用unsigned long long)!

    写高精度也不划算,那么如何解决这个问题呢?

    注意到 (a imes b) 可以拆成 (b)(a) 相加,那么利用类似快速幂的思想,把每次乘法合并改成加法合并不就行了!

    //(a*b)%k
    #include <cstdio>
    #define ll long long
    
    ll a, b;
    ll k;
    
    ll sum(ll x, ll p) { //目前要算 p 个 x 相加
        if(p==0) return 0;
        if(p==1) return x%k;
        ll n=sum(x, p/2);
        if(p%2==0) return (n+n)%k;
        else return (n+n+x)%k;
    }
    
    int main(void) {
        scanf("%lld%lld%lld", &a, &b, &k);
        printf("%lld
    ", sum(a, b));
        return 0;
    } 
    

    这样做就不会爆long long了。

    其实。。。这个东西是慢速乘,,,只不过因为和上面哪个快速幂的思想方式一毛一样才得名23333。

    总结

    注意到无论是快速幂,还是快速乘,都满足了结合律!所以,只要某种运算/操作满足结合律,那么便可以采用类似的方法!(e.g. 线段树)

    End.

    最后一次更新于2020/3/24。

  • 相关阅读:
    兑奖
    杨辉三角
    偶数求和
    进制转化
    填词
    等值数目
    Spring框架的七个模块
    数据库中的第1、2、3范式 (昨天没睡好,因为那个蚊子~~)
    关于eclipse 不编译或者找不到*.class的问题
    servlet生命周期的理解
  • 原文地址:https://www.cnblogs.com/BlueInRed/p/12617562.html
Copyright © 2011-2022 走看看