基础算法篇
快速排序
分治算法都有三步
1.分成子问题
2.递归处理子问题
3.子问题合并
分析:基于分治
1.确定分界点
2.调整区间
左边都小于x,右边都大于x
3.递归处理左右两段
void quick_sort(int q[],int l, int r){
//递归的终止情况
if(l >= r )return ;
int i = l-1,j= r+1,x = q[l + r>>1];
while(i < j){
do i++; while(q[i] < x);
do j--; while(q[i] > x);
if(i < j ) swap(q[i],q[j]);
}
}
//递归处理子问题
quick_sort(q,l,j);
quick_sort(q,j+1,r);
//子问题合并,快排这里不需要,但归并排序的核心在这一步骤
}
使用STL
#include<bits/stdc++.h>
using namespace std;
const int N = 100000010;
int a[N],n,m,i,j;
int main(){
ios :: sync_with_stdio(false);
cin >> n;
for(int i = 1; i <= n;i++){
cin >> a[i];
}
sort(a+1,a+1+n);
for(int i = 1;i<=n;i++){
cout << a[i] << " ";
}
cout << endl;
return 0;
}
求k个数
#include<iostream>
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int n ,k;
int q[N];
int quick_sort(int l ,int r,int k)
{
if(l==r) return q[l];
int x = q[l],i = l - 1,j = r + 1;
while(i<j)
{
while(q[++i] < x);
while(q[--j] > x);
if(i < j) swap(q[i],q[j]);
}
int s1 = j-l+1;
if(k <= s1)quick_sort(l,j,k);
else quick_sort(j+1,r,k-s1);
}
int main()
{
cin >> n >> k;
for(int i = 0;i < n ;i++)
{
scanf("%d",&q[i]);
}
cout << quick_sort(0,n-1,k) << endl;
return 0;
}
归并排序
#include<iostream>
using namespace std;
const int N = 100010;
int n ,a[N],temp[N];
void merge_sort(int l,int r)
{
//递归边界
if(l >= r) return ;
//步骤1 确定分界点 mid
int mid = l + r >>1;
//步骤2 递归左右区间
merge_sort(l,mid),merge_sort(mid +1,r);
//步骤3
int i = l,j = mid +1;
int k = l;
while(i <= mid && j <= r)
if(a[i] <= a[j]) temp[k++] = a[i++];
else temp[k++] = a[j++];
while(i <= mid) temp[k++] = a[i++];
while(j <= r) temp[k++] = a[j++];
//复制回原数组
for(int i = l;i<=r;i++)
a[i] = temp[i];
}
int main()
{
cin >> n;
for(int i = 0;i < n; i++) cin >> a[i];
merge_sort(0,n-1);
for(int i = 0;i < n; i++) cout << a[i] <<" ";
}
二分
数的范围
#include <iostream>
using namespace std;
const int maxn = 100005;
int n, q, x, a[maxn];
int main() {
scanf("%d%d", &n, &q);
for (int i = 0; i < n; i++) scanf("%d", &a[i]);
while (q--) {
scanf("%d", &x);
int l = 0, r = n - 1;
while (l < r) {
int mid = l + r >> 1;
if (a[mid] < x) l = mid + 1;
else r = mid;
}
if (a[l] != x) {
printf("-1 -1\n");
continue;
}
int l1 = l, r1 = n;
while (l1 + 1 < r1) {
int mid = l1 + r1 >> 1;
if (a[mid] <= x) l1 = mid;
else r1 = mid;
}
printf("%d %d\n", l, l1);
}
return 0;
}
数的三次方根
#include<iostream>
#include<iomanip>
using namespace std;
double n,l,r,mid;
bool flag;
double q(double a){return a*a*a;}
int main(){
cin>>n;
l=-10000,r=10000;
while(r-l>=1e-7){
mid=(l+r)/2;
if(q(mid)>=n) r=mid;
else l=mid;
}
cout<<fixed<<setprecision(6)<<l;
return 0;
}
高精度
高精度加法
1.变量实现
#include<iostream>
#include<vector>
using namespace std;
vector<int> sum (vector<int>& a,vector<int>& b)
{
vector<int> result;
if(a.size()<b.size()) return sum(b,a);
int t = 0;//进位
for(int i = 0;i < a.size() || t; i++) //a.size() >= b.size()
{
if(i<a.size()) t += a[i];
if(i<b.size()) t += b[i];
result.push_back(t%10);
t /= 10;
}
return result;
}
int main()
{
string a,b;
vector<int> c,d;
vector<int> result;//存放结果
cin >> a>>b;
//按个位,十位 ,百位,···n位存放
for(int i = a.size()-1; i>=0 ;i--) c.push_back(a[i]-'0');//将字符a[i]转换成数值
for(int i = b.size()-1; i>=0 ;i--) d.push_back(b[i]-'0');//将字符串b[i]转换成数值
result = sum(c,d);
for(int i = result.size()-1;i>=0;i--) cout << result[i];
return 0;
}
2.数组实现
//数组实现
#include<iostream>
using namespace std;
const int N = 100010;
int A[N],B[N],C[N];
int Add(int a[],int b[],int c[],int cnt){
int t = 0;//t表示进位
for(int i = 0;i<=cnt;i++){
t += a[i] + b[i];//进位加上a和b第i位上的数
c[i] = t %10; //c的值就是进位的个位数
t /=10; //把t的个位数去掉只剩下十位数,即只剩下这个位置的进位
}
if(t) c[++cnt] = 1;//如果t==1,表示还有一个进位,要补上
return cnt;
}
int main(){
string a,b;
cin >> a>>b;
//a和b倒着放进int数组 ,因为有进位,倒着放容易处理
int cnt1 = 0;
for(int i = a.size()-1;i>=0;i--)
A[++cnt1] = a[i] - '0';
int cnt2 =0;
for(int i = b.size()-1;i>=0;i--)
B[++cnt2] = b[i]-'0';
int tot = Add(A,B,C,max(cnt1,cnt2));
//因为A和B要倒着放,所以C也要到着输出
for(int i = tot;i>=1;i--)
cout << C[i];
}
高精度减法
1.变量实现
#include<iostream>
#include<bits/stdc++.h>
#include<vector>
using namespace std;
const int N = 1000010;
bool cmp(vector<int> &A,vector<int> &B){
if(A.size() != B.size()) return A.size() >= B.size();
for(int i = A.size();i >=0 ;i++){
if(A[i] != B[i])
return A[i] > B[i];
return true;
}
}
void trimZero(vector<int> &A)
{
while(A.back() == 0 && A.size() > 1) A.pop_back();
}
vector<int> sub(vector<int> &A,vector<int> & B)
{
vector<int> C;
int t = 0;
for(int i =0;i<A.size();i++){
t= A[i] - t;
if(i < B.size())
t -= B[i];
C.push_back( (t+10) % 10 );
if(t < 0)
t =1;
else
t = 0 ;
}
trimZero(C);
return C;
}
int main(){
string a,b;
cin >> a >> b;
vector<int> A,B,C;
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');
trimZero(A),trimZero(B);
if(cmp(A,B)) C = sub(A,B);
else{
C = sub(B ,A);
printf("-");
}
for(int i = C.size()-1 ; i >=0; i--) cout << C[i];
return 0;
}
2.数组实现
高精度乘法
高精度 * 低精度
#include<bits/stdc++.h>
#include<vector>
using namespace std;
vector<int> mul(vector<int> & A,int b){
vector<int> C;
int t = 0;
for(int i = 0 ; i<A.size() ; i++){
t += A[i] * b;
C.push_back(t % 10);
t /= 10;
}
while(t) {
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;
vector<int> C;
for(int i = a.size()-1; i >= 0 ;i --){
A.push_back(a[i] - '0');
}
C = mul(A,b);
for(int i = C.size()- 1; i>= 0 ;i--){
cout << C[i];
}
}
高精度* 高精度
#include<iostream>
#include<vector>
using namespace std;
vector<int> mul(vector<int> & A, vector<int> & B){
vector<int> C(A.size() + B.size(),0);//初始化为0,且999*99 最多5位
for(int i = 0;i < A.size();i++ ){
for(int j = 0;j < B.size();j++){
C[i+j] += A[i] * B[j];
}
}
int t= 0;
for(int i = 0;i < C.size();i++ ){
//i= C.size() - 1时,t一定小于10
t += C[i];
C[i] = t % 10;
t /= 10;
}
while(C.size() > 1 && C.back() == 0) C.pop_back();//必须要去前导0,因为最高位很可能时0
return C;
}
int main(){
string a,b;
cin >> a >> b;
vector <int > A,B,C;
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');
}
C= mul(A,B);
for(int i = C.size() -1 ;i >= 0 ;i--){
cout << C[i];
}
return 0;
}
高精度除法
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
//int r =0;
vector<int> div (vector<int> & A, int B ,int &r){//r传入r的地址,便于直接对余数r进行修改
vector<int> C;
for(int i = 0;i < A.size();i++){
r = r* 10+A[i];
C.push_back(r/B);
r = r % B;
}
//由于在除法运算中,高位到低位运算,因此C的前导零都在vector的前面而不是尾部,vector只有
//删除最后一个数字的pop_back是常数复杂度,而对于删除第一位没有相应的库函数可以使用,
//而且删除第一位,其余位也要前移 因此我们将C反转,这样0就位于数组的尾部,可以使用pop
//函数删除前导零
reverse(C.begin(),C.end());
while(C.size() > 1 && C.back() == 0) C. pop_back();
return C;
}
int main(){
string a;
int B, r =0;
cin >> a >> B;
vector<int> A;
vector<int> C;
for(int i = 0;i<a.size();i++) A.push_back(a[i] - '0');//注意这次的A是由高位传输至低位,由于
//除法的运算过程中,发现从高位进行处理
C = div(A,B,r);
for(int i = C.size()-1; i >= 0; i--) cout << C[i];
cout << endl << r;
cout << endl;
return 0;
}
前缀和与差分
前缀和
#include <iostream>
#include <vector>
using namespace std;
int s;int e;
int n,m;
const int N = 100100;
vector<int> v(N,0);
vector<int> preSum(N,0);
vector<pair<int,int>> vp;
int main()
{
cin >> n >> m;
for(int i =1;i <= n;i++){
cin >> v[i];
preSum[i] = preSum[i-1]+v[i];
}
while(m--){
cin >>s >> e;
cout << preSum[e] - preSum[s-1] << endl;
}
return 0;
}
二维数组求前缀和
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1e3+2;
const int MAXM = 1e3+2;
int sum[MAXN][MAXM] = {};
int main() {
int n,m,r,c;
cin>>n>>m;//>>r>>c;
int data;
for (int i=1; i<=n; i++) {
for (int j=1; j<=m; j++) {
cin >> data;
sum[i][j] = sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1]+data;
}
}
for (int i=1; i<=n; i++) {
for (int j=1; j<=m; j++) {
cout << sum[i][j] << " ";
}
cout << endl;
}
return 0;
}
差分
#include<iostream>
#include<vector>
using namespace std;
vector<int> s(100010,0),b(100010,0);
int main(){
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>s[i];//存放原数组
// for(int i=1;i<=n;i++) cout<<s[i]<<' ';
// cout<<endl;
for(int i=1;i<=n;i++) b[i] = s[i]-s[i-1];//构造差分数组
// for(int i=1;i<=n;i++) cout<<b[i]<<' ';
// cout<<endl;
while(m--){
int l,r,c;
cin>>l>>r>>c;
b[l]+=c;//将l和以后加c
b[r+1]-=c;//将r之后-c
}
// for(int i=1;i<=n;i++) cout<<b[i]<<' ';
// cout<<endl;
for(int i=1;i<=n;i++){
b[i]=b[i-1]+b[i];//将差分改为原数组
cout<<b[i]<<' ';
}
return 0;
}
差分矩阵
#include<iostream>
#include<cstdio>
using namespace std;
const int MAX_N = 1e3+5;
int a[MAX_N][MAX_N] = {0};
int s[MAX_N][MAX_N] = {0};
void insert(int x1,int y1,int x2,int y2,int v){
a[x1][y1] += v;
a[x2+1][y1] -= v;
a[x1][y2+1] -= v;
a[x2+1][y2+1] += v; //多减一次,加v进行抵消操作
}
int main(){
int n,m,q;
scanf("%d%d%d",&n,&m,&q);
for(int i = 1; i <= n; i++){
for(int j = 1;j <= m; j++){
int t;
scanf("%d",&t);
insert(i,j,i,j,t);
}
}
while(q--){
int x1,y1,x2,y2,t;
scanf("%d%d%d%d%d",&x1,&y1,&x2,&y2,&t);
insert(x1,y1,x2,y2,t);
}
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];
printf("%d",s[i][j]);
if(j != m)
printf(" ");
}
if(i!= n)
printf("\n");
}
return 0;
}
双指针算法
最长连续不重复子序列
核心思路:
遍历数组a中的每一个元素a[i], 对于每一个i,找到j使得双指针[j, i]维护的是以a[i]结尾的最长连续不重复子序列,长度为i - j + 1, 将这一长度与r的较大者更新给r。
对于每一个i,如何确定j的位置:由于[j, i - 1]是前一步得到的最长连续不重复子序列,所以如果[j, i]中有重复元素,一定是a[i],因此右移j直到a[i]不重复为止(由于[j, i - 1]已经是前一步的最优解,此时j只可能右移以剔除重复元素a[i],不可能左移增加元素,因此,j具有“单调性”、本题可用双指针降低复杂度)。
用数组s记录子序列a[j ~ i]中各元素出现次数,遍历过程中对于每一个i有四步操作:cin元素a[i] -> 将a[i]出现次数s[a[i]]加1 -> 若a[i]重复则右移j(s[a[j]]要减1) -> 确定j及更新当前长度i - j + 1给r。
注意细节:
当a[i]重复时,先把a[j]次数减1,再右移j。
# include <iostream>
using namespace std;
const int N = 100010;
int a[N], s[N];
int main()
{
int n, r = 0;
cin >> n;
for (int i = 0, j = 0; i < n; ++ i)
{
cin >> a[i];
++ s[a[i]];
while (s[a[i]] > 1) -- s[a[j++]]; // 先减次数后右移
r = max(r, i - j + 1) ;
}
cout << r;
return 0;
}
数组元素的目标和
(双指针) O(n)O(n)
i从 0开始 从前往后遍历
j从 m - 1开始 从后向前遍历
和纯暴力的O(n2)O(n2) 算法的区别就在于
j指针不会回退
#include <iostream>
#include <cstdio>
using namespace std;
const int N = 1e5 + 10;
int n, m, k;
int a[N], b[N];
#define read(x) scanf("%d",&x)
int main()
{
read(n), read(m), read(k);
for (int i = 0; i < n; i ++ ) read(a[i]);
for (int i = 0; i < m; i ++ ) read(b[i]);
for (int i = 0, j = m - 1; i < n; i ++) {
while(j >= 0 && a[i] + b[j] > k) j --;
if(j >= 0 && a[i] + b[j] == k) printf("%d %d\n", i, j);
}
return 0;
}
判断子序列
#include<iostream>
#include<cstdio>
using namespace std;
const int N=1e5+10;
int a[N],b[N];
int main()
{
int n,m;
scanf("%d%d",&n,&m);
for(int i = 0;i < n; i++) scanf("%d",&a[i]);
for(int j = 0;j < m; j++) scanf("%d",&b[j]);
int i = 0;
for(int j = 0;j < m; j++)
{
if(i < n&&a[i] == b[j]) i++;
}
if(i == n) puts("Yes");
else puts("No");
return 0;
}
位运算
二进制中1的个数
给定一个长度为n的数列,请你求出数列中每个数的二进制表示中1的个数。
#include<iostream>
using namespace std;
int lowbit(int x){
return x&(-x);
}
int main(){
int n;
cin>>n;
while(n--){
int x;
cin>>x;
int res=0;
while(x) x-=lowbit(x),res++;
cout<<res<<' ';
}
return 0;
}
离散化
区间和
此处略作分析,为什么要离散化呢,因为存储的下标实在太大了,如果直接开这么大的数组,根本不现实,第二个原因,本文是数轴,要是采用下标的话,可能存在负值,所以也不能,所以有人可能会提出用哈希表,哈希表可以吗?答案也是不可以的,因为哈希表不能像离散化那样缩小数组的空间,导致我们可能需要从-e9遍历到1e9(此处的含义就是假如我们需要计算1e-9和1e9区间内的值,那我们需要从前到后枚举,无论该值是否存在),因为哈希表不能排序,所以我们一般不能提前知道哪些数轴上的点存在哪些不存在,所以一般是从负的最小值到正的最大值都枚举一遍,时间负责度太高,于是就有了本题的离散化。
离散化的本质,是映射,将间隔很大的点,映射到相邻的数组元素中。减少对空间的需求,也减少计算量。
其实映射最大的难点是前后的映射关系,如何能够将不连续的点映射到连续的数组的下标。此处的解决办法就是开辟额外的数组存放原来的数组下标,或者说下标标志,本文是原来上的数轴上的非连续点的横坐标。
此处的做法是是对原来的数轴下标进行排序,再去重,为什么要去重呢,因为本题提前考虑了前缀和的思想,其实很简单,就是我们需要求出的区间内的和的两端断点不一定有元素,提前加如需要求前缀和的两个端点,有利于我们进行二分搜索,其实二分搜索里面我们一般假定有解的,如果没解的话需要特判,所以提前加入了这些元素,从而导致可能出现重复元素。
本文你用于存储这个关系的数组是alls[N];特地说明下,为什么要开300000+10呢,因为我前面说过了提前考虑了前缀和的因素,加上了2*m个点,又因为怕出现数组越界,多加了10。什么时候会用完300000个空间呢,那就是无重复元素,外加n和m都是1e5次方的打下。
下一步就是写提前数轴点对应的映射后的数组的下标的函数课,此题用的是二分,log(n + 2 * m)
int find(int x)
{
int l = 0, r = alls.size() - 1;
while(l < r)
{
int mid = l + r >> 1;
if(alls[mid] >= x) r = mid;
else l = mid + 1;
}
return r + 1;
}
为什么返回r + 1,这是变相的让映射后的数组从1开始。此处描述映射后的数组下标对应的数值用的是a数组。
剩下的就是已经讲过的了,前缀后算法,本题的难点是理清楚这个映射关系。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
typedef pair<int, int> PII;
const int N = 300010;
int a[N], s[N];
int n, m;
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;
if(alls[mid] >= x) r = mid;
else l = mid + 1;
}
return r + 1;
}
vector<int>:: iterator unique(vector<int> &a)
{
int j = 0;
for(int i = 0; i < a.size(); i ++)
if(!i || a[i] != a[i - 1])
a[j ++ ] = a[i];
return a.begin() + j;
}
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), 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;
}
区间合并
区间合并
给定 nn 个区间 [lili,riri],要求合并所有有交集的区间。
注意如果在端点处相交,也算有交集。
输出合并完成后的区间个数。
例如:[11,33]和[22,66]可以合并为一个区间[11,66]。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std ;
typedef pair<int,int> pii ;
vector<pii>nums,res ;
int main()
{
int st=-2e9,ed=-2e9 ; //ed代表区间结尾,st代表区间开头
int n ;
scanf("%d",&n) ;
while(n--)
{
int l,r ;
scanf("%d%d",&l,&r) ;
nums.push_back({l,r}) ;
}
sort(nums.begin(),nums.end()) ; //按左端点排序
for(auto num:nums)
{
if(ed<num.first) //情况1:两个区间无法合并
{
if(ed!=-2e9) res.push_back({st,ed}) ; //区间1放进res数组
st=num.first,ed=num.second ; //维护区间2
}
//情况2:两个区间可以合并,且区间1不包含区间2,区间2不包含区间1
else if(ed<num.second)
ed=num.second ; //区间合并
}
//(实际上也有情况3:区间1包含区间2,此时不需要任何操作,可以省略)
//注:排过序之后,不可能有区间2包含区间1
if(st!=-2e9&&ed!=-2e9) res.push_back({st,ed});
//剩下还有一个序列,但循环中没有放进res数组,因为它是序列中的最后一个序列
/*
for(auto r:res)
printf("%d %d\n",r.first,r.second) ;
puts("") ;
*/
//(把上面的注释去掉,可以在调试时用)
printf("%d",res.size()) ; //输出答案
return 0 ;
}