快速幂 & 龟速乘 & 快速乘

龟速乘和快速乘都是为了防止模数大于int, 导致爆long long的情况

关于O(1)快速乘和关于其特判的原因 - :Dra - 洛谷博客 (luogu.com.cn)

快速幂

原理

利用二进制的思想, 把a的k次幂中的k变成一个二进制数
然后根据二进制数的每一位1把a的k次幂求出来
例如 \(5 ^ {11}\)
\(11 = 1011 (2)\)
\(5 ^ {11} = 5^8 * 5^2 * 5^1\)
\(a ^ k = a^{k_3} * a^{k_1} * a^{k_0}\)

其中二进制中的每一位 \(a ^ {k_i}\) 都可以用倍增的方式求出
这样就是 \(O(\log n)\) 的时间复杂度, 求出一个数的k次幂
并且因为其分开乘的性质, 是可以算取模p后的值的

代码
int qmi(int a, int k, int p) // 求在取模p的情况下a的k次方
{
	int res = 1;
	while (k)
	{ 
		if (k & 1) res = (1ll * res * a) % p;
		k >>= 1;
		a = (1ll * a * a) % p;
	}
	return res;
}
解释一下代码

res是用来存放我们凑出的数使用的, 也就是最后的结果, 因为是乘法, 所以它初
始为1 (一个数的0次幂也是1, 而且res == 0会导致乘法全是0, 因此res初始为1),
中间的 a = (1ll * a * a) % p; 就是倍增求出二进制中a ^ ki的过程, while (k) k >>= 1
这段话会老老实实的把k拆成二进制, if (k & 1)则会把二进制中的每一位1取出来
这两个联动起来就是, 先看看k的最后一位是1还是0(if), 判断后, 把k的最后一位去掉(k >>= 1),
进入下一次循环, 再看看这时候的最后一位是1还是0(if), 直到k等于0, 这时候k的每一位我们也都判
断过了, a在这个过程中也会不断倍增(a = (1ll * a * a) % p), 变成对应的a ^ ki,.
(这里a的变化可以手动模拟试试) (这里的最后一位是指比如1011中最右边的一位)
if (k & 1) res = (1ll * res * a) % p;在if成立后res就会乘上这个答案, 也就是上面说的原理


补充

这个代码就是最常用的快速幂模版, 因为一个数的k次幂很容易直接爆int, 所以大多数都会取模
而两个int相乘, 在模数在int范围内下, 也很容易爆int, 所以要乘上1ll转换为long long类型

当然在模数超过int时, 直接乘就会爆掉long long, 这时候就会用到下面的龟速乘 或者 快速乘
PS: 还有__int128欢迎你

龟速乘

龟速乘和快速幂一样, 都是利用了二进制的原理,
把 a * k 的 k拆成二进制数, 根据每一位凑出来

如k = 5 = 101(2)
就可以用a + 4a凑出来

代码如下
LL qmul(LL a, LL k, LL p) // 龟速乘
{
    LL res = 0;
    while (k)
    {
        if (k & 1) res = (res + a) % p;
        a = (a + a) % p; 
        k >>= 1;
    }
    return res;
}

快速乘

快速乘,又称光速乘。

这玩意我也是新学, 但确实很巧妙。它原理就是 a % b == a - a/b * b。其中这篇博客讲的好 关于O(1)快速乘和关于其特判的原因 - :Dra - 洛谷博客 (luogu.com.cn)

先看这句话 rt = a * b - (LL)((long double)a / p * b + 0.5) * p;,后面(LL)((long double)a / p * b + 0.5)因为(long double)的原因,所以不会爆掉, 前面 a * b 和 后面再乘一个p就会爆掉 longlong。

但是没关系,我们只需要它的差值,\(p\) 是模数,\(p\) 一定在 longlong 范围内(这也是, 这个技巧正确的原因)。那么a * b - (LL)((double)a / p * b + 0.5) * p;的结果一定也在longlong 内。

因为a % b == a - a/b * b 这个原理,这就把结果求出来了, 但是因为精度问题,在(double)a / p * b中会损失精度,它实际上就是接近未知的 \(x\) 的一个数 , 但始终达不到 \(x\)。注意因为 double 的对小数的就近舍入,会导致它变大或者变小,或者不变。

如果精度损失使它(long double)a / p * b超过 \(x\) 且它下取整后的结果比 \(x\)\(1\),那么就会让 \(rt\) 成为负数;如果精度损失使他小于x, 同理就会让rt变成大于mod的正数。所以我们会得到一个和正确答案差值绝对值为 mod 或 0 的答案。

这里引用博客中的话:

考虑当 (long double)a / mod * b 的真实值是一个十分接近但小于某个整数 x 的值, 在计算过程中的精度损失会使其超过 x 并且使向下取整后的结果大 1, 同理其十分接近但大于某个整数会使结果小 1 , 故我们得到的答案与正确值之差的绝对值是 mod 或 0.

这里就可以看看那个 +0.5 的作用了。这相当于直接 “上取整”,这会使得计算出来的 \(rt\) 不会比正确答案大,而少判断一种情况,减少常数。如果不加的话就需要判断让 \(rt\) 大于 mod 的情况。

最后,注意不能对于太大的模数用这个算法 \(10^{10} \sim 10^{12}\) 为宜(但实际上很精准,即使干到 \(10^{18}\))。

代码
LL fqmul(LL a, LL b, LL p) // 快速乘
{
    LL rt = a * b - (LL)((long double)a / p * b + 0.5) * p;
    if (rt < 0) rt += p; // 判断小于0的情况
    return rt;
}
// 或者
LL fqmul(LL a, LL b, LL p)
{
	LL rt = a * b - (LL)((long double)a / p * b) * p;
	if (rt < 0) rt += p;
	if (rt >= p) rt -= p;
	return rt;
}

700

模版

#include <iostream>
#include <cstring>

using namespace std;

typedef long long LL;

LL n = 142, m = 12343, mod = 1e10 + 7;

LL fqmul(LL a, LL b, LL p) // 快速乘
{
    LL rt = a * b - (LL)((double)a / p * b + 0.5) * p;
    if (rt < 0) rt += p;
    return rt;
}

LL qmul(LL a, LL k, LL p) // 龟速乘
{
    LL res = 0;
    while (k)
    {
        if (k & 1) res = (res + a) % p;
        a = (a + a) % p; 
        k >>= 1;
    }
    return res;
}

LL qmi(LL a, int k, LL p) // 快速幂
{
    LL res = 1;
    while (k)
    {
        if (k & 1) res = res * a % p;
        k >>= 1;
        a = a * a % p;
    }
    return res;
}

LL l_qmi(LL a, int k, LL p) // 龟速_快速幂
{
    LL res = 1;
    while (k)
    {
        if (k & 1) res = qmul(res, a, p);
        k >>= 1;
        a = qmul(a, a, p);
    }
    return res;
}

LL f_qmi(LL a, int k, LL p) // 快速_快速幂
{
    LL res = 1;
    while (k)
    {
        if (k & 1) res = fqmul(res, a, p);
        k >>= 1;
        a = fqmul(a, a, p);
    }
    return res;
}


int main()
{
    cout << qmi(n, m, mod) << endl; // 会溢出 爆long long
    cout << l_qmi(n, m, mod) << endl; // O(logn * logn) 
    cout << f_qmi(n, m, mod) << endl; // O(logn)
    
    return 0;
}

神奇的 __int128

像上面的什么龟速乘, 快速乘, 其都要写一个函数, 龟速太慢, 快速有概率爆掉
在满足有__int128的情况下, 可以试试强转
__int128
如下代码, 这样即保证了效率(可普通乘法一样), 又稳定, noip可以使用
能用的话尽量先用它

LL qmi(LL a, LL k, LL p)
{
    LL res = 1;
    while (k)
    {
        if (k & 1) res = (__int128)res * a % p;
        k >>= 1;
        a = (__int128)a * a % p; 
    }
    return res;
}

PS: __int128 原生只支持四则运算, 即 + - * / 不包含%与正常的输入(如cin, scanf)
和正常的输出(cout, printf)

一般会搭配快读快写来使用

posted @ 2023-11-12 17:34  blind5883  阅读(373)  评论(0)    收藏  举报