目录
一、快速排序
思路:快排就是选择一个哨兵x,然后使得x左边都小于x,x右边都大于x。
怎么实现呢?
方法是双指针法,左边来一个指针i(其实不是真的指向地址的那个指针),右边来一个指针j
然后使i从左往右扫,直到q[i]>=x,停止扫描
使j从右往左扫,直到q[j]<=x,停止扫描
若i,j未相遇,则交换q[i]与q[j]
题目链接:快速排序
模板如下:
#include <iostream>
using namespace std;
const int N = 1e6 + 10;
int n;
int q[N];
void quick_sort(int q[], int l, int r){
if(l >= r) return;
int i = l - 1, j = r + 1;
int x = q[l + 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);
}
int main() {
cin >> n;
for(int i=0; i<n; i++){
cin >> q[i];
}
quick_sort(q, 0, n-1);
for(int i=0; i<n; i++){
cout << q[i] << " ";
}
cout << endl;
return 0;
}
二、归并排序
思路:二分区间,分为[l, mid],[mid+1, r]
然后,用两个指针i和j分别指向两个区间序列的第一位,即l和mid+1
然后,合并两个区间,利用tmp数组一个一个按照q[i]<=q[j]的条件去读进tmp数组中
然后,将末尾没比较完的数加到tmp的尾部
最后,把tmp中的数还给q中
题目链接:归并排序
#include <iostream>
using namespace std;
const int N = 1e6 + 10;
int n;
int q[N], tmp[N];
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]) tmp[k++] = q[i++];
else tmp[k++] = q[j++];
}
while(i <= mid) tmp[k++] = q[i++];
while(j <= r) tmp[k++] = q[j++];
for(int i = l, j = 0; i <= r; i++, j++){
q[i] = tmp[j];
}
}
int main() {
cin >> n;
for(int i=0; i<n; i++){
cin >> q[i];
}
merge_sort(q, 0, n-1);
for(int i=0; i<n; i++){
cout << q[i] << " ";
}
cout << endl;
return 0;
}
归并排序涉及到了逆序对数量的问题。
因为区间[l, mid]和[mid+1, r]都是有序且递增的,所以如果[mid+1, r]这个区间中第j个数小于了[l, mid]这个区间的第i个数,那么显然从i开始一直到mid都大于j,这样就构成了mid-i+1个逆序对,为所求。
题目链接:逆序对数量
代码如下:
#include <iostream>
using namespace std;
typedef long long LL;
const int N = 1e6 + 10;
int q[N], tmp[N];
int n;
LL merge_sort(int q[], int l, int r) {
if (l >= r) return 0;
int mid = l + r >> 1;//中位数
LL 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]) tmp[k++] = q[i++];
else {
tmp[k++] = q[j++];
res += mid - i + 1;
}
}
while (i <= mid) tmp[k++] = q[i++];
while (j <= r) tmp[k++] = q[j++];
for (i = l, k = 0; i <= r; i++, k++) {
q[i] = tmp[k];
}
return res;
}
int main() {
cin >> n;
for (int i = 0; i < n; i++) cin >> q[i];
LL res = merge_sort(q, 0, n - 1);
cout << res << endl;
return 0;
}
三、整数二分
思路:其中①为满足左边红色性质,找红色右端点,②为满足右边绿色性质,找绿色左端点。
注意:查找左半边时mid需要+1!
模板如下:
bool check(int x);
//左半边满足check性质
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;
}
//右半边满足check性质
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;
}
整数二分涉及数的范围的问题,可以先用二分查左端点(保证右边的数都>=x),再用二分查右端点(保证左边的数都<=x) ,即可。
题目链接:数的范围
代码如下:
#include <iostream>
using namespace std;
const int N = 1e6 + 10;
int q[N];
int main() {
int n, m;
cin >> n >> m;
for(int i=0;i<n;i++) cin >> q[i];
while(m--){//m个查询
int x;//查找的数
cin >> x;
//先查左端点
int l=0, r =n-1;
while(l<r){
int mid = l+r >> 1;
if(q[mid]>=x) r = mid;
else l = mid + 1;
}
if(q[l]!=x) cout << "-1 -1" << endl;
else{
cout <<l << " ";
//再查右端点
int l=0, r =n-1;
while(l<r){
int mid = l+r+1 >> 1;
if(q[mid]<=x) l = mid;
else r = mid - 1;
}
cout << l <<endl;
}
}
return 0;
}
四、浮点数二分
思路:和整数二分基本相同,但是不用考虑边界问题了,很简单
以下代码为利用二分法求数的平方根
#include <iostream>
using namespace std;
//利用二分法求数的平方根
int main(){
double x;
cin >> x;
double l, r;
if(x >= 1){//此时平方根介于0-x
l = 0, r = x;
while(r - l > 1e-8){
double mid = (l + r) / 2;
if(mid * mid > x) r = mid;
else l = mid;
}
}
else{//此时平方根介于x-1
l = x, r = 1;
while(r - l > 1e-8){
double mid = (l + r) / 2;
if(mid * mid > x) r = mid;
else l = mid;
}
}
printf("%.6lf\n", l);
return 0;
}
其中,r-l>=比最后要求保留的小数位数多两位即可! (经验)
五、高精度加法
#include <iostream>
#include <vector>
#include <string>
using namespace std;
//C = A + B
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];
if (i < B.size()) t += B[i];
C.push_back(t % 10);
t = t / 10;
}
if (t) C.push_back(1);//如果最后还有进位,得在最高位前再加1
return C;
}
int main() {
string a, b;
cin >> a >> b;
vector<int> A, B;
//倒序存入vector,最低位在a[0]
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);//auto:编译器会自动推断是什么类型的
for (int i = C.size() - 1; i >= 0; i--) printf("%d", C[i]);
cout << endl;
return 0;
}
六、高精度减法
#include <iostream>
#include <vector>
#include <string>
using namespace std;
//此函数判断是否有 A >= B
bool cmp(vector<int> A, vector<int> B) {//此时两个数都是倒序存储,数组第0位是最低位,而我们要从最高位开始比
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;//两数相等
}
//C = A - B
vector<int> sub(vector<int> &A, vector<int> &B) {//引用速度更快,不用读入整个数组
vector<int> C;
int t = 0;//借位(暂时当结果)
for (int i = 0; i < A.size(); i++) {//A肯定比B大
t = A[i] - t;
if (i < B.size()) t -= B[i];
C.push_back((t + 10) % 10);//如果t<0,t += 10,反之则t = t
//借位,0或者1
if (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;
//倒序存入vector,最低位在a[0]
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);//auto:编译器会自动推断是什么类型的
for (int i = C.size() - 1; i >= 0; i--) printf("%d", C[i]);
} else {//A < B
auto C = sub(B, A);
cout << "-";//负号
for (int i = C.size() - 1; i >= 0; i--) printf("%d", C[i]);
}
cout << endl;
return 0;
}
七、高精度乘法(大数乘小数)
#include <iostream>
#include <vector>
#include <string>
using namespace std;
vector<int> mul(vector<int> &A, int b) {
vector<int> C;
int t = 0;//进位
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--) A.push_back(a[i] - '0');
auto C = mul(A, b);//A × b
for (int i = C.size() - 1; i >= 0; i--) printf("%d", C[i]);
cout << endl;
return 0;
}
八、高精度除法(大数除小数)
#include <iostream>
#include <vector>
#include <string>
#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 = 10 * r + 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;
cin >> a >> b;
vector<int> A;
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--) printf("%d", C[i]);
cout << endl << r << endl;
return 0;
}
九、一维前缀和
有一个序列a[N]:a[1],a[2],a[3]...a[i]...
前缀和s[i]就是序列a[]的前i项的和。
如何求s[i]?
s[i] = s[i-1] + a[i],s[0]=0即可。
注:求前缀和时,a[i]、s[i]都是从1开始的,a[0]、s[0]都为0,可以通过在main函数外赋全局变量的方式初始化第零位。
作用?
可以O(1)地求得序列中一段l到r的部分和,即为res = s[r] - s[l-1]。
题目链接:前缀和
#include <iostream>
using namespace std;
const int N = 1e6 + 10;
int a[N], s[N];//全局变量a[0],s[0]自动为0
int main() {
int n, m;
scanf("%d %d", &n, &m);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
for (int i = 1; i <= n; i++) s[i] = s[i - 1] + a[i];//前缀和的初始化
while (m--) {
int l, r;
scanf("%d %d", &l, &r);
printf("%d\n", s[r] - s[l - 1]);//区间和的计算
}
return 0;
}
十、二维前缀和
与一维前缀和类似,仅将a[N]、s[N]改为二位矩阵即可。
如何求s[i][j]?
s[i][j] = s[i-1][j] + s[i][j-1] -s[i-1][j-1] + a[i][j]。
如何求(x1, y1)到(x2, y2)之间的二维前缀和呢?
res = s[x2][y2] - s[x2][y1-1] - s[x1-1][y2] + s[x1-1][y1-1]
题目链接:子矩阵的和
#include <iostream>
using namespace std;
const int N = 1e3 + 10;
int a[N][N], s[N][N];
int main() {
ios::sync_with_stdio(false);
int n, m, q;
cin >> n >> m >> q;
//输入
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
cin >> a[i][j];
}
}
//计算s[i][j]
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
s[i][j] = s[i-1][j] + s[i][j-1] - s[i-1][j-1] + a[i][j];
}
}
//求前缀和
while(q--){
int x1, y1, x2, y2;
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;
}
十一、一维差分
则a为b的前缀和数组,b为a的差分数组(与前缀和是个相反的概念)。
经常,我们需要对a数组的[l,r]区间每个数都加c,常规做法的时间复杂度是O(n)的baolibian,但是如果用了差分数组,可以达到O(1)。只需要bl+c,b(r+1)-c即可!
对插入为insert(i,i, a[i])的解释:
题目链接:差分
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int a[N], b[N];
void insert(int l, int r, int c){
b[l] += c;
b[r+1] -= c;
}
int main() {
ios::sync_with_stdio(false);
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++){
b[i] += b[i-1];//求前缀和
cout << b[i] << " ";
}
cout << endl;
return 0;
}
十二、二维差分
与一维差分类似,a[N][N]是b[N][N]的前缀和序列,反过来b是a的差分序列,如果我们需要对a中左上角为(x1, y1),右下角为(x2, y2)的这部分数全部加上c,则可以直接对其差分序列b进行O(1)的操作:b[x1][y1] +=c, b[x1][y2+1] -= c, b[x2+1][y1] -= c, b[x2+1][y2+1] += c。
构建差分序列的方法:insert(i, j, i, j, a[i][j])。
最后求二维前缀和,即可求出结果:b[i][j] += b[i-1][j] + b[i][j-1] - b[i-1][j-1]。
题目链接:差分矩阵
#include <iostream>
using namespace std;
const int N = 1e3 + 10;
int a[N][N], b[N][N];
void insert(int x1, int y1, int x2, int y2, int c) {
b[x1][y1] += c;
b[x1][y2 + 1] -= c;
b[x2 + 1][y1] -= c;
b[x2 + 1][y2 + 1] += c;
}
int main() {
ios::sync_with_stdio(false);
int n, m, q;
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 - 1][j] + b[i][j - 1] - b[i - 1][j - 1];//求二维前缀和
cout << b[i][j] << " ";
}
cout << endl;
}
return 0;
}
十三、双指针算法
双指针算法的优势是把O(n^2)的双重for循环优化成O(n),关键是找单调性。
先看一道题,很巧妙地用双指针算法清除了前面的数字。
题目链接:最长连续不重复子序列
AC代码如下:
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int a[N], s[N];
int main() {
ios::sync_with_stdio(false);
int n;
cin >> n;
int res = 0;
for (int i = 0; i < n; i++) cin >> a[i];
for (int i = 0, j = 0; i < n; i++) {
s[a[i]]++;
while (j < i && s[a[i]] > 1) {//有重复
s[a[j]]--;
j++;//右移
}
res = max(res, i - j + 1);//更新最大值
}
cout << res << endl;
return 0;
}
再看一题,巧妙地找到了单调性,利用双指针算法优化了时间复杂度。
题目链接:数组元素的目标和
a,b数组有序,i从a数组的头开始,j从b数组的尾开始,这就满足了单调性。
AC代码如下:
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int a[N], b[N];
int main() {
ios::sync_with_stdio(false);
int n, m, x;
cin >> n >> m >> x;
for (int i = 0; i < n; i++) cin >> a[i];
for (int i = 0; i < m; i++) cin >> b[i];
for (int i = 0, j = m - 1; i < n; i++) {
while (a[i] + b[j] > x && j >= 0)j--;//根据单调性,j左移
if (a[i] + b[j] == x) {
cout << i << " " << j << endl;
break;
}
}
return 0;
}
十四、位运算
主要介绍了lowbit运算,即为 return x & (-x),结果为x中最低位的1和更低位的所有0。
比如,x=(10101000)2,而~ x=(01010111)2,所以~ x+1=(01011000)2,x&(~ x+1)=(1000)2。
因此,如果不断地让x减去lowbit(x),直到x=0,则减去的次数即为x二进制表示中1的个数。
题目链接:二进制中1的个数
AC代码如下:
#include <bits/stdc++.h>
using namespace std;
int lowbit(int x){
x = x & (-x);
return x;
}
int main() {
ios::sync_with_stdio(false);
int n;
cin >> n;
while(n--){
int x;
cin >> x;
int cnt = 0;
while(x){
x -= lowbit(x);
cnt++;
}
cout << cnt << " ";
}
cout << endl;
return 0;
}