排序
NOTE
快速排序
最坏时间复杂度:O(nlogn)
基本步骤
- 确定比较的值 x(通常为左右端点的值/中间点的值)
- 将整区间划分为两部分,使分界点左侧的元素都小于 x,右侧都大于 x
做法:定义两个指针,由两个端点向中间遍历,当左指针遇到大于 x 的数时停下,右指针遇到小于 x 的数时停下,swap,继续向中间移动,移动到两指针相遇。此时满足左指针左侧的数值都小于 x,右指针右侧的数都大于 x

- 递归处理左侧和右侧
模板
void quick_sort(int q[], int l, int r){ if (l >= r) // 区间里没有数或只有一个数 return;
int x = q[l], i = l - 1, j = r + 1; // 下面用j做范围来递归时,x不能取到r,否则会有边界问题 // i和j此处放在数组边界的两侧,防止后续越界,因为下面要先移动指针再判断,有可能会移出去 while (i < j) // i和j迭代 { do i++; // 向右移动i while (q[i] < x); do j--; // 向左移动j while (q[j] > x); if (i < j) // 两个指针不相遇时,swap swap(q[i], q[j]); } quick_sort(q, l, j); // 处理区间【原数组左边界,j】的数据 quick_sort(q, j + 1, r); // 处理区间【j+1,原数组右边界】的数据}
// quick_sort(q,0,n-1)C 标准库实现的快速排序-qsort()
void qsort( void *ptr, count, size, cmp);
以升序排序 ptr 指向的给定数组。该数组包含 count 个元素,每个元素占用 size 字节。
cmp 为比较函数,如果第一个参数“小于”第二个参数,则返回负整数值;如果第一个参数“大于”第二个参数,则返回正整数值;如果参数相等,则返回零。
归并排序
基本步骤
- 取整个数组的中间位置 mid 作为分界点
- 递归排序[L,mid]和[mid+1,R]
- 将左右两个有序序列归并成一个有序序列
归并的实现流程
定义临时的数组用来存储结果,定义双指针
- 比较两个指针指向的更小的数,把较小的数存入结果数组
- 迭代指向较小的数的指针,重复进行第一步
- 当有一边没迭代完而另一边已经停下的时候,直接把没循环完的那边接在后面
- 完成归并后,结果存回原数组
注意:tmp[0]对应 arr[l],迭代 r-l+1 次
模板
void merge_sort(int arr[], int l, int r){ if(l >= r) return;
int mid = l + r >> 1; //确定分界点
merge_sort(arr,l,mid); merge_sort(arr,mid+1,r); //递归进行排序,此时整个数组已经被分成了各段有序的小数组
int k = 0, i = l,j = mid + 1; //定义双指针,i指向左半边起点,j指向右半边起点
//归并,合二为一 while(i <= mid && j <= r) //左半边和右半边不为空 { if(arr[i] < arr[j]) tmp[k++] = arr[i++]; //比较i,j指向的数,把较小的数放到当前位置上并迭代指针 else tmp[k++] = arr[j++]; // 注意别忘写else,算进等于的情况 }
//扫尾 //当有一边没循环完而另一边已经停下的时候,直接把没循环完的那边接在后面 while(i <= mid) tmp[k++] = arr[i++]; while(j <= r) tmp[k++] = arr[j++];
for(i = l, j = 0; i <= r; i++, j++) arr[i] = tmp[j]; //把临时存储的结果存回arr}
//merge_sort(arr,0,n-1)逆序对问题
https://www.acwing.com/problem/content/description/790/
https://www.luogu.com.cn/problem/P1116
TIP注意:第三种情况,每次要输出 q[j]时,向答案里加 mid-i+1
#include <bits/stdc++.h>using namespace std;typedef long long ll;int arr[100010],tmp[100010];
ll merge_sort(int arr[], int l, int r){ ll res = 0; if(l >= r) return 0; int mid = l + r >> 1;
res += merge_sort(arr,l,mid); res += merge_sort(arr,mid+1,r);
int k = 0, i = l, j = mid + 1; while(i <= mid && j <= r) { if(arr[i] <= arr[j]) tmp[k++] = arr[i++]; else { tmp[k++] = arr[j++]; res += mid - i + 1; } }
while(i <= mid) tmp[k++] = arr[i++]; while(j <= r) tmp[k++] = arr[j++];
for(i = l, j = 0; i <= r; i ++, j ++) arr[i] = tmp[j];
return res;
}int main(){ int n; scanf("%d", &n); for(int i = 0; i < n; i ++) { scanf("%d", &arr[i]); }
ll res = merge_sort(arr,0,n-1); cout << res << endl; return 0;}堆排序
https://oi-wiki.org/basic/heap-sort/
不稳定,时间复杂度
O(nlogn)
#include <bits/stdc++.h>using namespace std;
int n; // 需排序的数的总数const int N = 1e5 + 10;int heap[N], size;
void down(int k){ int t = k;
if (k * 2 <= size && heap[2 * k] < heap[t]) t = 2 * k; if (k * 2 + 1 <= size && heap[2 * k + 1] < heap[t]) t = 2 * k + 1;
if (t != k) { swap(heap[t], heap[k]); down(t); }}int main(){ cin >> n; size = n;
for (int i = 1; i <= n; i++) cin >> heap[i]; // 输入所有数
for (int i = n / 2; i >= 1; i--) // 建堆 down(i);
while (n--) { // 输出堆头 printf("%d ", heap[1]);
// 删除堆头 heap[1] = heap[size]; size--; down(1); } return 0;}// 变量名写size有的编译器会报错STL 的 sort()
基本用法
TIPStart 和 end 为排序区间的首尾地址
- 默认升序:
sort(start, end) - 降序:
sort(start, end, greater<T>()) - 自定义比较:
sort(start, end, cmp)(cmp为比较函数或 lambda 表达式) - 注:给 pair 排序时,sort 默认先排左端点
cmp 函数的写法
若第一参数在第二参数之前排序,则返回 true。 返回 true 的时候,代表两个比较对象的位置正确,不需要交换 注意:比较函数不可修改传递给它的对象
bool cmp(type &a,type &b){ return a>b; //降序,第一个最大 or return a<b; //升序,第一个最小 //大的在前写大于,小的在前写小于}二分
时间复杂度:O(logn)
TIP是否可以使用二分法的关键在于:二分后,能否判断出答案所在的区间,而不是数据是否有序。
整数二分查找(全闭区间写法)
例题(用到了两种模板):**https://www.acwing.com/problem/content/description/791/** 数的范围
基本步骤
- 找中间值 mid ,判断 mid 是否满足某种性质
- 通过 mid 的判断结果更新区间,保证区间里一定有答案
- 通过区间的划分状况选择模板
更新区间的规则
- 如果 a[mid] > target,target 最后一次出现的位置一定在 a[mid]之前,更新 r:r = mid - 1。
- 如果 a[mid] <= target,target 最后一次出现的位置可能在 mid 处,也可能在 a[mid]之后,更新 l:l = mid。
- 直到 l = r 时,区间内只有一个元素,这个元素就是 target 最后一次出现的位置。
因 int 下取整而导致的死循环问题
mid 用(l + r) / 2 计算时,如果程序中有 l = mid ,程序会陷入死循环。
mid 用(l + r + 1) / 2 计算时,如果程序中有 r = mid ,程序会陷入死循环。
模板
TIP写 l = mid 时,上面的 mid 要加 1!! return 的值是 l/r/mid,具体问题具体分析
bool check(int x) {/* ... */} // 检查x是否满足某种性质
// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用:// 找最小索引(被查找数第一次出现的位置)// 最大值最小int bsearch_1(int l, int r){ while (l < r) { int mid = l + r >> 1; if (check(mid)) r = mid; // check()判断mid是否满足性质 else l = mid + 1; } return l;}
// 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用:// 找最大索引(被查找数最后一次出现的位置)// 最小值最大int bsearch_2(int l, int r){ while (l < r) { int mid = l + r + 1 >> 1; // 补加1防止死循环 if (check(mid)) l = mid; else r = mid - 1; } return l;// 二分结束后,l和r相等,故return二者之一即可浮点数二分查找
例题:**https://www.luogu.com.cn/problem/P1024** 求解一元三次方程
注意:浮点数二分的左右边界可以做到严格折半,故直接更新区间即可
bool check(double x) {/* ... */} // 检查x是否满足某种性质
double bsearch_3(double l, double r){ const double eps = 1e-6; // eps 表示精度,取决于题目对精度的要求,一般比要求的有效数字多两位 while (r - l > eps) //二分的“停止”条件,需要一个最小单位约束二分的进行,否则无限二分了 { double mid = (l + r) / 2; if (check(mid)) r = mid; else l = mid; } return l;STL 的二分查找函数
C++ 标准库中实现了查找首个不小于给定值的元素的函数 std::lower_bound 和查找首个大于给定值的元素的函数 std::upper_bound,二者均定义于头文件
lower_bound:在一个有序序列中进行二分查找,返回指向第一个 大于等于 𝑥 的元素的位置的迭代器.如果不存在这样的元素,则返回尾迭代器.lower_bound(v.begin(),v.end(),x).upper_bound:在一个有序序列中进行二分查找,返回指向第一个 大于 𝑥 的元素的位置的迭代器.如果不存在这样的元素,则返回尾迭代器.upper_bound(v.begin(),v.end(),x).
二者均采用二分实现,调用前必须保证容器有序。
lower_bound 的定义是基于“插入”的,而非查找,即在一个非严格单调(存在相等元素)的有序容器中,插入一个数并继续保持容器的有序性,可以插在哪
二分答案
解题的时候往往会考虑枚举答案然后检验枚举的值是否正确。若满足单调性,则满足使用二分法的条件。把这里的枚举换成二分,就变成了「二分答案」。

例题:https://www.luogu.com.cn/problem/P1873
#include <bits/stdc++.h>using namespace std;int a[1000005];int n, m;
bool check(int k) { // 检查可行性,k 为锯片高度 long long sum = 0; for (int i = 1; i <= n; i++) // 检查每一棵树 if (a[i] > k) // 如果树高于锯片高度 sum += (long long)(a[i] - k); // 累加树木长度 return sum >= m; // 如果满足最少长度代表可行}
int find() { int l = 1, r = 1e9 + 1; // 因为是左闭右开的,所以 10^9 要加 1 while (l + 1 < r) { // 如果两点不相邻 int mid = (l + r) / 2; // 取中间值 if (check(mid)) // 如果可行 l = mid; // 升高锯片高度 else r = mid; // 否则降低锯片高度 } return l; // 返回左边值}
int main() { cin >> n >> m; for (int i = 1; i <= n; i++) cin >> a[i]; cout << find(); return 0;}
二分的开区间写法、循环不变量
https://www.bilibili.com/video/BV1AP41137w7/
区间的定义:表示我们需要知道这个区间内的元素和 target 的关系
L 和 R 之间就是还没确定大小的数,开和闭决定了边界和最终结束时的位置
无论二分查找还是二分答案,都有“循环不变量”,即在整个二分过程中,始终满足成立的表达式,例如 check(left) == false
每次写二分的时候,用红蓝染色法模拟一遍,判断如何更新区间,并找出循环不变量,判定查找完以后,最后两个指针在哪
//假定数组长度为nint bsearch(int arr[], int l, int r){ l = -1; right = n; // 初始开区间(l,r),l和r都是无法取到的数 while(l + 1 < r) // 进入循环的条件:区间不为空 { mid = (l + r) / 2; if(check(mid)) l = mid; //(mid,r) else r = mid; //(l,mid) } return r;}高精度
原理:用程序模拟竖式运算
大整数的 IO
由于 c++ 的数据存储类型存在上限,故在存储极大的整数时,可以考虑用数组/vector 存储每位数的数字
首先以 string 的形式读入,再把每位数扣下来,放到数组里
为了运算进位方便,应“倒着存”,即个位数位于数组的第一位
例如:123456789,应使 a[0]=9,a[9]=1
同理,输出的时候,从数组的最后一位倒着输出
注意,输出的时候需要去掉答案前面的 0,即前导 0
使用普通数组
string a, b; int A[1e6],B[1e6]; cin >> a >> b; int lena = a.size(), lenb = b.size(); for (int i = lena - 1; i >= 0; i--) { A[lena - i] = a[i] - '0'; } for (int i = lenb - 1; i >= 0; i--) { B[lenb - i] = b[i] - '0'; } //注意此写法A[0]和B[0]没存数,值为0,若使用此种方法,后面应该从1开始遍历使用 vector
string a, b; vector<int> A, B; cin >> 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');高精度加法
#include <iostream>#include <vector>
using namespace std;
vector<int> add(vector<int> &A, vector<int> &B) //使用&提高处理效率{ vector<int> C;
int t = 0; //负责进位的变量 for (int i = 0; i < A.size() || i < B.size(); i ++ ) { if (i < A.size()) t += A[i];//判断位置i上有没有数,并加上本身和进位 if (i < B.size()) t += B[i];
C.push_back(t % 10); //把当前位数放到答案里 t /= 10; //进位,此时t为0或1 }
if (t) C.push_back(t); //最高位的进位,若最后一轮t>10,则/=10后不为0,此时最高位进1 return C;}
int main(){ string a, b; // 读入两个大整数 vector<int> A, B; //存储两个大整数的数组 cin >> a >> b; for (int i = a.size() - 1; i >= 0; i -- ) A.push_back(a[i] - '0'); //倒序存位数,-'0'是把字符变成整数 for (int i = b.size() - 1; i >= 0; i -- ) B.push_back(b[i] - '0');
vector<int> C = add(A, B);//可使用auto等价vector<int>
for (int i = C.size() - 1; i >= 0; i -- ) cout << C[i]; //倒着输出 cout << endl;
return 0;}高精度减法
注意事项
- IO 时,注意先判断 A 和 B 的大小关系,若 A<B 则算 B-A,在输出时加负号
- 模拟减法运算思路时,注意是倒着算的,也就是对于 vector 里面存的数,是向后一位借 1
- 注意返回结果前,需要去掉前面的 0,例如 123-120 的结果实际上是 003
模板(a,b 均为正数)
#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; //第i位A的值,考虑是否被前一位借走了1
if(i < B.size()) t -= B[i]; //判断B在这一位上有没有数,有的话就减
C.push_back((t + 10) % 10); //存这一位的结果,t>=0时输出t,t<0时输出(t+10)
if(t < 0) t = 1; //若t小于0,则需要借位 else t = 0; }
**while(C.size() > 1 && C.back() == 0) C.pop_back(); //减法做完后,去掉前导0** return C;}
int main(){ string a, b; vector<int> A, B; cin >> 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)) //比较A,B的大小 { auto C = sub(A,B); for (int i = C.size() - 1; i >= 0; i -- ) cout << C[i]; cout << endl; } else //若A<B则算B-A,在输出时加负号 { auto C = sub(B,A); cout << "-"; for (int i = C.size() - 1; i >= 0; i -- ) cout << C[i]; cout << endl; }
return 0;}高精度乘法
高精度乘法的进位既可以边迭代边处理,也可先相乘每位,后处理进位。
高精度乘低精度(同时完成乘法和进位)
TIP在高精度的乘法运算中,计算过程类似于下表达式。 例如,2516 = 161 + 5610 + 26*100 = 1506 第一个数代表高精度数组每一位所存放的数 第二个数代表低精度的数,所以高精度的数组的每一位都要乘一遍低精度的数 第三个数即为数组中存放的顺序,即 a[0]代表个位,a[1]代表十位,a[2]代表百位,等等
// C = A * b, A >= 0, b > 0vector<int> mul(vector<int> &A, int b){ vector<int> C; int t = 0; //表示进位 for (int i = 0; i < A.size() || t; i ++ ) //i循环完或t为0时停止循环,因此停止循环时t必为0 { 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();//去掉前导0 return C;}高精度乘高精度(先算乘法,后进位)
思路
- 枚举 A 和 B 的每一位,并把 A 和 B 的每一位都进行相乘,储存在数组 C 里。
- 遍历数组 C,当前位等于 %10,下一位 +=当前位/10
- 去掉前导 0
模板
vector<int> mul(vector<int> &A, vector<int> &B){ vector<int> C(1e6); 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; i < A.size() + B.size(); i++) //处理进位 { C[i + 1] += C[i] / 10; C[i] %= 10; } while (C.size() > 1 && C.back() == 0) C.pop_back(); return C;}高精度除法
TIP注意:除法的运算与前三种运算不同,是从最高位开始的,但是实际问题中通常混合不同运算,所以仍然要倒着存进 vector,但从 vector 的最后一位开始运算
高精度除低精度
TIPreverse 的原因:当做完除法运算时,此时的前导 0 位于 vector 的开头,但是 vector 删除第一个元素不方便,所以 reverse,并用 pop_back 去除最后一个元素 而输出的时候是倒着输出,最后就会负负得正,输出正确的结果 注意部分题目中,余数可能整型溢出!

#include <iostream>#include <vector>#include <algorithm>
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;}
int main(){ string a; vector<int> A;
int B; cin >> a >> B; for (int i = a.size() - 1; i >= 0; i -- ) A.push_back(a[i] - '0');
int r; //余数 auto C = div(A, B, r);
for (int i = C.size() - 1; i >= 0; i -- ) cout << C[i]; //注意倒着输出
cout << endl << r << endl;
return 0;}高精度例题——计算阶乘和(高精度乘低精度,高精度加法)
https://www.luogu.com.cn/problem/P1009
#include <bits/stdc++.h>using namespace std;int arr[1000], res[1000];int main(){ int n; cin >> n; arr[0] = res[0] = 1; for (int i = 2; i <= n; i++) { for (int j = 0; j <= 100; j++) { res[j] *= i; //注意每一位都要乘i } for (int j = 0; j <= 100; j++) //处理乘法后的进位,即把结果按位存放在res里 { if (res[j] > 9) { res[j + 1] += res[j] / 10; res[j] %= 10; } } for (int k = 0; k < 100; k++) { arr[k] += res[k]; //高精度加高精度 if (arr[k] > 9) //处理加法后的进位 { arr[k + 1] += arr[k] / 10; arr[k] %= 10; } } } int len = 100; for (int i = 100; i >= 0; i--) //去掉前导0 { if (arr[i] != 0) break; else len--; } for (int i = len; i >= 0; i--) cout << arr[i]; return 0;}前缀和与差分
前缀和与差分互为逆运算
注意:前缀和与差分数组序列初始化时,下标应当从 1 开始。并初始化第 0 位为 0
此写法可确保递推公式的普遍性,不需要特判 l=1 的情况
前缀和
前缀和可以简单理解为「数列的前 n 项的和」,是一种重要的预处理方式。 例如:a6 + a7 + a8 = S8 - S5 通过 𝑂(𝑛) 时间的预处理,能够将单次查询 [l ,r] 区间和的复杂度降低到 𝑂(1)
一维前缀和
- 一维前缀和数组的初始化:
for (int i = 1; i <= n; i ++ ) s[i] = s[i - 1] + a[i];
(前 i 个数的和=前 i-1 个数的和 + 第 i 个数)
- 区间 [l ,r] 的元素和:
s[r] - s[l - 1]
二维前缀和
- 二维前缀和数组的初始化:
sum[i][j] = sum[i-1][j] + sum[i][j-1] - sum[i-1][j-1] + arr[i][j]
- 二维前缀和的求和公式:
sum[x2][y2] - sum[x1-1][y2] - sum[x2][y1-1] + sum[x1-1][y1-1]

差分
差分是一种与前缀和相对的策略,是前缀和的逆运算。相较于给定某一序列求它的差分,竞赛中更为常见的情景是,通过维护差分序列的信息,实现多次区间修改。在区间修改结束后,可以通过前缀和恢复原序列的信息,实现对原序列的查询。注意修改操作一定要在查询操作之前。—— OI wiki (常常用于维护多次对序列的一个区间加上一个数,并在之后一次或多次询问序列某一位的取值)
一维差分
基本思路
给定一个数组 a,现要构造一个 b 数组,使得 ai = b1 + b2 + … + bi
即对于序列 ai,它的差分序列是:𝐷𝑖=𝑎𝑖−𝑎(𝑖−1), 𝑎0=0 也可知,ai 序列是 bi 序列的前缀和
完成操作后,需要求差分序列的前缀和以恢复被修改的原序列
一维差分序列的插入操作
void insert(int l, int r, int c){ b[l] += c; b[r + 1] -= c;}
构造时:for(int i = 1; i <= n; i++) insert(i,i,a[i]); //a[i]为原数组
例题-Acwing797
https://www.acwing.com/problem/content/799/
输入一个长度为 n 的整数序列。 接下来输入 m 个操作,每个操作包含三个整数 l,r,c,表示将序列中 [l,r] 之间的每个数加上 c。 请你输出进行完所有操作后的序列。 输入格式 第一行包含两个整数 n 和 m。 第二行包含 n 个整数,表示整数序列。 接下来 m 行,每行包含三个整数 l,r,c,表示一个操作。 输出格式 共一行,包含 n 个整数,表示最终序列。
#include <bits/stdc++.h>using namespace std;const int N = 100010;int n,m,a[N],b[N];void insert(int l, int r, int c){ b[l] += c; b[r + 1] -= c;}int main(){ scanf("%d%d", &n, &m); for(int i = 1; i <= n; i++) scanf("%d", &a[i]); for(int i = 1; i <= n; i++) insert(i,i,a[i]); //差分序列的初始化,看作在b数组的[i,i]区间内插入a[i] //b[2]首先-=了a[1],再+=了a[2],则有b[2] = a[2] - a[1],下面以此类推
while(m--) { int l, r, c; scanf("%d%d%d", &l, &r, &c); //对于m次操作,在差分序列上+c insert(l,r,c); }
for(int i = 1; i <= n; i++) b[i] += b[i-1];//对已经修改过的差分数组求前缀和,得到原数组修改后的结果 for(int i = 1; i <= n; i++) printf("%d ", b[i]); return 0;}二维差分
基本思路
TIP完成操作后,需要求差分序列的前缀和,把差分序列恢复成被修改的原序列
二维差分序列的插入函数
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;}
构造时:for(int i = 1; i <= n; i++) for(int j = 1; j <= m; j++) insert(i, j, i, j, a[i][j]); //a[i][j]为原数组
例题-Acwing798
位运算
对于左移操作,需要确保移位后的结果能被原数的类型容纳,否则行为也是未定义的。对一个负数执行左移操作也未定义。左移一位相当于*2 对于右移操作,右侧多余的位将会被舍弃,而左侧较为复杂:对于无符号数,会在左侧补 0;而对于有符号数,则会用最高位的数(其实就是符号位,非负数为 0,负数为 1)补齐。 算术右移一位相当于除以 2 并向下取整
求一个十进制数 n 的二进制表示的第 k+1 位
- 把第 k 位移到最后一位,n >> k(将 n 的 2 进制向右移 k 位)
- 看个位是多少的做法:n 按位与 1, n & 1
即有公式:n >> k & 1
lowbit 操作
int lowbit(int x) { // x 的二进制中,最低位的 1 以及后面所有 0 构成的数值。 // lowbit(0b01011000) == 0b00001000 // ~~~~^~~~ // lowbit(0b01110010) == 0b00000010 // ~~~~~~^~ return x & -x; // x二进制的最后一位1的位置}功能
lowbit(x):返回 x 二进制的最后一位 1 的位置(实质上为 x & -x)
x = 1010, lowbit(x) = 10 x = 1010000, lowbit(x) = 10000
函数实现的原理
x & -x = x & (~x+1)
-x 相当于 x 取反加 1,原码反码补码的知识。
区间合并
给定 n 个区间 [l,r],要求合并所有有交集的区间。 注意如果在端点处相交,也算有交集。 例如:[1,3] 和 [2,6] 可以合并为一个区间 [1,6]。 [1,4] 和 [4,6] 可以合并为一个区间 [1,6]。
解决方法
- 用 pair 存储每个区间,放进 vector pair 数组中
- 按区间左端点排序
- 模板(注意代码细节)

模板函数
typedef pair<int, int> PII;
void merge(vector<PII> &segs){ vector<PII> res; sort(segs.begin(), segs.end()); // 排序左端点
int start = -2e9, end = -2e9; // 左右端点初始化成负无穷,保证和所有的给定区间都没有交集
for (auto seg: segs) // 从小到大扫描所有左端点,开始合并 if (end < seg.first) // 画板中黄色的情况 { if (start != -2e9) // 特判最开始的情况,因为无穷小,所以必定进入黄色的情况,但不应该把它放进结果 res.push_back({start, end}); // 把当前区间放入结果
start = seg.first, end = seg.second; } else end = max(end, seg.second); // 画板中紫色的情况
if (start != -2e9) res.push_back({start, end}); // 考虑结束循环时的区间,即最后一个左端点。 // 因为这是最后的一个区间,所以不可能继续进行合并,直接放进结果。 // 同样,此区间也不能是一开始的负无穷区间
segs = res; // 把res覆盖到segs里}贡献法
想象一下,你面前有一堆问题,比如“计算所有连续子数组的和的总和”。最直接的想法就是把所有子数组都列出来,分别求和再加起来。但这样太慢了,如果数组有 n 个元素,子数组的数量是 O(n²) 级别,当 n 很大时计算量巨大。
对于“计算所有连续子数组的和的总和”的问题,贡献法提供了一种思路:
我们不直接枚举所有子数组,而是反过来思考每个元素在最终答案中起了多大作用。
也就是说,我们分析每个元素会被多少个不同的子数组包含,然后把这个元素的值乘以它出现的次数,最后把所有元素的贡献加起来,就得到了答案。
这种方法的核心是将整体问题拆解成每个独立元素的贡献,往往能把复杂度从 O(n²) 降到 O(n) 或 O(n log n)。
离散化
for (int i = 1; i <= n; i++) { cin >> a[i]; b[i] = a[i]; } sort(b + 1, b + n + 1); l = unique(b + 1, b + n + 1) - b - 1; // l表示去重后剩下的元素个数 for (int i = 1; i <= n; i++) cout << lower_bound(b + 1, b + l + 1, a[i]) - b; // 查找序号并输出
