前言
本博客从常见的算法模板出发,对常见算法进行归类并给出相应的模板,希望对您能有所帮助。
提高熟练度的方法:多敲几遍,大约3~5遍。
一、二分模板
整数二分的一般步骤:
- 确定一个区间[L,R],使得目标值在该区间中
- 找一个判断条件,使得该判断条件:①具有二段性 ②答案是二段性的分界点。(二段性:前半段满足、后半段不满足)
第一类:ans是红色区间的右端点
while(l<r)
{
int mid=(l+r+1)/2;
if(m红色) l=m;
else r=mid-1;
}
第二类:ans是绿色区间的左端点
while(l<r)
{
int mid=(l+r)/2;
if(m绿色) r=m;
else l=mid+1;
}
- 分析中点M在该判断条件下是否成立,如果成立,考虑答案在哪个区间;如果不成立,考虑答案在哪个区间
- 更新方式写的是 R = M i d R=Mid R=Mid,则不用做任何处理;如果更新方式写的是 L = M i d L=Mid L=Mid,则需要在计算 M i d Mid Mid时加上1。
模板一:
int l=0,r=n-1;
while(l<r)
{
int mid=(l+r)/2;
if(q[mid]>=k) r=mid;
else l=mid+1;
}
if(q[l]==k) cout << l << endl;
模板二:
int l=0,r=n-1;
while(l<r)
{
int mid=(l+r+1)/2;
if(q[mid]<=k) l=mid;
else r=mid-1;
}
if(q[l]==k) cout << l << endl;
1.1 整数范围(整数二分)
求解代码:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010;
int q[N];
int n,m;
int main()
{
cin >> n >> m;
for(int i=0;i<n;i++) cin >> q[i];
while (m -- )
{
int k;
cin >> k;
int l=0,r=n-1;
while(l<r)
{
int mid=(l+r)/2;
if(q[mid]>=k) r=mid;
else l=mid+1;
}
if(q[l]==k)
{
cout << l << ' ';
r=n-1;
while(l<r)
{
int mid=l+r+1>>1;
if(q[mid]<=k) l=mid;
else r=mid-1;
}
if(q[l]==k) cout << r << endl;
}
else cout << "-1 -1" << endl;
}
return 0;
}
1.2 数的三次方根(浮点数二分)
如果答案保留$k$位小数,那么二分时区间长度只需满足
:
r
−
l
>
1
0
−
(
k
+
2
)
r-l>10^{-\left( k+2 \right)}
r−l>10−(k+2)
求解代码:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
int main()
{
double n;
cin >> n;
double l=-100,r=100;
while(r-l>1e-8)
{
double mid=(l+r)/2;
if(mid*mid*mid>=n) r=mid;
else l=mid;
}
printf("%.6f",l);
}
1.3 机器人跳跃问题
check函数
:找到能满足check函数最小的e,因此,满足check时,另r=mid。
求解代码:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010;
int H[N],n;
bool check(int e)
{
for(int i=1;i<=n;i++)
{
e=2*e-H[i];
if(e<0) return false;
if(e>1e5) return true;
}
return true;
}
int main()
{
cin >> n;
for(int i=1;i<=n;i++) cin >> H[i];
int l=0,r=1e5;
while(l<r)
{
int mid=l+r>>1;
if(check(mid)) r=mid;
else l=mid+1;
}
cout << l << endl;
}
1.4 剪绳子
最优化问题转通过增加一个条件换为判定性问题
假设如果我们知道绳子需要切成多长例如为x,然后对每段绳子除以x,累加总和后看是否大于等于需要切成的数量。
如何求x
,那我们就可以通过对区间分割的方式也就是二分的方式来进行确定。
判断能总共剪出长度为 mid 的绳子多少根:总共能剪出的根数=每一根绳子能剪出的根数之和。
如果能剪出长度为 m i d mid mid的绳子,则 l = m i d l=mid l=mid,如果不能,则 r = m i d r=mid r=mid。关键:如果mid成立,任何小于等于mid的值一定成立。
求解代码:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010;
int L[N];
int n,m;
bool check(double x)
{
int res=0;
for(int i=0;i<n;i++)
{
res+=L[i]/x;
}
return res>=m;
}
int main()
{
cin >> n >> m;
for(int i=0;i<n;i++) cin >> L[i];
double l=0,r=1e9;
while(r-l>1e-4)
{
double mid=(l+r)/2;
if(check(mid)) l=mid;
else r=mid;
}
printf("%.2lf",l);
}
1.5 分巧克力
分析思路同上题:剪绳子
求解代码:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010;
int h[N],w[N];
int n,m;
bool check(int x)
{
int res=0;
for(int i=0;i<n;i++)
res+=(h[i]/x)*(w[i]/x);
return res>=m;
}
int main()
{
cin >> n >> m;
for(int i=0;i<n;i++) cin >> h[i] >> w[i];
int l=0,r=1e5;
while(l<r)
{
int mid=(l+r+1)/2;
if(check(mid)) l=mid;
else r=mid-1;
}
cout << l << endl;
}
1.6 四平方和
求解代码:
1.7 最佳牛围栏
算法思想:
求解代码:
二、排序模板
2.1 快速排序
2.1.1 快速排序
算法思想:分治法求解
- 确定分界点,直接取左边界
q[l]
,取中间值q[(l+r)/2]
,取右边界q[r]
,随机取一个数。 - 调整区间,将区间一分为二。保证左边区间里面所有的数都
≤x
,右边区间里面所有的数都≥x
。重点
- 递归处理左右两段,将两段拼接在一块。
代码求解:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010;
int n;
int q[N];
void quick_sort(int q[],int l,int r)
{
if(l>=r) return; //递归的终止情况。判断边界,区间中没有数或者只有一个数,不用排序
//第一步:分解子问题
int x=q[(l+r)/2],i=l-1,j=r+1;//分界点x,左右两个指针
while(i<j)
{
do i++;while(q[i]<x);//保证左边的数小于x
do j--;while(q[j]>x);//保证右边的数大于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] <<' ';
return 0;
}
2.1.2 第k个数
算法思想:
将k值当做物理地址的值,比如第5个数其实就是数组4的位置,第2个数就是数组1的位置
每次只需要判断k在左区间还是右区间,一直递归查找k所在区间
最后只剩一个数时,只会有数组[k]一个数,返回数组[k]的值就是答案
- 找到分界点
x
,q[L]
,q[(L+R)/2]
,q[R]
- 左边所有数
≤x
,右边所有数≥x
- 递归排序
Left
,递归排序Right
k≤S[L]
,S[L]为左边≤x的数的个数,此时递归Left
k>S[L]
,此时递归Right
,第k-S[L]
个小的数
代码求解:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010;
int q[N];
int n,k;
int quick_sort(int l,int r,int k)
{
if(l>=r) return q[l];
int x=q[(l+r)/2],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]);
}
int sl=j-l+1;
if(k<=sl) return quick_sort(l,j,k);
return quick_sort(j+1,r,k-sl);
}
int main()
{
cin >> n >> k;
for(int i=0;i<n;i++) cin >> q[i];
cout << quick_sort(0,n-1,k);
return 0;
}
2.2 归并排序
2.2.1 归并排序
算法思想:分治
- 确定分界点mid:mid=(l+r)/2
- 递归排序左边q[l…mid]和右边q[mid+1…r]
- 归并:合二为一。步骤:①主体合并:至少有一个小数组添加到tmp数组中,②收尾:可能存在的剩下的一个小数组的尾部直接添加到 tmp 数组中,③复制回来:tmp 数组覆盖原数组。
时间复杂度: O ( n l o n g n ) O(nlong_n) O(nlongn)
求解代码:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010;
int n;
int q[N],tmp[N];
void merge_sort(int q[],int l,int r)
{
if(l>=r) return;
int mid=(l+r)/2;
merge_sort(q,l,mid),merge_sort(q,mid+1,r);
int k=0,i=l,j=mid+1;
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] << ' ';
return 0;
}
2.2.2 逆序对数量
算法思想:
- 左半边内部的逆序对数量:merge_sort(L,mid)
- 右半边内部的逆序对数量:merge_sort(mid+1,R)
- S[j]=mid-i+1
求解代码:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010;
int n;
int q[N],tmp[N];
long long merge_sort(int l,int r)
{
if(l>=r) return 0;
int mid=(l+r)/2;
long long res=merge_sort(l,mid)+merge_sort(mid+1,r);
int k=0,i=l,j=mid+1;
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(int i=l,j=0;i<=r;i++,j++) q[i]=tmp[j];
return res;
}
int main()
{
cin >> n;
for(int i=0;i<n;i++) cin >> q[i];
cout << merge_sort(0,n-1) << endl;
return 0;
}
三、高精度
vector相关内容:
头文件
#include<vector>
vector<int> A;
A.push_back(1);
3.1 高精度加法(两个大整数相加)
A+B 位数
1
0
6
10^6
106
算法思想:
- 大整数存储。将每一位存到数组中,考虑到进位,让低位在前,第0位存个位。
- 大整数运算。模拟人工加法,Ai+Bi+t(进位)
不压位做法:
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 100010;
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/=10;
}
if(t) C.push_back(1);
return C;
}
int main()
{
string a,b;
vector<int> A,B;
cin >> a >> b;
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);
for(int i=C.size()-1;i>=0;i--) printf("%d",C[i]);
}
压位做法:压9位
#include <iostream>
#include <vector>
using namespace std;
const int base = 1000000000;
vector<int> add(vector<int> &A, vector<int> &B)
{
if (A.size() < B.size()) return add(B, A);
vector<int> C;
int t = 0;
for (int i = 0; i < A.size(); i ++ )
{
t += A[i];
if (i < B.size()) t += B[i];
C.push_back(t % base);
t /= base;
}
if (t) C.push_back(t);
return C;
}
int main()
{
string a, b;
vector<int> A, B;
cin >> a >> b;
for (int i = a.size() - 1, s = 0, j = 0, t = 1; i >= 0; i -- )
{
s += (a[i] - '0') * t;
j ++, t *= 10;
if (j == 9 || i == 0)
{
A.push_back(s);
s = j = 0;
t = 1;
}
}
for (int i = b.size() - 1, s = 0, j = 0, t = 1; i >= 0; i -- )
{
s += (b[i] - '0') * t;
j ++, t *= 10;
if (j == 9 || i == 0)
{
B.push_back(s);
s = j = 0;
t = 1;
}
}
auto C = add(A, B);
cout << C.back();
for (int i = C.size() - 2; i >= 0; i -- ) printf("%09d", C[i]);
cout << endl;
return 0;
}
3.2 高精度减法(两个大整数相减)
A-B 位数 1 0 6 10^6 106
算法思想:
- 判断,若A≥B,直接算A-B
- 若A<B,算-(B-A),保证大数减去小数
求解代码:
#include <iostream>
#include <cstring>
#include <algorithm>
#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()-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;
int t=0;//t表示进位
for(int i=0;i<A.size();i++)
{
t=A[i]-t;
if(i<B.size()) t=t-B[i];//判断B是否存在,B的位数比A的位数少
C.push_back((t+10)%10);//t≥0 或者 t<0
if(t<0) t=1;//借位
else t=0;
}
while(C.size()>1 && C.back()==0) C.pop_back();//去掉前导0
return C;
}
int main()
{
string a,b;
vector<int> A,B;
cin >> a >> b;
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))
{
auto C=sub(A,B);
for(int i=C.size()-1;i>=0;i--) printf("%d",C[i]);
}
else
{
auto C=sub(B,A);
printf("-");
for(int i=C.size()-1;i>=0;i--) printf("%d",C[i]);
}
return 0;
}
3.3 高精度乘法
A*a
l
e
n
(
A
)
≤
1
0
6
len(A)≤10^6
len(A)≤106 a≤10000
算法思想:
求解代码:
#include <iostream>
#include <cstring>
#include <algorithm>
#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() || 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);
for(int i=C.size()-1;i>=0;i--) printf("%d",C[i]);
return 0;
}
3.4 高精度除法
A/a
算法思想:
除法从最高位开始算
求解代码:
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
vector<int> div(vector<int> A,int b,int &r)//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;
}
reverse(C.begin(),C.end());//C[0]存储的是低位,要将C反转
while(C.size()>1 && C.back()==0) C.pop_back();//除去前导0
return C;
}
int main()
{
string a;
int b;
vector<int> A;
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--) printf("%d",C[i]);
cout << endl << r << endl;
return 0;
}
四、前缀和模板
4.1 前缀和思想
算法思想:
s [ i ] = a [ 1 ] + a [ 2 ] + . . . + a [ n ] s[i]=a[1]+a[2]+...+a[n] s[i]=a[1]+a[2]+...+a[n]
- 如何求 S [ i ] S[i] S[i]?s[i]=s[i-1]+a[i],s[0]=0
- 作用:快速求出原数组中一段数的和
- 例如:[l,r]一段区间中的和?s[r]-s[l-1]
4.2 一维前缀和
代码:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010;
int n,m;
int a[N],s[N];
int main()
{
cin >> n >> m;
for(int i=1;i<=n;i++) cin >> a[i];
for(int i=1;i<=n;i++) s[i]=s[i-1]+a[i];
while (m -- )
{
int l,r;
cin >> l >> r;
cout << s[r]-s[l-1] << endl;
}
return 0;
}
4.3 二维前缀和——子矩阵的和
思想:
|
|
- S[i][j]如何求?
S [ i ] [ j ] = S [ i − 1 ] [ j ] + S [ i ] [ j − 1 ] − S [ i − 1 ] [ j − 1 ] + a [ i ] [ j ] S[i][j]=S[i-1][j]+S[i][j-1]-S[i-1][j-1]+a[i][j] S[i][j]=S[i−1][j]+S[i][j−1]−S[i−1][j−1]+a[i][j] -
(
x
1
,
y
1
)
(x_1,y_1)
(x1,y1),
(
x
2
,
y
2
)
(x_2,y_2)
(x2,y2)这一子矩阵中的所有数之和为:
S [ x 2 ] [ y 2 ] − S [ x 2 ] [ y 1 − 1 ] − S [ x 1 − 1 ] [ y 2 ] + S [ x 1 − 1 ] [ y 1 − 1 ] S[x_2][y_2]-S[x_2][y_1-1]-S[x_1-1][y_2]+S[x_1-1][y_1-1] S[x2][y2]−S[x2][y1−1]−S[x1−1][y2]+S[x1−1][y1−1]
代码:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010;
int a[N][N],s[N][N];
int n,m,q;
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++)
{
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;
printf("%d\n",s[x2][y2]-s[x1-1][y2]-s[x2][y1-1]+s[x1-1][y1-1]);//求子矩阵的部分和
}
}
五、差分模板
5.1 差分
类似于数学中的求导和积分,差分可以看成前缀和的逆运算。
差分数组:
首先给定一个原数组a
:a[1], a[2], a[3],,,,,, a[n];
然后我们构造一个数组b
:b[1] ,b[2] , b[3],,,,,, b[i];
使得a[i] = b[1] + b[2 ]+ b[3] +,,,,,, + b[i]
也就是说,a
数组是b
数组的前缀和数组,反过来我们把b
数组叫做a
数组的差分数组。换句话说,每一个a[i]
都是b
数组中从头开始的一段区间和。
考虑如何构造差分b数组?
最为直接的方法
如下:
a[0 ]= 0;
b[1] = a[1] - a[0];
b[2] = a[2] - a[1];
b[3] =a [3] - a[2];
........
b[n] = a[n] - a[n-1];
图示:
我们只要有b
数组,通过前缀和运算,就可以在O(n)
的时间内得到a
数组 。
话说有这么一个问题:
给定区间[l ,r ]
,让我们把a数组中的[ l, r]
区间中的每一个数都加上c
,即 a[l] + c , a[l+1] + c , a[l+2] + c ,,,,,, a[r] + c;
暴力做法是for
循环l
到r
区间,时间复杂度O(n)
,如果我们需要对原数组执行m
次这样的操作,时间复杂度就会变成O(n*m)
。有没有更高效的做法吗? 考虑差分做法。
始终要记得,a
数组是b
数组的前缀和数组,比如对b
数组的b[i]
的修改,会影响到a
数组中从a[i]
及往后的每一个数。
首先让差分b
数组中的 b[l] + c
,a
数组变成 a[l] + c ,a[l+1] + c,,,,,, a[n] + c;
然后我们打个补丁,b[r+1] - c
, a
数组变成 a[r+1] - c,a[r+2] - c,,,,,,,a[n] - c;
为啥还要打个补丁?
我们画个图理解一下这个公式的由来:
b[l] + c
,效果使得a
数组中 a[l]
及以后的数都加上了c
(红色部分),但我们只要求l
到r
区间加上c
, 因此还需要执行 b[r+1] - c
,让a
数组中a[r+1]
及往后的区间再减去c
(绿色部分),这样对于a[r]
以后区间的数相当于没有发生改变。
因此我们得出一维差分结论:给a
数组中的[ l, r]
区间中的每一个数都加上c
,只需对差分数组b
做 b[l] + = c, b[r+1] - = c
。时间复杂度为O(1)
, 大大提高了效率。
总结:
5.1.1 问题
核心:
将 a [ l r ] a[l~r] a[l r]全部加上c,等价于 b [ l ] + = c b[l]+=c b[l]+=c, b [ r + 1 ] − = c b[r+1]-=c b[r+1]−=c
代码:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010;
int a[N],b[N];
int n,m;
void insert(int l,int r,int c)
{
b[l]+=c;
b[r+1]-=c;
}
int main()
{
cin >> n >> m;
for(int i=1;i<=n;i++) cin >> a[i];//输入数据
for(int i=1;i<=n;i++) insert(i,i,a[i]);//刚开始a[]是有值的,可以看成在全是0的基础上进行了n步的插入操作,在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];//求原数组,即差分数组b[]前缀和
for(int i=1;i<=n;i++) printf("%d ",b[i]);//输出原数组
return 0;
}
5.2 二维差分——差分矩阵
如果扩展到二维,我们需要让二维数组被选中的子矩阵中的每个元素的值加上c,是否也可以达到O(1)的时间复杂度。答案是可以的,考虑二维差分。
a[][]
数组是b[][]
数组的前缀和数组,那么b[][]
是a[][]
的差分数组
原数组: a[i][j]
我们去构造差分数组: b[i][j]
使得a
数组中a[i][j]
是b
数组左上角(1,1)
到右下角(i,j)
所包围矩形元素的和。
如何构造b
数组呢?
我们去逆向思考。
同一维差分,我们构造二维差分数组目的是为了 让原二维数组a
中所选中子矩阵中的每一个元素加上c
的操作,可以由O(n*n)
的时间复杂度优化成O(1)
已知原数组a
中被选中的子矩阵为 以(x1,y1)
为左上角,以(x2,y2)
为右下角所围成的矩形区域;
始终要记得,a
数组是b
数组的前缀和数组,比如对b
数组的b[i][j]
的修改,会影响到a
数组中从a[i][j]
及往后的每一个数。
假定我们已经构造好了b
数组,类比一维差分,我们执行以下操作
来使被选中的子矩阵中的每个元素的值加上c
b[x1][y1] + = c;
b[x1,][y2+1] - = c;
b[x2+1][y1] - = c;
b[x2+1][y2+1] + = c;
每次对b
数组执行以上操作,等价于:
for(int i=x1;i<=x2;i++)
for(int j=y1;j<=y2;j++)
a[i][j]+=c;
我们画个图去理解一下这个过程:
b[x1][ y1 ] +=c
; 对应图1 ,让整个a
数组中蓝色矩形面积的元素都加上了c
。
b[x1,][y2+1]-=c
; 对应图2 ,让整个a
数组中绿色矩形面积的元素再减去c
,使其内元素不发生改变。
b[x2+1][y1]- =c
; 对应图3 ,让整个a
数组中紫色矩形面积的元素再减去c
,使其内元素不发生改变。
b[x2+1][y2+1]+=c
; 对应图4,让整个a
数组中红色矩形面积的元素再加上c
,红色内的相当于被减了两次,再加上一次c
,才能使其恢复。
我们将上述操作封装成一个插入函数:
void insert(int x1,int y1,int x2,int y2,int c)
{ //对b数组执行插入操作,等价于对a数组中的(x1,y1)到(x2,y2)之间的元素都加上了c
b[x1][y1]+=c;
b[x2+1][y1]-=c;
b[x1][y2+1]-=c;
b[x2+1][y2+1]+=c;
}
我们可以先假想a
数组为空,那么b
数组一开始也为空,但是实际上a
数组并不为空,因此我们每次让以(i,j)
为左上角到以(i,j)
为右下角面积内元素(其实就是一个小方格的面积)去插入 c=a[i][j]
,等价于原数组a
中(i,j)
到(i,j)
范围内 加上了 a[i][j]
,因此执行n*m
次插入操作,就成功构建了差分b
数组.
这叫做曲线救国。
代码如下:
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
insert(i,j,i,j,a[i][j]); //构建差分数组
}
}
5.2.1 问题
代码:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010;
int a[N][N],b[N][N];
int n,m,q;
void insert(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()
{
scanf("%d %d %d",&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++)//刚开始a[]是有值的,可以看成在全是0的基础上进行了n步的插入操作,在(i,j)-(i,j)位置上加上了a[i][j]
for(int j=1;j<=m;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++)//求原数组即b[][]数组的前缀和
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++)//打印输出b[][]数组
{
for(int j=1;j<=m;j++)
{
printf("%d ",b[i][j]);
}
printf("\n");
}
return 0;
}
六、双指针算法
算法模板:
for(int i=0,j=0;i<n;i++)
{
while(j<i && check(i,j))
j++;
//每道题目的具体逻辑
}
作用:
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
O(n^2)
//将上面的朴素算法优化到O(n)
例子:将字符串中的单词分隔开来
#include <iostream>
#include <cstring>
#include <algorithm>
#include <string.h>
using namespace std;
int main()
{
char str[1000];
gets(str);
for(int i=0;i<n;i++)
{
int j=i;
while(j<n && str[j]!=' ') j++;
for(int k=i;k<j;k++) cout << str[k];
cout << endl;
i=j;
}
return 0;
}
6.1 最长连续不重复子序列
算法思想:
- 遍历数组
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
。
代码:
//朴素做法:
for(int i=0;i<n;i++)
for(int j=0;j<=i;j++)
if(check(i,j))
{
res=max(res,i-j+1);
}
//双指针算法:O(n) 单调性
for(int i=0,j=0;i<n;i++)
{
while(j<=i && check(i,j)) j++;
res=max(res,i-j+1);
}
//i从0到n枚举 j往左最远能到什么地方
AC代码:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010;
int n;
int a[N],s[N];
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]]++;//记录当前数出现的次数为1
while(s[a[i]]>1)//若当前数出现的次数为2,要剔除当前数
{
s[a[j]]--;//将a[j]移出
j++;
}
res=max(res,i-j+1);//每次更新res,求得最大值
}
cout << res << endl;
}
6.2 数组元素的目标和
算法思想:
先写暴力做法,找i
和j
之间的单调关系,降低时间复杂度
代码:
#include <iostream>
#include <cstring>
#include <algorithm>
#include <cmath>
using namespace std;
const int N = 100010;
int a[N],b[N];
int n,m,x;
int main()
{
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(j>=0 && a[i]+b[j]>x) j--;
if(a[i]+b[j]==x) printf("%d %d",i,j);
}
return 0;
}
6.3 判断子序列
算法思想:
双指针算法
1.j
指针用来扫描整个b
数组,i指针用来扫描a
数组。若发现a[i]==b[j]
,则让i指针后移一位。
2.整个过程中,j
指针不断后移,而i
指针只有当匹配成功时才后移一位,若最后若i==n
,则说明匹配成功。
为什么双指针做法是正确的?
整个过程中j
指针不断扫描b
数组并且向后移动,相当于不断给i
指针所指向的a
数组创建匹配的机会,只有匹配成功时i
指针才会向后移动一位,当i==n
时,说明全部匹配成功。
代码:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010;
int n,m;
int a[N],b[N];
int main()
{
cin >> n >> m;
for(int i=0;i<n;i++) cin >> a[i];
for(int i=0;i<m;i++) cin >> b[i];
int i=0,j=0;
while(i<n&&j<m)
{
if(a[i]==b[j]) i++;
j++;
}
if(i==n) cout << "Yes" << endl;
else cout << "No" << endl;
}
6.4 日志统计
二元组排序
算法思想:
- 1.对所有的赞按照时间从小到大排序
- 2.通过双指针
i
,j
维护长度不大于d
的区间,并记录该区间的中所有帖子获得的赞数
代码:
#include <iostream>
#include <cstring>
#include <algorithm>
#define x first
#define y second
using namespace std;
typedef pair<int,int> PII;
const int N = 100010;
PII logs[N];
int n,d,k;
int cnt[N];//用来记录一个id号获得的赞数,表示形式为cnt[id]++;
bool st[N];//用来标记id号,因为id <= 1e5,所以可以利用遍历来输出。
int main()
{
scanf("%d %d %d",&n,&d,&k);
for(int i=0;i<n;i++) scanf("%d %d",&logs[i].x,&logs[i].y);
sort(logs,logs+n);//排序时默认以first为准排序
//双指针算法, i走在前面,j走在后面
for(int i=0,j=0;i<n;i++)
{
int id=logs[i].y;//t表示为i时刻的id号
cnt[id]++;//在j时刻,id号为t的大佬日记获得了一个赞,给t大佬加一分
//两个指针跨越的时间超过了d,早期的赞过期了
while(logs[i].x-logs[j].x>=d)
{
cnt[logs[j].y]--;//就是这位大佬,获赞的时间太久远了,赞作废了,哭去吧
j++;//在logs[j].x时刻的太久远了,往前挪挪。
//这个循环,直到最后一个赞不过期为止。
}
//记录热帖的id号,好知道谁才是大佬
if(cnt[id]>=k) st[id]=true;
}
//遍历一遍id号,展现大佬
for(int i=0;i<=100000;i++)
if(st[i])
printf("%d\n",i);
return 0;
}
七、位运算
7.1 二进制中1的个数
算法思想:
1.n的二进制表示中第k位是几?n>>k&1
n=15=(1111)2
①先把第k位数字移到最后一位,n>>k
②看个位是几 x&1
2.返回n的最后一位1:lowbit(n)=n& -n
使用lowbit
操作,进行,每次lowbit
操作截取一个数字最后一个1后面的所有位,每次减去lowbit
得到的数字,直到数字减到0,就得到了最终1的个数。
lowbit原理:x&(-x),取反x加1
根据计算机负数表示的特点,如一个数字原码是10001000
,他的负数表示形式是补码,就是反码+1
,反码是01110111
,加一则是01111000
,二者按位与得到了1000
,就是我们想要的lowbit操作
输出10的二进制各个位:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
int main()
{
int n=10;
for(int k=3;k>=0;k--) cout << (n>>k&1) << ' ';
return 0;
}
AC代码:
#include <iostream>
#include <cstring>
#include <algorithm>
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++;//每次减去x的最后一位1
cout << res << ' ';
}
}
求反码:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
int main()
{
int n=10;
unsigned int x=-10;
for(int i=31;i>=0;i--) cout << (x>>i&1);
return 0;
}
八、离散化–整数
8.1 区间和
算法思想:
代码: