前言
理解算法思路,背代码模板,用相应的模板进行默写,加深记忆;
能够默写后,删除重写4-5遍将模板记熟
一、排序
1.1快速排序
算法思想:
分治;
步骤:
重点为第二步,调整区间。
算法模板:
暴力做法:
开辟两个额外空间,用于存放大于和小于x的值,然后迭代;
非暴力做法:
代码实现:
void quick_sort(int q[],int l, int r)//q[]传入某一个数组,不需要指定大小,传入时只要传入数组首地址就行(q或者&q[0])
{
if(l >= r) return;//void-return
int x = q[l],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);//如果写i的话,(q,l,i-1),(q,i,r) 那么x不能取到q[l](左边界),否则死循环,写j的话,右边界同理,x不能取到q[r]
}
1.2归并排序
算法思想:
分治;
步骤:
方法:
先进行l和r部分的递归,此时left和right部分以及是有序数组,然后通过如下方法进行两个有序序列的合二为一
代码实现:
void merge_sort(int q[],int l,int r)
{
if ( l >= r) return;
int mid = (l + r)/2,i = l,j = mid + 1;//确定中点和分界点
merge_sort(q,l,mid),merge_sort(q,mid+1,r);//递归左右两边
//进行归并
int k = 0 ;
while(i <= mid && j <= r)
{
if(q[i] <= q[j]) tmp[k++] = q[i++];
else if(q[i] > q[j]) tmp[k++] = q[j++];
}//写法记住
//把没有遍历到的部分直接接到后面
while(i <= mid)
tmp[k++] = q[i++];
while(j <=r)
tmp[k++] = q[j++];
//把tmp里的数放回q里面
for (i = l,j = 0;i <=r;i++,j++)
q[i] = tmp[j];
}
时间复杂度
快排,归并 o(nlogn)
二、二分查找
将数组分为两部分每部分一种性质,二分是在查找两个性质的边界点
2.1 整数二分
代码实现:
case1:mid在左边
//===二分查找======case1:mid在左边
int divide_two_1(int q[],int x,int l,int r)
{
if(l >= r) return 0;
while(l<r)
{
int mid = (r+l)/2;//向下取整
if(q[mid] >= x)r = mid;//r = mid,则mid不需要+1
else l = mid + 1;
}
return l;
}
case2:mid在右边
//===二分查找======case2:mid在右边
int divide_two_2(int q[],int x,int l,int r)
{
if(l>=r) return 0;
while(l < r)
{
int mid = (l + r +1)/2;
if(mid <= x ) l = mid;//l = mid,则mid需要+1
else r = mid-1;
}
return r;//写l和r一样,最终l = r
}
区别在于到底是l = mid(要+1)(若不+1会死循环)还是r = mid
2.2 浮点数二分
相比整数二分查询更加简单,不用判断边界
代码实现:
//用二分法求根号x的值
int main()
{
double x;
cin >> x;
double l = 0,r = x;
while(r - l >1e-6)
{
double mid = (r + l)/2;
if(mid*mid >= x) r = mid;//mid和根号x进行比较
else l = mid;
}
cout << l;
return 0;
}
三、高精度运算
c++中没有大整数的类型,一般不用整型来存储,用数组来存储,对数组进行运算成为高精度运算
3.1两个大整数相加:A + B
思路:
每一位对应相加,然后有一个t来表示进位
代码实现:
vector<int> add(vector<int> &A,vector<int> &B)
{
vector<int> C;
int t = 0;//用t来表示对应位数相加的和
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];
//if(t > 9)
//k = 1
//用取余来表示此位相加结果
C.push_back(t % 10);
//用整除来表示要进的位
t /= 10;
}
if(t)
C.push_back(t);
return C;
}
cin>>a>>b;//
//==将大整数存入vector里面,倒着存入,在string里低位在末尾,push_back到vector里面后低位就在开头了
int main()
{
...
vector<int> A,B;
for(int i = a.size() - 1;i>= 0; i--)
A.push_back(a[i] - '0');//存的是字符,要减去‘0’得ascii码才是对应的数字
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];
...
}
3.2大整数减法
思路:
t为被借位,需要判断Ai和Bi的大小关系
代码实现:
大整数大小判断:
bool cmp(vector<int> &a,vector<int> &b)
{
if(a.size() != b.size()) return a.size() > b.size();
else
{
for(int i = a.size()-1;i >= 0;i--)
{
if(a[i]!=b[i])
return a[i] > b[i];
}
return true;
}
}
相减操作:
vector<int> substr(vector<int> &a,vector<int> &b)
{
int t;
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);没有区分a[i]-b[i]结果的正负,负的结果不应该直接存入
if(t>=0)
c.push_back(t);
else
c.push_back(t+10);
//==标准写法=====(正确)
// t = a[i] + t;
// if(i < b.size())
// t -= b[i];
// c.push_back((t+10)%10);//用(t+10)%10替代了两种情况的判断
//==============
if(t < 0)
t = -1;
else
t = 0;
}
while(c.size() >1 &&c.back() == 0)c.pop_back();//移除多余的0,尾部开始移除,因为倒着存的,尾部就是最高位的多余的0
return c;
}
int main()
{
...
if(cmp(A,B))
{
auto c = substr(A,B);
for(int i = c.size() - 1; i >= 0; i--)
cout << c[i];
}
else
{
auto c = substr(B,A);
cout<< "-";
for(int i = c.size() - 1; i >= 0; i--)
cout << c[i];
}
...
}
3.3高精度整数乘以低精度整数
思路:
代码实现:
//==乘法======高精度整数乘以低精度整数
vector<int> mul(vector<int> &A,int b)
{
vector<int> c;
int t;
for(int j = 0;j<A.size()||t;j++)//进位t不为零的话仍会进行循环
{
if(j<A.size())
t +=b*A[j];
c.push_back(t%10);
t/=10;
}
return c;
}
int main()
{
...
string a;
int k;
vector<int> A;
cin>>a>>k;
for(int i = a.size() - 1;i>= 0; i--)
A.push_back(a[i] - '0');//存的是字符,要减去‘0’得ascii码才是对应的数字
auto c = mul(A,k);
for(int i = c.size() - 1; i >= 0; i--)
cout << c[i];
...
}
3.4 高精度整数除以低精度整数
思路:
代码实现:
//==除法===高精度整数除以低精度整数
//除法从高位开始算,一般单独计算除法时可以首部存高位,但为了和+-*兼容,还是让低位存储在首部
//A[i]/b=商[i],t=A[i]%b为余数
vector<int> div(vector<int> &A,int b,int &k)//此处比较巧妙,除法需要返回商和余数,所以我们用int &k来表示余数,使用引用可以该改变外部k的值
{
vector<int> c;
k = 0;
for( int i =A.size()-1;i >= 0;i--)
{
k = k*10 + A[i];
c.push_back(k/b);
k%=b;
}
//c.back() 和 c.end() 的区别在于它们的返回值类型和语义不同。
//c.back() 返回一个元素的引用,表示容器中最后一个元素,
//而 c.end() 返回一个迭代器,指向容器中最后一个元素之后的位置
//c.begin()是第一个元素,c.end()-1=c.back();
reverse(c.begin(),c.end());//需要传入末尾,即最后一位的下一位
//把前面多余的0去掉,因为是倒着存的,所以弹出最后一位就是弹出最高位
while(c.size()>=1&&c.back()==0)c.pop_back();
return c;
}
int main()
{
...
string a;
int b,r;
vector<int> A;
cin>>a>>b;
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<<"余"<<r;
return 0;
...
}
四、前缀和
4.1前缀和(前n项和)
代码实现:
#include<iostream>
using namespace std;
const int N = 1e6+10;
int n,m;
int a[N],s[N];
int main()
{
//==========求数组某一区间的和
cin>>n;
for(int i = 0; i < n; i++)
cin>>a[i];
s[0] = a[0];
//求前缀和
for(int i = 1; i < n; i++)
s[i] = s[i-1] + a[i];
//使用前缀和
int r,l;
cin>>r>>l;
cout<<s[l] - s[r-1];
return 0;
}
4.2求子矩阵的和
思路:
代码实现:
#include<iostream>
using namespace std;
const int N = 1010;
int n,m,q;
int a[N][N],s[N][N];
int main()
{
//========求子矩阵的和=====
cout<<"test2";
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] = 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;
cout<<s[x2][y2] - s[x2][y1-1] - s[x1-1][y2] + s[x1-1][y1-1]<<endl;
}
return 0;
}
4.3差分
一维差分
(构造b不重要,只要满足ai=bi,那么ai就为前缀,bi就是差分)
应用:对一个数组的[l,r]部分,每个数加上C
题目描述:
输入一个长度为n的整数序列。
接下来输入m个操作,每个操作包含三个整数l, r, c,表示将序列中[l, r]之间的每个数加上c.请你输出进行完所有操作后的序列。
思路:
把b[l]+c,那么a[l]之后所有点都会+c,然后令b[r+1]-c,那么a[r+1]以及其后的数都会-c,这样就能够保证只有[l,r]+c;
若使用暴力解法需要遍历整个数组,时间复杂度o(n),使用差分操作时间复杂度o(1);
代码实现:
若假设任意a[i]=0,如果在[i,i]中加入a[i]可以实现数组中数插入
#include<iostream>
using namespace std;
const int N = 100010;
int n,m;
int a[N],b[N];
void insert(int l,int r ,int c)
{
b[l]+=c;
b[r + 1]-=c;
}
int main()
{
//===差分===
//本代码中写循环均为i = 1起始,实际上相当于没有使用a[0]而已,给出数组空间很大,够用;
//这里这样写是因为后面涉及b[i] +=b[i-1],需要留一位冗余项
cin>>n>>m;
for(int i = 1;i <= n;i++)
cin>>a[i];
//应用插入,将a[i]赋值给b[i]
for(int i = 1;i <= n;i++)
insert(i,i,a[i]);
while(m--)
{
int l,r,c;
cin>>l>>r>>c;
insert(l,r,c);//唯一的重点操作
}
//对自己进行差分,应用差分思想
//1.b[n] = 所有b[i]之和 (不针对n可行,针对任意i也b[i]=sum(k=0-i)b[k]),因此b[i]时自己的差分
//可以这样理解,对数组b[n]求前缀和,但是为了减少数组的使用,直接把结果更新到b[n]上,
//而在这之前已经执行了insert操作,那么更新后的结果就是+c的结果
for(int i = 1; i<= n; i++)b[i] +=b[i-1];
for(int i = 1; i <=n; i++)cout<<b[i];
return 0;
}
2.二维差分
(同样不用考虑如何构造)
(对于矩阵中的(xi,yi)点来说,这一点差分+c,则后续所有大于xi和yi的点的前缀和都+c,反之-c同理)
应用:
输入一个n行m列的整数矩阵,再输入q个操作,每个操作包含五个整数x1, y1, x2, y2,c,其中(x1,y1)和(x2, y2)表示一个子矩阵的左上角坐标和右下角坐标。
每个操作都要将选中的子矩阵中的每个元素的值加上c。请你将进行完所有操作后的矩阵输出。
代码实现
#include<iostream>
using namespace std;
const int N = 1010;
int n,m,q;
int a[N][N],b[N][N];
void insert_mat(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++)
cin>>a[i][j];
for(int i = 1;i <= n;i++)
for(int j = 1;j <= m;j++)
insert_mat(i,j,i,j,a[i][j]);
while(q--)
{
int x1,x2,y1,y2,c;
cin>>x1>>y1>>x2>>y2>>c;
insert_mat(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];
for(int i =1;i <= n;i++)
{
for(int j = 1;j <= m;j++)
cout<<b[i][j];
puts("");//输出换行符
}
return 0;
}
五.双指针算法
1.类型
1.两个指针指向同一个序列
2.两个指针指向两个不同序列
2.基本结构
大部分双指针算法都是这样的结构:
先更新i,然后进行j的更新,while中先判断j的范围,然后检查是否某一种性质,都满足就j++。
核心用途:
降低时间复杂度,双指针虽然存在两层但每一层都执行次数都小于n,整体小于2n,因此可以把如下的双重循环的暴力做法,复杂度优化到o(n)
3.举例
思考方式:先想暴力做法,再优化
3.1输入字符串,把每个单词输出
#include <iostream>
#include <string.h>
using namespace std;
//输入一个字符串,把每个单词输出处理(单词以空格隔开)
int main()
{
char str[1000];
gets(str);
int n = strlen(str);
for(int i = 0;str[i];i++)
{
int j = i;
while(j < n && (str[j]!=' '))j++;//使得i,j指向一段的两端
//输出这段字符
for(int k = i;k<j;k++)cout<<str[k];
cout<<endl;
i = j;
}
return 0;
}
3.2 给定一个长度为n整数序列,找到一个最长的不包含重复数字的连续子序列输出其长度
思路:红指针向右遍历,绿指针用来找举例红指针的最大距离,然后如果比之前的距离大就覆盖
暴力求解:
双指针算法:
j位置具有单调性,i往后走,j不能往左走(往左的话[j,i’]不成立那么[j’,i’]也不成立),因此只需要j++,j不用每次都遍历数组,这个问题由于这个单调性才能用双指针优化
代码实现:
int n;
const int N = 100000;
int a[N],s[N];//a[N]是总长度,S[N]是中间ij段长度
int main()
{
cin>>n;
for(int i = 0;i < n;i++)cin>>a[i];
int res = 0;
for(int i = 0,j = 0;i < n;i++)
{
s[a[i]]++;//记录每个数字出现的次数
while(j<=i&&s[a[i]] > 1)//s[a[i]] > 1表示这个数字出现过了,即出现重复,此时就要向右移动右指针
{
s[a[j]]--;//由于要准备向右移动j指针,则要把之前j指针指向的数排除,即使其对应的数出现次数减一
j++;//右移j指针
}
res = max(res,i - j + 1);
}
cout<<res<<endl;
return 0;
}
精髓点:用s[N]来计数数字出现次数
二.位运算
2.1 n的二进制表示中第k位是几(个位开始 )
核心思路:
n>>k&1
(n>>k表示将n表示的二进制数右移三位,&同1异0)
代码:
int main()
{
int n = 10;
for(int k =3;k >= 0;k--)cout<<(n>>k&1)<<endl;
return 0;
}
返回每一位对应的二进制数是多少,即二进制转换
2.2 返回n的最后一位1
核心思路:
x&-x
-x=~x+1(补码)
代码:实现统计输入数字二进制中1的个数
int lowbit(int x)
{
return x&(~x+1);
}
int main()
{
int n ;
cin >> n;//n表示可以多次输入,计算多个数
while(n--)
{
int x;
cin >> x;
int res = 0;
while(x)
{
x -= lowbit(x);//返回的只有最低为1的数,1的位置不变,而且因为只有最低位一样,其余位置为0.相减只有最低位消失
res++;
}
cout << res;
}
return 0;
}
三.离散化
对于一组有序的数,值很大但是个数很小,开一个大数组很浪费,我们就需要把这些映射到从零开始的连续自然数,此过程被称为离散化,
存在注意问题:
1.a中可能有重复元素;
2.如何算出a里面每一个值映射后为多少,即找出x在a中的下标为多少,二分查找;
模板步骤:
应用:给定一个无限长的数轴,数值全为0,现在有n个操作,每次操作将对应的位置x上数加c,接下来,进行m次询问,每次询问包含两个整数的和,你需要求出区间[l,r]之间所有俩个数和的数组集合
代码实现:
typedef pair<int,int> PII;//pair 是一个标准库类型,表示两个值的有序组合,通常用于需要返回两个值的函数、数据结构和算法中
const int N = 300000;
int n,m;//
int a[N],s[N];//a数组是存的数,s数组是前缀和
vector<int> alls;//所有需要离散化的值
vector<PII> add,query;
int find(int x)
{
int l = 0,r = alls.size() - 1;
while(l < r)
{
int mid = l + r >> 1;//>>1相当于除以2
if(alls[mid] >= x) r = mid;
else l = mid + 1;
}
return r + 1;
}
int main()
{
cin>>n>>m;
for(int i = 0; i < n;i++)
{
int x,c;
cin>>x>>c;
add.push_back({x,c});
alls.push_back(x);
}
for(int i = 0; i < m;i++)
{
int l,r;
cin>>l>>r;
query.push_back({l,r});
//????
alls.push_back(l);
alls.push_back(r);
}
//去重复
sort(alls.begin(),alls.end());
alls.erase(unique(alls.begin(),alls.end()),alls.end());
//处理插入
for(auto item :add)
{
int x = find(item.first);
a[x] += item.second;
}
//预处理前缀和
for(int i = 1; i <=alls.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;
}
return 0;
}
四.区间合并
将有交集的区间进行合并
举例:
将上述区间合并之后,如下图:
得到三个区间
步骤:
1.按照区间左端点把每个区间进行排序
2.扫描所有区间,扫描过程中将所有有交集的区间进行合并
代码实现:
using namespace std;
typedef pair<int,int> PII;
const int N = 100000;
int n;
vector<PII> segs;
void merge(vector<PII> &segs)//和全局的segs重名了会不会有影响
{
vector<PII> res;
sort(segs.begin(),segs.end());
int st = -2e9,ed = -2e9;
for(auto seg : segs)
if(ed < seg.first)
{
if(st != -2e9)
res.push_back({st,ed});
st = seg.first,ed = seg.second;
}
else
ed = max(ed,seg.second);
if(st !=-2e9)res.push_back({st,ed});
segs = res;
}
int main()
{
cin >> n;
for(int i = 0;i < n;i++)
{
int l,r;
cin >>l>>r;
segs.push_back({l,r});
}
merge(segs);
cout << segs.size() << endl;
return 0;
}