【学习笔记】快速幂&“快速”乘

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

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


快速幂

问题描述:见P1226

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


我们发现一个性质:

\[x^2=x\times x \]

\[x^4=x\times x\times x\times x=x^2\times x^2 \]

\[x^8=x\times x\times x\times x\times x\times x\times x\times x=x^2\times x^2\times x^2\times x^2=x^4\times 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\times x \]

\[x^5=x^2\times x^2\times x \]

\[... \]

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

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

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

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

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


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

  • 如果当前的 \(p\) 为 0 ,直接返回1。因为任何数的0次方都为1。
  • 如果当前的 \(p\) 为1,返回 \(x\) 即可。因为 \(x^1=x\)
  • 计算出当前指数 \(p\) 缩小一般之后的结果 \(a\)
  • 如果当前的 \(p\) 为偶数,则返回 \(a^2\) ;否则返回 \(a^2\times 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\ \text{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\times b\) 的值,怎么办?

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

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

注意到 \(a\times 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\n", sum(a, b));
    return 0;
} 

这样做就不会爆long long了。

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

总结

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

End.

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

猜你喜欢

转载自www.cnblogs.com/BlueInRed/p/12617562.html