寒假集训第一周总结
一.排序算法
1.1 快速排序
【模板】快速排序
原理:选定数组中的一个数作为基准数,将数组分为小于等于基准数和大于基准数两个区间,利用递归的方法将数组不断细分下去,直至整个数列有序。
//快速排序
void sort(vector<int> &v,int l,int r)
{
int b=l,mid=l+1;
for(int i=l+1;i<=r;i++)
{
if(v[i]<=v[b])
{
swap(v[i],v[mid]);
mid++;
}
}
swap(v[b],v[mid-1]);
if(l<r)
{
sort(v,l,mid-2);
sort(v,mid,r);
}
}
第k小个数
执行快速排序之后输出第k个数
#include<iostream>
using namespace std;
const int N=5e6+10;
void sort(int *a,int l,int r)
{
int b=l,m=l+1;
for(int i=l+1;i<=r;i++)
{
if(a[i]<=a[b]) swap(a[i],a[m]),m++;
}
swap(a[m-1],a[b]);
if(l<r)
{
sort(a,l,m-2);
sort(a,m,r);
}
}
int main(void)
{
int a[N]={0},n,k;
cin>>n>>k;
for(int i=0;i<n;i++) cin>>a[i];
sort(a,0,n-1);
cout<<a[k]<<endl;
return 0;
}
1.2 归并排序
【模板】归并排序
原理:利用分治的思想,将数组不断划分为两个部分,分别保证两个部分有序,然后通过两个指针的移动将两个有序数列合并到一个数列当中。
//通过两个指针的移动在O(n)的时间复杂度下将两个有序数组合并为一个有序数组
void merge(vector<int> &v,int start,int mid,int end)
{
vector<int> t;
int l=start,r=mid+1;
while(l<=mid&&r<=end)
{
if(v[l]<=v[r])
{
t.push_back(v[l]);
l++;
}
else
{
t.push_back(v[r]);
r++;
}
}
for(int i=l;i<=mid;i++) t.push_back(v[i]);
for(int i=r;i<=end;i++) t.push_back(v[i]);
for(int i=start,j=0;i<=end;i++,j++) v[i]=t[j];
return;
}
//分别对两个部分进行归并排序,最后合并
void merge_sort(vector<int> &v,int start,int end)
{
if(start==end) return;
else
{
int mid=(start+end)/2;
merge_sort(v,start,mid);
merge_sort(v,mid+1,end);
merge(v,start,mid,end);
}
}
逆序对的数目
归并排序的合并环节,会保证左右两个数列有序,因此只需要(m+1-l)就可以统计出当前合并的区间中逆序对的数目。
#include<iostream>
#include<vector>
using namespace std;
long long ans=0;
void merge(vector<int> &v,int start,int mid,int end)
{
vector<int> t;
int l=start,r=mid+1;
while(l<=mid&&r<=end)
{
if(v[l]<=v[r])
{
t.push_back(v[l]);
l++;
}
else
{
t.push_back(v[r]);
ans+=mid+1-l;
r++;
}
}
for(int i=l;i<=mid;i++) t.push_back(v[i]);
for(int i=r;i<=end;i++) t.push_back(v[i]);
for(int i=start,j=0;i<=end;i++,j++) v[i]=t[j];
return;
}
void merge_sort(vector<int> &v,int start,int end)
{
if(start==end) return;
else
{
int mid=(start+end)/2;
merge_sort(v,start,mid);
merge_sort(v,mid+1,end);
merge(v,start,mid,end);
}
}
int main(void)
{
vector<int> v;
int n;
cin>>n;
for(int i=0;i<n;i++)
{
int x;
cin>>x;
v.push_back(x);
}
merge_sort(v,0,n-1);
cout<<ans<<endl;
cout<<endl;
return 0;
}
二.二分
1.1 【模板】二分
二分查找必须保证数列有序。
原理:定义三个指针分别为:left,mid,right,初始时分别位于数列的左端,中间和右端。判断mid位置的数与待查找数的关系,进行缩减区间或者退出循环的操作。若循环结束之后又left<=right,则说明此时mid位置就是目标数字,否则说明数列中不存在该数字。
int find(vector<int> &v,int goal)
{
int mid,left=0,right=v.size()-1;
while(left<=right)
{
mid=(left+right)/2;
if(v[mid]==goal) break;
else if(v[mid]>goal) right=mid-1;
else left=mid+1;
}
if(left<=right) return mid;
else return -1;
}
该题目要求输出目标数字的区间,可以直接查找,然后对左右进行扩充。
ans=find(v,goal);
if(ans==-1) cout<<-1<<' '<<-1; //不存在目标数则直接返回-1
else
{
int l=ans,r=ans;
while(l>0&&v[l-1]==goal) l--; //扩充左侧
while(r<n-1&&v[r+1]==goal) r++; //扩充右侧
cout<<l<<' '<<r;
}
1.2 数的三次方根
利用二分的原理不断缩减区间,找到目标数则直接退出循环,否则在规定次数结束之后退出,求出近似值。
int main(void)
{
int n=10000;
double a,m,l=-30,r=30;
cin>>a;
while(n--)
{
m=(l+r)/2.0;
if(pow(m,3)==a) break;
else if(pow(m,3)>a) r=m;
else l=m;
}
printf("%.6lf\n",m);
return 0;
}
三.前缀和差分
前言:前缀和算法主要用于求多次询问时某一区间的和,差分算法主要用于多次询问时批量改变某一区间的值。两者都是提高效率的算法。
建议:使用前缀和和差分算法时,尽量从数组下标为1的点开始,便于求前缀和数组和差分数组,而且进行操作时候不需要考虑边界问题。
1.1 【模板】一维前缀和
在输入的时候,直接让当前元素加上前一项元素,即可得到前缀和数组。
这是因为每一项元素都是包括本身在内前面所有元素的和。
最后输出区间和的时候只需要v[r+1]-v[l]即可,注意必须是r+1,否则区间和不包括下标为r的元素。
int main(void)
{
int n,T,a[N]={0};
cin>>n>>T;
for(int i=1;i<=n;i++)
{
cin>>a[i];
a[i]+=a[i-1];
}
while(T--)
{
int l,r;
cin>>l>>r;
if(l>r) swap(l,r);
cout<<a[r]-a[l-1]<<endl;
}
return 0;
}
1.2 【模板】前缀和矩阵
如图所示,我们要求的标红位置的点正是红色区域+绿色区域+蓝色区域-黑色区域。
以下是代码:
int main(void)
{
int n,m,T,a[N][N]={0};
cin>>n>>m>>T;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
cin>>a[i][j];
a[i][j]+=a[i-1][j]+a[i][j-1]-a[i-1][j-1];
}
}
while(T--)
{
int x1,y1,x2,y2;
cin>>x1>>y1>>x2>>y2;
cout<<a[x2][y2]-a[x2][y1-1]-a[x1-1][y2]+a[x1-1][y1-1]<<endl;
}
return 0;
}
1.3 【模板】一维差分
差分和前缀和完全相反。可以将给出的数组当作前缀和数组,利用当前元素减去前一项元素就可以得出差分数组。
注意:差分数组的元素进行加减,会影响的是其后方的所有元素。
int main(void)
{
int n,T,a[N]={0},s[N]={0};
cin>>n>>T;
for(int i=1;i<=n;i++)
{
cin>>a[i];
s[i]=a[i]-a[i-1];
}
while(T--)
{
int l,r,c;
cin>>l>>r>>c;
s[l]+=c,s[r+1]-=c;
}
for(int i=1;i<=n;i++)
{
s[i]+=s[i-1];
cout<<s[i]<<' ';
}
cout<<endl;
return 0;
}
1.4 【模板】差分矩阵
与前缀和矩阵完全相反。
//这是求差分矩阵的过程,s[][]为差分矩阵,a[][]为原数组
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++) s[i][j]=a[i][j]-a[i-1][j]-a[i][j-1]+a[i-1][j-1];
}
//批量对原数组某一区域进行增减
while(T--)
{
int x1,y1,x2,y2,c;
cin>>x1>>y1>>x2>>y2>>c;
s[x1][y1]+=c,s[x1][y2+1]-=c,s[x2+1][y1]-=c,s[x2+1][y2+1]+=c;
}
//还原回原数组
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++) a[i][j]=s[i][j]+a[i-1][j]+a[i][j-1]-a[i-1][j-1];
}
四.双指针
1.1 最长连续不重复子序列
数组h[N]用来统计j~i之间元素的个数。
当子序列中出现重复元素的时候,i向j移动,直到没有重复。
随时更新序列的最长长度。
int max_array(vector<int> &v)
{
int ans=0,h[N]={0};
for(int i=0,j=0;i<v.size();i++)
{
h[v[i]]++;
while(h[v[i]]>1)
{
h[v[j]]--;
j++;
}
ans=max(ans,i-j+1);
}
return ans;
1.2 数组目标元素的和
两个数组都是递增的,两个指针,一个指针从a数组的左端开始移动,另一个指针从b数组的右端开始移动。
当a[i]+b[j]>x时,j指针向左移动。移动到最左端还没找到解,则将i指针向右移动一个,直至找到唯一解。
for(int i=0,j=m-1;i<n;i++)
{
while(j>0&&a[i]+b[j]>goal) j--;
if(a[i]+b[j]==goal)
{
cout<<i<<' '<<j<<endl;
break;
}
}
1.3 判断子序列
两个指针初始位于两个数组最左端,当v1[i]==v2[j]时两指针向右移动,判断下一位,否则返回false,若最终完成循环则返回true;
bool jud(vector<int> &v1,vector<int> &v2)
{
for(int i=0,j=0;i<v1.size();i++)
{
while(v1[i]!=v2[j]&&j<v2.size()-1) j++;
if(v1[i]==v2[j])
{
j++;
continue;
}
else return false;
}
return true;
}
五.位运算操作
位运算符分为按位和移位两种。
按位运算符:
1.”~“按位取反,顾名思义,将1变为0,0变为1;
2.”|“按位或,只要有一位为真,那么结果就为真;
3.“&”按位与,只有两者同为真,结果才为真;
4.“^”按位异或,两者相异为真。
移位运算符:
“>>”和“<<”向右向左移动一位,并用0补足,针对2的幂提供快捷的乘法和除法运算。
二进制中1的个数
int类型是4位共32字节,逐字节判断即可
//n是要判断的数
for(int i=0;i<32;i++,n>>=1)
{
if(n&1) ans++;
}
cout<<ans<<' ';
六.离散化
区间和
使用额外的数组和二分查找进行映射,将无法作为数组下标的数映射为可以作为数组下标的数。
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
const int N=3e5+10;
struct node
{
int first;
int second;
};
vector<int> dis;
int find(int goal)
{
int ans=lower_bound(dis.begin(),dis.end(),goal)-dis.begin();
if(ans<dis.size()&&dis[ans]==goal) return ans+1;
return -1;
}
int main(void)
{
vector<node> ope,que;
int n,m,a[N]={0};
cin>>n>>m;
for(int i=0;i<n;i++)
{
int x,c;
cin>>x>>c;
ope.push_back({x,c});
dis.push_back(x);
}
for(int i=0;i<m;i++)
{
int l,r;
cin>>l>>r;
que.push_back({l,r});
dis.push_back(l);
dis.push_back(r);
}
sort(dis.begin(),dis.end());
dis.erase(unique(dis.begin(),dis.end()),dis.end());
for(int i=0;i<n;i++)
{
int t=find(ope[i].first);
a[t]+=ope[i].second;
}
for(int i=1;i<=dis.size();i++) a[i]+=a[i-1];
for(int i=0;i<m;i++)
{
int l=find(que[i].first),r=find(que[i].second);
cout<<a[r]-a[l-1]<<endl;
}
return 0;
}
七.区间和并
区间和并
首先需要将所有的区间放入数组中,然后按照区间左端点的大小进行排序。
每一次只维护一个区间,当区间的左端点大于当前维护的区间右端点时,将当前的区间放入到另一个数组中,然后更新左右端点;否则更新右端点为当前维护区间的右端点和当前遍历的右端点中较大者。
struct node
{
int l;
int r;
};
//代码核心部分
int merge(vector<node> &v)
{
vector<node> t;
int st=-2e9,en=-2e9;
for(int i=0;i<v.size();i++)
{
if(v[i].l>en)
{
if(st!=-2e9) t.push_back({st,en});
st=v[i].l,en=v[i].r;
}
else en=max(en,v[i].r);
}
t.push_back({st,en});
return t.size();
}
八.高精度
由于高精度的数据位数过多,使用string读入数据,然后转换成vector 类型。
除了除法之外都是倒着存入数组中的,这样便于计算。
1.1 高精度加法
vector<int> add(vector<int> &a,vector<int> &b)
{
vector<int> r;
int t=0;
for(size_t i=0;i<a.size()||i<b.size();i++)
{
if(i<a.size()) t+=a[i];
if(i<b.size()) t+=b[i];
r.push_back(t%10);
t/=10;
}
if(t) r.push_back(t);
return r;
}
1.2 高精度减法
当被减数小的时候,只需要交换减数和被减数的位置,在结果上添加“-”即可。
注意需要去前导零。
//该部分判断读入的两个数的大小关系,方便判断结果的正负。
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;
}
}
//减法的部分,保证a>=b
vector<int> sub(vector<int> &a,vector<int> &b)
{
vector<int> r;
int t=0;
for(size_t i=0;i<a.size();i++)
{
t=a[i]-t;
if(i<b.size()) t-=b[i];
r.push_back((t+10)%10);
if(t<0) t=1;
else t=0;
}
while(r.size()>1&&r.back()==0) r.pop_back();
return r;
}
1.3 高精度乘法
只有一个因数是高精度的,最后也需要去前导零。
vector<int> mul(vector<int> &a,int b)
{
vector<int> r;
int t=0;
for(size_t i=0;i<a.size()||t;i++)
{
if(i<a.size()) t+=a[i]*b; //当前元素*b+上一次运算的进位
r.push_back(t%10);
t/=10; //计算进位
}
while(r.size()>1&&r.back()==0) r.pop_back();
return r;
}
1.4 高精度除法
//r储存余数
int r=0;
vector<int> div(vector<int> &a,int b)
{
vector<int> c;
for(size_t i=0;i<a.size();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();
reverse(c.begin(),c.end());
return c;
}