目录
[1.1 快速排序](#1.1 快速排序)
[1.2 第k个数](#1.2 第k个数)
[2.1 归并排序](#2.1 归并排序)
[2.2 逆序对的数量](#2.2 逆序对的数量)
[3.1 数的范围](#3.1 数的范围)
[3.2 数的三次方根](#3.2 数的三次方根)
[4.1 高精度加法](#4.1 高精度加法)
[4.2 高精度减法](#4.2 高精度减法)
[4.3 高精度乘法](#4.3 高精度乘法)
[4.4 高精度除法](#4.4 高精度除法)
[5.1 前缀和](#5.1 前缀和)
[5.2 子矩阵的和](#5.2 子矩阵的和)
[5.3 差分](#5.3 差分)
[5.4 差分矩阵](#5.4 差分矩阵)
[6.1 最长连续不重复的子序列](#6.1 最长连续不重复的子序列)
[6.2 数组元素的目标和](#6.2 数组元素的目标和)
[6.3 判断子序列](#6.3 判断子序列)
1、快速排序
1.1 快速排序
1. 确定分界点:q[l]、q[r]、q[l + r >> 2]、随机
2. 调整区间
我们要将区间调整为下面这样子
当q[i] < x时,i++,当q[i] >= x时,i停止。当q[j] > x时,j--,当q[j] <= x时,j停止。判断一下此时是否i < j,若i < j则交换,否则就出循环。
此时为什么是正确的呢?
因为在任意时刻,i左边区间内的数都是小于等于x的,j右边区间内的数都是大于等于x的
3. 递归处理左右两段
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int q[N], n;
void quick_sort(int l, int r)
{
if (l >= r) return ;
int i = l - 1, j = r + 1, x = q[l];
while (i < j)
{
while (q[ ++ i] < x) ;
while (q[ -- j] > x) ;
if (i < j) swap(q[i], q[j]);
}
quick_sort(l, j);
quick_sort(j + 1, r);
}
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; i ++ ) scanf("%d", &q[i]);
quick_sort(0, n - 1);
for (int i = 0; i < n; i ++ ) printf("%d ", q[i]);
return 0;
}
讲解代码中几个需要注意的地方:
- 为什么while(i < j)里面的两个while循环不需要先判断i < j?
i < j是为了防止越界访问。我们先来看结束时,i和j的分布情况
(1) i和j可能会相遇在某一个x,此时i == j
(2) 因为i左边的数是<=x,j右边的数是>=x,所以i一定会停在j右边的区间的第一个数,j一定会停在i左边的区间的第一个数。最先开始的时候,i的左边没有数,j的右边没有数,会不会出现意外呢?不会的,以此时选取x=q[l]为例,一开始q[i] == x,i是不会动的,j会先动。若q[j] <= x,那么交换完q[i]和q[j]后,i++,j--,i的左边有值,j的右边有值。若q[j] > x,则j--找<=x的元素,找到后,两者一交换,左右就都不为空了。
- 为什么一上来直接i++、j--?
因为无论是正常走,还是交换完,都需要让i++、j--。
注意:初始化时i = l - 1,j = r + 1
- 递归时的区间选择
上面我们选取的分界点是x = q[l],递归时就是使用j,若选取的分界点是x = q[r],递归时就是使用i,否则就会出现死循环
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int q[N], n;
void quick_sort(int l, int r)
{
if (l >= r) return ;
int i = l - 1, j = r + 1, x = q[r];
while (i < j)
{
while (q[ ++ i] < x) ;
while (q[ -- j] > x) ;
if (i < j) swap(q[i], q[j]);
}
quick_sort(l, i - 1);
quick_sort(i, r);
}
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; i ++ ) scanf("%d", &q[i]);
quick_sort(0, n - 1);
for (int i = 0; i < n; i ++ ) printf("%d ", q[i]);
return 0;
}
所以当选取的分界点是q[l]或q[r]时,要注意迭代区间的更新
当然,选取分界点时也可以直接使用随机数法或区中间值的方法
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int q[N], n;
void quick_sort(int l, int r)
{
if (l >= r) return ;
swap(q[l], q[l + rand() % (r - l + 1)]);
int i = l - 1, j = r + 1, x = q[l];
while (i < j)
{
while (q[ ++ i] < x) ;
while (q[ -- j] > x) ;
if (i < j) swap(q[i], q[j]);
}
quick_sort(l, j);
quick_sort(j + 1, r);
//quick_sort(l, i - 1);
//quick_sort(i, r); 也行
}
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; i ++ ) scanf("%d", &q[i]);
quick_sort(0, n - 1);
for (int i = 0; i < n; i ++ ) printf("%d ", q[i]);
return 0;
}
此时会有疑问,就是为什么此时分界点选择的是q[l],但是递归的时候可以用i呢?
实际上也是不行的,比如像5 4 1 3 2,若rand() == 2,变为1 4 5 3 2,若用i仍然会死循环,可以是因为rand()不一定是2
这个算法相比较我们之前的快速排序算法是有优势的
void QuickSort(int* a, int left, int right)//Hoare法
{
if (left >= right)
return;
int begin = left, end = right;
//随机选key
int randi = rand() % (right - left + 1);
randi += begin;
Swap(&a[left], &a[randi]);
int keyi = left;
while (begin < end)
{
while(begin < end && a[end] >= a[keyi])
end--;
while(begin < end && a[begin] <= a[keyi])
begin++;
Swap(&a[begin], &a[end]);
}
Swap(&a[begin], &a[keyi]);
QuickSort(a, left, begin - 1);
QuickSort(a, begin + 1, right);
}
我们之前的算法在遇到全相等的情况时,时间复杂度是O(N^2),因为遇到==基准值时是会跳过去的,而现在的代码不会
1.2 第k个数
可以直接使用快速排序先将数组排序,然后再输出第k个数,时间复杂度是O(N*logN)
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1e5 + 10;
int q[N],n,k;
void quick_sort(int l, int r)
{
if(l >= r) return;
swap(q[l], q[l + rand() % (r - l + 1)]);
int i = l - 1,j = r + 1,x = q[l];
while(i < j)
{
while(q[++i] < x) ;
while(q[--j] > x) ;
if(i < j) swap(q[i], q[j]);
}
quick_sort(l, j);
quick_sort(j + 1, r);
}
int main()
{
scanf("%d%d", &n, &k);
for(int i = 0;i < n;i++) scanf("%d", &q[i]);
quick_sort(0, n - 1);
printf("%d\n", q[k - 1]);
return 0;
}
也可以使用快速选择算法
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1e5 + 10;
int q[N],n,k;
int quick_sort(int l, int r,int k)
{
if(l >= r) return q[l];
int i = l - 1,j = r + 1,x = q[l];
while(i < j)
{
while(q[++i] < x) ;
while(q[--j] > x) ;
if(i < j) swap(q[i], q[j]);
}
int sl = j - l + 1;
if(sl >= k) return quick_sort(l, j, k);
else return quick_sort(j + 1, r, k - sl);
}
int main()
{
scanf("%d%d", &n, &k);
for(int i = 0;i < n;i++) scanf("%d", &q[i]);
cout<<quick_sort(0, n - 1, k)<<endl;
return 0;
}
这道题是保证第k个数在区间里面,所以区间里面至少有1个数,可以l==r作为递归结束条件,但是快速排序中一定要是l >= r,因为快速排序中l可能大于r
2、归并排序
2.1 归并排序
-
确定分界点 mid = (l + r) / 2
-
递归排序左右区间
-
归并 ---- 将两个有序数组合二为一
注意:在这个步骤中,为了保证排序的稳定性,当两指针指向的值相等时,要将第一个数组的值放入临时数组
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int q[N], tmp[N], n;
void merge_sort(int l,int r)
{
if(l >= r) return;
int mid = (l + r) / 2;
merge_sort(l, mid);
merge_sort(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];
}
int main()
{
scanf("%d", &n);
for(int i = 0;i < n;i++) scanf("%d", &q[i]);
merge_sort(0, n - 1);
for(int i = 0;i < n;i++) printf("%d ", q[i]);
return 0;
}
2.2 逆序对的数量
可以像归并排序时一样,将数组划分为两部分,此时逆序对会有3种情况
我们只需要一直向下递归,直到每个区间都只有一个值,就能都变成第三种情况
并且我们可以在归并的同时就行排序,因为当q[i] > q[j]时,则q[i]到q[mid]都是大于q[j]的
在"并"的过程中,利用两个有序数组计算逆序对的数量
此时q[i] > q[j],那么前半数组剩余的值都和q[j]构成逆序对
注意,逆序对的数量最多会大于int,所以开long long
#include<iostream>
using namespace std;
typedef long long ll;
const int N = 1e5 + 10;
int q[N], tmp[N], n;
ll merge_sort(int q[], int l, int r)
{
if(l >= r) return 0;
int mid = l + r >> 1;
ll res = 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++];
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()
{
scanf("%d", &n);
for(int i = 0;i < n;i++) scanf("%d", &q[i]);
ll res = merge_sort(q, 0, n - 1);
printf("%lld\n", res);
return 0;
}
3、二分
bool check(int x) {/* ... */} // 检查x是否满足某种性质
// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用:
int bsearch_1(int l, int r)
{
while (l < r)
{
int mid = l + r >> 1;
if (check(mid)) r = mid; // check()判断mid是否满足性质
else l = mid + 1;
}
return l;
}
// 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用:
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;
}
作者:yxc
来源:AcWing
模板不需要特意去记什么时候在计算中点时要+1,只需要写出check后,根据更新判断
为什么计算中点时有时候要+1,有时候不需要呢?
因为l + r >> 1是向下取整,当l = r - 1时,若chack为真,则没变,会死循环
3.1 数的范围
这道题是整数二分,分为找左端点和右端点
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int a[N], n, q, k;
int main()
{
scanf("%d %d", &n, &q);
for(int i = 0;i < n;i++) scanf("%d", &a[i]);
while(q--)
{
scanf("%d", &k);
// 先找左端点
int l = 0, r = n - 1;
while(l < r)
{
int mid = l + r >> 1;
if(a[mid] < k) l = mid + 1;
else r = mid;
}
if(a[l] != k) printf("-1 -1\n");
else
{
// 找右端点
printf("%d ", l);
l = 0, r = n - 1;
while(l < r)
{
int mid = l + r + 1 >> 1;
if(a[mid] > k) r = mid - 1;
else l = mid;
}
printf("%d\n", l);
}
}
return 0;
}
3.2 数的三次方根
这道题是浮点数二分,初始时让l等于数的范围的左端点,r等于数的范围的右端点,一直二分,直到l和r之间的距离足够小即可。如何定义足够小呢?如果题目的浮点数保留6位小数,则while的循环条件时r - l > 1e-8
注意:浮点数二分不能直接取l=0,r=x,因为当0<x<1时,可能会出错,如求二次方跟时,x=0.01,而0.01的二次方根是0.1。在这道题中就不需要考虑这个,因为数的范围已经确定了
#include<iostream>
using namespace std;
int main()
{
double n;
scanf("%lf", &n);
double l = -10000,r = 10000;
while(r - l > 1e-8)
{
double mid = (r + l) / 2;
if(mid * mid * mid < n) l = mid;
else r = mid;
}
printf("%lf\n", l);
return 0;
}
浮点数二分相对于整数二分更加简单,因为没有+1-1,所以不需要考虑边界问题
4、高精度
这里的高精度是指大数之间的加减乘除运算
4.1 高精度加法
高精度加法,A + B,通常A和B是大小在0到10^6的整数
大整数的存储通常使用数组来存储,并且最好是倒过来存储,方便进位
使用数组进行加法的过程就是模拟整数加法,Ci = Ai + Bi + t,t表示进位
#include<iostream>
#include<string>
#include<vector>
using namespace std;
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');
vector<int> C = add(A, B);
for(int i = C.size() - 1;i >= 0;i--) printf("%d", C[i]);
return 0;
}
4.2 高精度减法
高精度减法,A - B,通常A和B是大小在0到10^6的整数
因为一道题中结果可能很大,会使用到高精度的加减乘除,所以四种运算的存储方式相同
若Ai - Bi - t大于等于0,Ci = Ai - Bi - t
若Ai - Bi - t小于0,Ci = Ai - Bi - t + 10,t表示借位
#include<iostream>
#include<string>
#include<vector>
using namespace std;
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;
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;
}
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)) // A >= B
{
vector<int> C = sub(A, B);
for(int i = C.size() - 1;i >= 0;i--) printf("%d", C[i]);
}
else
{
vector<int> C = sub(B, A);
printf("-");
for(int i = C.size() - 1;i >= 0;i--) printf("%d", C[i]);
}
return 0;
}
sub中是默认A大于等于B的
(t + 10) % 10,当t >= 0时结果是不变的,当t < 0时,这个位置需要借位
还需要处理前导0,如789-786,此时A是9 8 7,B是6 8 7,减完之后C是3 0 0,反转过后也就是003,我们需要的是3,所以需要处理前导0
4.3 高精度乘法
高精度乘法是一个很大的数字乘一个较小的数字,len(A)<=10^6,b<=10000
我们这里是将b当成一个整体,与A的每一位相乘
Ci = (Ai * b + t) % 10,t表示进位
#include<iostream>
#include<vector>
#include<string>
using namespace std;
int b;
vector<int> mul(vector<int>& A, int b)
{
int t = 0;
vector<int> C;
for(int i = 0;i < A.size() || t;i++) // 结束条件是A遍历完,并且t也为0
{
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(); // 处理前导0
return C;
}
int main()
{
vector<int> A;
string a; cin>>a>>b;
for(int i = a.size() - 1;i >= 0;i--) A.push_back(a[i] - '0');
vector<int> C = mul(A, b);
for(int i = C.size() - 1;i >= 0;i--) printf("%d", C[i]);
return 0;
}
同样需要处理前导0,如A = [3, 2, 1],b = 0,结果是C = [0, 0, 0]
4.4 高精度除法
高精度除法通常A <= 10^6,b <= 10000
除法是从最高位开始计算的,而加减乘都是从最低位计算,为了保持统一,除法计算完reverse
#include<iostream>
#include<vector>
#include<string>
#include<algorithm>
using namespace std;
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(); // 处理前导0
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');
int r; // 余数
vector<int> C = div(A, b, r);
for(int i = C.size() - 1;i >= 0;i--) printf("%d", C[i]);
printf("\n");
printf("%d\n", r);
return 0;
}
同样需要处理前导0,如1234 / 11,A = [4, 3, 2, 1],b = 11,计算完后C = [0, 1, 1, 2],逆置完后C = [2, 1, 1, 0]
5、前缀和与差分
5.1 前缀和
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int q[N], dp[N], n, m, l, r;
int main()
{
scanf("%d %d", &n, &m);
for(int i = 1;i <= n;i++)
{
scanf("%d", &q[i]);
dp[i] = dp[i - 1] + q[i];
}
while(m--)
{
scanf("%d %d", &l, &r);
printf("%d\n", dp[r] - dp[l - 1]);
}
return 0;
}
5.2 子矩阵的和
#include<iostream>
using namespace std;
const int N = 1010;
int m, n, q, x1, y1, x2, y2;
int a[N][N], dp[N][N];
int main()
{
scanf("%d%d%d", &n, &m, &q);
for(int i = 1;i <= n;i++)
{
for(int j = 1;j <= m;j++)
{
scanf("%d", &a[i][j]);
dp[i][j] = dp[i - 1][j] + dp[i][j - 1] - dp[i - 1][j - 1] + a[i][j];
}
}
while(q--)
{
scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
printf("%d\n", dp[x2][y2] - dp[x2][y1 - 1] - dp[x1 - 1][y2] + dp[x1 - 1][y1 - 1]);
}
return 0;
}
5.3 差分
差分就是前缀和的逆过程
给定一个原数组a,我们要求差分数组b,使得a数组是b数组的前缀和,即a[i]=b[1]+b[2]+...+b[i]
- 获得差分数组
很容易就能想到b[i] = a[i] - a[i - 1]
- 使用差分数组
我们要对给定的区间[l, r]内的每个a数组加上c,按照正常做法,编译a数组的区间[l, r],每个数加上c,时间复杂度是O(m * n)。
此时就可以使用差分数组了。因为a数组是b数组的前缀和,所以我们会发现对a数组的区间[l, r]每个数加上c,实际上只需要对b[l] += c,b[r + 1] -= c
注意,因为我们对b进行了操作,但没对a操作,所以此时a数组名义上是b数组的前缀和,但是数值上已经不是了,所以m次操作完成后,要重新求一遍a
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int a[N], b[N];
int m, n, l, r, c;
int main()
{
scanf("%d %d", &n, &m);
for(int i = 1;i <= n;i++) scanf("%d", &a[i]);
for(int i = 1;i <= n;i++) b[i] = a[i] - a[i - 1];
while(m--)
{
scanf("%d %d %d", &l, &r, &c);
b[l] += c;
b[r + 1] -= c;
}
for(int i = 1;i <= n;i++)
{
a[i] = a[i - 1] + b[i];
printf("%d ", a[i]);
}
return 0;
}
我们会发现,两步是可以使用同一个函数的。第二步是在a数组的区间[l, r]上插入值c。我们可以把第一步抽象为,假设a数组全为0,那么b数组也全为0,现在需要往a数组的[i, i]区间插入数值a[i],因为a数组并不是真的全为0,所以此时a数组既是b数组名义上的前缀和,也是数值上的前缀和
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int a[N], b[N];
int m, n, l, r, c;
void insert(int l, int r, int c)
{
b[l] += c;
b[r + 1] -= c;
}
int main()
{
scanf("%d %d", &n, &m);
for(int i = 1;i <= n;i++)
{
scanf("%d", &a[i]);
insert(i, i, a[i]);
}
while(m--)
{
scanf("%d %d %d", &l, &r, &c);
insert(l, r, c);
}
for(int i = 1;i <= n;i++)
{
a[i] = a[i - 1] + b[i];
printf("%d ", a[i]);
}
return 0;
}
5.4 差分矩阵
这个就是二维差分,与前面的差分是类似的
#include<iostream>
using namespace std;
const int N = 1010;
int n, m, q, x1, y1, x2, y2, c;
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()
{
scanf("%d %d %d", &n, &m, &q);
for(int i = 1;i <= n;i++)
{
for(int j = 1;j <= m;j++)
{
scanf("%d", &a[i][j]);
insert(i, j, i, j, a[i][j]);
}
}
while(q--)
{
scanf("%d %d %d %d %d", &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] = a[i - 1][j] + a[i][j - 1] - a[i - 1][j - 1] + b[i][j];
printf("%d ", a[i][j]);
}
printf("\n");
}
return 0;
}
6、双指针算法
6.1 最长连续不重复的子序列
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1e5 + 10;
int n;
int a[N], b[N];
int main()
{
scanf("%d", &n);
for(int i = 0;i < n;i++) scanf("%d", &a[i]);
int ans = 0;
for(int i = 0,j = 0;i < n;i++)
{
b[a[i]]++;
while(b[a[i]] > 1)
{
b[a[j]]--;
j++;
}
ans = max(ans, i - j + 1);
}
printf("%d", ans);
return 0;
}
6.2 数组元素的目标和
#include<iostream>
using namespace std;
typedef long long ll;
const int N = 1e5 + 10;
int n, m, x;
ll a[N], b[N];
int main()
{
scanf("%d %d %d", &n, &m, &x);
for(int i = 0;i < n;i++) scanf("%lld", &a[i]);
for(int i = 0;i < m;i++) scanf("%lld", &b[i]);
int i = 0,j = m - 1;
while(i < n && j >= 0)
{
long long sum = a[i] + b[j];
if(sum < x) i++;
else if(sum > x) j--;
else
{
printf("%d %d\n", i, j);
i++, j--;
}
}
return 0;
}
6.3 判断子序列
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int n, m;
int a[N], b[N];
int main()
{
scanf("%d %d", &n, &m);
for(int i = 0;i < n;i++) scanf("%d", &a[i]);
for(int i = 0;i < m;i++) scanf("%d", &b[i]);
int j = 0;
for(int i = 0;i < m;i++)
{
if(j >= n) break;
if(a[j] == b[i]) j++;
}
if(j == n) printf("Yes");
else printf("No");
return 0;
}
7、位运算
位运算中,主要是两种问题
-
n的二进制表示中第k位是几 n >> k & 1,例如n=15,二进制是1010,左边是高位,右边是低位,下标是3210
-
lowbit(x):返回x的最后一位1,例如x=15,二进制是1010,lowbit(x)=10
lowbit是如何求的呢?x&-x = x&(~x+1)
lowbit可用于统计x的二进制表示中1的个数,每次将x中的一位1减掉,知道x变成0
使用方法一解题:
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int n, a[N], b[N];
int main()
{
scanf("%d", &n);
for(int i = 0;i < n;i++) scanf("%d", &a[i]);
for(int i = 0;i < n;i++)
{
int sum = 0;
for(int k = 0;k <= 31;k++)
if((a[i] >> k & 1) == 1) sum++;
b[i] = sum;
}
for(int i = 0;i < n;i++) printf("%d ", b[i]);
return 0;
}
使用方法二解题:
// 方法二
int lowbit(int x)
{
return x & -x;
}
int main()
{
int n;
scanf("%d", &n);
while(n--)
{
int x;
scanf("%d", &x);
int res = 0;
while(x) x -= lowbit(x), res++;
cout<<res<<" ";
}
cout<<endl;
return 0;
}
8、离散化
这道题数轴上的数字是[-10^9, 10^9],一共是2*10^9个数,会进行n次插入和m次查询,每次查询会输入一个区间,n和m最大都是10^5,也就是说最多会用到3*10^5个数,这与2*10^9比起来是十分小的,如果我们要开一个2*10^9的数组,显然有很多空间是会浪费掉的。于是,这道题使用了离散化
因为值域太大,所以将用到的下标映射到从0开始的自然数(从1开始也行),映射的过程就称为离散化
我们先开一个数组,将用到的下标全部放进这个数组中,然后再进行离散化
离散化时要注意两个问题:
(1)用到的下标可能是存在重复的,所以要先去重
(2)如何将下标映射到从0开始的自然数?此时需要用到二分,利用其在存放数组中的下标,一个数映射后的值就是其在数组中的下标
具体步骤就是
将{x,c},{l,r}键值对分别放入add,query中,将用到的坐标x、l、r放入alls中
此时alls是一个无序、有重复的数组,并且里面的值大小差异可能是很大的
然后对alls进行排序+去重,这样就可以将alls中的值映射为下标(排序后,x、l、r的相对位置不会发生变化,所以可以进行映射)
数组a存储映射后的值,s存储a的前缀和,因为需要求前缀和,所以a从1开始,只需要修改一下二分的返回值即可
对每一次插入,先找一下x映射后的值,也就是x在alls中的下标,a[k] += c
全部放入a数组后,再对a数组求前缀和
对于每一次询问,先找一下l、r映射后的值,也就是l、r在alls中的下标,s[r] - s[l - 1]
以下面这个样例为例
add {1,2} {3,6} {7,5}
query {1,3} {4,6} {7,8}
alls 1 3 7 1 3 4 6 7 8
排序 + 去重
alls 1 3 4 6 7 8
下标 0 1 2 3 4 5
对插入操作用到的值1 3 7映射后的下标进行插入操作
a 2 6 0 0 5 0
下标 1 2 3 4 5 6
对a求前缀和
s 2 8 8 8 13 13
下标 1 2 3 4 5 6
对于第一次询问{1,3},s[2] - s[0] = 8
对于第二次询问{4,6},s[4] - s[2] = 0
对于第三次询问{7,8},s[6] - s[4] = 5
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
typedef pair<int, int> PII;
const int N = 300010;
int n, m;
int a[N], s[N]; // a是存离散化后的值的数组,s是存a的前缀和
vector<int> alls; // 存需要离散化的元素,x、l、r需要离散化,去重就是对alls去重
vector<PII> add, query;
// add存要在x位置增加c的键值对
// query存每次询问的左右区间l和r
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()
{
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;
}
这里使用任何一个二分模板均可
9、区间合并
这道题就是首先将区间全部读入到一个vector<pair<int, int>>中,然后根据区间的左端点对区间进行排序,然后对所有区间进行遍历,遍历过程中将有交集的区间进行合并,合并好的区间放入一个新的数组中
遍历:维护当前区间,遍历后面的区间,看后面区间与当前区间的关系。因为已经按区间左端点进行排序了,所以关系只有3种
#include<iostream>
#include<algorithm>
using namespace std;
typedef pair<int, int> PII;
int n;
vector<PII> segs, res; // segs读入所有区间,res存放合并好的区间
void merge(vector<PII>& segs)
{
sort(segs.begin(), segs.end());
int st = -2e9, ed = -2e9; // 最先开始让维护的区间位于最左边,并且第一次更新区间不能放入res
for(int i = 0;i < n;i++)
{
if(ed < segs[i].first)
{
if(i != 0) res.push_back({st, ed});
st = segs[i].first;
ed = segs[i].second;
}
else ed = max(ed, segs[i].second);
}
if(st != -2e9) res.push_back({st, ed});
// 最后还需要将最后维护的区间放入,此时要判断一下,因为有可能segs中没有任何区间
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;
}