目录

前言

一、快速排序法及其扩展

快速排序法

介绍

思路 + 步骤

模拟代入

模板

练习

扩展(求第k个数)

思路

代码

二、归并排序法

归并排序

思路

思路 + 步骤

模拟代入

模板

练习

应用(逆序对的数量)

介绍

思路

模拟代入

模板

练习

三、二分

整数二分

大致步骤

详细步骤(两模板)

模板

模拟代入

练习

实数二分

介绍

练习

四、高精度算法

介绍

高精度加法

不压位步骤

压位步骤

练习

高精度减法

介绍

练习

高精度乘法

高精度乘以低精度

高精度乘以高精度

高精度除法

高精度除以低精度

高精度除以高精度

五、前缀和与差分

前缀和

介绍+思路

模板

练习

扩展

思路

练习


前言

在学习完C语言后,并刷了许多道题发现刷题暂时不适合我了,应该去学习新的知识点于是开始学习y总的算法基础课。经过了一个月,差不多已经弄懂了第一讲的内容,特来向大家分享,可能有错误之处,希望大家见谅!

之前在公众号上发过这样的文章,但是公众号没有目录,也没人看,就专门来CSDN上发。感兴趣的可以关注我的公众号:阿辉的大本营。每天会分享一道算法题,感兴趣的可以关注一下公众号!!!

一、快速排序法及其扩展

快速排序法

介绍

快速排序法的核心思想分治。

分治就是把一个大问题分为两个或以上的相同子问题,再把子问题分为更小的子问题...直到子问题可以直接简单求解为止,这是原问题的解就是子问题解之和

快速排序是通过使用分治法策略把一个串行分为两个子串行。快速排序又是分而治之排序算法上的典型应用。

思路 + 步骤

1、取基准点

在这个数组里面取一个点作为基点,可以取左端点右端点、中间值随机取

2、划分区间

遍历整个数组,把小于基准点的放在左边,大于基准点的放在右边。传统方法是:开两个数组,分别把元素存里面。这里使用双指针算法,定义两个指针,分别从两边往中间走,当满足条件时就继续走,一个不满足条件时就停止原地,等待另一个指针不满足条件,之后两个指针指向的元素互换;再接着往下走,直至走到基准点!!!

3、递归排序左右两边

这一步也是和上面一样,把小于基准点的区间和大于基准点的区间再次重复1、2操作。当递归结束,数组就已经排好序了。需要注意的是,递归函数的退出。递归排序区间时,到最后区间元素个数就为1,那这时候怎样退出递归呢?​需要在quick_sort()函数里面写个判断条件,当区间元素个数为1时,就退出。

模拟代入

现在上一个实例,来给大家模拟一下快速排序法的过程,帮助大家理解!!!

如果题目给了我们一个数组,让我们对这个数组进行排序。这个数组为{1, 7, 5, 4, 2, 6, 3}。

1、取基准点

可以随便取,也可以去左右端点,也可以取中间值

2、划分区间 

区间是通过双指针算法来划分的,让一个指针从数组最前面的元素的前一位另一个指针从数组的最后一位元素的下一位 同时往中间走。然后判断是否满足条件

 开始

两个指针先往中间走一位

此时,i指向的元素小于基准点,j指向的元素小于基准点。然后 j指针跳出do while循环(不满足do while循环条件)j = 6。但是i指针还是支持往后走 。还没有轮到后面的if(i < j)语句执行

​此时 i 指针指向的元素大于基准点,不满足do while循环条件,跳出do while循环,i = 2;现在两个指针都跳出了 do while循环,可以执行if语句了,正好条件为真,于是这两个指针指向的元素互换

然后 i 指针和 j 指针继续进入do while循环

此时 i 指针指向的元素大于基准点,i 跳出do while循环,i = 2​;但是 j 指针指向的元素大于基准点,继续往中间走。不执行if语句

此时 j 指针指向的元素小于基准点,跳出do while循环,j = 4;执行if语句,正好满足条件,就两个指向的元素进行互换

互换后,两个指针1继续往中间走,走到了基准点,while循环结束,代表区间划分已经结束了,左边的区间都小于基准点,右边的区间都大于基准点

3、递归排序左右区间

就是重复以上的1、2步操作,就是区间变小了而已

最后再来一个动图,再帮大家理解一下(动图来自菜鸟教程)

 模板

void quick_sort(int nums[],int l,int r)
{if(l >= r)//判断是否只有一个元素或者没有元素return;//如果没有直接退出函数int i = l - 1,j = r + 1,x = nums[(l + r) / 2];//i取左端点的的前面一位,j去右端点的右边一位;方便使用do while循环
//先往前走一位再判断,如果不这样,会把第一位和最后一位漏掉。基准点随便取while(i < j){do i++;while(nums[i] < x);//当满足小于基准点时,继续往下走do j--;while(nums[j] > x)//与i一样if(i < j)//当i和j跳出do while循环,说明都不满足条件,需要互换{swap(nums[i],nums[j]);//这个是头文件为algorithm的标准库函数,如果没有的话这样写//int t = nums[i];//nums[i] = nums[j];//nums[j] = t;}}//经过上面循环操作,已经划分好区间了,下面就是递归排序左右区间了quick_sort(nums,l,j);//递归排序左区间quick_sort(nums,j + 1,r);//递归排序右区间
//当基准点为数组中间元素,必须这样写,不然排序错误
}

练习

学习了以上内容后,快来试一下模板好用不好用吧!练习一道小题

看完题了吧,很简单的。下面是题解

#include <iostream>
#include <algorithm>using namespace std;const int N = 1e5 + 10;
int nums[N];void quick_sort(int nums[],int l,int r)
{if(l >= r)return;int i = l - 1,j = r + 1,x = nums[(l + r) / 2];while(i < j){do i++;while(nums[i] < x);do j--;while(nums[j] > x);if(i < j)swap(nums[i],nums[j]);}quick_sort(nums,l,j);quick_sort(nums,j + 1,r);
}int main()
{int n;cin >> n;for(int i = 0;i < n;i++)cin >> nums[i];quick_sort(nums,0,n - 1);for(int i = 0;i < n;i++)cout << nums[i] << ' ';return 0;
}

扩展(求第k个数)

学习完快速排序法后,我们应该知道快速排序法并不是只能超快排序的,还能进行一些操作,比如快速找到从小到大的第k个数​。趁热打铁,赶紧来讲一下怎么可以快速选出第k​小的数。

思路

让我们来回忆一下快速排序法的步骤。第一步:取基准点。第二步:划分区间。第三步:递归排序左右两边。找到第k小的数,只需要改变第三步。为什么呢?题目没有让我们排序,只是让我们找到第k小的数,只需要比较一下左区间的元素个数(sl) 与 k的大小,如果sl大于k,就说明答案在左区间,递归排序左边;反之答案在右区间,就递归排序右边。注意:此时区间元素个数为1时,要返回这个元素​。

代码

#include <iostream>
#include <algorithm>
​
using namespace std;
​
const int N = 1e5 + 10;
int nums[N];
​
int quick_sort(int nums[],int l,int r,int k)
{if(l >= r)return nums[l];//返回nums[r]也是这个结果,因为递归结束时,i = jint i = l - 1,j = r + 1,x = nums[(l + r) / 2];while(i < j){do i++;while(nums[i] < x);do j--;while(nums[j] > x);if(i < j)swap(nums[i],nums[j]);}int sl = j - l + 1;//统计左区间元素个数if(sl >= k)//如果sl大于等于k,说明答案在左边return quick_sort(nums,l,j,k);else//反之在右边return quick_sort(nums,j + 1,r,k - sl);
}
​
int main()
{int n,k;cin >> n >> k;for(int i = 0;i < n;i++)cin >> nums[i];cout << quick_sort(nums,0,n - 1,k) << endl;return 0;
}

二、归并排序法

归并排序

思路

归并排序是建立在归并操作上的有效的排序算法,该算法的核心是分治。这个分治与快速排序法的分治不同,快速排序法是用数组元素分治,归并排序是用下标分治


思路 + 步骤

1、取基准点(是数组下标而不是数组元素)

2、递归排序左右区间

关于这个递归排序左右区间,递归后就排好序了,大家可能不能理解为什么可以排好序,等第三步写出来就知道了,

3、归并-合二为一

怎么合二为一呢?此时我们需要一个额外的临时数组来暂时存储排完序的数组。然后利用双指针算法,从这两个区间的首元素开始走,来比较左右区间里面的元素

最小,谁先存储到临时数组

但是当这一步结束时,能会存在一个区间指针已经把元素走完了,但是另一个区间还有元素没有走完

所以需要在这一步后面再加上两个while循环把没有走完的元素存储到临时数组里面,防止特殊情况发生。


看完第三步,应该理解了第二步是怎么利用递归排序的吧!

模拟代入

假如题目给我们一个数组,让我们进行排序。这个数组为{1, 7, 5, 4, 2, 6, 3}。

1、取基准点

​基准点推荐去数组的中间元素的下标

2、递归排序左右区间

以基准点为分界点,把[l,mid] 和 [mid + 1,r]递归排序。现在来看一下

取新基准点

双指针算法,存临时数组

最后两个while循环把没有存到临时数组里面的元素存进去

3、合并--合二为一

当左右区间排完序后,就开始利用双指针算法,从这两个区间的首元素开始走

将指针指向的元素和j指针指向的元素进行比较,如果谁最小,谁就被存到临时数组里面。

直到一个指针走到末尾结束。

此时一个区间已经遍历完了,但是另一个区间还有一个元素。这该怎么办呢?这就是下面的两个循环的作用了,让那些没用遍历完的区间的元素继续遍历,直到到末尾,并把剩下的元素存到临时数组。

再给大家放一下归并排序的动图,帮助大家更快地逻辑(来自菜鸟教程)

模板

void merge_sort(int nums[], int l, int r)
{if (l >= r) return;//如果数组只有1个元素,就退出,递归的结束条件
​int mid = l + r >> 1;//取基准点
​merge_sort(nums, l, mid), merge_sort(nums, mid + 1, r);//递归排序左右区间
​int k = 0, i = l, j = mid + 1;while (i <= mid && j <= r)//利用双指针算法,来排序{if (nums[i] <= nums[j]) //谁最小谁先存到临时数组里面tmp[k ++ ] = nums[i ++ ];else tmp[k ++ ] = nums[j ++ ];}while (i <= mid) //防止左区间没有遍历完tmp[k ++ ] = nums[i ++ ];while (j <= r) //防止右区间没有遍历完tmp[k ++ ] = nums[j ++ ];
​for (i = l, j = 0; i <= r; i ++, j ++ ) //物归原主,把排好序的临时数组赋值给原数组nums[i] = tmp[j];
}

练习

#include <iostream>using namespace std;const int N = 1e5 + 10;
int nums[N],tmp[N];void merge_sort(int nums[],int l,int r)
{if(l >= r)//如果数组只有一个元素,就退出,既用在刚开始判断是否只有1个元素,也作为递归的结束条件return;int mid = (l + r) / 2;merge_sort(nums,l,mid),merge_sort(nums,mid + 1,r);//递归排序左右区间int i = l,j = mid + 1,cnt = 0;while(i <= mid && j <= r)//双指针,比较指针指向的元素大小{if(nums[i] <= nums[j])//谁小,谁先存到临时数组tmp[cnt++] = nums[i++];elsetmp[cnt++] = nums[j++];}while(i <= mid)//这两个while循环,是为了防止指针没有走完,还有元素没有存到临时数组tmp[cnt++] = nums[i++];while(j <= r)tmp[cnt++] = nums[j++];for(i = l,j = 0;i <= r;i++,j++)//物归原主,把排好序的数值赋值给原数组nums[i] = tmp[j];
}int main()
{int n;cin >> n;for(int i = 0;i < n;i++)cin >> nums[i];merge_sort(nums,0,n - 1);for(int i = 0;i < n;i++)cout << nums[i] << ' ';cout << endl;return 0;
}

应用(逆序对的数量)

介绍

在做这道题之前,我们先来了解一下逆序对,看一下百度百科上的解释

估计看着比较懵,举个例子,数组为{3,2,1,5,4},3和2、1都是逆序对;逆序对是从数组里面选两个数,前面的数字比后面大

逆序对的数量求解有三种做法,分别是 枚举法(双层for循环)、归并排序法、树状数组;目前我只会前两种,本题解是用归并排序来做。还是分治的思想。为什么可以用归并排序来做呢?不知道你们有没有思考过。

思路

​归并排序是把一个区间一分为二,递归排序左右区间,然后使用两个指针比较大小,谁指向的元素小,那个小的元素就先存到数组里面。比较大小,逆序对不就是前面的数字比后面的数字大吗?此时,完全可以通过比较大小,来记录逆序对的数量!!!那么在归并排序里面,逆序对不是只有一种情况,一共有三种情况。分别为:左区间的逆序对右区间的逆序对一个在左区间一个在右区间的逆序对,如图所示(鼠标画的难受,凑合看看吧)

​此时这三种情况的逆序对数量怎么求呢?

第一种情况:左区间逆序对的数量等于 merge_sort(nums,l,mid)

第二种情况:右区间逆序对的数量等于 merge_sort(nums,mid + 1,r)

为什么这两种情况可以求出左右区间的数量?请特别注意递归,看第三种情况。

第三种情况:此时左右区间都已经排好序了,如果不理解的话,往上翻一下,再看看归并排序。

当两个指针指向的元素进行比较时,如果第一个指针的元素大于第二个,说明i指针后面的元素(包含i指针)都比此时j指针指向的数大,都构成了逆序对,因为左右区间是升序!!!,此时逆序对数量为mid - i + 1。为什么加1?因为这是数组下标,是从0开始的,要加1。

注意:此时i指针前面的元素都小于j指针此时指向的元素,因为当 i 指针动时,说明左区间的元素比右区间的小,且区间都是升序的。

模拟代入

假如题目给我们一个数组,让我们求逆序对的数量。这个数组为{1, 7, 5, 4, 2, 6, 3}。

1、取基准点

2、递归排序左右区间并且得到左右区间的逆序对数量

现在不明白没关系,看第三步

3、求一左一右元素的逆序对

使用两个指针遍历左右数组

当i指针指向的元素小于j指针指向的元素时,把i指针的元素存到临时数组,继续往下走

当i指针指向的元素大于j指针指向的元素时,逆序对数量为midmid - i + 1,因为区间是升序,i指针以及后面的元素都大于j指针指向的元素,都是逆序对

j指针往后走,再次比较大小

j指针的元素还是小于i指针的元素,还是res += mid - i + 1;把小的元素存进去,j指针后移

后移后

再比较指针指向的元素大小,i指针小于j指针指向的元素,小元素存入数组,逆序对数量不变。

i指针指向的元素还是小于j指针,i指针指向的元素存入临时数组,逆序对不变

此时i指针指向的元素大于j指针指向的元素,逆序对再加上mid - i + 1;


这是左右区间逆序对的数量。看完第三步后,第二步应该知道是为什么可以得到左右区间的逆序对的数量了吧,就是区间变成各自区间的一半后进行重复操作而已!!!

模板

long long merge_sort(int nums[],int l,int r)
{if(l >= r)return 0;//数组只有一个元素,退出函数,也做函数递归的结束条件int mid = (l + r) / 2;//取基准点//得到左区间里面和右区间里面的逆序对的数量long long res = merge_sort(nums,l,mid) + merge_sort(nums,mid + 1,r);int i = l,j = mid + 1,cnt = 0;//两个指针,和临时数组下标while(i <= mid && j <= r){//如果i指针指向的元素小于j指针的,把i指针的元素存入数组if(nums[i] <= nums[j])tmp[cnt++] = nums[i++];//i指针元素大于j指针的,说明此时i指针以及之后的元素都比现在j指针的大//是逆序对,else{res += mid - i + 1;tmp[cnt++] = nums[j++];}}while(i <= mid)//防止指针没有走完左区间tmp[cnt++] = nums[i++];while(j <= r)//防止指针没有走完右区间tmp[cnt++] = nums[j++];for(int i = l,j = 0;i <= r;i++,j++)//物归原主nums[i] = tmp[j];return res;
}

练习

先不要向下看,等下再来看代码

#include <iostream>
​
using namespace std;
​
typedef long long LL;//给long long起个别名,太长了
​
const int N = 1e5 + 10;
int nums[N],tmp[N];
​
LL merge_sort(int nums[],int l,int r)
{if(l >= r)return 0;int mid = (l + r) / 2;LL res = merge_sort(nums,l,mid) + merge_sort(nums,mid + 1,r);int i = l,j = mid + 1,cnt = 0;while(i <= mid && j <= r){if(nums[i] <= nums[j])tmp[cnt++] = nums[i++];else{res += mid - i + 1;tmp[cnt++] = nums[j++];}}while(i <= mid)tmp[cnt++] = nums[i++];while(j <= r)tmp[cnt++] = nums[j++];for(int i = l,j = 0;i <= r;i++,j++)nums[i] = tmp[j];return res;
}
​
int main()
{int n;cin >> n;for(int i = 0; i < n;i++)cin >> nums[i];cout << merge_sort(nums,0,n - 1) << endl;
}

三、二分

二分,就是一分为二。就是在有序序列里面,通过不断地二分,进而缩小解的范围,从而更快地寻找满足条件的解

本质:如果可以找到某种性质,使得整个区间一分为二,其中一半区间满足条件,另一半区间不满足条件;二分就可以寻找性质的边界。

整数二分

整数二分稍微有点复杂,主要是需要处理边界问题,比较麻烦,不过还是挺简单的。整数二分主要有两种情况,第一种情况是把区间分为[l,mid] 和[mid + 1,r];第二种是把区间分为[l,mid - 1] 和[mid,r];现在大家可能不知道这是什么意思,没关系看下去。下面我会介绍什么时候怎么划分区间!

大致步骤

1、取中间值mid

注意这个中间值和前面的归并排序一样,取的是数组元素的下标,并不是数组元素。

而mid = (l + r) / 2或者mid = ( l + r + 1) / 2;看情况取

2、判断mid是否满足某种性质

什么意思呢?就是判断此时的基准点的右边满足某种性质,基准点的左边都不满足这种性质,就可以把整个区间一分为二一半满足一半不满足。那么二分就可以寻找这两个性质的边界。

而寻找这两种不同的边界,就是两个不同的模板。让我们来看看这两个不同的模板吧。

详细步骤(两模板)

a、寻找红颜色性质的边界

先取中间值mid = (l + r) / 2(先不加一),然后每次判断一下中间值是否满足红颜色的性质。此时有两种情况

第一种是满足,即true,说明mid在红色区间里面

既然知道了在红颜色里面,而我们要寻找红颜色的边界,所以答案在mid右边,包含mid;此时把一个区间一分为二,分成[l,mid - 1] 和 [mid + 1,r];更新方式为l = mid;

第二种是不满足,即false,说明mid在蓝色区间里面。

既然mid在蓝色区间里面,而我们要寻找红颜色的边界,所以答案一定在mid - 1的左边(包含mid - 1)为什么一定不包含mid呢?因为mid在蓝颜色区间里面,一定不能满足红颜色区间的性质。至多mid的前一位mid - 1满足。更新方式是r = mid - 1;看到这里,我们前面mid取值要加上1,mid = (l + r + 1)/ 2。

为什么要加1?因为C++里面除法是向下取整,当l = r - 1时,mid = (l + r)/2。此时mid = l(向下取整),而如果check()成功了,更新方式为l = mid;这不是一个死循环吗?所以为了防止死循环,所以要加1变成向上取整mid = r。

b、寻找蓝颜色的边界点

先取中间值mid,mid = (l + r )/ 2;然后每次判断是否满足蓝颜色的性质,此时判断有两种情况

第一种是满足,即true,说明mid在蓝颜色区间里面

既然已经知道mid在蓝颜色区间里面,而我们要寻找蓝颜色的边界,所以答案一定在mid左边(包含mid,因为mid1在蓝颜色区间里面)。所以整个区间就被一分为二成两个区间,[l,mid],[mid + 1,r]。更新方式为r = mid;

第二种是不满足,即false,说明mid在红色区间里面

既然知道了mid在红色区间里面,我们要寻找蓝颜色边界,所以答案一定在mid右边(不包含mid,因为mid在红颜色区间里面),更新方式为l = mid + 1;

模板

区间被划分为[l,mid - 1] 和 [mid,r];

int bsearch_1(int l, int r)
{while (l < r){int mid = l + r + 1 >> 1;//取中间值if (check(mid)) //判断是否满足某种性质l = mid;//更新区间else r = mid - 1;}return l;
}

区间被划分为[l,mid] 和 [mid + 1,r]

int bsearch_2(int l, int r)
{while (l < r){int mid = l + r >> 1;//取中间值if (check(mid)) //判断满足某种性质r = mid;//更新区间else l = mid + 1;}return l;
}

模拟代入

如果给你一个数组nums,为{1,3,5,7,9}。让你查找第一个大于等于6的数字的下标

1、取中间值mid

不管三七二十一,先把mid = (l + r) ​/ 2;等到后面再看区间是怎么划分的,然后再决定加不加1;

2、判断mid是否满足某种性质

这个题是寻找蓝色区间边界,也就是第二个模板​。

mid的下标为2,nums[mid] = 5,小于题目要找的元素,所以答案在右边,更新区间,l = mid + 1,更新后的mid = (3 + 4)/ 2,向下取整为3

此时mid大于6,且左边的都小于5,右边的都大于6,所以返回这个数的下标。

练习

大家先不要着急看代码啊,自己静下心来,认真思考一下!!!

代码

#include <iostream>
​
using namespace std;
​
const int N = 1e6 + 10;
int nums[N];
​
int main()
{int n,m;scanf("%d%d",&n,&m);for(int i = 0;i < n;i++)scanf("%d",&nums[i]);while(m--){int x;scanf("%d",&x);
​int l = 0,r = n - 1;//求元素起始位置while(l < r){int mid = (l + r) / 2;
​if(nums[mid] >= x)//由于mid也满足,答案在[l,mid]r = mid;//更新区间elsel = mid + 1;}
​if(nums[l] != x)//判断nums[l]和判断nums[r] 结果是一样的,因为当while循环结束,l = rcout << "-1 -1" << endl;else//求元素终止位置{cout << l << ' ';//或者cout << r << ' ';因为l和r相等
​int l = 0,r = n - 1;while(l < r){int mid = (l + r + 1) / 2;//为了防止死循环,+1if(nums[mid] <= x)//由于mid也满足,所以答案在[mid,r];l = mid;//更新区间elser = mid - 1;}cout << r << endl;}}return 0;
}

实数二分

介绍

实数的二分比较简单,由于比较稠密,可以被整除,所以没有什么边界问题,不需要加1。核心是精度的确定,一般有两种方法。第一种是r -l>esp;esp = 10^-(k + 2),k为要保留的位数;第二种方法是不管三七二十一,for循环100次差不多就可以得到精确的数了。

double b_sreach(double l,double r)
{while(r - l > esp)//确定精度{double mid = (l + r) / 2;if(check())r = mid;elsel = mid//不同于整数,不需要加1}return l;//return r;
}

double b_search(double l,double r)
{for(int i = 0;i < 100;i++)//直接循环100次{int mid = (l + r) / 2;if(check())r = mid;elsel = mid;}return l;//return r
}

实数二分很简单的,重要的是精度的确定。现在大家都理解了吧,下面让我们来一起练习一下吧!

练习

不要着急看哦,自己思考一下,看看能不能做出来,再继续往下看

#include <iostream>
​
using namespace std;
​
int main()
{double x;cin >> x;double l = -100,r = 100;while(r - l > 1e-8){double mid = (l + r) / 2;if(mid * mid * mid >= x)r = mid;elsel = mid;}printf("%.6lf",l);return 0;
}

四、高精度算法

介绍

高精度算法是处理大数字的数学计算方法,在一般的科学的计算中,会经常算到小数点后几百位或者更多,当然也可能是几千亿几百亿的大数字。一般这类数字我们统称为高精度数

高精度算法是用计算机对于超大数据的一种模拟加,减,乘,除,乘方阶乘开方等运算。对于非常庞大的数字无法在计算机中正常存储,于是,将这个数字拆开拆成一位一位的,或者是四位四位的存储到一个数组中, 用一个数组去表示一个数字,这样这个数字就被称为是高精度数。高精度算法就是能处理高精度数各种运算的算法。

下面将会讲解四种常见的高精度计算,包括高精度加法、减法、高精度乘低精度、高精度除低精度;再加一个高精度乘高精度。其中还包括压位操作。

这几种操作会有重复操作,本篇文章只会在高精度加法里面详细讲解一下,后面的就简单说一下。

高精度加法

不压位步骤

第一步:数据的存储

首先,我们需要考虑的是,当我们用字符串接收数据后,用什么存储数据呢?常见的有两种,第一种就是数组,第二种是vector容器。由于vector容器自带size函数,所以本文采用vector容器存储数据​。

第二,我们是正序存储数据,还是倒序存储数据呢?这里推荐使用倒序​。为什么呢?因为加法可能存在进位,举个例子,由四位数进位到五位数,如果是正序存储的话,就需要把元素都后移一位,比较麻烦。而如果是倒序的话,就直接在末尾添加元素就可以了,十分方便。

for(int i = a.size();i >= 0;i--)A.push_back(a[i] - '0');
for(int i = b.size();i >= 0;i--)B.push_back(b[i] - '0');

第二步:人工模拟加法

数据处理完了,就开始进行加法操作。这个是核心,人工模拟加法,就是用代码模拟出小时候学习加法的操作。

如果低位数相加大于10的话,就减去10,得到余数;如果小于10的话,就不变。在用计算机语言进行该操作时,无论相加的结果是大于10还是小于10,直接进行取余(%)操作,就能得到准确的结果。

还有一个需要注意的是,当两个数的对应位数相加之前,要判断当前位数是否存在数字,然后决定是否相加。

vector<int> add(vector<int> &A,vector<int> &B)
{//判断两个数位数,以此保证第一个参数的位数大于第二个参数if(A.size() < B.size())return add(B,A);vector<int> C;//储存结果int t = 0;//进位for(int i = 0;i < A.size();i++){t += A[i];//先加上A上的数if(i < B.size())//如果B位数有数,那么也加上t += B[i];C.push_back(t % 10);//得到该位数的数字t /= 10;//用于进位}if(t)//判断最后的t是否为0,如果不为零,说明还需要进位C.push_back(t);return C;//返回结果
}

压位步骤

首先我们先来了解一下压位。当数据过大时,此时long long存储不下,因此需要vector或者数组存储,然后计算。

而一般vector或者数组中的每一个元素是int,如果每一个位置只是存储0~9一位数字的话,比较浪费空间,并且计算也比较慢。因此可以让每个位置存储连续的多位数字,这被称为压位。

注意:加法可以压九位;乘法一般压四位,不能压五位,因为十万的平方爆int了

第一步:压位存入容器

既然是压位,就自然不能和不压位的那样一位一位的存到数组里面,而是要满九位或者是遍历完字符串了,才能存到容器里面。既然是倒序存入容器,就涉及到反转问题。

//s表示反转后数字,j为计数器,t用来反转数字
for(int i = a.size() - 1,s = 0,j = 0,t = 1;i >= 0;i--){//得到数字反转的结果s += (a[i] - '0') * t;t *= 10,j++;//当j等于9,或者字符串遍历完了,就把这几位数字存到容器了了里面if(j == 9 || i == 0){A.push_back(s);s = j = 0;t = 1;}}

第二步:人工模拟加法

和上面不压位的大致一样,就是取余的底数变了,不是10,而是1000000000,因为想要得到九位数上的个数,必须要%1000000000。说不清楚,自己脑补一下吧。

//base在函数外面已经定义过了,为1e9
vector<int> add(vector<int> &A,vector<int> &B)
{if(A.size() < B.size())return add(B,A);vector<int> C;int t  = 0;for(int i = 0;i < A.size();i++){t += A[i];if(i < B.size())t += B[i];//底数变了,想要得到九位数的余数,只能%1e9C.push_back(t % base);t /= base;}if(t)C.push_back(t);return C;
}

第三步:输出

输出也需要讲一下,因为是压九位操作的,所以输出时不满九位要补零。但是第一次输出比较特殊,不需要补零,其他的都需要补零。

cout << C.back();
for(int i = C.size() - 2;i >= 0;i--)printf("%09d",C[i]);

讲完了这些步骤后,让我们来练习一些吧!!!

练习

不要着急看题解哦,自己动脑思考一下,这样才知道自己前面懂了没有!

​压位代码

#include <iostream>
#include <vector>
​
using namespace std;
​
const int base = 1e9;
​
vector<int> add(vector<int> &A,vector<int> &B)
{if(A.size() < B.size())return add(B,A);vector<int> C;int t  = 0;for(int i = 0;i < A.size();i++){t += A[i];if(i < B.size())t += B[i];//底数变了,想要得到九位数的余数,只能%1e9C.push_back(t % base);t /= base;}if(t)C.push_back(t);return C;
}
​
int main()
{string a,b;cin >> a >> b;vector<int> A,B;//s是结果数字;j是次数,等于9就把九位数字存到容器,t为反转操作的,用来控制位数for(int i = a.size() - 1,s = 0,j = 0,t = 1;i >= 0;i--){//得到数字反转的结果s += (a[i] - '0') * t;t *= 10,j++;//当j等于9,或者字符串遍历完了,就把这几位数字存到容器了了里面if(j == 9 || i == 0){A.push_back(s);s = j = 0;t = 1;}}//一样操作,处理bfor(int i = b.size() - 1,s = 0,j = 0,t = 1;i >= 0;i--){s += (b[i] - '0') * t;t *= 10,j++;if(j == 9 || i == 0){B.push_back(s);s = j = 0;t = 1;}}auto C = add(A,B);cout << C.back();//特殊处理第一位,不需要补0//剩下的不行满九位数,不够的补0for (int i = C.size() - 2; i >= 0; i -- ) printf("%09d", C[i]);cout << endl;return 0;
}

高精度减法

介绍

高精度减法和高精度加法差不了多少。存储数据方式一样。之后在进行减法之前,要判断减数和被减数大小,以此判断结果是正负。核心思想就是人工模拟减法。这个人工模拟减法主要在于借位,如果对应的位数不够减就借高位数,因此高位数要减一。还有一步,注意可能有前导零(比如001),所以要进行删除前导零的操作,之后就是输出了。

下面就不讲解压位的步骤了,因为和加法差不多

第一步:数据的存储

在进行数据存储的方法和高精度加法一样,还是倒序,为什么呢?因为加减乘除要保持一致,这是因为,高精度不会单独出题,一出题就是加减乘除一起,方便格式统一,所以都用倒序。

for(int i = a.size() - 1;i >= 0;i--)A.push_back(a[i] - '0');

第二步:比较减数和被减数大小

在进行减法操作之前,必须对减数和被减数进行比较,以此判断结果正负,方便输出负号(-)。如果减数大于被减数,就不需要改动,直接传参即可;如果减数小于被减数,需要把这两个参数位置互换(减数变成被减数,被减数变为减数,然后输出负号,在输出参数位置互换的结果。这里比较大小时,自定义了一个函数

bool cmp(vector<int> &A,vector<int> &B)
{if(A.szie() != B.size())//如果位数不同,直接返回bool值return A.size() > B.size();//位数相同只能比较对应位数的大小,由于从字符串存到容器里面是倒序//所以容器的最后一位是该数字的最高位,从高位开始比较,返回bool值for(int i = A.size() - 1;i >= 0;i--){if(A[i] != B[i])return A[i] > B[i];}//如果到这函数还没有返回值,就说明这两个数相等,返回truereturn true;
}

第三步:人工模拟减法

还记得小学刚学减法的时候吗?先从低位开始,如果此时减数上的数大于被减数上的数,就直接减;否则就往前借位加上10再减;而前一位在进行减法操作时需要减1,因为它被借位了。减法就是这个思想,大家应该可以轻松理解吧!

vector<int> sub(vector<int> &A,vector<int> &B)
{vector<int> C;for(int i = 0,t = 0;i < A.size();i++){t = A[i] - t;//t表示借位,只能为0或1if(i < B.size())t -= B[i];C.push_back((t + 10) % 10);//无论正负,加上10,求余数不影响if(t < 0)//如果t小于0,说明需要借位t = 1;else//反之不需要借位t = 0;}while(C.size() > 1 && C.back() == 0)//去重前导零C.pop_back();return C;
}

练习

先不要着急看代码,做一道题检测一下自己吧

不压位代码

#include <iostream>
#include <vector>
​
using namespace std;
​
bool cmp(vector<int> &A,vector<int> &B)//比较函数
{if(A.size() != B.size())//位数不同比位数return A.size() > B.size();for(int i = A.size() - 1;i >= 0;i--)//位数相同,比较对应位数的数的大小{if(A[i] != B[i])return A[i] > B[i];}return true;
}
​
vector<int> sub(vector<int> &A,vector<int> &B)
{vector<int> C;for(int i = 0,t = 0;i < A.size();i++){t = A[i] - t;//t表示借位,只能为0或1if(i < B.size())t -= B[i];C.push_back((t + 10) % 10);//无论正负,加上10,求余数不影响if(t < 0)//如果t小于0,说明需要借位t = 1;else//反之不需要借位t = 0;}while(C.size() > 1 && C.back() == 0)//去重前导零C.pop_back();return C;
}
​
int main()
{string a,b;cin >> a >> b;vector<int> A,B;for(int i = a.size() - 1;i >= 0;i--)//把数据存到容器里面A.push_back(a[i] - '0');for(int i = b.size() - 1;i >= 0;i--)B.push_back(b[i] - '0');vector<int> C;//比较减数和被减数大小,如果减数大于等于被减数,不变if(cmp(A,B))C = sub(A,B);//反之,位置互换,同时输出负号else{C = sub(B,A);cout << '-';}for(int i = C.size() - 1;i >= 0;i--)cout << C[i];return 0;
}

压位代码

#include <iostream>
#include <vector>
​
using namespace std;
​
const int N = 1e9;
​
bool cmp(vector<int> &A,vector<int> &B)//比较减数和被减数关系
{if(A.size() != B.size())return A.size() > B.size();for(int i = A.size() - 1;i >= 0;i--){if(A[i] != B[i])return A[i] > B[i];}return true;
}
​
vector<int> sub(vector<int> &A,vector<int> &B)
{vector<int> res;for(int i = 0,t = 0;i < A.size();i++){t = A[i] - t;if(i < B.size())t -= B[i];res.push_back((t + N) % N);//这里要加上10都要变为1e9,因为压位为九位if(t < 0)//t只能为0或1,因为要么不借位,要么借1t = 1;elset = 0;}while(res.size() > 1 && res.back() == 0)//去除前导零res.pop_back();return res;
}
​
int main()
{string a,b;cin >> a >> b;vector<int> A,B;for(int i = a.size() - 1,t = 1,j = 0,s = 0;i >= 0;i--){s += (a[i] - '0') * t;//和昨天的的加法压位操作一样t *= 10,j++;//s是反转后的数字,j用来计数if(j == 9 || i == 0)//到九位或者遍历完了{A.push_back(s);s = j = 0;t = 1;}}for(int i = b.size() - 1,t = 1,j = 0,s = 0;i >= 0;i--){s += (b[i] - '0') * t;t *= 10,j++;if(j == 9 || i == 0){B.push_back(s);s = j = 0;t = 1;}}vector<int> C;if(cmp(A,B))//比较减数和被减数关系C = sub(A,B);else{C = sub(B,A);cout << '-';}cout << C.back();//第一个最特殊,不满九位,不需要补零for(int i = C.size() - 2;i >= 0;i--)//剩下的都需要补零printf("%09d",C[i]);cout << endl;return 0;
}

高精度乘法

高精度乘法有两种,第一个是常见的高精度乘低精度;第二个是不常见的高精度乘以高精度。这两种思路有点不一样,分别讲一下思路。就不详细写步骤,比较都和什么的高精度加法减法差不多,就是核心代码不一样而已

高精度乘以低精度

核心思路

高精度乘以低精度的代码模拟操作,和咱们小时候学的乘法不一样。不论低精度是几位数,把低精度看成一个整体

这样做的好处是不需要再把每一位相乘后的结果相加后再进位了,直接进位处理,比较简单方便。

下面咱们来看看,乘法操作的具体步骤,把低精度看成一个整体,乘以高精度的每一位数。

这样有点抽象,这样吧,来上一个实例

这个是不是有点小意外,不用在相加了,只需要看进位就可以了!!!

不压位模板

vector<int> mul(vector<int> &A,int b)
{vector<int> C;//存储结果int t = 0;//进位for(int i = 0;i < A.size();i++){t += A[i] * b;//存储每一位数相乘的结果C.push_back(t % 10);//得到该位的数字t /= 10;//用来进位}if(t)//如果t != 0 说明还有进位,就把它添加到末尾C.push_back(t);while(C.size() > 1 && C.back() == 0)//去除前导零C.pop_back();return C;
}

这个模板有点麻烦,可以把后面的if语句和for循环合并起来,这样for循环的结束条件为i遍历完了,或者进位t处理完了!!!

vector<int> mul(vector<int> &A,int b)
{vector<int> C;//存储结果int t = 0;//进位//把if和for循环合并,结束循环有两种情况,i遍历完了或者t为空for(int i = 0;i < A.size() || t;i++){if(i < A.size())t += A[i] * b;//低精度与高精度每一位数相乘的结果C.push_back(t % 10);//该位数上的数字t /= 10;//处理进位}while(C.size() > 1 && C.back() == 0)//去除前导零C.pop_back();return C;
}
​

压位模板 

//C为结果,A*b,里面的N根据压几位而定,10^N
vector<int> mul(vector<int> &A,int b)
{vector<int> C;//存储结果int t = 0;//进位//把if和for循环合并,结束循环有两种情况,i遍历完了或者t为空for(int i = 0;i < A.size() || t;i++){if(i < A.size())t += A[i] * b;//进位C.push_back(t % N);//得到该位数的数字t /= N;//处理进位}while(C.size() > 1 && C.back() == 0)//去除前导零C.pop_back();return C;
}

练习

不压位代码

#include <iostream>
#include <vector>
​
using namespace std;
​
vector<int> mul(vector<int> &A,int b)
{vector<int> C;//存储结果int t = 0;//进位//把if和for循环合并,结束循环有两种情况,i遍历完了或者t为空for(int i = 0;i < A.size() || t;i++){if(i < A.size())t += A[i] * b;C.push_back(t % 10);t /= 10;}while(C.size() > 1 && C.back() == 0)C.pop_back();return C;
}
​
int main()
{string a;int b;cin >> a >> b;vector<int> A;for(int i = a.size() - 1;i >= 0;i--)//存到容器AA.push_back(a[i] - '0');auto C = mul(A,b);for(int i = C.size() - 1;i >= 0;i--)cout << C[i];cout << endl;return 0;
}

压四位代码

因为int类型只能压四位,10000*10000就会爆int

#include <iostream>
#include <vector>
​
using namespace std;
​
const int N = 10000;//超过4位会爆int
​
vector<int> mul(vector<int> &A,int b)
{vector<int> C;//存储结果int t = 0;//进位//把if和for循环合并,结束循环有两种情况,i遍历完了或者t为空for(int i = 0;i < A.size() || t;i++){if(i < A.size())t += A[i] * b;C.push_back(t % N);t /= N;}while(C.size() > 1 && C.back() == 0)C.pop_back();return C;
}
​
int main()
{string a;int b;cin >> a >> b;vector<int> A;//s为反转后的数,j为计数器,t为反转用的for(int i = a.size() - 1,j = 0,s = 0,t = 1;i >= 0;i--)//存到容器A{s += (a[i] - '0') * t;t *= 10,j++;if(j == 4 || i == 0)//压4位{A.push_back(s);//当输入容器里面后,必须把那些重复使用的元素,重新初始化s =  j = 0;t = 1;}}auto C = mul(A,b);cout << C.back();//第一位比较特殊,不用补位for(int i = C.size() - 2;i >= 0;i--)printf("%04d",C[i]);//后面的不满4位要补位cout << endl;return 0;
}

来看一下压位和不压位的区别,看看能快上多少毫秒(ms)

高精度乘以高精度

核心思路

高精度乘以高精度的思路和高精度乘以低精度完全不一样。高精度乘以高精度就是拟人工乘法竖式乘法,然后分别相加

这样还是有点抽象,还是举个例子吧!!!

总之就分为两步,这两个数的每一位数交叉相乘,之后把对应的位数上面的数字加上,最后统一处理进位,就能得到结果了。

不压位模板

vector<int> mul(vector<int> A, vector<int> B)
{vector<int> C(A.size() + B.size());//定义结果大小
​//利用双层for循环,让每一位数交叉相乘for (int i = 0; i < A.size(); i ++ )for (int j = 0; j < B.size(); j ++ )C[i + j] += A[i] * B[j];//得到每一位交叉相乘的结果//统一处理进位for (int i = 0, t = 0; i < C.size() || t; i ++ ){t += C[i];if (i >= C.size()) //如果将要输出的位数比定义的多,就添加在末尾C.push_back(t % 10);else //否则,就在原位上改变数字就可以了C[i] = t % 10;t /= 10;}
​while (C.size() > 1 && !C.back()) //去除前导零C.pop_back();
​return C;
}

压四位模板

vector<int> mul(vector<int> &A,vector<int> &B)
{vector<int> C(A.size() + B.size());//定义容器C大小for(int i = 0;i < A.size();i++){for(int j = 0;j < B.size();j++)C[i+j] += A[i] * B[j] ;//A和B的每一位数相乘}//统一处理进位//把后面的if语句和for循环合并了for(int i = 0,t = 0;i < C.size() || t;i++){t += C[i];if(i > C.size())//如果进位还有数据,添加到末尾C.push_back(t % N);else//如果此时的位数在容器范围里面,就直接再该位数上改就行了C[i] = t % N;t /= N;}while(C.size() > 1 && C.back() == 0)//去除前导零C.pop_back();return C;
}

练习

不压位代码

#include<iostream>
#include <vector>
​
using namespace std;
​
vector<int> mul(vector<int> &A,vector<int> &B)
{vector<int> C(A.size() + B.size());//定义容器C大小for(int i = 0;i < A.size();i++){for(int j = 0;j < B.size();j++)C[i+j] += A[i] * B[j] ;//A和B的每一位数相乘}//统一处理进位for(int i = 0,t = 0;i < C.size() || t;i++){t += C[i];if(i > C.size())C.push_back(t % 10);elseC[i] = t % 10;t /= 10;}while(C.size() > 1 && C.back() == 0)//去除前导零C.pop_back();return C;
}
​
int main()
{string a,b;cin >> a >> b;vector<int> A,B;for(int i = a.size() - 1;i >= 0;i--)A.push_back(a[i] - '0');for(int i = b.size() - 1;i >= 0;i--)B.push_back(b[i] - '0');auto C = mul(A,B);for(int i = C.size() - 1;i >= 0;i--)cout << C[i];cout << endl;return 0;
}

压四位代码

#include<iostream>
#include <vector>
​
using namespace std;
​
const int N = 1e4;
​
vector<int> mul(vector<int> &A,vector<int> &B)
{vector<int> C(A.size() + B.size());//定义容器C大小for(int i = 0;i < A.size();i++){for(int j = 0;j < B.size();j++)C[i+j] += A[i] * B[j] ;//A和B的每一位数相乘}//统一处理进位//把后面的if语句和for循环合并了for(int i = 0,t = 0;i < C.size() || t;i++){t += C[i];if(i > C.size())//如果进位还有数据,添加到末尾C.push_back(t % N);else//如果此时的位数在容器范围里面,就直接再该位数上改就行了C[i] = t % N;t /= N;}while(C.size() > 1 && C.back() == 0)//去除前导零C.pop_back();return C;
}
​
int main()
{string a,b;cin >> a >> b;vector<int> A,B;//压位操作,不懂的话,看看前面的加减怎么压位的for(int i = a.size() - 1,s = 0,j = 0,t = 1;i >= 0;i--){s += (a[i] - '0') * t;t *= 10,j++;if(j == 4 || i == 0){A.push_back(s);s = j = 0;t = 1;}}for(int i = b.size() - 1,s = 0,j = 0,t = 1;i >= 0;i--){s += (b[i] - '0') * t;t *= 10,j++;if(j == 4 || i == 0){B.push_back(s);s = j = 0;t = 1;}}auto C = mul(A,B);cout << C.back();//第一位特殊,不用补零for(int i = C.size() - 2;i >= 0;i--)//后面的不满4位要补零printf("%04d",C[i]);cout << endl;return 0;
}

高精度除法

高精度除法稍微有点复杂,分为高精度除以低精度高精度除以高精度。这两个对数据的处理和输出和上面的加减乘操作都一样,不在详细写了,主要来讲一下除法的核心思路

高精度除以低精度

思路

这个除法操作和上面的乘法操作一样,把低精度的数看成一个整体,然后开始进行除法操作。还记得小时候怎么进行除法的吗?人是先找到可以除的位数,从那里往后开始除

而计算机比较呆,不会自动选择可以除的地方,不论能不能除,只能从第一位开始进行除法,从而导致有前导零的存在,所以最后要注意去除前导零

这样的除法操作的原理还记得吗?上一个原理图

为了让它们格式统一,我们可以把第一个t1改一下

这样就可以看出商和余数的关系了吧,t[i] = (r[i-1]*10+A[i])% b;

这样你们可能还有点迷糊,下面来一个实例,让你们感受一下!!!

用代码表示是这样的


//A是被除数,b是除数,r是余数
vector<int> div(vector<int> &A,int b,int &r)
{vector<int> C;//商r = 0;//由于是倒序存的,都是要从高位进行运算,所以应该从容器最后一位for(int i = A.size() - 1;i >= 0;i--){r = r * 10 + A[i];//每一位的余数C.push_back(r / b);//每一位上的商r %= b;//每一位的余数}return C;
}

并不是只有这些,别忘了计算机是从第一位开始进行除法操作的,千万记得去除前导零哦。

但是商是正序存到结果容器里面的,所以要把商反转一下,再去除前导零,最后再倒序输出

reverse(C.begin(),C.end());
while(C.size() > 1 && C.back() == 0)
{C.pop_back();
}

模板


vector<int> div(vector<int> &A,int b,int &r)
{vector<int> C;//商r = 0;//余数初始化为0for(int i = A.size() - 1;i >= 0;i--){r = r * 10 + A[i];//每一位上的数C.push_back(r / b);//每一位商r %= b;//余数}reverse(C.begin(),C.end());//商为正序,需要反转while(C.size() > 1 && C.back() == 0)//去除前导零C.pop_back();return C;
}

练习

------------------------------------------------(不要着急看代码)-----------------------------------------------------

不压位代码


#include <iostream>
#include <vector>
#include <algorithm>using namespace std;vector<int> div(vector<int> &A,int b,int &r)
{vector<int> C;//商r = 0;//余数初始化为0for(int i = A.size() - 1;i >= 0;i--){r = r * 10 + A[i];//每一位上的数C.push_back(r / b);//每一位商r %= b;//余数}reverse(C.begin(),C.end());//商为正序,需要反转while(C.size() > 1 && C.back() == 0)//去除前导零C.pop_back();return C;
}int main()
{string a;int b,r;cin >> a >> b;vector<int> A;for(int i = a.size() - 1;i >= 0;i--)A.push_back(a[i] - '0');auto C = div(A,b,r);for(int i = C.size() - 1;i >= 0;i--)cout << C[i];cout << endl << r << endl;return 0;
}

压四位代码

#include <iostream>
#include <vector>
#include <algorithm>using namespace std;const int N = 1e4;vector<int> div(vector<int> &A,int b,int &r)
{vector<int> C;//商r = 0;//余数初始化为0for(int i = A.size() - 1;i >= 0;i--){r = r * N + A[i];//每一位上的数C.push_back(r / b);//每一位商r %= b;//余数}reverse(C.begin(),C.end());//商为正序,需要反转while(C.size() > 1 && C.back() == 0)//去除前导零C.pop_back();return C;
}int main()
{string a;int b,r;cin >> a >> b;vector<int> A;for(int i = a.size() - 1,s = 0,j = 0,t = 1;i >= 0;i--)//压位{s += (a[i] - '0') * t;t *= 10,j++;if(i == 0 || j == 4){A.push_back(s);//压进去后,记得重新初始化s = j = 0;t = 1;}}auto C = div(A,b,r);cout << C.back();//第一个输出不用补零for(int i = C.size() - 2;i >= 0;i--)printf("%04d",C[i]);//其他的不满四位都需要补零cout << endl << r << endl;return 0;
}

高精度除以高精度

高精度除以高精度,平时用的比较少,就暂时不说了。(主要是我现在还不会)不过可以提供一下思路,用减法模拟除法,不过效率比较慢。

五、前缀和与差分

前缀和

介绍+思路

前缀和,是给定一个数组a1、a2、a3、a4、...ai,然后前缀和就是原数组中前i个数之和,就是高中数列的前n项和,只不过不是等差数列、等比数列。

其次,我们需要知道第 i 项和前 i - 1 项和的关系。S[i] = S[i - 1] + a[i]这个应该可以理解吧。换个高中公式,就是Sn-Sn-1 = an。这下应该明白了吧。知道了上面的那个公式,我们可以干什么啊?当然是把给前缀和数组赋值。不再需要使用for循环以O(n)的时间复杂度求前缀和了;而是O(1),更加快速方便。

注意:给前缀和数组赋值时,需要从1开始,而不是0。为什么呢?当 i 等于0时,S[0]=S[-1] + a[0]。这样就会越界访问了,所以要从1开始,并且令S[0] = 0(全局变量定义前缀和数组时,默认每一项都为零)

最后有了前缀和数组怎么求区间和呢?很简单的,就一个公式,区间l,r的前缀和为s[r] - s[l - 1]。大家可能有点诱惑,接下来看这张图片,你就明白了

详细地写出来了S[l]、S[r] 、[l,r]的区间和,应该可以看得出规律吧!

[l,r]的区间和 = S[r] - S[l - 1]

So,你get到了吗?

模板

#include <iostream>using namespace std;const int N = 1e5 + 10;
int nums[N],s[N];//nums数组为原数组,s数组为前缀和数组
int n;//元素个数int main()
{for(int i = 1;i <= n;i++)cin >> nums[i];for(int i = 1;i <= n;i++)s[i] = s[i - 1] + a[i];//处理前缀和数组//接下来就是你想要询问的区间和的操作了,不再具体写了//区间和公式:l~r的区间和 = s[r] - s[l - 1];
}

练习

传送阵:795. 前缀和 - AcWing题库

这个和上面讲的模板一样吧,就直接套板子吧!

#include <iostream>using namespace std;const int N = 1e5 + 10;
int nums[N],s[N];//原数组和前缀和数组int main()
{int n,m;cin >> n >> m;for(int i = 1;i <= n;i++)//下标从1开始存入原数组cin >> nums[i];for(int i = 1;i <= n;i++)//给前缀和数组赋值s[i] = s[i - 1] + nums[i];while(m--){int l,r;cin >> l >> r;cout << s[r] - s[l - 1] << endl;//公式}return 0;
}

扩展

掌握了求一维数组的前缀和的方法后,来看看二维数组的前缀和怎么求吧!

思路

二维数组的前缀和的核心也是两个公式,一个是给前缀和数组赋值的,另一个就是求一部分二维数组的前缀和了。只不过公式与一维数组有所不同而已!

下面这是一个二维数组,让我们用这个数组来讲解一下知识点

首先,我们需要知道S[i,j]表示什么,是哪些的和?

这时候,上面黄色块的和为S[3,3]。那么S[3,4]表示哪些的和呢?

所以懂了吗?S[i,j]表示i行j列的数字之和。那么S[i,j]是如何计算的呢?


其实它有一个公式S[i,j] = S[i - 1,j] + S[i,j - 1] - S[ i-1 ,j - 1] + a[i,j];下面我来演示一下怎么搞的

S[i - 1,j]就是这个a[i,j]的左下角到二维数组左上角围成的面积

S[i,j - 1]就是a[i,j]的右上角与二维数组的左上角围成的面积

S[i - 1,j - 1]就是a[i,j]的左上角与二维数组左上角围成的面积

所以,你理解这个公式了吗?之所以减去S[i-1,j - 1]是因为加了两遍。


下面最最最核心的来了,就是子矩阵的求和,有了左上角(x1,y1)和右下角(x2,y2)的坐标,求子矩阵的和就是一个公式而已。

子矩阵的和=S[x2,y2] - S[x1 - 1,y2] - S[x2,y1 - 1] + S[x1 - 1,y1 - 1]

可能还是有一点抽象,大家看一下y总的讲解吧!

前缀和公式讲解

练习

传送阵:796. 子矩阵的和 - AcWing题库

-----------------------------------------------------------代码分割线-------------------------------------------------------

#include <iostream>using namespace std;const int N = 1010;
int s[N][N];int main()
{int n,m,q;cin >> n >> m >> q;for(int i = 1;i <= n;i++){for(int j = 1;j <= m;j++){    scanf("%d",&s[i][j]);//读取数据,并给二维数组赋值s[i][j] += s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1];//公式    }}int x1,x2,y1,y2;while(q--){cin >> x1 >> y1 >> x2 >> y2;int res = s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1];//公式cout << res << endl;}return 0;
}

差分

介绍 + 思路

有一个原始数列a1 、a2、a3 ......an,构造一个新数列b1、b2......bn,使得ai = b1 + b2 +... + bi,使得ai是数列b的前缀和b是a的差分,是前缀和的逆运算 。

差分的应用主要是解决一种操作:给定一个区间[l,r],把A数组里面这些区间全部加上一个C,只需要在B数组修改两个数就可以了

这两个数是B[ l ] +C,B[ r + 1 ] - C。为什么呢?

B[ l ] + C会导致,从前缀和数组A[ l ]开始,每一个前缀和数组都加上C(因为al及以后的数组元素求和时会加上bl,而bl又加上了C)。

而我们只是想让前缀和数组a [ l ,r ]区间里面的元素加上C,所以需要进行的操作是 B[r + 1] - C;和上面同理。

但是有的人可能不会构造差分数组,其实不需要构造差分数组。只需要把差分数组看成全为0的数组。但是前缀和数组有数啊,差分数组却为0,肯定不对啊!!!所以就对差分数组进行插入操作,这样就可以和前缀和数组对应上了。

步骤

1、构建差分数组

构建差分数组,不需要我们想,就利用前缀和数组来进行构建。那怎么构建呢?

我们把前缀和数组的值想象成是在数值为0差分数组上进行插入操作的,利用前缀和数组逆向构建差分数组。

for(int i = 1;i <= n;i++)
{cin >> a[i];insert(i,i,a[i]);
}

而insert函数就是我们上面提到的插入操作,具体逻辑请看代码


void insert(int l,int r,int c)
{b[l] += c;b[r + 1] -= c;
}

这样就保持了只在区间为[l,r]上加上常数。

2、插入操作

就是上面的插入函数,在给定的区间上的每个数加上常数。

练习

传送阵:797. 差分 - AcWing题库

-----------------------------------------------------------代码分割线-------------------------------------------------------

#include <iostream>using namespace std;const int N = 1e5 + 10;
int a[N],b[N];//数组a为前缀和数组,数组b为差分数组//插入操作
void insert(int l,int r,int c)
{b[l] += c;//从l开始都加上常数Cb[r + 1] -= c;//从r + 1开始都减去常数C//这样保持了只把区间[l,r]加上常数C,其他区间都不变
}int main()
{int n,m;cin >> n >> m;for(int i = 1;i <= n;i++){cin >> a[i];insert(i,i,a[i]);//给差分数组赋值}while(m--){int l,r,c;cin >> l >> r >> c;insert(l,r,c);//在差分数组进行插入操作}for(int i = 1;i <= n;i++){a[i] = a[i - 1] + b[i];//前缀和求和cout << a[i] << ' ';}return 0;
}

扩展

学习了一维数组的差分,来学习二维数组的差分吧!

思路

一维数组的差分是构造一个数组使得是原数组的前缀和,二维数组的差分也是如此。构造一个二维数组使得是原数组的前缀和。

核心:给定一个数组a[ i ][ j ],构造差分矩阵,使得a[ ][ ]是 b[ ] [ ]的二维前缀和。但是我们不需要构造差分数组,只需要把原数组看成全是0的数组,然后通过核心操作构建出来

核心操作:给以(x1,y1)为左上角、(x2,y2)为右上角的子矩阵中所以的数a[ i , j ]都加上C

S[ 2,2 ]表示为(2,2)与右下角围成的面积

对于差分数组的影响:S[ x1, y1 ] += C;

                                    S[x1, y2 + 1] -= C;

                                    S[ x2 + 1,y1] -= C;

                                    S[x2 + 1,y2 + 1] += C;

二维数组差分

模板

void insert(int x1,int y1,int x2,int y2,int c)
{b[x1][y1] += c;b[x2 + 1][y1] -= c;b[x1][y2 + 1] -= c;b[x2 + 1][y2 + 1] += c;
}

举例

如果我们想让以(2,2)为左上角、(4,4)为右下角的子矩阵加上常数C,看模板。

首先把(x1,y1)加上C,所以这个数及其后的数组的前缀和都被加上常数C,就是(2,2)加上常数C

然后再把(x2+1,y1)减去常数C,就是(5,2)减去常数C。

之后是(x1+1,y2)减去常数C,也就是(1,5)减去常数C

(黑色的表示,被减去常数C)

最后(x2+1,y2+1)加上常数C

这样就完成对一个区间加上常数C。

练习

传送阵:798. 差分矩阵 - AcWing题库

#include <iostream>using namespace std;const int N = 1010;
int a[N][N],b[N][N];
int n,m,q;
void insert(int x1,int y1,int x2,int y2,int c)
{b[x1][y1] += c;b[x2 + 1][y1] -= c;b[x1][y2 + 1] -= c;b[x2 + 1][y2 + 1] += c;
}
int main()
{cin >> n >> m >> q;for(int i = 1;i <= n;i++){for(int j = 1;j <= m;j++){scanf("%d",&a[i][j]);insert(i,j,i,j,a[i][j]);//二维差分数组}}while(q--){int x1,y1,x2,y2,c;cin >> x1 >> y1 >> x2 >> y2 >> c;insert(x1,y1,x2,y2,c);}for(int i = 1;i <= n;i++){for(int j = 1;j <= m;j++){b[i][j] += b[i - 1][j] + b[i][j - 1] - b[i - 1][j - 1];//二维数组的前缀和printf("%d ",b[i][j]);}cout << endl;}return 0;
}

AcWing算法基础课 第一讲小结(持续更新中)相关推荐

  1. AcWing算法基础课第一讲(2):高精度加减乘除、前缀和、差分

    文章目录 1. 高精度加法 2. 高精度减法 3. 高精度乘低精度 4. 高精度除以低精度 5. 一维前缀和 6. 二维前缀和 7. 一维差分 8. 二维差分 1. 高精度加法 这里讲解两个大整数的加 ...

  2. AcWing 算法基础课第一节基础算法1排序、二分

    1.该系列为ACWing中算法基础课,已购买正版,课程作者为yxc 2.y总培训真的是业界良心,大家有时间可以报一下 3.为啥写在这儿,问就是oneNote的内存不够了QAQ ACwing C++ 算 ...

  3. 项目管理培训资料(第一讲+第二讲,持续更新中.....)

    这是公司内部进行的项目管理培训资料,培训由我主持.目前已经进行了两讲,半个月一次:P 前两讲注重的是理论知识,没有太多实际的指导性内容,这也和培训的受众有关.本系列培训针对的是公司所有的开发人员,因此 ...

  4. Acwing算法基础课第二讲——数据结构

    模拟单链表 方法:两个数组,一个存该索引位置的 val , 另一个存该索引位置的下一位置(即下一个位置的索引是啥). 其中,第0号位置仅表示 链表的 head,不进行使用. #include < ...

  5. 【机器学习】算法面试知识点整理(持续更新中~)

    1.监督学习(SupervisedLearning):有类别标签的学习,基于训练样本的输入.输出训练得到最优模型,再使用该模型预测新输入的输出: 代表算法:决策树.朴素贝叶斯.逻辑回归.KNN.SVM ...

  6. KNN算法数据归一化处理(持续更新中)

    数据归一化处理 公式:(每个值-最小值)/(最大值-最小值) 数据归一化处理,不会改变数据原有的分布情况 模拟的数据集 data = [[-1,201],[-0.5,189],[0,199],[1,1 ...

  7. 算法与数据结构模版(AcWing算法基础课笔记,持续更新中)

    AcWing算法基础课笔记 文章目录 AcWing算法基础课笔记 第一章 基础算法 1. 排序 快速排序: 归并排序: 2. 二分 整数二分 浮点数二分 3. 高精度 高精度加法 高精度减法 高精度乘 ...

  8. 背包四讲 (AcWing算法基础课笔记整理)

    背包四讲 背包问题(Knapsack problem)是一种组合优化的NP完全问题.问题可以描述为:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高 ...

  9. 网络流题目详讲+题单(提高版)(持续更新中......)

    网络流题目详讲+题单(提高版)(持续更新中......) 标签:图论--网络流 PS:如果你觉得自己还不够强(和我一样弱),可以去入门版看看 阅读体验:https://zybuluo.com/Junl ...

最新文章

  1. Zabbix 3.0 从入门到精通(zabbix使用详解)
  2. tensorflow 的输入层和输出层维度注意事项
  3. awk截取字符命令_Linux运维基础技能: 脚本编程与Linux命令
  4. prometheus下载慢_Prometheus + Grafana 监控 SpringBoot
  5. centos 上 crontab 计划任务 ,这个版本解释的比较清晰
  6. 软件测试实验报告下载 实验一到实验五
  7. 数据库查询语句慢如何优化_常见Mysql的慢查询优化方式
  8. dt程序网站服务器配置,ZKWeb 官网与演示站点的部署步骤 (Linux + Nginx + Certbot)
  9. ROS学习笔记01:安装ROS - 玩小海龟
  10. [专栏精选]Unity中动态构建NavMesh
  11. IntelliJ IDEA设置maven
  12. 3.1Python数据处理篇之Numpy系列(一)---ndarray对象的属性与numpy的数据类型
  13. ssh相关命令Linux,Linux SSH常用命令 [长期更新]...
  14. WPF 辅助开发工具
  15. 为什么我的世界服务器显示红叉,我的世界藏宝图怎么看红叉
  16. es6之扩展运算符 三个点(...)
  17. id 查找apple_Apple ID忘记了怎么办 Apple ID找回方法【详细介绍】
  18. js 骂人不带脏字 (!(~+[]) + {})[--[~+““][+[]] * [~+[]] + ~~!+[]] + ({} + [])[[~!+[]] * ~+[]] 图解
  19. pe系统如何读取手机_什么是otg(pe系统如何读取手机)
  20. HTTP请求方法、GET和POST的区别

热门文章

  1. Android 编译速度优化黑科技 - RocketX
  2. 【论文导读】- Subgraph Federated Learning with Missing Neighbor Generation(FedSage、FedSage+)
  3. 树莓派4B的引脚控制简单demo
  4. mysql几种性能测试的工具使用
  5. ABP框架 - 实体
  6. tableau -- 月销售额年同比增长
  7. 微信支付成功后服务器宕机了,今天微信出现大面积宕机,可能与支付宝有关?...
  8. 滴滴实时计算发展之路及平台架构实践
  9. 漫画:网站访问缓慢怎么办?
  10. MFC二叉树可视化绘制 (C++)—— 插入、删除、先序遍历、中序遍历、后序遍历、层序遍历(基于平衡二叉树实现)