前言:
写了四天,2W字,终于把第一讲写好了,所有题都重做了一遍,算是温故而知新,有些地方写的仓促,慢慢修改吧
排序
1.快速排序
作用:
算法思想:
主要思想——分治
步骤 1: 选取中枢 x(可以是任意值) 步骤 2: 将区间分为两部分,左边部分满足所有值 <= x,右边部分满足所有值 >= x 步骤 3: 递归左右部分
模板:
void quick_sort(int q[], int l, int r)
{
if (l >= r) return ;
int x = q[(l + r) >> 1], 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);
}
例题:
AcWing 786. 第k个数
AC代码:
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 1e5 + 10;
int n, k;
int q[N];
void quick_sort(int q[], int l, int r)
{
if (l >= r) return ;
int x = q[(l + r) >> 1], 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);
}
int main( )
{
cin >> n >> k;
for (int i = 0; i < n; i ++) cin >> q[i];
quick_sort(q, 0, n - 1);
cout << q[k - 1];
return 0;
}
2.归并排序
作用:
排序
算法思想:
主要思想——分治
步骤 1: 选取中点 mid 步骤 2: 将区间分为两部分,递归排序左右两部分 步骤 3: 归并——合二为一
模板:
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]) tmp[k ++] = q[i ++];
else tmp[k ++] = q[j ++];
}
while (i <= mid) tmp[k ++] = q[i ++];
while (j <= r) tmp[k ++] = q[j ++];
for (i = l, j = 0; i <= r; i ++, j ++ ) q[i] = tmp[j];
}
例题:
AcWing 788. 逆序对的数量
AC代码:
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 100005;
typedef long long LL;
int n;
int q[N], tmp[N];
LL merge_sort(int q[], int l, int r)
{
if (l >= r) return 0;
int mid = l + r >> 1;
LL res = 0;
res += 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]) 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 (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(q, 0, n - 1) << endl;
return 0;
}
二分
1.整数二分
作用:
以O(log n)的时间复杂度进行查找
算法思想:
步骤 1 : 确定一个区间,使得ans在区间内 步骤 2 : 找到一个性质,性质满足: (1)性质具有二段性,即整个区间一段满足该性质,另一段不满足 (2)ans 为二段性的分界点
步骤 3 : 确定使用哪个模板
第一类 : 当 ans 是 红色区间的右端点 将[L,R]分成[L, Mid - 1] 和 [Mid, R]:
if (Mid 为红色) 说明 ans 在 [Mid, R] else 说明 ans 在[L, Mid - 1]
第二类 : 当 ans 是 绿色区间的左端点 将[L,R]分成[L, Mid] 和 [Mid + 1, R]
if (Mid 为红色) 说明 ans 在 [Mid + 1, R] else 说明 ans 在[L, Mid]
模板:
bool check(int x) {}
int bsearch_1(int l, int r)
{
while (l < r)
{
int mid = l + r >> 1;
if (check(mid)) r = mid;
else l = mid + 1;
}
return l;
}
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;
}
例题:
AcWing 789. 数的范围
AC代码:
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 1e5 + 5;
int q[N];
int main()
{
int n, m;
cin >> n >> m;
for (int i = 0; i < n; i ++) cin >> q[i];
while (m --)
{
int x;
cin >> x;
int l = 0, r = n;
while (l < r)
{
int mid = l + r >> 1;
if (q[mid] >= x) r = mid;
else l = mid + 1;
}
if (q[l] != x) cout << "-1 -1" << endl;
else
{
cout << l << ' ';
int l = 0, r = n - 1;
while (l < r)
{
int mid = l + r + 1 >> 1;
if (q[mid] <= x) l = mid;
else r = mid - 1;
}
cout << r << endl;
}
}
return 0;
}
总结:
- 写 l = mid 时计算 mid 要 + 1,写 r = mid时计算 mid 不 + 1
2.浮点数二分
算法思想:
步骤 1 : 确定一个区间,使得ans在区间内 步骤 2 : 找到一个性质,性质满足: (1)性质具有二段性,即整个区间一段满足该性质,另一段不满足 (2)ans 为二段性的分界点 浮点数二分无需考虑区间的划分,由于是稠密的,所以每次都会确定地划分成[L, Mid] [Mid, R],整数二分当区间只剩一个数时停止循环,而浮点数二分当区间长度足够小的时候停止 while 循环
模板:
bool check(double x) {}
double bsearch_3(double l, double r)
{
const double eps = 1e-6;
while (r - l > eps)
{
double mid = (l + r) / 2;
if (check(mid)) r = mid;
else l = mid;
}
return l;
}
例题:
AcWing 790. 数的三次方根
AC代码:
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 10005;
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("%.6lf\n", l);
return 0;
}
高精度
1.高精度加法
作用:
解决两个大整数相加的问题
算法思想:
步骤 1 : 倒序存储 用动态数组倒序存储两个大整数,由于有进位的原因,且在vector后面插数相对容易,所以采用倒序存储
原数:1357910 在vector中 为 [ 0,1,9,7,5,3,1]
步骤 2 : 模拟加法 对于A和B的每一位,我们记为A[i],B[i],而 t[i] 为上一位进过来的进位(初始值为0) 所以对于每一位 我们只需记录 C[i] = A[i] + B[i] + t[i],若 C[i] > 10,取 C[i] % 10 存入答案,t[i +1] = 1;若 C[i] < 10,取 C[i] 存入答案, t[i +1] = 0 代码表示:
if (i < A.size()) t += A[i];
if (i < B.size()) t += B[i];
C.push_back(t % 10);
t /= 10;
模板:
vector<int> add(vector<int> &A, vector<int> &B)
{
vector<int> C;
for (int i = 0, t = 0; i < A.size() || i < B.size() || t; i ++)
{
if (i < A.size()) t += A[i];
if (i < B.size()) t += B[i];
C.push_back(t % 10);
t /= 10;
}
return C;
}
注意点:
t 可能A,B遍历完了最后还需要进一位 1,所以要加到循环条件里
例题:
AcWing 791. 高精度加法
AC代码:
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
string a, b;
vector<int> A, B;
vector<int> add(vector<int> &A, vector<int> &B)
{
vector<int> C;
for (int i = 0, t = 0; i < A.size() || i < B.size() || t; i ++)
{
if (i < A.size()) t += A[i];
if (i < B.size()) t += B[i];
C.push_back(t % 10);
t /= 10;
}
return C;
}
int main( )
{
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 --) cout << C[i];
cout << endl;
return 0;
}
2.高精度减法
作用:
解决两个大整数相减的问题
算法思想:
步骤 1 : 倒序存储 步骤 2 : 模拟减法 类比高精度加法,C[i] = A[i] - B[i] - t[i],这里的 t[i] 取0或1,表示是否被上一位借走了,如果 C[i] < 0,则将 C[i] + 10 存入答案,t[i + 1] = 1 表示向高位借了10过来,若 C[i] > 0 ,t[i + 1] = 0 表示没有向高位借,将 C[i] 存入答案。
模板:
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);
if (t < 0) t = 1;
else t = 0;
}
while (C.size() > 1 && C.back() == 0) C.pop_back();
return C;
}
例题:
AcWing 792. 高精度减法
AC代码:
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 1e5 + 10;
string a, b;
vector<int> A, B;
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;
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);
if (t < 0) t = 1;
else t = 0;
}
while (C.size() > 1 && C.back() == 0) C.pop_back();
return C;
}
int main( )
{
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 --) cout << C[i];
}
else
{
auto C = sub(B, A);
cout << '-';
for (int i = C.size() - 1; i >= 0; i --) cout << C[i];
}
cout << endl;
return 0;
}
3.高精度乘法
作用:
解决大整数乘小整数的问题
算法思想:
步骤 1 : 倒序存储 步骤 2 : 模拟乘法 C[i] = A[i] * b ,若C[i] > 10,则将 C[i] 模上10存入答案,其余为进位 t = C[i] / 10 ,若 C[i] < 10,则直接 将 C[i] 存入答案,t = 0,无进位
注意点:
可能含有前导0
模板:
vector<int> mul(vector<int> &A, int b)
{
vector<int> C;
for (int i = 0, t = 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;
}
例题:
AcWing 793. 高精度乘法
AC代码:
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 1e5 + 10;
string a;
int b;
vector<int> A;
vector<int> mul(vector<int> &A, int b)
{
vector<int> C;
for (int i = 0, t = 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( )
{
cin >> a >> b;
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 --) cout << C[i];
cout << endl;
return 0;
}
4.高精度除法
作用:
解决大整数除小整数的问题
算法思想:
步骤 1 : 倒序存储 其实除法是从高位开始除,但是一般高精度题目中,不会光只涉及除法,所以为了考虑对加减乘除的兼容性,我们除法也采用倒序存储,最后对结果进行逆序即可 步骤 2 : 模拟除法 每次用上一位的余数,进行计算,C[i] = r[i] * 10 + a[i] ,将 C[i] / b 的整数部分存入答案,其余为余数 r[i + 1] = C[i] % b,准备进行下一位的除法运算
注意点:
可能含有前导0
模板:
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;
}
reverse(C.begin(), C.end());
while(C.size() > 1 && C.back() == 0) C.pop_back();
return C;
}
例题:
AcWing 794. 高精度除法
AC代码:
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 1e5 + 10;
string a;
int b;
vector<int> A;
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;
}
reverse(C.begin(), C.end());
while(C.size() > 1 && C.back() == 0) C.pop_back();
return C;
}
int main( )
{
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;
}
前缀和与差分
1.一维前缀和
作用:
快速求出一个静态数组中某个区间的所有数的和
算法思想:
Eg: 求a数组中[L,R]的元素和 暴力求解: 时间复杂度在O(N),当需要求很多次时,会导致超时
for(int i=L;i<=R;i++)
ans+=a[i];
步骤 1 : 处理前缀和数组 引进一个数组S,满足 S[n] 等于原数组前n个数值之和,即
S[0]=a[0]; S[1]=a[0]+a[1]; S[2]=a[0]+a[1]+a[2]; … S[n]=a[0]+a[1]+a[2]+…+a[n];
步骤 2 : 求区间和 求区间 [ L,R] 的和,即求 a[L] + a[L+1] + a[L+2] + … + a[R]
由 S[R] = a[0]+a[1]+a[2]+…+a[R] 且 S[L-1]=a[0]+a[1]+a[2]+…+a[L-1] 得 a[L] + a[L+1] + a[L+2] + … + a[R] 等于 S[R] - S[L-1]
由于是数组访问,所以每次查询的时间由 O(N) 降到了 O(1)
注意点:
(1)原数组下标从 1 开始 (2)前缀和数组需要预处理 s[0]=0,即原数组前 0 个数的和(即为0)
例题:
AcWing 795. 前缀和
AC代码:
?
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=100010;
int a[N];
int s[N];
int n,m;
int main( )
{
cin>>n>>m;
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;
}
return 0;
}
总结:
前缀和是一个简单的算法,本质上就是用了另一个数组进行了空间换时间的操作(比赛中时间很宝贵,空间往往充裕),一维前缀和只要记住两点:
- 初始化:s[i]=s[i-1]+a[i] (s[0]=0)
- 求值:a[l,r]=s[r]-s[l-1]
2.二维前缀和
作用:
快速求出一个静态矩阵中某个区域(子矩阵)所有数的和
算法思想:
Eg:给定一个3X4的矩阵,求出点(2,1)和点(3,4)围成的矩形中的元素之和(红色区域部分) 带尺寸的图片: 暴力解法: 两重for循环求解,时间复杂度高为O(NM) 在一维前缀和数组中,S[i]表示的是原数组前 i 个数的和,那我们应该考虑是否可以仿照一维前缀和的思路来解决二维前缀和问题。
步骤 1 : 处理前缀和矩阵 二维前缀和矩阵中,S[i][j]表示了左上角矩阵的所有数的和,比如:
S[1][1] = a[1][1]; S[2][1] = a[1][1] + a[2][1]; S[2][2] = a[1][1] + a[1][2] + a[2][1] + a[2][2];
所以原矩阵的前缀和矩阵为:
下面要考虑的就是如何推导出求S[i][j]的公式,可见图中绿色部分为S[i-1][j],红色部分为S[i][j-1],蓝色部分为S[i-1][j-1],而我们要求的S[i][j]就等于 a[i][j]单个元素 + 绿色部分 + 红色部分,由于容斥原理,蓝色部分在加的过程中被计算了两次,所以要减去一个蓝色部分。 所以我们不难推导出
S[i][j] = a[i][j] + S[i-1][j] + S[i][j-1] - S[i-1][j-1]
步骤 2 :求点(x1,y1)与点(x2,y2)构成的矩形中的全部元素之和 如图所示,蓝色区域 = 红色区域 - 绿色区域 - 紫色区域 + 粉色区域(容斥原理),即
a[x1,y1 ~ x2,y2] = S[x2][y2] - S[x2][y1-1] - S[x1-1][y2] + S[x1-1][y1-1]
时间复杂度由 O(NM) 降到了 O(1)
注意点:
(1)原矩阵下标从 1 开始 (2)前缀和矩阵需要预处理一下 s[0][0~M] = 0,s[0~N][0] = 0 (其中没有元素)
例题:
AcWing 796. 子矩阵的和
AC代码:
?
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 1010;
int n,m,q;
int a[N][N],s[N][N];
int main( )
{
cin >> n >> m >> q;
for (int i = 1; i <= n; i ++)
for (int j = 1; j <= m; j ++)
{
cin >> a[i][j];
s[i][j] = a[i][j] + s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1];
}
int x1,y1,x2,y2;
while (q --)
{
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;
}
总结:
与一维前缀和思想相同,二维前缀和也是用空间换时间的想法,只需记住两点:
- 初始化: S[i][j] = a[i][j] + S[i-1][j] + S[i][j-1] - S[i-1][j-1] (S[0][0~M] = 0,S[0~N][0] = 0)
- 求值: a[x1,y1 ~ x2,y2] = S[x2][y2] - S[x2][y1-1] - S[x1-1][y2] + S[x1-1][y1-1];
3.一维差分
作用:
一维数组区间 [l, r] 上的数需要被修改很多次时,能避免复杂度O(N)的循环
算法思想:
步骤 1 : 构造差分数组 构造数组 b 使得原数组 a 是 b 的前缀和,即满足 a[i] = b[1] + b[2] + b[3] + b[4] + … + b[i],例如:
b[1] = a[1]; b[2] = a[2] - a[1]; … b[n] = a[n] - a[n -1];
步骤 2 : 区间修改 要使得 a 数组的 [l, r] 区间内每个数 += c,由于
a[l] = b[1] + b[2] + … + b[l] ; a[l + 1] = b[1] + b[2] + … + b[l] + b[l + 1] ; a[l + 2] = b[1] + b[2] + … + b[l] + b[l + 1] + b[l + 2]; … a[r] = b[1] + b[2] + … + b[l] + b[l + 1] + b[l + 2] + … + b[r]; a[r + 1] = b[1] + b[2] + … + b[l] + b[l + 1] + b[l + 2] + … + b[r] + b[r + 1]; …
可见,我们只需令 b[l] += c,就可以使 a[l] 往后所有的数都 + c,同时由于我们的区间为[l, r] 所以我们需要将 a[r + 1] 往后的数都不变,即 - c,同样只需将 b[r + 1] -= c 即可,如图粉色为区间的最终变化 步骤 3 : 求前缀和 a[i] = a[i - 1] + b[i]
模板:
void insert(int l, int r, int c)
{
b[l] += c;
b[r + 1] -= c;
}
注意点:
在构造差分数组时,可以将 a[ ] 看成最初全为 0 的数组,所以 b 数组初始也全为0,对于输入的每个 a[i],调用insert 函数,看成对 [i, i] 这个区间 进行+= a[i] 即可
例题:
AcWing 797. 差分
AC代码:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100005;
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]);
while (m --)
{
int l, r, c;
cin >> l >> r >> c;
insert(l, r, c);
}
for (int i = 1; i <= n; i ++) a[i] = a[i - 1] + b[i];
for (int i = 1; i <= n; i ++) cout << a[i] << ' ';
return 0;
}
总结:
4.二维差分
作用:
二维数组区域 [ x1,y1 ~ x2,y2 ] 上的数需要被修改很多次时,能避免复杂度O(NM)的循环
算法思想:
仿照一维差分,我们只需要求得 insert 函数即可,如图,我们需要将(x2,y2)后面的数进行还原,由于容斥原理,粉色区域被减了两次,需要加一次回来 同样,对于差分数组的构造,我们只需将输入的每个a[i][j] 看作 对(i, j) ~ (i, j) 的区域修改即可
模板:
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;
}
例题:
AcWing 798. 差分矩阵
AC代码:
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 1005;
int n, m, q;
int a[N][N], b[N][N];
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( )
{
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(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 ++)
for (int j = 1; j <= m; j ++)
a[i][j] = b[i][j] + a[i - 1][j] + a[i][j - 1] - a[i - 1][j - 1];
for (int i = 1; i <= n; i ++)
{
for (int j = 1; j <= m; j ++)
cout << a[i][j] << ' ';
cout << endl;
}
return 0;
}
双指针
作用:
优化复杂度为O(N2)的双重 for 循环
算法思想:
对于一般的朴素做法
for (int i = 0; i < n; i ++)
for (int j = 0; j < n; j ++)
if (check(i, j))
{
}
可以找到一个性质,使得 j 指针不用每次从 0 开始后移,即
for (int i = 0; i < n; i ++)
{
while (j 可取的范围 && check(i, j)) j ++;
}
例题 1 :
AcWing 799. 最长连续不重复子序列
朴素做法:
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 100005;
int a[N], s[N];
int main( )
{
int n;
cin >> n;
for (int i = 0; i < n; i ++) cin >> a[i];
int res = 0;
for (int i = 0; i < n; i ++)
for (int j = 0; j < i; j ++)
if (check(i, j))
res = max(res, i - j + 1);
cout << res << endl;
return 0;
}
双指针AC代码:
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 100005;
int a[N], s[N];
int main( )
{
int n;
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 (s[a[i]] > 1)
{
s[a[j]] --;
j ++;
}
res = max(res, i - j + 1);
}
cout << res << endl;
return 0;
}
例题 2 :
AcWing 800. 数组元素的目标和
朴素做法:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e5 + 5;
int a[N],b[N];
int main( )
{
int n, m, x;
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; i < n; i ++)
for (int j = 0; j < m; j ++)
if (a[i] + b[j] == x)
cout << i << ' ' << j << endl;
return 0;
}
双指针AC代码:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e5 + 5;
int a[N],b[N];
int main( )
{
int n, m, x;
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 (j >= 0 && a[i] + b[j] == x)
cout << i << ' ' << j << endl;
}
return 0;
}
例题 3 :
AcWing 2816. 判断子序列
双指针AC代码:
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 1e5 + 5;
int a[N], b[N];
int main( )
{
int n, m;
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;
return 0;
}
位运算
1.返回 n 的二进制表示的第k位
作用:
求 n 的 二进制表示中 第 k 位是多少 (个位是第0位)
算法思想:
步骤 1 : 先把 n 右移 k 位
n >> k;
解释: 例如将 1010 右移一位得 101,右移两位得 10,右移三位就得 1,所以右移k位就是把第 k 位放到个位上来 ? 步骤 2 : 看个位是多少
x & 1;
步骤 3 : 将两步骤合并
n >> k & 1
模板:
n >> k & 1
例题:
求 n = 10 的二进制表示(即依次输出 n 二进制表示的每一位是多少)
AC代码:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
int main( )
{
int n;
cin >> n;
for (int k = 3; k >= 0; k --)
cout << (n >> k & 1);
return 0;
}
2.lowbit操作
作用:
返回 x 的最后一位 1和它后面的0,此操作会在 树状数组 中运用
算法思想:
若 x = 1010 lowbit(x) = 10 若 x = 1000100 lowbit(x) = 100 若 x = 101010110 lowbit(x) = 10
模板
x & -x
解释: 若 x = 1010,所以 -x 为原数的补码(取反+1)即 -x = 0110,再对每一位进行 & 运算,运算结果为 x & -x = 0010
例题:
AcWing 801. 二进制中1的个数
AC代码:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010;
int n;
int lowbit(int x)
{
return x & -x;
}
int main( )
{
cin >> n;
int x;
while(n --)
{
cin >> x;
int cnt = 0;
while(x)
{
x -= lowbit(x);
cnt ++;
}
cout << cnt << ' ';
}
return 0;
}
离散化
作用:
值域很大,但稀疏,可以采用映射的方式,将其离散化
算法思想:
采用映射的方式,将原数组上散列的每个数值按顺序映射到从 0 或 1 开始的自然数,如图: 步骤 1 : a数组中可能有重复元素 先利用 sort 进行排序,再利用 unique函数 去重
步骤 2 : 映射,即如何算出原数组中下标 x 离散化后的值 要找出下标 x 离散化后的值,即找出下标 x 在离散化数组中是第几个数,用二分求解即可
模板:
vector<int> alls;
sort(alls.begin(), alls.end());
alls.erase(unique(alls.begin(), alls.end()), alls.end());
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;
}
例题:
AcWing 802. 区间和
AC代码:
#include <iostream>
#include <algorithm>
#include <cstring>
#include <vector>
using namespace std;
const int N = 300005;
typedef pair<int, int> PII;
int a[N], s[N];
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;
}
int main()
{
int n, m;
cin >> n >> m;
while(n --)
{
int x, c;
cin >> x >> c;
add.push_back({x, c});
alls.push_back(x);
}
while(m --)
{
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 : alls)
cout << item << endl;
for (auto item : add)
{
int x = find(item.first);
cout << item.first << ' ' << x << endl;
int c = item.second;
a[x] += c;
}
for (int i = 1; i <= alls.size(); i ++) s[i] = s[i - 1] + a[i];
for (auto item : query)
{
int l = find(item.first);
int r = find(item.second);
}
return 0;
}
总结:
区间合并
作用:
将区间进行合并
算法思想:
类似 贪心 + 模拟 的思想 步骤 1 : 对区间左端点进行排序 pair 的 sort 排序,默认先以第一个关键字,再以第二个关键字进行排序 步骤 2 : 维护一个区间,并扫描之后的区间 我们将起点记为st,终点记为ed,分为四种情况(实际三种) 情况一: 后面区间的st 在 维护区间st 的前面,如图 由于事先进行了排序,所以扫描的后面区间 st 必然是 <= 当前维护区间的 st,此种情况不存在
情况二: 后面区间的st 在 维护区间st 的后面,ed 在 维护区间 ed 的前面,即维护区间包含了后面区间,如图 在这种情况下,无需改变区间,继续向后扫描下一个区间
情况三: 后面区间的st 在 维护区间st 的后面,ed 在 维护区间 ed 的后面,即维护区间与后面区间存在交集,如图 这种情况下,我们需要求两个区间的并集,即 维护区间的 st 不动,将ed 改为后面区间的ed
情况四: 后面区间的st 在 维护区间ed 的后面,即维护区间与后面区间没有交集,如图 这种情况下,两区间没有交集,由于我们事先进行了排序,所以从该红色区间往后,所有的区间都不会再与当前维护的区间有交集,将当前维护的蓝色区间加到答案方案里,并将红色区间作为新的维护区间,继续向后扫描
对于情况二,三,我们可以合并考虑,只需要令ed = 蓝色的 ed 和 红色的 ed 中的较大值即可
模板:
void merge(vector<PII> &segs)
{
vector<PII> res;
sort(segs.begin(), segs.end());
int st = segs[0].first, ed = segs[0].second;
for (auto item : segs)
if (ed < item.first)
{
if (st != -2e9) res.push_back({st, ed});
st = item.first;
ed = item.second;
}
else ed = max(ed, item.second);
if (segs.size() != 0) res.push_back({st, ed});
segs = res;
}
例题:
AcWing 803. 区间合并
AC代码:
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
typedef pair<int, int> PII;
const int N = 100005;
int n;
vector<PII> segs;
void merge(vector<PII> &segs)
{
vector<PII> res;
sort(segs.begin(), segs.end());
int st = segs[0].first, ed = segs[0].second;
for (auto item : segs)
if (ed < item.first)
{
if (st != -2e9) res.push_back({st, ed});
st = item.first;
ed = item.second;
}
else ed = max(ed, item.second);
if (segs.size() != 0) 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;
}
总结:
- 区间合并函数参数需要引用进行取址,对原数组进行修改
- 维护的最后一个区间不能漏掉
|