C++算法进阶系列之倍增算法解决求幂运算

news2024/12/23 12:51:50

1. 引言

学习倍增算法,先了解什么是倍增以及倍增算法的优势。如果面前有一堆石子,要求计算出石子的总数量。

这是一个简单的数数问题,可以:

  • 一颗石子一颗石子的数。
  • 两颗石子两颗石子的数。
  • 三颗石子三颗石子的数。
  • 或者更多颗石子更多颗石子的数……

在石子很多的情况下,每一次选择更多石子的方式数,毫无疑问可以快速得到最后的结果,倍增算法便是基于这种数数的理念。

但是,倍增算法不是以固定的数量来数,而是以 2 的倍数来数,如先数2个、再数 4个、再数8个……这种等比数列的方式递增前进。

为什么选择 2 的倍数递增?

于底层逻辑而言,计算机使用二进制存储数据,计算机处理十进制数字时,会把数字转换成二进制形式。

如十进制 17 的二进制为 10001,以 2 为底数展开为 1X24+0X23+0X22+0X21+1X20=1X16+0X8+0X4+0X2+0X1。如果不考虑01的系数,任何一个表达式都可以表示抽象成形如1+2+4+8+16……的样子,也就是任何一个数字都可以表示成 2 的倍数累加。

17=16+1;13=8+4+1;……

倍增算法常用于快速幂、求LCA(最近公共祖先)、O(1)求区间极值、求后缀数组……等一系列问题。倍增思想的核心就是通过预处理规模为幂次大小的子问题,然后将原始问题看作成这些子问题的合并。

2. 快速幂

快速幂的问题并不复杂,但是可以帮助我们理解倍增算法的基本思想。

问题描述:

假如要求an(an次幂),n可以是一个较大的正整数。此题本质是累乘问题,即进行n次乘a的计算,时间复杂度是O(n)级别的。如下代码使用暴力方案实现累乘过程。

#include <iostream>
using namespace std;
int main(int argc, char** argv) {
	int a,n;
	cin>>a>>n;
	//初始化累乘结果 
	int res=1;
	for(int i=0;i<n;i++){
		res*=a;
	}
	cout<<res<<endl;
	return 0;
}

居于对求幂运算的数学运算法则的认知,根据其法则,可以改善方案,提升性能。

2.1 转换一

可以对 an 进行如下转换:

  • n 为偶数时, an=(an/2)2
  • n 为奇数时, an=(an/2)2*an%2

转换法则是典型的递归套路,如下代码是使用递归算法的具体实现。

#include <iostream>
using namespace std;
long long int ksm(int  a,int n) {
	long long int ans=1;
	if(a==1)return 1;
	if(n==1)return a;
	ans=ksm(a ,n/2);
	ans*=ans;
	if(n%2==1) //如果是奇数
		ans*=a;
	return ans;
}
int main(int argc, char** argv) {
	int a,n;
	cin>>a>>n;
	long long int ans=ksm(a,n);
	cout<<ans;
	return 0;
}

现分析一下上述实现的时间复杂度。

现假设 a=2,n=10,其递归调用过程如下图所示。

1.png

递归算法的时间复杂度计算公式:基函数的时间复杂度X递归的深度。ksm函数的时间复杂度为O(1),递归次数为logn,最终时间复杂度为O(logn)。递归调用过程中,回溯结果都是以 2 的幂次方返回。

2.2 转换二

假设 a=2,n=10,因为10的二进制1010,所以,a10 可以写成 a1010,而 1010=1X23+0X22+1X21+0X20。故 a10 也可以写成 a1X23+0X22+1X21+0X20 ,根据幂运算法则: a1X23+0X22+1X21+0X20 = a1X23 * a0X22* a1X21* a0X20=a8*a2

为了研究这个表达式的通用含义,假设 n=255,其二进制值为 11111111 ,目的是抛开 01系数的干扰抽象出通用表达式,便于寻找其规律:

a255=*a128*a64*a32 *a16*a8*a4*a2*a1

抽象出来的表达式本质还是累乘操作,累乘的次数由 n 转化为二进制后的位数决定。如 n=255 ,二进制位数为8,累乘次数即为 8,其结果为 log255的值。如果是暴力迭代相乘,a255 需要迭代255次,幂化后的表达式,迭代只需要8次,时间复杂度由原来的 O(n)提升到了O(logn)

在整个累乘表达式中,且满足如下的迭代关系:

  • a1=a。
  • a2=a1*a1
  • a4=a2*a2
  • a8=a4*a4
  • ……
  • a128=a64*a64

抽象化表达式时,选择了一个很特殊的数字 255,迭代 8 次,且最终结果是这 8 次迭代的累乘。如果 n=10,其二进制为 1010,转换后的表达式应该为a10= a8 * a2,需要漏掉 a1和a4的值。

根据前面的迭代推导关系,a8 的值由 a4决定,虽然 a4 的值不是最终结果中的一部分,但是推导过程不能省略。

其求值过程应该如下:

  • 求解出 a1的值: a1=a。因为 1010的最后一位为 0,a1 的值不累乘到最终结果中。因为要使用 a1 推导出 a2 的值,前面 a1 的值需要存储,在整个求值过程,可以设置 2 个变量,一个用来存储一路推导出来的值,即ai(i=1,2,4,8),一个用来存储最终结果值。
//存储迭代值
int base =a;
//存储最终结果值
int res=1;
if( 1010 的最后一位是 0 )
	base 的值不能被累乘到 res 中。
  • 第二次迭代,base=base*base,迭代出 a2 的值,因为1010的倒数第 2 位的值为 1。把此次 base 中的值累乘到 res
//第二次迭代
base *=base;
if( 1010的倒数第二位是 1 )
    把 base 的值累乘到 res 中。
    res*=base
  • 第三次迭代,base=base*base,迭代出 a4 的值,因为 1010的倒数第 3 位的值为 0,此次迭代的值不能累乘到 res 中。
//第三次迭代
base *=base;
if( 1010的倒数第三位是 0 )
    base 中的值不能累乘到 res 中。
  • 第四迭代,base=base*base,迭代出 a8 的值,因为1010的倒数第 1 位的值为 1。把此次 base 中的值累乘到 res
//第四次迭代
base *=base;
if( 1010的倒数第四位是 1 )
    base 值需要累乘到 res 中。
     res*=base

四次后,整个求解过程结束。从上面的分析可知:

  • 指数转化成二进制后,二进制有多少位,需要迭代多少次。
  • 每次迭代都会得到一个中间值(存储在 base中),至于此值需不需要累乘到最终结果中,则需要根据二进制中的 01决定。如果是 0则不需要,1则需要。

有了基本的流程思路就可提供具体的操作。

一种方案是把指数转换的二进制以字符串类型存储。

#include <iostream>
#include <stack>
using namespace std;

/*
* 将数字转换成二进制字符串
*/
string binary(int num) {
	string s="";
	while( num>0 ) {
		s+=num % 2+'0';
		num=num/2;
	}
	return s;
}
/*
*快速幂
*/
int ksm(int a,int n) {
	string s= binary(n);
    //字符串二进制的长度
	int len=s.length();
    //初始化推导数
	int base=a;
    //结果
	int res=1;
	if(s[0]=='1')
        //如果最后一位是 1 ,则初始化为推导数
		res*=base;
	for(int i=1; i<len; i++) {
		base*=base;
		if(s[i]=='1')res*=base;
	}
	return res;
}

int main(int argc, char** argv) {
	int a,n;
	cin>>a>>n;
	int res= ksm(a,n);
	cout<<res;
	return 0;
}

上述算法方案虽然可行,但是,很不优雅。快速幂的本质是累乘,其解决过程并不难,但是,算法中会有一个分支,就是需要根据二进制某位的值是 0 还是 1 决定是否累乘到结果中,所以,解决本题目的关键就是在每一次迭代过程中如何检查二进制的某位的值是0还是1

凡是涉及到二进制操作时,第一想法便是使用位运算符。此题使用 &(与运算)>>(右移运算)便可解决。其流程如下:

  • 每次迭代时,把n=10101相与(n&1),可判断最后一位是是 1还是 0。位运算法则,11相与为101相与为0
  • 当前迭代结束时,把n=1010向右移一位(n>>1),则 n=101。为下一次迭代做准备。
  • 重复上述两步,直到 n=0
//使用位运算符实现快速幂
int ksm(int a,int n) {
	int base=a;
	int res=1;
	while(n>0) {
		if(n & 1==1 ) {
			//如果 n 最后一位是 1
			res*=base;
		}
		base*=base;
		//右移
		n=n>>1;
	}
	return res;
}

第二种方案的代码量明显少于第一种方案,且没有改变数据本身的类型。时间复杂度都是O(logn)

3. 前缀和

给定一个长度为 N 的数列 A ,然后进行若干次查询 , 每一次给定一个整数 T , 求出最大的 k , 满足 A[1]+A[2]……A[K]<=T。

第一种解题方案,先对数列进行前缀和预处理,然后使用二分查找算法。

  • 对原数列预处理,求出前缀和。

2.png

  • 如输入 T=22,求 k。可使用二查找算法。

3.png

编码实现:

#include <iostream>
#include <stack>
using namespace std;
int main() {
	//原数组
	int nums[8]= {4,1,7,3,5,6,2,8};
	//前缀和数组
	int sum[8]= {0};
	for(int i=0; i<8; i++) {
		if(i==0)sum[i]=nums[i];
		else sum[i]=sum[i-1]+nums[i];
	}
	int t,k;
	cin>>t;
	int left=0,right=7;
	int midPos,midVal;
	//二分查找
	while(left<=right) {
		midPos=(right+left)>>1;
		midVal=sum[midPos];
		if( midVal<=t )left=midPos+1;
		else right=midPos-1;
	}
	k=right;
	cout<<"最大的位置:"<<k<<endl;;
	return 0;
}

二分查找的时间复杂度为O(logn)

也可以使用倍增算法实现查找K的过程。

#include<iostream>
using namespace std;
int main() {
	//原数组
	int nums[8]= {4,1,7,3,5,6,2,8};
	//前缀和数组
	int sum[8]= {0};
	for(int i=0; i<8; i++) {
		if(i==0)sum[i]=nums[i];
		else sum[i]=sum[i-1]+nums[i];
	}
	int k=0,i=1,s=0,t;
	cin>>t;
	while(i!=0) {
		if(s+sum[k+i]-sum[k]<=t && sum[k+i]-sum[k]>0) {
			s+=sum[k+i]-sum[k];
			k+=i;
			i<<=1;
		} else i>>=1;
	}
	printf("%lld\n",k);
	return 0;
}

4. 总结

倍增算法在现实生活也经常看到,工人搬运货物时,通过每一次搬运尽量多的货物达到快速搬完所有货物的工作。倍增算法的应用领域还较多,后文再续。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1006674.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

一志愿复录比接近1:1,计算机专业招生名额近百人,杭州师范大学考情分析

杭州师范大学 考研难度&#xff08;☆☆&#xff09; 内容&#xff1a;23考情概况&#xff08;拟录取和复试分析&#xff09;、院校概况、23初试科目、23复试详情、各专业考情分析、各科目考情分析。 正文893字预计阅读&#xff1a;3分钟 2023考情概况 杭州师范大学计算机相…

TRICONEX 4351B数字量输入模块

TRICONEX 4351B是一种数字量输入模块&#xff0c;通常用于工业控制和安全系统中。这个模块的主要功能和特点可能包括以下方面&#xff1a; 数字量输入&#xff1a;4351B模块允许连接多个数字量输入信号。这些输入通常用于监测开关状态、传感器信号或其他数字逻辑信号。 高密度…

FPGA-结合协议时序实现UART收发器(一):UART协议、架构规划、框图

FPGA-结合协议时序实现UART收发器&#xff08;一&#xff09;&#xff1a;UART协议、架构规划、框图 记录FPGA的UART学习笔记&#xff0c;以及一些细节处理&#xff0c;主要参考奇哥fpga学习资料。 本次UART主要采用计数器方法实现&#xff0c;实现uart的稳定性发送和接收功能…

利用微信二维码来实现中秋节快乐

环境准备&#xff1a; 1、python环境&#xff1b; 2、微信公众号申请&#xff1b; 实现思路是&#xff0c;将微信公众号的中秋节快乐的页面链接&#xff0c;隐藏到二维码里面&#xff0c;如果你发送的对方扫描了这个二维码&#xff0c;就会弹出对应的中秋节祝福页面。(*^▽^*…

【送书活动】畅销书《Kali Linux高级渗透测试》更新版速速查收~

文章目录 每日一句正能量前言本书概况读者对象赠书活动目录 每日一句正能量 其实&#xff0c;人生很多东西无所谓最好的&#xff0c;只要你认为值得就是最好。 前言 对于企业网络安全建设工作的质量保障&#xff0c;业界普遍遵循PDCA&#xff08;计划&#xff08;Plan&#xf…

【广州华锐互动】煤矿提升机作业VR互动实训平台

在煤矿行业中&#xff0c;安全性是无可忽视的首要任务。传统的煤矿工人培训方法&#xff0c;如理论课堂讲解、实地操作演示&#xff0c;尽管具有一定的效果&#xff0c;但往往无法真实地模拟出煤矿的复杂环境&#xff0c;工作人员在没有真正接触煤矿的情况下&#xff0c;很难理…

【web开发】7、Django(2)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 一、部门列表二、部门管理&#xff08;增删改&#xff09;三、用户管理过渡到modelform组件四、modelform实例&#xff1a;靓号操作五、自定义分页组件六、自定义有…

OpenCV(四十):图像分割—漫水填充

1.漫水填充原理 图像分割中的漫水填充&#xff08;Flood Fill&#xff09;算法是一种基于区域增长的像素分类方法。其原理是在图像中从种子点开始&#xff0c;逐渐向周围扩展&#xff0c;并根据一定的条件决定是否将相邻的像素归属于同一区域。 漫水填充的基本原理如下&#x…

香港银行开户内地见证流程

香港公司内地见证开户流程&#xff1a; 资料准备——银行进行资料预审——预审通过&#xff0c;预约面谈&#xff08;确定面谈时间以及在内地指定城市的分行进行面谈&#xff09;——携带齐全资料至内地指定城市分行&#xff0c;在当地银行职员的见证下签署资料——面谈通过&a…

python-爬虫-三字代码网站爬取

三字代码 http://www.6qt.net/ 爬取城市、三字代码、所属国家、国家代码、四字代码、机场名称、英文名称、查询次数 import requestsurl http://www.6qt.net/ r requests.get(url) r.encodinggb2312 print(r.text)使用xpath解析&#xff0c;得到城市名 html.fromstring(html…

管理固定资产怎么界定优化

固定资产的管理和利用是至关重要的一环。然而&#xff0c;如何准确地界定和管理这些资产&#xff0c;以实现最大的效益&#xff0c;却是一个需要深思熟虑的问题。本文旨在探讨行政管理中固定资产的界定方法以及如何进行优化管理。  我们需要明确固定资产的概念。固定资产是指…

ABB AV94a控制模块

多功能性&#xff1a; 控制模块通常设计为多功能设备&#xff0c;可以执行各种控制任务&#xff0c;包括监测传感器数据、执行逻辑操作、生成输出信号等。 可编程性&#xff1a; 许多现代控制模块都具有可编程功能&#xff0c;使用户能够根据需要自定义其行为&#xff0c;从而…

深刻理解Java中方法调用的参数传递

Java方法调用的参数传递 首先给结论&#xff1a;Java中均为值传递。 下面通过概念分析代码示例的方式&#xff0c;实现深刻理解值传递的含义&#xff0c;避免死记硬背。 Java的两种数据类型 基本数据类型&#xff0c;比如int&#xff0c;double&#xff0c;boolean等&#x…

flex布局实现 内容区域高度自适应

如果可以实现记得点赞分享&#xff0c;谢谢老铁&#xff5e; 一、背景说明 对于纵向排列布局&#xff0c;且上中下个个模块都是自动高度。当我们针对中间部分需要自适应高度且进行滚动时&#xff0c;那我们就可以用flex: 1 来处理。 二 、先看效果图 二 、flex布局 <!DO…

如何使用CMD恢复删除的分区?

分区删除后可以恢复吗&#xff1f; 磁盘分区旨在二级存储上创建一个或多个区域&#xff0c;然后你可以单独管理每个区域&#xff0c;这些区域就是分区。因此&#xff0c;对新安装的存储设备进行分区是很重要的环节&#xff0c;只有分区后才可以在这些设备上创建文件并保存数…

unity 使用声网(Agora)实现语音通话

第一步、先申请一个声网账号 [Agora官网链接]&#xff08;https://console.shengwang.cn/&#xff09; 第二步在官网创建项目 &#xff0c;选择无证书模式&#xff0c;证书模式需要tokenh和Appld才能通话 第三步 官网下载SDK 然后导入到unity&#xff0c;也可以直接在unity商店…

云链商城连锁门店新零售O20系统以零售商城

云链商城连锁门店新零售O20系统以零售商城、门店收银、多渠道进销存、客户管理、互动营销、导购助手、多种奖励模式和数据分析等功能&#xff0c;赋能多品牌连锁门店实现线上线下商品、会员、场景的互联互通&#xff0c;助推企业快速实现营销、服务、效率转型升级&#xff0c;为…

可编程交易区块为DeFi机器人提供强大动力

对于选择基金投资的人来说&#xff0c;一个基本指导原则就是寻找那些管理费最低的基金。资本应该是在运转&#xff0c;而不是用于支付费用。同样&#xff0c;Mysten Lab的Capy交易机器人利用可编程交易区块&#xff08;Programmable Transaction Blocks &#xff0c;PTBs&#…

文章生成器在线使用-自动生成文章的工具

大家好啊&#xff0c;今天我要和大家聊聊一个非常热门的话题——在线文章生成器。主要是帮助我们解决写作困扰&#xff0c;节省大量的时间和精力。我也常遇到常为了写一篇好文章而愁眉苦脸呢&#xff0c;我测试过可以帮助我们生成优质的文章&#xff0c;确实让我们的写作变得简…

1. 微信公众号申请加认证

文章目录 微信公众号申请流程指引微信公众号申请流程注册微信公众号申请完成银行卡账号验证 如何查询微信公众号审核通过登录微信公众号平台后&#xff08;如下图&#xff09;致电微信客服热线 微信公众号认证流程指引微信公众号认证流程选择 微信认证/开通选择验证方式。填写微…