【基础算法】倍增

倍增思想

倍增,顾名思义就是翻倍。它能够使线性的处理转化为对数级的处理,极大地优化时间复杂度。


一. 快速幂算法

1. 什么是快速幂

  • level 1

    当我们要计算 a 64 a^{64} a64 时,我们可以一个一个算的方式即: a × a × ⋯ a a\times a\times\cdots a a×a×⋯a,这样算 64 次即可得出答案,但是当数很大时,我们这个做法显然是会超时的。

  • level 2

    于是,我们可以利用幂运算的性质,即:

a 1 × a 1 = a 2 a 2 × a 2 = a 4 ⋮ a 32 × a 32 = a 64 a^1\times a^1=a^2\\ a^2\times a^2=a^4\\ \vdots\\ a^{32}\times a^{32}=a^{64} a1×a1=a2a2×a2=a4⋮a32×a32=a64

​ 这样的话,我们只需要算 6 次就可以快速得到结果了,我们通过一个翻倍 的过程,将时间复杂度直接从 O ( n ) O(n) O(n) 优化到了 O ( log ⁡ N ) O(\operatorname{log}N) O(logN)。

  • level 3

    但是, a 64 a^{64} a64 这个例子有点特殊了,我们发现它的指数,也就是 a n a^n an 中的 n n n,恰好是 2 的幂。那如果不是呢?比如 a 105 a^{105} a105。

    注意到,虽然 105 不是 2 的幂,但是它可以拆解成几个 2 的幂的和的形式:
    105 = 1 + 8 + 32 + 64 105=1+8+32+64 105=1+8+32+64

    于是,通过指数运算的性质,我们有:
    a 105 = a 1 + 8 + 32 + 64 = a 1 × a 8 × a 32 × a 64 \begin{aligned} a^{105}&=a^{1+8+32+64}\\ &=a^1\times a^8\times a^{32}\times a^{64}\end{aligned} a105=a1+8+32+64=a1×a8×a32×a64

    这样的话,我们就可以利用 level 2 中计算出的结果来求出 a 105 a^{105} a105 了,那么我们是如何得到 105 = 1 + 8 + 32 + 64 105=1+8+32+64 105=1+8+32+64 的呢?

  • level 4

    这时候就要用到我们的二进制 了,我们高中的时候都学过一个数表示成二进制的转换方法。例如:二进制数 1011,它的值在 10 进制中可以表示为 1 × 2 0 + 1 × 2 1 + 0 × 2 2 + 1 × 2 3 1\times2^0+1\times2^1+0\times2^2+1\times2^3 1×20+1×21+0×22+1×23,也就是 11。

    通过这个转换方法,我们就可以把 a a a 的指数 n n n 转换成为 2 k 2^k 2k 的序列之和的形式。具体这个 2 k 2^{k} 2k 是乘 0 还是乘 1,这就看对应的二进制位上是 0 还是 1 了。

以上的过程就是倍增思想中的快速幂算法。


2. 模运算的性质与技巧

上面的快速幂算法从时间上 解决了计算某一个数的某次方的过程,但是如果的底数和指数都非常大,最终结果连 long long 都存不下的时候,我们往往会对结果进行取模 ,也就是通常题目会让你计算 a b   m o d   p a ^ {b}\bmod p abmodp 的结果。 但是我们在计算 a b a^{b} ab 的过程中就有可能超出存储范围,这个时候我们不能等到算完再取模,而是要在算的过程中边算边取模。下面介绍模运算的几个性质:

(1)当计算过程中只有 "加法" 和 "乘法" 的时候,取模可以放在任意的位置。

也就是说,如果我们计算
( a × b × c × d )   m o d   p (a\times b \times c \times d) \bmod p (a×b×c×d)modp

时,它的结果等同于
( ( ( ( ( a × b )   m o d   p ) × c )   m o d   p ) × d )   m o d   p (((((a\times b)\bmod p) \times c)\bmod p) \times d) \bmod p (((((a×b)modp)×c)modp)×d)modp

也等同于
( ( a   m o d   p ) × ( b   m o d   p ) × ( c   m o d   p ) × ( d   m o d   p ) )   m o d   p ((a\bmod p) \times (b\bmod p) \times (c\bmod p) \times (d\bmod p)) \bmod p ((amodp)×(bmodp)×(cmodp)×(dmodp))modp
(2)当计算过程中存在减法时,结果可能是负数,此时如果需要补正则需要使用 "模加模" 的技巧。

也就是当我们计算
( a − b )   m o d   p (a-b)\bmod p (a−b)modp

时, b b b 有可能大于 a a a,此时结果是一个负数,那么我们可以对 a − b a - b a−b 先模 p p p,再加上一个 p p p 变成正数,再取模:
( ( a − b )   m o d   p + p )   m o d   p ((a - b)\bmod p + p)\bmod p ((a−b)modp+p)modp


3. 【模板】快速幂 ⭐

【题目链接】

P1226 【模板】快速幂 - 洛谷

【题目描述】

给你三个整数 a , b , p a,b,p a,b,p,求 a b   m o d   p a^b \bmod p abmodp。

【输入格式】

输入只有一行三个整数,分别代表 a , b , p a,b,p a,b,p。

【输出格式】

输出一行一个字符串 a^b mod p=s,其中 a , b , p a,b,p a,b,p 分别为题目给定的值, s s s 为运算结果。

【示例一】

输入

复制代码
2 10 9

输出

复制代码
2^10 mod 9=7

【说明/提示】

样例解释

2 10 = 1024 2^{10} = 1024 210=1024, 1024   m o d   9 = 7 1024 \bmod 9 = 7 1024mod9=7。

数据规模与约定

对于 100 % 100\% 100% 的数据,保证 0 ≤ a , b < 2 31 0\le a,b < 2^{31} 0≤a,b<231, a + b > 0 a+b>0 a+b>0, 2 ≤ p < 2 31 2 \leq p \lt 2^{31} 2≤p<231。

cpp 复制代码
#include<iostream>

using namespace std;

typedef long long LL;

LL a, b, p;

// 快速幂模板
LL q_pow(LL a, LL b, LL p)
{
    LL res = 1;
    while(b)
    {
        // 如果指数对应的二进制的当前位为 1
        if(b & 1) res = res * a % p;
        a = a * a % p;
        b >>= 1;
    }
    return res;
}

int main()
{
    cin >> a >> b >> p;
    
    printf("%lld^%lld mod %lld=%lld", a, b, p, q_pow(a, b, p));

    return 0;
}

二、六十四位整数乘法 ⭐

【题目链接】

P10446 64位整数乘法 - 洛谷

【题目描述】

求 a a a 乘 b b b 对 p p p 取模的值。

【输入格式】

第一行输入整数 a a a,第二行输入整数 b b b,第三行输入整数 p p p。

【输出格式】

输出一个整数,表示 a*b mod p 的值。

【示例一】

输入

复制代码
3
4
5

输出

复制代码
2

【说明/提示】

1 ≤ a , b , p ≤ 1 0 18 1 \le a,b,p \le 10^{18} 1≤a,b,p≤1018


1. 解题思路

这道题与快速幂算法思路几乎一样, a × b a\times b a×b 本质上就是 b b b 个 a a a 相加。直接相乘或者先模再乘都是会溢出的,我们也不可能真的就写一个循环来循环 b b b 次,这个时候就要用到倍增的思想。

一个数通过它的二进制可以表示成为多个 2 的幂相加的形式,比如
13 × 11 = 13 × ( 1 × 2 0 + 1 × 2 1 + 0 × 2 2 + 1 × 2 3 ) = 13 × 1 + 13 × 2 + 13 × 0 + 13 × 8 \begin{aligned} 13 \times 11 &= 13 \times (1\times2^0+1\times2^1+0\times2^2+1\times2^3)\\&= 13 \times 1 + 13 \times 2 + 13 \times 0 + 13 \times 8 \end{aligned} 13×11=13×(1×20+1×21+0×22+1×23)=13×1+13×2+13×0+13×8

那么这个本来需要循环 11 次的运算就变成了只需要 4 次,大大降低了时间复杂度。原理就是把 "累加次数" 不断加倍,这样计算的话,不但时间复杂度低,边计算边取模就不会溢出了。


2. 代码实现

cpp 复制代码
#include<iostream>

using namespace std;

typedef long long LL;

LL a, b, p;

LL solve(LL a, LL b, LL p)
{
    LL res = 0;
    while(b)
    {
        // 如果 b 对应的二进制当前位为 1
        if(b & 1) res = (res + a) % p;
        a = (a + a) % p;
        b >>= 1;
    }
    return res;
}

int main()
{
    cin >> a >> b >> p;

    cout << solve(a, b, p) << endl;

    return 0;
}
相关推荐
愚润求学9 分钟前
【动态规划】01背包问题
c++·算法·leetcode·动态规划
会唱歌的小黄李28 分钟前
【算法】贪心算法入门
算法·贪心算法
轻语呢喃1 小时前
每日LeetCode : 两数相加--链表操作与进位的经典处理
javascript·算法
钢铁男儿1 小时前
C# 接口(接口可以继承接口)
java·算法·c#
zl_vslam2 小时前
SLAM中的非线性优化-2D图优化之激光SLAM cartographer前端匹配(十七)
前端·人工智能·算法
dying_man3 小时前
LeetCode--44.通配符匹配
算法·leetcode
Paper Clouds4 小时前
代码随想录|图论|15并查集理论基础
数据结构·算法·leetcode·深度优先·图论
Paper Clouds4 小时前
代码随想录|图论|14有向图的完全可达性
数据结构·算法·深度优先·图论·宽度优先
遇见尚硅谷4 小时前
C语言:游戏代码分享
c语言·开发语言·算法·游戏
不喜欢学数学er4 小时前
第二十八天:贪心算法part02(第八章)
算法·贪心算法