算法基础
快速排序(对标GESP五级)
基本概念
快速排序通过选择一个基准元素,然后重新排列数组,使得所有小于基准的元素都移到基准的左边,所有大于基准的元素都移到基准的右边。这个操作称为分区。基准的选择可以多样化,常见的方法包括选择第一个元素、最后一个元素、中间元素,或者随机选择一个元素作为基准。若对 2 5 4 6 1 采用快速排序,数组下标从 0 开始,基准元素采用中间元素,则排序过程如下所示:
- 初始状态:
- 数组:2 5 4 6 1
- 左右边界:l = 0, r = 4
- 基准值 x 选择中间元素 q[(0+4)/2] = q[2] = 4
- 第一轮划分:
- 初始化指针:i = -1, j = 5
- i 右移:i = 0(指向 2,2 < 4) -> i = 1(指向 5,5 ≥ 4,停止)
- j 左移:j = 4(指向 1,1 < 4,停止)
- 交换 q[1] 和 q[4],数组变为:2 1 4 6 5
- 继续移动指针:
- i = 2(指向 4,4 ≥ 4,停止)
- j = 3(指向 6,6 > 4) -> j = 2(指向 4,4 ≤ 4,停止)
- 此时 i = 2,j = 2,循环结束
- 递归子数组:
- 左子数组 [0, 2]:2 1 4
- 右子数组 [3, 4]:6 5
- 左子数组 [2 1 4]
- 排序基准值 x = q[1] = 1
- 指针移动后交换 q[0] 和 q[1],数组变为:1 2 4
- 递归子数组:左子数组 [0, 0]:1(已排序) 右子数组 [1, 2]:2 4(已排序)
- 右子数组 [6 5]
- 排序基准值 x = q[3] = 6指针移动后交换 q[3] 和 q[4],数组变为:5 6
- 递归子数组:左子数组 [3, 3]:5(已排序)右子数组 [4, 4]:6(已排序)
- 最终排序结果1 2 4 5 6
实际上,基准元素也可以选择区间的左端点元素和区间的右端点元素,并不仅限于选择区间的中间元素。快速排序在最佳和平均情况下的时间复杂度是 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n),但在最坏情况下(如数组已排序或所有元素相等)会退化为 O ( n 2 ) O(n^2) O(n2),是否退化取决于枢轴选择策略。它通常是原地排序,是不稳定的排序方式,相等元素的相对位置可能在排序过程中改变。
代码模板
快速排序模板的核心思路可以分为以下几个步骤:
-
终止条件:当待排序区间的左边界 l 大于等于右边界 r 时,无需排序,直接返回
-
选择基准值:取区间中间位置的元素 q[l + r >> 1] 作为基准值 x
-
双指针分区:
- 初始化左右指针 i = l - 1、 j = r + 1
- 循环移动双指针:
- 左指针 i 向右移动,直到找到大于等于基准值 x 的元素
- 右指针 j 向左移动,直到找到小于等于基准值 x 的元素
- 若此时 i < j,交换 q[i] 和 q[j] ,确保左侧元素≤基准值、右侧元素≥基准值
-
递归排序子区间:
- 以 j 为分界点,递归排序左区间 [l, j] 和右区间 [j + 1, r]
- 由于分区后 j 的位置一定满足左区间元素≤右区间元素,因此递归可覆盖整个原区间
该模板采用基准元素为序列的中间元素:
c++
void quick_sort(int q[],int l,int r)
{
if(l >= r)return;
int x = q[l + r >> 1],i = l - 1,j = r + 1;
while(i < j)
{
do i ++ ;while(q[i] < x);
do j -- ;while(q[j] > x);
if(i < j)swap(q[i],q[j]);
}
quick_sort(q,l,j);
quick_sort(q,j + 1,r);
}
应用场景
常常用于快速选择(第k个数),即快速选择排序后序列中的第 k k k 个数字:
c++
int quick_select(int q[],int l,int r,int k)
{
if(l >= r)return q[l];
int x = q[l + r >> 1],i = l - 1,j = r + 1;
while(i < j)
{
do i ++ ;while(q[i] < x);
do j -- ;while(q[j] > x);
if(i < j)swap(q[i],q[j]);
}
int sl = j - l + 1;
if(k <= sl)return quick_select(q,l,j,k);
else return quick_select(q,j + 1,r,k - sl);
}
一般说来,在写编程题时如果使用到快排我们通常不会手搓模板,都是直接调用 <algorithm>库中的 sort 函数(GESP四级-排序,可与 cmp 结合实现自定义排序,解决结构体排序问题)。至于说代码模板的学习究竟有什么意义?可以这么理解,通过学习快速排序的代码模板,我们可以了解到其是借用递归和分治的思想实现,也可以了解到其时间复杂度和空间复杂度如何分析(GESP五级-快速排序)。
简单例题
c++
#include <iostream>
#include <vector>
#include <algorithm>
#include <iomanip>
using namespace std;
struct S
{
int a;
double b;
string c;
};
int cmp(const struct S &A,const struct S &B)
{
if(A.a != B.a)return A.a < B.a;
}
int main()
{
vector<S> A;
int n;
cin >> n;
while(n -- )
{
S t;
cin >> t.a >> t.b >> t.c;
A.push_back(t);
}
sort(A.begin(),A.end(),cmp);
for(int i = 0;i < A.size();i ++ )
{
cout << A[i].a << ' ' << setiosflags(ios::fixed) << setprecision(2) << A[i].b << ' ' << A[i].c << endl;
}
return 0;
}
c++
//本题目考察的是非常简单的结构体排序算法
//也就是非常简单的利用算法库algorithm库当中的sort函数和自定义cmp函数结合
//而后实现sort排序的方式重定义的效果
//从而对于结构体所有的数据进行整体的排序
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 300 + 10;
struct Student
{
int Y;
int S;
int E;
int sum;
int num;
};
//自定义cmp函数
int cmp(const struct Student &A,const struct Student &B)
{
if(A.sum != B.sum)return A.sum > B.sum;//总分第一优先级比较
else if(A.Y != B.Y)return A.Y > B.Y;//语文第二优先级比较
else return A.num < B.num;//序号第三优先级比较
}
int main()
{
Student stu[N];
int n;
cin >> n;
//对于结构体的数据进行输入
for(int i = 1;i <= n;i ++ )
{
stu[i].num = i;
cin >> stu[i].Y >> stu[i].S >> stu[i].E;
stu[i].sum = stu[i].Y + stu[i].S + stu[i].E;
}
//sort与cmp结合排序
sort(stu + 1,stu + 1 + n,cmp);
//输出前五名
for(int i = 1;i <= 5;i ++ )cout << stu[i].num << ' ' << stu[i].sum << endl;
}
c++
//这道题考察的知识点是快速选择算法
//注意陷阱点,数轴上的元素皆为货舱选址,也就是选址只能在商店建立
//这也就使货舱的地址在所有的商店地址进行排序之后的中位数位置
//那么对于偶数来说,假设下标1-n,即下标从1开始的话,那么我们发现 n / 2 必定为中位数的下标
//但是若是奇数个元素个数的话,根据c++向下取整的原则,我们(n + 1) / 2 才为中位数下标
//综合奇数和偶数个元素的不同情况,发现偶数的情况是奇数情况的一个子集
//因此我们可以推算出排序之后中位数即货舱选址的下标是(n + 1) / 2
#include <iostream>
using namespace std;
const int N = 1000000 + 10;
int a[N];
//首先,我们利用分治的思想手打一个快速排序的算法模板
//而后对于快速排序算法我们进行修改,使之成为快速选择算法
//即使算法能够在进行排序的同时还可以利用下标进行查找我们所需要的元素
//有点类似于利用下标所进行的二分查找
int quick_sort(int q[],int l,int r,int k)
{
if(l >= r) return q[l];
int x = q[l + r >> 1],i = l - 1,j = r + 1;
while(i < j)
{
do i ++ ;while(q[i] < x);
do j -- ;while(q[j] > x);
if(i < j)swap(q[i],q[j]);
}
int sl = j - l + 1;
if(k <= sl)return quick_sort(q,l,j,k);
else return quick_sort(q,j + 1,r,k - sl);
}
int main()
{
int n;
cin >> n;
for(int i = 1;i <= n;i ++ )cin >> a[i];
//调用自定义的快速选择算法
int idx = quick_sort(a,1,n,(n + 1) / 2);
int ans = 0;
//累计求和
for(int i = 1;i <= n;i ++ )ans += abs(a[i] - idx);
//答案输出
cout << ans << endl;
return 0;
}
归并排序(对标GESP五级)
基本概念
首先递归地将数组分成两个子数组,每个子数组再继续分成更小的数组,直到每个子数组只包含一个元素或为空。然后将两个排序好的子数组合并成一个最终的排序数组。合并时,从两个数组的起始位置开始比较,选择两者中较小的元素放入结果数组中,然后移动指针,重复此过程,直到所有元素都被合并。排序过程如下图所示:

归并排序在所有情况下(最佳、平均、最坏)的时间复杂度均为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)。归并排序需要与原始数据集同样大小的额外空间,因此其空间复杂度为 O ( n ) O(n) O(n)。归并排序是稳定的排序算法,即相同元素的原始顺序在排序后不会改变。
代码模板
归并排序模板的核心思路可以分为以下几个步骤:
-
终止条件:当待排序区间的左边界 l 大于等于右边界 r 时,无需排序,直接返回
-
划分区间:计算区间中点 mid = l + r >> 1,将区间 [l, r] 划分为左区间 [l, mid] 和右区间 [mid + 1, r]
-
递归排序子区间:
- 递归对左区间 [l, mid] 进行归并排序
- 递归对右区间 [mid + 1, r] 进行归并排序
-
合并有序子区间:
- 初始化指针:i 指向左区间起点 l,j 指向右区间起点 mid + 1,k 指向临时数组 tep 的起点 0
- 比较合并:循环比较 q[i] 和 q[j],将较小的元素放入临时数组 tep 中,并移动对应指针
- 处理剩余元素:当左区间还有剩余元素时,将其依次放入 tep;当右区间还有剩余元素时,将其依次放入 tep
- 复制回原数组:将临时数组 tep 中的有序元素复制到原数组 q 的 [l, r] 区间,完成合并
c++
void merge_sort(int q[],int l,int r)
{
if(l >= r)return;
int mid = l + r >> 1;
merge_sort(q,l,mid);
merge_sort(q,mid + 1,r);
int i = l,j = mid + 1,k = 0;
while(i <= mid && j <= r)
{
if(q[i] <= q[j])tep[k ++ ] = q[i ++ ];
else tep[k ++ ] = q[j ++ ];
}
while(i <= mid)tep[k ++ ] = q[i ++ ];
while(j <= r)tep[k ++ ] = q[j ++ ];
for(int i = l,j = 0;i <= r;i ++ ,j ++ )q[i] = tep[j];
}
应用场景
在GESP五级当中将会首次接触到归并排序概念,对于归并排序的考法同快速排序一样,也是以概念题和理解记忆为主,编程题一般不会涉及到。其模板变式常常用于计算逆序对/顺序对的个数,在更高级别可能会用到,待到高级别时,例如六级《小杨的握手问题》,则可以利用归并排序的模板将其魔改为求取顺序对的个数,非常方便。
c++
int merge_sort(int q[],int l,int r)
{
if(l >= r)return 0;
int mid = l + r >> 1;
int res = merge_sort(q,l,mid) + merge_sort(q,mid + 1,r);
int i = l,j = mid + 1,k = 0;
while(i <= mid && j <= r)
{
if(q[i] <= q[j])tep[k ++ ] = q[i ++ ];
else
{
tep[k ++ ] = q[j ++ ];
res += mid - i + 1;
}
}
while(i <= mid)tep[k ++ ] = q[i ++ ];
while(j <= r)tep[k ++ ] = q[j ++ ];
for(int i = l,j = 0;i <= r;i ++ ,j ++ )q[i] = tep[j];
return res;
}
简单例题
c++
#include <iostream>
#define int long long
using namespace std;
const int N = 2e6 + 10;
int q[N],tep[N];
int merge_sort(int q[],int l,int r)
{
if(l >= r)return 0;
int mid = l + r >> 1;
int res = merge_sort(q,l,mid) + merge_sort(q,mid + 1,r);
int i = l,j = mid + 1,k = 0;
while(i <= mid && j <= r)
{
if(q[i] <= q[j])tep[k ++ ] = q[i ++ ];
else
{
tep[k ++ ] = q[j ++ ];
res += mid - i + 1;
}
}
while(i <= mid)tep[k ++ ] = q[i ++ ];
while(j <= r)tep[k ++ ] = q[j ++ ];
for(int i = l,j = 0;i <= r;i ++ ,j ++ )q[i] = tep[j];
return res;
}
signed main()
{
int n;
cin >> n;
for(int i = 1;i <= n;i ++ )cin >> q[i];
cout << merge_sort(q,1,n) << endl;
return 0;
}
c++
#include <iostream>
#define int long long
using namespace std;
const int N = 2e6 + 10;
int q[N],tep[N];
int merge_sort(int q[],int l,int r)
{
if(l >= r)return 0;
int mid = l + r >> 1;
int res = merge_sort(q,l,mid) + merge_sort(q,mid + 1,r);
int i = l,j = mid + 1,k = 0;
while(i <= mid && j <= r)
{
if(q[i] <= q[j])tep[k ++ ] = q[i ++ ];
else
{
tep[k ++ ] = q[j ++ ];
res += i - l;
}
}
while(i <= mid)tep[k ++ ] = q[i ++ ];
while(j <= r)
{
tep[k ++ ] = q[j ++ ];
res += mid - l + 1;
}
for(int i = l,j = 0;i <= r;i ++ ,j ++ )q[i] = tep[j];
return res;
}
signed main()
{
int n;
cin >> n;
for(int i = 1;i <= n;i ++ )cin >> q[i];
cout << merge_sort(q,1,n) << endl;
return 0;
}
二分(对标GESP五级)
二分查找算法又称折半查找算法,具体来讲在数组有序的前提下,通过比较数组中间的数据与目标数据的大小,可以得知目标数据是在数组的左边还是右边。因此,比较一次就可以把查找范围缩小一半,所以叫作二分查找。重复执行上述的比较操作就可以找到目标数据,或得出目标数据不存在的结论。二分查找分为整数二分和浮点二分:
整数二分
查找满足条件的左边界的核心思路可以分为以下几个步骤:
-
初始化边界:设置左指针 l 为序列的左边界,右指针 r 为序列的右边界
-
循环二分:当 l < r 时,进入循环
- 计算中点:mid = l + r >> 1
- 判断条件:若 a[mid] 满足 ≥ x,则将右边界收缩至 mid
- 调整左边界:若 a[mid] 不满足 ≥ x,则将左边界调整为 mid + 1
-
终止结果:当循环结束时,l == r,此时的 l / r 即为序列中第一个满足 ≥ x 的元素位置
c++
int l = 序列左边界,r = 序列右边界;
while(l < r)
{
int mid = l + r >> 1;
if(a[mid] >= x)r = mid;
else l = mid + 1;
}
查找满足条件的右边界的核心思路可以分为以下几个步骤:
-
初始化边界:设置左指针 l 为序列的左边界,右指针 r 为序列的右边界
-
循环二分:当 l < r 时,进入循环
- 计算中点:mid = l + r + 1 >> 1,取右中点,避免死循环
- 判断条件:若 a[mid] 满足 ≤ x,则将左边界扩展至 mid
- 调整右边界:若 a[mid] 不满足 ≤ x,则将右边界收缩为 mid - 1
-
终止结果:当循环结束时,l == r,此时的 l / r 即为序列中最后一个满足 ≤ x 的元素位置
c++
int l = 序列左边界,r = 序列右边界;
while(l < r)
{
int mid = l + r + 1 >> 1;
if(a[mid] <= x)l = mid;
else r = mid - 1;
}
可以发现,对于整数二分来说,模板不同,左右边界的更新条件不同,背住就行。
浮点二分
浮点二分(以查找三次方根为例)的核心思路可以分为以下几个步骤:
-
初始化边界:设置左指针 l 为一个较小的浮点数,右指针 r 为一个较大的浮点数
-
循环二分:当 r - l 大于设定的精度(如保留6位小数时用1e-8)时,进入循环
- 计算中点:mid = (l + r) / 2
- 判断条件:若 mid 的三次方(mid * mid * mid)满足 ≥ n,则将右边界收缩至 mid(r = mid)
- 调整左边界:若 mid 的三次方不满足 ≥ n,则将左边界调整为 mid(l = mid)
-
终止结果:当循环结束时,r - l 小于等于设定精度,此时的 l(或 r)即为满足精度要求的 n 的三次方根,可按指定格式输出(如%.6lf)
c++
double l = -0x3f3f3f3f,r = 0x3f3f3f3f;
while(r - l > 1e-(这里填写保留的小数位数))
{
double mid = (l + r) / 2;
// 假设 mid 为待查找的三次方根
if(mid * mid * mid >= n)r = mid;
else l = mid;
}
可以发现,对于浮点二分来说,边界的更新条件就简便许多了,直接把对应左右边界用 mid 更新即可。
简单例题
c++
#include <iostream>
using namespace std;
const int N = 2e6 + 10;
int a[N];
int main()
{
int n,q;
cin >> n >> q;
for(int i = 0;i < n;i ++ )cin >> a[i];
while(q -- )
{
int x;
cin >> x;
int l = 0,r = n - 1;
while(l < r)
{
int mid = l + r >> 1;
if(a[mid] >= x)r = mid;
else l = mid + 1;
}
if(a[l] != x)cout << "-1 -1" << endl;
else
{
cout << l;
l = 0,r = n - 1;
while(l < r)
{
int mid = l + r + 1 >> 1;
if(a[mid] <= x)l = mid;
else r = mid - 1;
}
cout << ' ' << l << endl;
}
}
return 0;
}
c++
#include <iostream>
using namespace std;
int main()
{
double n;
cin >> n;
double l = -0x3f3f3f3f,r = 0x3f3f3f3f;
while(r - l > 1e-8)
{
double mid = (l + r) / 2;
if(mid * mid * mid >= n)r = mid;
else l = mid;
}
printf("%.6lf",l);
return 0;
}
c++
#include <iostream>
using namespace std;
const int N = 2e6 + 10;
int h[N],w[N];
int n,k;
bool check(int x)
{
int res = 0;
for(int i = 1;i <= n;i ++ )res += (h[i] / x) * (w[i] / x);
if(res >= k)return true;
else return false;
}
int main()
{
cin >> n >> k;
for(int i = 1;i <= n;i ++ )cin >> h[i] >> w[i];
int l = 0,r = 1e6;
while(l < r)
{
int mid = l + r + 1 >> 1;
if(check(mid))l = mid;
else r = mid - 1;
}
cout << l << endl;
return 0;
}
c++
#include <iostream>
using namespace std;
const int N = 2e6 + 10;
int a[N];
int n,m;
bool check(double x)
{
int res = 0;
for(int i = 1;i <= n;i ++ )res += a[i] / x;
if(res >= m)return true;
else return false;
}
int main()
{
cin >> n >> m;
for(int i = 1;i <= n;i ++ )cin >> a[i];
double l = 0,r = 0x3f3f3f3f;
while(r - l > 1e-4)
{
double mid = (l + r) / 2;
if(check(mid))l = mid;
else r = mid;
}
printf("%.2lf",l);
return 0;
}
c++
#include <iostream>
#include <map>
using namespace std;
const int N = 2e6 + 10;
int a[N];
int l,n,m;
bool check(int x)
{
int res = 0,last = 0;
for(int i = 1;i <= n;i ++ )
{
if(a[i] - last < x)res ++ ;
else last = a[i];
}
if(res <= m)return true;
else return false;
}
int main()
{
cin >> l >> n >> m;
for(int i = 1;i <= n;i ++ )cin >> a[i];
n ++ ;
a[n] = l;
int l = 0,r = 0x3f3f3f3f;
while(l < r)
{
int mid = l + r + 1 >> 1;
if(check(mid))l = mid;
else r = mid - 1;
}
cout << l << endl;
return 0;
}
高精度(对标GESP五级)
基本概念
在学习完 C ++ 语言后,我们已经了解了几种数据类型的相关概念,并且也知道每种数据类型能够容纳的数字范围是有限的。一般情况下我们使用 int 类型,再大一点我们就使用 long long 类型。如果需要存储或者使用更大的整数我们该怎么办呢?我们就可以使用数组来模拟非常长的整数。这个时候就用高精度来模拟实现啦。
高精度加法
高精度加法模板的核心思路可以分为以下几个步骤:
-
数据预处理:
- 读取输入的两个大整数字符串 a 和 b
- 将字符串转换为向量 A 和 B,存储时逆序存放,方便从低位到高位逐位计算
-
加法运算(add 函数):
- 初始化进位 t 为 0,用于存储每一位相加的进位
- 循环遍历两个向量,直到处理完所有位数:
- 累加当前位的数值和进位 t
- 将当前位的结果(t % 10)存入结果向量 C
- 更新进位 t(t /= 10)
- 若循环结束后仍有进位(t ≠ 0),将进位 1 存入 C
-
结果输出:
- 由于结果向量 C 是逆序存储的,输出时从最后一位开始遍历,依次打印每个元素,得到正确的加法结果
c++
#include <iostream>
#include <vector>
using namespace std;
vector<int> add(vector<int> A,vector<int> B)
{
int t = 0;
vector<int> C;
for(int i = 0;i < A.size() || i < B.size();i ++ )
{
if(i < A.size())t += A[i];
if(i < B.size())t += B[i];
C.push_back(t % 10);
t /= 10;
}
if(t)C.push_back(1);
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 = add(A,B);
for(int i = C.size() - 1;i >= 0;i -- )cout << C[i];
return 0;
}
高精度减法
高精度减法模板的核心思路可以分为以下几个步骤:
-
数据预处理:
- 读取输入的两个大整数字符串 a 和 b
- 将字符串转换为向量 A 和 B,存储时逆序存放,方便从低位到高位逐位计算
-
比较大小:
- 若两个向量长度不同,长度更长的向量对应更大的数
- 若长度相同,从最高位向低位比较,首个数值更大的向量对应更大的数
- 若所有位都相同,则两数相等
-
减法运算:
- 初始化借位 t 为 0,用于存储每一位相减的借位
- 循环遍历被减数向量 A 的每一位:
- 累加当前位的数值和借位 t
- 若减数向量 B 未越界,减去 B 的当前位数值
- 计算当前位结果:(t + 10) % 10(确保结果非负),存入结果向量 C
- 更新借位 t:若 t < 0 说明有借位,t 设为 -1;否则 t 设为 0
- 去除结果向量末尾的无效前导零
-
结果输出:
- 若 A ≥ B,直接输出 sub(A, B) 的结果
- 若 A < B,先输出负号,再输出 sub(B, A) 的结果
- 输出时从结果向量 C 的最后一位开始遍历,得到正确的减法结果
c++
#include <iostream>
#include <vector>
using namespace std;
vector<int> sub(vector<int> A,vector<int> B)
{
int t = 0;
vector<int> C;
for(int i = 0;i < A.size();i ++ )
{
t += A[i];
if(i < B.size())t -= B[i];
C.push_back((t + 10) % 10);
if(t < 0)t = -1;
else t = 0;
}
while(C.size() > 1 && C.back() == 0)C.pop_back();
return C;
}
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 true;
else if(A[i] < B[i])return false;
}
return true;
}
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');
if(cmp(A,B))
{
auto C = sub(A,B);
for(int i = C.size() - 1;i >= 0;i -- )cout << C[i];
}
else
{
cout << "-";
auto C = sub(B,A);
for(int i = C.size() - 1;i >= 0;i -- )cout << C[i];
}
return 0;
}
高精度乘法
高精度乘法模板的核心思路可以分为以下几个步骤:
-
数据预处理:
- 读取输入的两个大整数字符串 a 和 b
- 将字符串转换为向量 A 和 B,存储时逆序存放,方便从低位到高位逐位计算
-
乘法运算:
- 初始化结果向量 C,长度为 A 和 B 的长度之和
- 逐位计算乘积:
- 双重循环遍历 A 的每一位 i 和 B 的每一位 j
- 将 A[i] 与 B[j] 的乘积累加到 C[i + j] 位置(模拟手工乘法中"第 i 位乘第 j 位结果落在第 i + j 位"的规则)
- 处理进位:
- 初始化进位 t 为 0,遍历 C 的每一位(或直到进位为 0)
- 累加当前位数值和进位 t,更新当前位为 t % 10,进位更新为 t / 10
- 若遍历完 C 仍有进位,向 C 末尾追加进位的个位
- 去除结果向量末尾的无效前导零
-
结果输出:
- 由于结果向量 C 是逆序存储的,输出时从最后一位开始遍历,依次打印每个元素,得到正确的乘法结果
c++
#include <iostream>
#include <vector>
using namespace std;
vector<int> mul(vector<int> A,vector<int> B)
{
vector<int> C(A.size() + B.size());
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() == 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];
return 0;
}
高精度除法
高精度除法(大整数除以小整数)模板的核心思路可以分为以下几个步骤:
-
数据预处理:
- 读取输入的大整数字符串 a 和小整数 b
- 将字符串 a 转换为向量 A,存储时逆序存放,但计算时从高位到低位处理
-
除法运算:
- 初始化余数 r 为 0,结果向量 C 用于存储商
- 从最高位向低位遍历:
- 计算当前余数:r = r * 10 + A[i]
- 计算当前位的商:C.push_back(r / b),并更新余数 r = r % b
- 反转商向量 C
- 去除结果向量末尾的无效前导零
-
结果输出:
- 商的输出:从结果向量 C 的最后一位开始遍历,依次打印每个元素,得到正确的商
- 余数的输出:直接打印引用返回的余数 r
c++
#include <iostream>
#include <vector>
#include <algorithm>
#define int long long
using namespace std;
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;
}
reverse(C.begin(),C.end());
while(C.size() > 1 && C.back() == 0)C.pop_back();
return C;
}
signed main()
{
string a;
cin >> a;
int b,r;
cin >> 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;
return 0;
}
前缀和与差分(GESP未涉及)
虽然 GESP 未涉及,但可极大的优化对一维数组和二维数组区间处理操作的效率。
前缀和
一维前缀和
一维前缀和是一种用于快速计算数组中连续子区间和的预处理技术。对于数组 a,其前缀和数组 s 定义为:s[i] 表示数组 a 中前 i 个元素的总和(即 s[i] = a[1] + a[2] + ... + a[i],通常下标从1开始方便计算)。
实现方式:
-
预处理前缀和数组:
- 初始化 s[0] = 0(便于边界计算)
- 遍历原数组 a,按公式 s[i] = s[i - 1] + a[i] 计算前缀和(i 从 1 到数组长度 n)
-
计算区间和:
- 对于区间 [l, r](即从第 l 个元素到第 r 个元素),其和为 s[r] - s[l-1]
- 原理:s[r] 是前 r 个元素的和,s[l-1] 是前 l-1 个元素的和,两者相减即得到区间 [l, r] 的和
应用场景:
主要用于频繁查询数组中连续子区间的和的场景,例如:
- 多次查询某个区间内元素的总和(如成绩统计中查询某段学号的总分)。
- 优化需要反复计算区间和的算法(如动态规划中减少重复计算)。
通过预处理(O(n) 时间),每次查询可在 O(1) 时间内完成,大幅提升效率,尤其适合数据量大、查询次数多的情况(如 n 和 m 达 1e5 级别)。
c++
#include <iostream>
using namespace std;
const int N = 2e6 + 10;
int a[N],s[N];
int n,m;
int main()
{
cin >> n >> m;
for(int i = 1;i <= n;i ++ )
{
cin >> a[i];
s[i] = s[i - 1] + a[i];
}
while(m -- )
{
int l,r;
cin >> l >> r;
cout << s[r] - s[l - 1] << endl;
}
return 0;
}
二维前缀和
二维前缀和是用于快速计算二维数组(矩阵)中子矩阵元素和的预处理技术。对于矩阵 a,其前缀和矩阵 s 定义为:s[i][j] 表示以矩阵左上角 (1,1) 为顶点、右下角 (i,j) 为顶点的矩形区域内所有元素的总和。
实现方式:
-
构建前缀和矩阵:
- 初始化前缀和矩阵 s,与原矩阵 a 同维度(下标从1开始,方便边界计算)。
- 按公式计算:s[i][j] = a[i][j] + s[i-1][j] + s[i][j-1] - 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]
- 原理:用大区域前缀和减去上方和左方的多余区域,再补回被多减的左上角重叠区域。
应用场景:
主要用于频繁查询矩阵中任意子矩阵元素和的场景,例如:
- 多次查询图像中某块区域的像素总和、二维数组中某子矩阵的元素和等
通过预处理(O(n*m) 时间),每次子矩阵和查询可在 O(1) 时间内完成,大幅提升效率,尤其适合矩阵规模大、查询次数多的场景。
c++
#include <iostream>
using namespace std;
const int N = 5050;
int a[N][N],s[N][N];
int n,m,q;
int main()
{
cin >> n >> m >> q;
for(int i = 1;i <= n;i ++ )
{
for(int j = 1;j <= m;j ++ )
{
cin >> a[i][j];
}
}
for(int i = 1;i <= n;i ++ )
{
for(int j = 1;j <= m;j ++ )
{
s[i][j] = a[i][j] + s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1];
}
}
while(q -- )
{
int x1,y1,x2,y2;
cin >> x1 >> y1 >> x2 >> y2;
cout << s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1] << endl;
}
return 0;
}
差分
一维差分
一维差分是一种用于快速对数组中连续区间进行增减操作的预处理技术。对于数组 a,其差分数组 b 定义为:b[1] = a[1],b[i] = a[i] - a[i-1](i ≥ 2)。通过差分数组可以反推原数组:a[i] = b[1] + b[2] + ... + b[i](即差分数组的前缀和就是原数组)。
如何实现:
- 构建差分数组 :
- 初始化差分数组 b,长度与原数组 a 一致
- 按定义计算:b[1] = a[1],b[i] = a[i] - a[i-1](i ≥ 2)
- 区间增减操作 :
- 若要对原数组 a 的区间 [l, r] 中所有元素加 c,只需对差分数组 b 做:b[l] += c,b[r+1] -= c
- 原理:差分数组的前缀和是原数组,b[l] += c 会使从 l 开始的所有元素都加 c,b[r+1] -= c 会抵消 r+1 及之后的增量,从而精准作用于 [l, r]
- 恢复原数组 :
- 对修改后的差分数组 b 求前缀和,即可得到更新后的原数组 a
应用场景:
主要用于需要频繁对数组连续区间进行增减操作的场景,例如:
- 多次给某个区间内的元素加 / 减一个值
- 高效处理区间更新后需要获取最终数组的问题
通过差分处理,每次区间操作可在 O(1) 时间内完成,最后通过一次前缀和(O(n) 时间)恢复原数组,大幅优于直接遍历区间修改的 O(n) 单次操作效率,尤其适合区间操作频繁、数据量大的场景。
c++
#include <iostream>
using namespace std;
const int N = 2e6 + 10;
int a[N],b[N];
int n,m;
void insert(int l,int r,int c)
{
b[l] += c;
b[r + 1] -= c;
}
int main()
{
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 ++ )
{
b[i] += b[i - 1];
cout << b[i] << ' ';
}
return 0;
}
二维差分
二维差分是用于快速对二维数组(矩阵)中子矩阵元素进行增减操作的预处理技术。对于矩阵 a,其差分数组 b 定义为:通过对 b 进行特定操作后,求其前缀和可得到原矩阵 a,且能高效实现对任意子矩阵的批量增减。
实现方式:
-
构建差分数组:
- 初始化差分数组 b,与原矩阵 a 同维度(下标从1开始,方便边界计算)。
- 对于原矩阵 a 中的元素 a[i][j],通过 insert(i,j,i,j,a[i][j]) 构建初始差分矩阵,即对单个元素 (i,j) 加 a[i][j]
-
子矩阵增减操作:
- 若要对以 (x1,y1) 为左上角、(x2,y2) 为右下角的子矩阵中所有元素加 c,调用 insert 函数:b[x1][y1] += c,b[x2+1][y1] -= c,b[x1][y2+1] -= c,b[x2+1][y2+1] += c。
- 原理:通过在差分矩阵的四角设置增减量,后续求前缀和时,可使增量 c 仅作用于目标子矩阵,抵消外部区域的影响。
-
恢复原矩阵:
- 对修改后的差分数组 b 求二维前缀和:b[i][j] += b[i-1][j] + b[i][j-1] - b[i-1][j-1],得到更新后的原矩阵。
应用场景:
主要用于频繁对矩阵中任意子矩阵进行增减操作的场景,例如:
- 多次给图像中某块区域的像素值增减、二维数组中某子矩阵的元素统一调整等
通过预处理和单次操作(O(1) 时间),最后通过一次二维前缀和计算(O(n*m) 时间)得到结果,大幅优于直接遍历子矩阵修改的效率,尤其适合矩阵规模大、操作频繁的场景。
c++
#include <iostream>
using namespace std;
const int N = 5050;
int a[N][N],b[N][N];
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 n,m,q;
int main()
{
cin >> n >> m >> q;
for(int i = 1;i <= n;i ++ )
{
for(int j = 1;j <= m;j ++ )
{
cin >> 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][j - 1] + b[i - 1][j] - b[i - 1][j - 1];
cout << b[i][j] << ' ';
}
cout << endl;
}
return 0;
}
简单例题
c++
#include <iostream>
#include <algorithm>
#define int long long
using namespace std;
const int N = 2e6 + 10;
int a1[N],s1[N];
int a2[N],s2[N];
signed main()
{
int n;
cin >> n;
for(int i = 1;i <= n;i ++ )
{
cin >> a1[i];
a2[i] = a1[i];
}
for(int i = 1;i <= n;i ++ )s1[i] = s1[i - 1] + a1[i];
sort(a2 + 1,a2 + 1 + n);
for(int i = 1;i <= n;i ++ )s2[i] = s2[i - 1] + a2[i];
int m;
cin >> m;
while(m -- )
{
int f;
cin >> f;
int l,r;
cin >> l >> r;
if(f == 1)cout << s1[r] - s1[l - 1] << endl;
else if(f == 2)cout << s2[r] - s2[l - 1] << endl;
}
return 0;
}
c++
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 2e6 + 10;
int a[N],b[N];
void insert(int l,int r,int c)
{
b[l] += c;
b[r + 1] -= c;
}
int main()
{
int n,k;
cin >> n >> k;
for(int i = 1;i <= n;i ++ )
{
a[i] = 0;
insert(i,i,a[i]);
}
while(k -- )
{
int l,r;
cin >> l >> r;
insert(l,r,1);
}
for(int i = 1;i <= n;i ++ )b[i] += b[i - 1];
sort(b + 1,b + 1 + n);
cout << b[n / 2 + 1] << endl;
return 0;
}
c++
#include <iostream>
using namespace std;
const int N = 5050;
int a[N][N],s[N][N];
int main()
{
int n;
cin >> n;
for(int i = 1;i <= n;i ++ )
{
for(int j = 1;j <= n;j ++ )
{
cin >> a[i][j];
}
}
for(int i = 1;i <= n;i ++ )
{
for(int j = 1;j <= n;j ++ )
{
s[i][j] = a[i][j] + s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1];
}
}
int ans = -0x3f3f3f3f;
for(int x1 = 1;x1 <= n;x1 ++ )
{
for(int y1 = 1;y1 <= n;y1 ++ )
{
for(int x2 = x1;x2 <= n;x2 ++ )
{
for(int y2 = y1;y2 <= n;y2 ++ )
{
ans = max(ans,s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1]);
}
}
}
}
cout << ans << endl;
return 0;
}
c++
#include <iostream>
using namespace std;
const int N = 5050;
int a[N][N],b[N][N];
void insert(int x1,int y1,int x2,int y2)
{
b[x1][y1] += 1;
b[x2 + 1][y1] -= 1;
b[x1][y2 + 1] -= 1;
b[x2 + 1][y2 + 1] += 1;
}
int n,m,q;
int main()
{
cin >> n >> m;
while(m -- )
{
int x1,y1,x2,y2;
cin >> x1 >> y1 >> x2 >> y2;
insert(x1,y1,x2,y2);
}
for(int i = 1;i <= n;i ++ )
{
for(int j = 1;j <= n;j ++ )
{
b[i][j] += b[i - 1][j] + b[i][j - 1] - b[i - 1][j - 1];
cout << b[i][j] << ' ';
}
cout << endl;
}
return 0;
}
双指针(GESP有所涉及)
真题中常常被应用于单词提取和区间提取操作。
基本概念
双指针算法是一种通过设置两个指针(或索引) 在序列(如数组、链表、字符串等线性结构)上移动,来高效解决问题的算法技巧。
其核心思想是:通过指针的 "移动规则" 避免暴力解法中的冗余遍历,将时间复杂度从 O(n²) 或更高优化为 O(n) 或 O(n + m)(n、m 为序列长度)。这里的 "指针" 并非特指 C/C++ 中的指针类型,而是泛指用于标记位置的变量(如数组下标、链表节点引用等)。
双指针的应用场景多样,但核心是根据问题特性设计指针的初始位置和移动规则。常见的应用模式可分为以下几类:
- 同向双指针
两个指针从同一端(通常是起始位置)出发,沿相同方向移动,常用于处理 "连续子序列 / 子串" 问题:用两个指针 i 和 j(i >= j)维护一个 "窗口" [j, i],窗口内的元素满足特定条件。固定 j 时,移动 i 扩大窗口;当窗口不满足条件时,移动 j 缩小窗口,确保窗口始终有效。
示例:最长连续不重复子序列
- i 作为 "右指针",从左到右遍历序列,负责扩大窗口
- j 作为 "左指针",当窗口内出现重复元素时右移,负责缩小窗口,保证窗口内无重复
- 每次调整后,计算窗口长度 i - j + 1,更新最大值
- 反向双指针
两个指针分别从序列的两端(左端和右端)出发,向中间移动,常用于 "有序序列的匹配" 问题:利用序列的 "有序性",通过比较两个指针指向的元素,决定哪个指针移动,快速缩小查找范围。
示例:数组元素的目标和
- 数组 a 递增,b 递增,l 从 a 的左端出发,r 从 b 的右端出发
- 若 a[l] + b[r] < x:需增大和,右移 l(a[l] 变大)
- 若 a[l] + b[r] > x:需减小和,左移 r(b[r] 变小)
- 直到找到和为 x 的 pair,避免了暴力枚举所有 pair 的 O(n*m) 复杂度
- 跨序列双指针
两个指针分别在两个不同的序列上移动,常用于 "匹配 / 包含关系" 问题(如判断子序列):用指针 i 遍历短序列 A,指针 j 遍历长序列 B,通过匹配元素推动指针移动,验证 A 是否能被 B 按顺序包含。
示例:判断子序列
- l 遍历子序列 a,r 遍历主序列 b
- 若 a[l] == b[r]:匹配成功,l 和 r 同时后移(继续匹配下一个元素)
- 若不匹配:仅 r 后移(在 b 中继续找 a[l])
- 若 l 遍历完 a,则 a 是 b 的子序列,否则不是
应用场景
c++
#include <iostream>
using namespace std;
const int N = 2e6 + 10;
int a[N],cnt[N],ans = 1;
int main()
{
int n;
cin >> n;
for(int i = 1;i <= n;i ++ )cin >> a[i];
for(int i = 1,j = 1;i <= n;i ++ )
{
cnt[a[i]] ++ ;
while(cnt[a[i]] > 1)
{
cnt[a[j]] -- ;
j ++ ;
}
ans = max(ans,i - j + 1);
}
cout << ans << endl;
return 0;
}
c++
#include <iostream>
using namespace std;
const int N = 2e6 + 10;
int a[N],b[N];
int n,m,x;
int main()
{
cin >> n >> m >> x;
for(int i = 1;i <= n;i ++ )cin >> a[i];
for(int i = 1;i <= m;i ++ )cin >> b[i];
int l = 1,r = m;
while(l <= n && m >= 1)
{
if(a[l] + b[r] < x)l ++ ;
else if(a[l] + b[r] == x)
{
cout << l - 1 << ' ' << r - 1 << endl;
break;
}
else r -- ;
}
return 0;
}
c++
#include <iostream>
using namespace std;
const int N = 2e6 + 10;
int a[N],b[N];
int main()
{
int n,m;
cin >> n >> m;
for(int i = 1;i <= n;i ++ )cin >> a[i];
for(int i = 1;i <= m;i ++ )cin >> b[i];
int l = 1,r = 1;
while(l <= n && r <= m)
{
if(a[l] == b[r])
{
l ++ ;
r ++ ;
}
else r ++ ;
}
if(l == n + 1)cout << "Yes" << endl;
else cout << "No" << endl;
return 0;
}
位运算(对标GESP三级)
基本概念
位运算算法是利用二进制数的位操作(如左移、右移、异或、与、或等)实现的高效算法,其核心是通过直接操作二进制位减少计算步骤,时间复杂度通常为 O(1) 或 O(n),在算法优化中应用广泛。以下是几个基础且常用的位运算算法:
位运算算法是利用二进制数的位操作(如左移、右移、异或、与、或等)实现的高效算法,其核心是通过直接操作二进制位减少计算步骤,时间复杂度通常为O(1)或O(n),在算法优化中应用广泛。以下是几个基础且常用的位运算算法:
左移运算:
左移运算(<<)是将一个数的二进制位向左移动指定的位数,右侧空位补0。 对于整数 x,左移 n 位(x << n)的结果等价于 x * 2^n(前提是不发生溢出)。 最常用的是左移1位(x << 1),直接实现 x * 2。
当需要计算 x * 2^n 时,左移运算比乘法更高效。
右移运算:
右移运算(>>)是将一个数的二进制位向右移动指定的位数,对于非负整数 x,右移 n 位(x >> n)的结果等价于 x // 2^n(向下取整的除法)。 最常用的是右移1位(x >> 1),直接实现 x // 2。
常常用于替代除法运算:计算 x // 2^n 时,右移比除法更高效。 也可以用来取中间值:如 (l + r) >> 1 。
按位异或:
按位异或是指两个二进制位相同则为0,不同则为1(00=0,11=0,0^1=1)。其核心性质如下:
- 0与任何数异或等于该数:x ^ 0 = x。
- 两个相同的数异或等于0:x ^ x = 0。
- 异或满足交换律和结合律:a ^ b ^ c = a ^ c ^ b = (a ^ b) ^ c。
常常应用于:
不使用临时变量交换两个数 :
利用性质 1 和 2,可在不额外开辟空间的情况下交换 a 和 b:
cpp
a = a ^ b; // 此时a存储a^b的结果
b = a ^ b; // b = (a^b) ^ b = a ^ (b^b) = a ^ 0 = a(完成b的更新)
a = a ^ b; // a = (a^b) ^ a = b ^ (a^a) = b ^ 0 = b(完成a的更新)
寻找数组中唯一出现一次的数:
若数组中除一个数出现 1 次外,其余数均出现 2 次,可通过异或全体元素找到这个数(出现 2 次的数异或后为 0,最终结果为唯一出现 1 次的数):
cpp
int findUnique(vector<int>& nums) {
int res = 0;
for (int x : nums) res ^= x;
return res; // 例如nums = [2,3,2,1,3],结果为1
}
判断两个数是否相等:
若 a ^ b = 0,则 a == b(利用性质2)。
二进制枚举:
状态压缩是用二进制数的每一位表示一个"状态"(如"选"或"不选"),通过枚举二进制数的所有可能值,遍历所有状态组合。
- 对于 n 个元素,可用 n 位二进制数表示其选取状态:第 i 位为1表示选取第 i 个元素,为0表示不选。
- 枚举范围为 0 ~ 2^n - 1(共 2^n 种状态),适用于 n ≤ 20 的场景(2^20 ≈ 1e6,枚举代价可接受)。
例如枚举集合的所有子集:
对于集合 {0, 1, 2}(n = 3),用 3 位二进制数表示子集:
- 000(0):空集
- 001(1):{0}
- 010(2):{1}
- 011(3):{0, 1}
- 100(4):{2}
- ...
- 111(7):{0, 1, 2}
lowbit函数:
lowbit(x) 定义为:返回 x 的二进制表示中"最低位的1及其后面的0"组成的数。 计算公式:lowbit(x) = x & (-x)(基于补码特性:-x = ~x + 1,与 x 相与后仅保留最低位的 1)。
常常应用于:
计算二进制中1的个数
循环用 x -= lowbit(x) 消除最低位的1,直到 x = 0,计数循环次数:
cpp
int countOne(int x) {
int cnt = 0;
while (x) {
x -= lowbit(x); // 每次消除最低位的1
cnt ++;
}
return cnt; // 例如 x = 5(101),结果为2
}
应用场景
c++
#include <iostream>
using namespace std;
const int N = 2e6 + 10;
int a[N];
int lowbit(int x)
{
return x & (-x);
}
int main()
{
int n;
cin >> n;
for(int i = 1;i <= n;i ++ )
{
cin >> a[i];
int cnt = 0;
while(a[i])
{
a[i] -= lowbit(a[i]);
cnt ++ ;
}
cout << cnt << ' ';
}
return 0;
}
离散化(GESP未涉及)
基本概念
当数据值域极大且稀疏,且处理时仅关注数据的相对关系而非具体值时,直接使用原始数据会导致空间浪费(如数组下标超限)或效率低下。离散化能在不改变数据相对关系的前提下,将大范围原始数据映射为小范围连续索引(如 0~n-1,n 为不同元素数量),大幅压缩值域。因此,对于值域大且稀疏、仅需关注相对关系的数据,需通过离散化优化处理。
离散化是一种对数据的转换技术,核心是 "映射"。该映射需满足两个关键条件:
- 保序性:原始数据的大小关系与映射后索引的大小关系一致
- 压缩性:映射后的值域远小于原始值域,且连续
离散化的目标是将原始数据转换为离散索引,需明确 "原始元素->索引" 的对应关系。建立对应关系需经过四步:
- 提取原始数据中所有不同元素
- 对去重后的元素排序,保证保序性
- 为排序后的元素分配连续索引
- 用索引替换原始数据中的元素
离散化后需快速根据原始元素找到对应索引。去重并排序后的元素集合是有序的,而有序集合可通过二分查找快速定位元素位置。因此,其实现也需要通过二分查找来高效获取原始元素的离散化索引。
应用场景
c++
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
const int N = 2e6 + 10;
typedef pair<int,int> PII;
vector<int> idxs;
vector<PII> add,query;
int a[N],s[N];
int find(int x)
{
int l = 0,r = idxs.size() - 1;
while(l < r)
{
int mid = l + r >> 1;
if(idxs[mid] >= x)r = mid;
else l = mid + 1;
}
return l + 1;
}
int main()
{
int n,m;
cin >> n >> m;
while(n -- )
{
int x,c;
cin >> x >> c;
add.push_back({x,c});
idxs.push_back(x);
}
while(m -- )
{
int l,r;
cin >> l >> r;
query.push_back({l,r});
idxs.push_back(l);
idxs.push_back(r);
}
sort(idxs.begin(),idxs.end());
idxs.erase(unique(idxs.begin(),idxs.end()),idxs.end());
for(int i = 0;i < add.size();i ++ )
{
int id = find(add[i].first);
a[id] += add[i].second;
}
for(int i = 1;i <= idxs.size();i ++ )s[i] = s[i - 1] + a[i];
for(int i = 0;i < query.size();i ++ )
{
int L = find(query[i].first);
int R = find(query[i].second);
cout << s[R] - s[L - 1] << endl;
}
return 0;
}
区间合并(GESP有所涉及)
涉及GESP 四级可能出现的模拟问题、五级可能出现的区间贪心问题、七级可能出现的区间 dp 问题。
基本概念
区间合并算法是一种用于处理多个区间的经典算法,核心目标是将重叠或相邻的区间合并为一个连续的大区间,最终得到一组互不重叠的区间。其本质是通过排序和线性遍历,消除区间之间的冗余重叠部分,简化区间集合。
以下是一些基本概念:
- 区间:由两个端点定义的范围,通常表示为 [l, r](l 为左端点,r 为右端点,且 l ≤ r)。
- 重叠 / 相邻区间:对于两个区间 [l1, r1] 和 [l2, r2],若 l2 ≤ r1(即第二个区间的左端点不超过第一个区间的右端点),则两区间重叠或相邻(如 [1,3] 与 [2,5] 重叠,[1,4] 与 [4,6] 相邻),可合并为 [min(l1,l2), max(r1,r2)]。
- 区间合并 :将所有重叠或相邻的区间合并,最终得到一组互不重叠、无相邻的区间
实现步骤
-
排序:
-
区间合并的核心是判断 "后一个区间是否与前一个已合并的区间重叠",需保证区间按一定顺序排列以简化判断
-
按区间左端点 l 升序排序后,所有可能与当前区间重叠的区间必然在其右侧(左端点不更小),可线性遍历处理
-
因此,第一步需将所有区间按左端点 l 从小到大排序
-
-
遍历合并:
-
排序后,区间的左端点呈递增趋势,只需用一个 "当前合并区间" 跟踪已合并的范围,即可判断后续区间是否需要合并
-
遍历排序后的区间时,若当前区间的左端点 l i l_i li ≤ "当前合并区间的右端点 r",则两区间重叠 / 相邻,需合并(更新 r 为两者右端点的最大值);否则,"当前合并区间" 已无法与后续区间重叠,将其加入结果集,并用当前区间重新初始化 "当前合并区间"
-
因此,第二步需通过一次线性遍历,动态维护 "当前合并区间",完成所有重叠区间的合并
-
-
加入最后一个合并区间:
-
遍历结束后,最后一个 "当前合并区间" 未被加入结果集
-
该区间是最终不重叠区间集合的一部分
-
因此,需将最后一个 "当前合并区间" 加入结果集,得到最终合并后的区间列表
-
应用场景
c++
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
typedef pair<int,int> PII;
vector<PII> Q;
int main()
{
int n;
cin >> n;
for(int i = 1;i <= n;i ++ )
{
int l,r;
cin >> l >> r;
Q.push_back({l,r});
}
sort(Q.begin(),Q.end());
int ans = 0;
int st = Q[0].first,ed = Q[0].second;
for(int i = 0;i < Q.size();i ++ )
{
if(Q[i].first <= ed)ed = max(ed,Q[i].second);
else
{
ans ++ ;
st = Q[i].first;
ed = Q[i].second;
}
}
ans ++ ;
cout << ans << endl;
return 0;
}