解题索引:
1.第一类:二分快速幂的pow形式(由朴素到快速)
第一类:求pow(a,b)
已知两个正整数,a,b(a,b<=10^5)
求:f(a,b) =a^b
【1】 一步步带你认识二分快速幂
解法一:朴素解法:
C++/C代码实现:
int f(int a,int b){
int jie=1;
while(b--) ans*=a; //循环b次,每次jie乘上一个a
return jie;
}
- 暂不考虑溢出问题,仅就速度来看:
f(2,10)=1024
f(2,30)=1,073,741,824
f(2,100)=1,267,650,60,228,229,401,496,703,205,376
f(2,100000000000)=……………………
- 很容易发现,这段算法的时间复杂度为O(n),同时,我们发现一些特点
- 求a^b过程中不断对a进行重复的乘b
- 该问题中具有很相似的子问题
- 线性的重复?很容易想到二分的思想
- 可不可以将a^b二分成多个子问题,逐一破解
- 首先:让我们看看一个简单的例子:5^16,你会怎么求?
-
- 第一步: 看到一个5——没必要没必要算
-
- 第二步: 算出5^2 =5 X 5
-
- 第三步: 算出5^4= (5^2) X(5^2)
-
- 第四步: 算出5^8 = (5^4) X (5^4)
-
- 第五步: 算出5^16 = (5^8) X (5^8)
- 仅仅算了4次,对比原来的16缩了三倍!
- 假如b再大一点,为1000000000……,用二分,我们则可以直接少算b-log(b)次,快了不知多少倍
- 遇到奇数,我们只需先将以算出的结果X a,再继续二分即可
看到这里,理解了5^16(/17)的🌰祝贺你,已经弄懂了二分快速幂。
【2】pow形式二分快速幂的C/C++模板
typedef long long ll;//命别名,方便书写
ll qspow(ll a,ll b){ //递归写法
if(b==0)return 1;
ll res=qspow(a*a,(b>>1)); //对正整数右移,等效于/2
if(b&1) res=res*a; //b&1,b为奇数则结果为true
return res; //为偶数则为false
}
typedef long long ll;
ll qspow(ll a,ll b){ //非递归写法
ll res=1;
while(b){
if(b&1){
res=res*a;
}
a=a*a;
b>>=1;
}
return res;
}
2.第二类:二分快速幂的横幂运算形式(深入研究)
第二类:横幂运算
**已知三个正整数,a,b,c(a,b,c<=10^5),求:
f(a,b,c)=a^b mod c
(mod,在代码中表示为%)
朴素解法:
long long f(long long a,long long b, long long c){
a=(a%c+c)%c;//🐉对a为负数的情况处理,等价转换,
long long jie=1%c;
while(b--) jie=jie%c*a; //先模再乘,保证每次运算后的结果小于模数
return jie;
}
补充:
- 这里求a的b次幂(次方)对c取余数的计算,被称为横幂运算。
- 模乘的性质∨
- ab mod c= (a mod c)(b mod c) mod c
- 证明思想:记a= r_a +m_a * c;b= r_a + m_a * c,代入左式得ab mod c=r_a*r_b mod c即右式
有了这个性质,我们发现,求横幂运算与第一类无本质差别,只要记得在第一类的每次乘法后%c即可,写成数学表示如下
【1】 横幂运算式的二分快速幂,代码模板C/C++
typedef long long ll;//对第一类稍加改动即可
ll qspowmod(ll a,ll b,ll c){ //递归写法
if(b==0)return 1%c;
ll res=qspowmod(a*a%c,(b>>1),c);
if(b&1) res=res*a%c;
return res;
}
typedef long long ll;
ll qspowmod(ll a,ll b,ll c){ //非递归写法
ll res=1%c;
a%=c; //先把a调小一点
while(b){
if(b&1){
res=(res*a)%c;//❤1️⃣防溢出:可改为res=(res%c)*(a%c)%c
}
a=(a*a)%c;//❤2️⃣防溢出:可改为a=(a%c)*(a%c)%c
b>>=1;
}
return res;
}
注:进行多次a%c与res%c的目的是防止溢出,这是横幂运算的关键一点,尽可能的把转化到更小的乘积结果
【2】 极致:横幂运算式的二分快速幂,再进一步防溢出,优化。
typedef long long ll;
ll qs_mul(ll a,ll b, ll c){ //对乘法优化,防止相乘时所乘结果溢出
ll jie=0;
while(b){
if(b&1)jie=(jie+a)%c;
b>>=1;
a=(a+a)%c; //用二分的加法代替乘法
}
return jie;//保证所乘结果永远小于c,防爆!
}
ll qs_pow(ll a,ll b,ll c){//将原来的乘法都用上面的函数代替
ll res=1;
while(b){
if(b&1)res=qs_mul(res,a,c);
b>>=1;
a=qs_mul(a,a,c);
}
return res;
}
3.分治(二分)思想的体现
- 任何可以用计算机求解的问题所需的计算时间都与其规模有关。问题越小,也越容易处理
- 分治法的设计思想是,将一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破,分而治之。
- 这里,每步二分,将原问题分解成两个子问题,再反复二分,最终将原问题分解成若干个子问题。使得子问题与原问题类型一致而规模却不断缩小,就很容易求出子问题的解,再将各子问题的解综合,就是原问题的解了。
- 分治过程——|
- 原问题是求a^ b,子问题是求a^ n,(n<b),最后合并问题解,由a^1递推知a ^ b.