基本算法
排序算法
- 快速排序
- 归并排序
查找算法
- 整数二分
- 浮点数二分
高精度算法
- 高精度加法
- 高精度减法
- 高精度乘法
- 高精度除法
前缀和和差分
- 一维前缀和
- 二维前缀和
- 一维差分
- 二维差分
双指针算法
位运算
离散化
区间合并
排序算法
快速排序
**思想:确定一个分界点(分界点可以是左端点,右端点,中点和随机),然后用两个指针从头和尾分别朝中间遍历数据直到两指针相遇,将数据按照与分界点的相对大小放在指针与端点构成的区间内,并递归此操作。
代码:
#include<iostream>
//创建数组
const int N = 100010;
int q[N];
using namespace std;
void quick_sort(int q[] ,int l ,int r)
{
if( l >= r) return;
int mid = q[l + r >> 1];//确定分界点
int i = l - 1 ,j = r + 1;
//用双指针遍历
while(i < j)
{
//注意这里要用dowhile语句,否则这里会出现死循环(左右边界问题)
do i ++; while(q[i] < mid);
do j --; while(q[j] > mid);
if(i < j) swap(q[i] , q[j]);
}
//递归操作
quick_sort(q ,l ,j);
quick_sort(q ,j + 1 , r);
}
int main()
{
int n;
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] << ' ';
}
归并排序
思想:将一堆数据分成两个部分,将两个部分给重新合并为一个有序的数组并重复该操作。
代码:
#include<iostream>
using namespace std;
const int N = 100010;
int q[N] ,b[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])
{
b[k ++] = q[i ++];
}else{
b[k ++] = q[j ++];
}
}
while(i <= mid) b[k ++] = q[i ++];
while(j <= r) b[k ++] = q[j ++];
//把有序的数据放回原数组中
for(int i = l,j = 0;i <= r;i ++ ,j ++) q[i] = b[j];
}
int main()
{
int n;
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] << ' ';
}
二分法
二分的实质就是在一堆有一定次序的数中找到目标,查找的方法是通过用一个给定的条件来缩小查找的范围,直到查找到目标或满足退出的条件。
我们可以通过二分法来在给定的答案数范围中寻找正确的答案,即二分答案。
整数二分
代码:
bool check(int x) {/* ... */} // 条件函数
//这两种方法划分的实质就是mid这一个数被分在左区间还是右区间
// 区间[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;
if (check(mid)) l = mid;
else r = mid - 1;
}
return l;
}
浮点数二分
代码:
bool check(double x) {/* ... */} // 条件函数
//因为没有了整数删去小数的限制,这里左右边界都可以是mid
double bsearch_3(double l, double r)
{
const double eps = 1e-6; // eps 表示精度,取决于题目对精度的要求
while (r - l > eps)//这里也可以让查找循环100次
{
double mid = (l + r) / 2;
if (check(mid)) r = mid;
else l = mid;
}
return l;
}
高精度算法
高精度算法目的是防止因为数据类型的限制让答案出现误差。它的实质就是用数组存储一个较大数的每一位数,用算竖式的方法来进行计算
高精度加法
代码:
#include<iostream>
#include<string>
#include<vector>
using namespace std;
vector<int> add(vector<int> &a,vector<int> &b)
{
if(a.size() < b.size()) return add(b ,a);
vector<int> c;
int t = 0;
//t 是进位
for(int i = 0 ;i < a.size() ;i ++ )
{
t += a[i];
if(i < b.size()) t += b[i];
c.push_back(t % 10);
t /= 10;
}
if(t) c.push_back(t);
return c;
}
int main()
{
string s1 ,s2;
vector<int> a ,b;
cin >> s1 >> s2;
//因为列竖式是从个位开始算的,所以把个位存在最前面,函数内比较好算。
for(int i = s1.size() - 1;i >= 0;i --) a.push_back(s1[i] - '0');
for(int i = s2.size() - 1;i >= 0 ;i --) b.push_back(s2[i] - '0');
auto c = add(a ,b);
for(int i = c.size() - 1;i >= 0;i --) cout << c[i];
}
高精度减法
代码:
#include<iostream>
#include<string>
#include<vector>
using namespace std;
//判断两种情况:1.被减数和减数的长度大小,2.同长度下a和b谁大。
bool check(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;
if(i < b.size()) t -= b[i];
c.push_back((t + 10) % 10);
//这样可以避免t是负数时的计算,可以理解成约瑟夫环
if(t >= 0) t = 0;
else t = 1;
//存在借位时 t 借一位
}
while(c.back() == 0 && c.size() > 1) c.pop_back();
//删去前置0
return c;
}
int main()
{
vector<int> a ,b ,c;
string s1 ,s2;
cin >> s1 >> s2;
for(int i = s1.size() - 1;i >= 0;i --) a.push_back(s1[i] - '0');
for(int i = s2.size() - 1;i >= 0;i --) b.push_back(s2[i] - '0');
if(check(a ,b)) c = sub(a ,b);
else
{
c = sub(b , a);
cout << '-';
}
for(int i = c.size() - 1;i >= 0;i --) cout << c[i];
}
高精度乘法
代码:
#include<iostream>
#include<string>
#include<vector>
using namespace std;
vector<int> mul(vector<int> a,int b)
{
vector<int> c;
int t = 0;//t 是进位
for(int i = 0 ;i < a.size(); i ++)
{
t += a[i] * b;
c.push_back(t % 10);
t /= 10;
}
if(t) c.push_back(t);
while(c.size() > 1 && c.back() == 0) c.pop_back();
//删去前置0
return c;
}
int main()
{
string s1;
vector<int> a;
int b;
//注意这里只讨论了a 较大 ,b 较小的情况
cin >> s1 >> b;
for(int i = s1.size() - 1; i >= 0; i --) a.push_back(s1[i] - '0');
auto c = mul(a , b);
for(int i = c.size() - 1; i >= 0; i --) cout << c[i];
}
高精度除法
代码:
#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;
}
//这里的反转是为了除掉前置0 , 如果不反转的话输出的数前面会多出0
reverse(C.begin(), C.end());
//除去前置0
while (C.size() > 1 && C.back() == 0) C.pop_back();
return C;
}
int main()
{
string a;
vector<int> A;
int B;
//注意这里只考虑A 是大数据 ,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;
}
前缀和算法
前缀和的目的是 求出这一个区间内的所有数的和 ,和遍历后求和的方法不同的是这可以用O(1)的时间复杂度来求出值,这在大数据量的情况下非常好用
一维前缀和
代码:
#include<iostream>
using namespace std;
const int N = 10e6 + 10;
int a[N] , s[N];
//a是原数组 ,s是前缀和数组
int main()
{
int n , m , x;
cin >> n >>m;
//注意这里的循环是从1开始的 , 目的是 1.防止算前缀和数组的时候下标越界 2.符合前缀和数组的规则 (就是是第一个数前面的数的和默认是0)。
for(int i = 1;i <= n;i ++ ) cin >> a[i] , s[i] += s[i - 1] + a[i];
int l ,r ;
while(m -- )
{
cin >> l >> r;
cout << s[r] - s[l - 1] << endl;
}
}
二维前缀和
二维前缀和其实是求出一个大矩阵中的子矩阵的和,这更加能够体现前缀和算法在时间复杂度上的优势
代码:
#include<iostream>
using namespace std;
const int N =1010;
int a[N][N], s[N][N];
int main()
{
int r , c;
cin >> r >> c;
for(int x = 1;x <= r; x ++)
{
for(int y = 1;y <= c; y ++)
{
cin >> a[x][y];
s[x][y] = s[x - 1][y] + s[x][y - 1] - s[x - 1][y - 1] + a[x][y];
}
}
int x1 ,y1 ,x2 ,y2;
while(cin >> x1 >> y1 >> x2 >> y2)
cout << s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1] << endl;
}
差分算法
差分就是前缀和的逆运算,也就是说一个区间内差分数组的前缀和就是它本身
查分数组的构造:
一维差分
代码:
#include<iostream>
using namespace std;
const int N = 100010;
int a[N] , b[N];
void insert(int l ,int r , int x)
{
b[l] += x;
b[r + 1] -= x;
}
int main()
{
int m , n ;
cin >> n >> m;
for(int i = 1;i <= n; i ++) cin >> a[i] , insert(i , i , a[i]);
//这里的insert(i , i , a[i])是在构造最初的差分数组 ,就是在a[i]这个位置上加上一个值,
//并使后面的值不变
int l , r ,x;
while(cin >> l >> r >> x) insert(l , r , x);
//在l 到 r 这个区间上加上一个数 x
for(int i = 1;i <= n;i ++)
{
b[i] += b[i - 1];
cout << b[i] << ' ';
}
}
二维差分
二维差分就是让一个大矩阵的一个子矩阵中的所有元素同时改变。
我们以一个全为0的矩阵来举例
完成后的结果是红色子矩阵内所有的元素都加上了c。
代码:
#include<iostream>
using namespace std;
const int N = 1010;
int a[N][N] ,b[N][N];
//b是差分数组 , 把一个子矩阵中的元素加上一个数
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()
{
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]);
}
}
int x1 ,y1 ,x2 ,y2 ,c;
while(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;
}
双指针算法
双指针算法指的是在遍历对象的过程中,是使用两个相同方向(快慢指针)或者相反方向(对撞指针)的指针进行扫描,从而达到相应的目的的一种方法。
双指针算法的最终目的是节省程序运行的时间,也就是把暴力解法进行优化。
比如下面的例题
给定一个`长度为 n 的整数序列,请找出最长的不包含重复的数的连续区间,输出其长度
我们可以用暴力的方法来搜索有效的子序列
int main()
{
int n , maxn = 0;
cin >> n;
for(int i = 0;i < n;i ++ ) cin >> q[i];
for(int i = 0 ,j = 0;i < n ;i ++ )
{
for(j = 0;j <= i;j ++)
{
if(check(i , j)) maxn = max(maxn , i - j + 1);
//check函数目的是判断它是不是无重复的区间
}
}
cout << maxn << endl;
}
但这两重循环带来的是极长的运行时间
我们可以看见 i指针 一定在 j指针 之后。双指针算法就是在优化 j指针 的移动上来节省时间。
在运行时我们发现在区间时不满足条件时,j指针一定会向后移动,直至区间满足条件,且下一次区间不满足条件时一定是i指针所在的数出现了重复。
所以就有了以下解法:
#include<iostream>
using namespace std;
const int N = 100010;
int q[N] ,s[N];
//s数组记录了每一个数出现的个数
int main()
{
int n;
cin >> n;
for(int i = 0;i < n ;i ++) cin >> q[i];
int max1 = -1;
for(int i = 0 ,j = 0; i < n; i ++)
{
s[q[i]] ++;
while(s[q[i]] > 1)
//就是消除i所在的数的重复
{
s[q[j]] --;
j ++;
}
max1 = max(j - i + 1, max1);
}
cout << max1 << endl;
}
离散化
离散化是在不改变数据相对大小的条件下,对数据进行相应的缩小来进行对数据的操作
离散化的目的是降低算法的时间复杂度
eg:
假定有一个无限长的数轴,数轴上每个坐标上的数都是 0。现在,我们首 先进行 n 次操作,每次操作将某一位置 x 上的数加 c。接下来,进行 m 次询 问,每个询问包含两个整数 l 和 r,你需要求出在区间 [l,r] 之间的所有数的 和。
这种情况下的数据大小是无限的,但他们有相对的大小,就是这组数具有单调性。但是我们用到的数是有限的。我们要节省算法的时间就要把没用的数给剔除。
所以我们就可以创造以下的映射:
用这种方法(把有改动的数和查询范围的左右边界重新组成一个数组)就可以有效地提高搜索的效率。
代码:
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
typedef pair<int , int> PII;
const int N = 1000010;
vector<PII> add , query;
vector<int> all;
int a[N] , s[N];
int find(int x)
{
int l = 0,r = all.size() - 1;
while(l < r)
{
int mid = l + r >> 1;
if(all[mid] >= x) r = mid;
else l = mid + 1;
}
return r + 1;
//我们让映射后的下标从 1 开始,因为我们是用前缀和来求值的
}
int main()
{
int n , m;
cin >> n >> m;
for(int i = 0;i < n;i ++)
{
int x , c;
cin >> x >> c;
add.push_back({x , c});
all.push_back(x);
//把改动的数放入映射后的数组
}
for(int i = 0;i < m;i ++)
{
cin >> l >> r;
query.push_back({l , r});
all.push_back(l);
all.push_back(r);
//把查询范围的左右边界放入映射后的数组
}
sort(all.begin() , all.end());
all.erase(unique(all.begin() , all.end()) , all.end());
//对数据进行排序和去重
// 结果是让 alls 内的数据按照一定的顺序,下标从 0 开始排成离散化的数字
for(auto item : add)
{
int x = find(item.first) , c = item.second;
a[x] += c;
//将某一位置 x 上的数加 c
}
for(int i = 1;i <= all.size(); i ++) s[i] = s[i - 1] + a[i];
//前缀和求值
for(auto item : query)
{
int l = find(item.first) , r = find(item.second);
cout << s[r] - s[l - 1] << endl;
}
}
位运算
求n的第k位数字:
x = n >> k & 1
lowbit操作(返回n的最后一位1)
比如 n = 10001000
返回值是1000
lowbit(n) = n & -n
区间合并
区间合并就是把几个区间按照某种性质合并成一个区间
在区间合并时我们会先把区间按照左端点进行排序,然后处理右边界的情况
代码如下
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
typedef pair<int , int> PII;
vector<PII> segs , res;
//segs 是原来的区间 ,res 是合并后的区间
void merge(vector<PII> segs)
{
int l = -2e9 , r = -2e9;
//这里的-2e9是负无穷的意思,可以用其他的大数代替
sort(segs.begin() , segs.end());
for(auto seg : segs)
{
if(r < seg.first)
{
if(l != -2e9) res.push_back({l , r});
// l != -2e9 的目的是防止把无穷给录进去
l = seg.first , r = seg.second;
//更新区间
}else r = max(r , seg.second);
//合并区间
}
if(l != -2e9) res.push_back({l , r});
// 1.防止把无穷给录进去 2.把最后一个区间补录进去
}