双指针+位运算+离散化+区间合并
双指针+位运算+离散化+区间合并
双指针算法
可以是两个指针分别指向两个序列,也可以是两个指针指向一个序列,维护一段区间
核心思想:将 \(O(n^2)\) 优化到 \(O(n)\)
本质上就是通过找到单调性进行优化
双指针算法算法模板:
for (int i = 0, j = 0; i < n; i ++ )
{
while (j < i && check(i, j)) j ++ ;
// 具体问题的逻辑
}
常见问题分类:
(1) 对于一个序列,用两个指针维护一段区间
(2) 对于两个序列,维护某种次序,比如归并排序中合并两个有序序列的操作
最长连续不重复子序列
最长的不包含重复数字的连续子序列的长度
1 2 2 3 5 -> 2 3 5 len = 3
暴力做法:先枚举终点,再枚举起点(j为起点,i为终点)
for(int i = 0; i < n; i ++)
{
for(int j = 0; j <= i; j ++)
{
if(check(j, i))
{
res = max(res, i - j + 1);
}
}
}
优化做法:
本质上还是枚举每一个i,看左边的j离他最远的话是在哪个位置
我们现在只需要枚举每一个i,然后判断j需不需要向后移动
//双指针算法
for(int i = 0; i < n; i ++)
{
while(j <= i && check(j, i)) j ++;
res = max(res, i - j + 1);
}
- 遍历数组\(a\)中的每一个元素\(a[i]\), 对于每一个\(i\),找到\(j\)使得双指针\([j, i]\)维护的是以\(a[i]\)结尾的最长连续不重复子序列,长度为\(i - j + 1\), 将这一长度与\(res\)的较大者更新给\(res\)。
- 对于每一个\(i\),如何确定\(j\)的位置:由于\([j, i - 1]\)是前一步得到的最长连续不重复子序列,所以如果\([j, i]\)中有重复元素,一定是\(a[i]\),因此右移\(j\)直到\(a[i]\)不重复为止(由于\([j, i - 1]\)已经是前一步的最优解,此时\(j\)只可能右移以剔除重复元素\(a[i]\),不可能左移增加元素,因此,\(j\)具有“单调性”、本题可用双指针降低复杂度。
- 用数组\(s\)记录子序列\(a[j - i]\)中各元素出现次数,遍历过程中对于每一个\(i\)有四步操作:输入\(a[i]\) -> 将\(a[i]\)出现次数\(s[a[i]]\)加1 -> 若\(a[i]\)重复则右移\(j\)(\(s[a[j]]\)要减1) -> 确定\(j\)及更新当前长度\(i - j + 1\)给\(r\)。
位运算
求n的二进制表示中第k位为几
n = 15 = (1111)2
- 先把第k位移到最后一位 n >> k
- 看个位是几 x & 1
lowbit:返回x的最后一位1
x= (1010)2 lowbit(x) = (10)2
x = (101000)2 lowbit(x) = (1000)2
lowbit(n) = n & -n = n & (~ x + 1)
可以用于求n里面1的个数
int lowbit(int x)
{
return x & -x;
}
int res = 0;
while(x) x -= lowbit(x), res ++; //每次减去x的最后一位1
cout<<res;
// 或者
int cnt = 0;
while(x)
{
if(x & 1) cnt ++;
x >>= 1;
}
位运算算法模板:
求n的二进制表示中第k位数字: n >> k & 1
返回n的最后一位1:lowbit(n) = n & -n
离散化
整数的离散化
一个数列,值很大,但是数量不多(数据范围在[0, 109] ,数量在[0, 105] )
a[] : 1 , 3 , 100, 2000, 500000 这几个下标的元素有用,其他都没有用
b[] : 0 , 1 , 2 , 3 , 4
问题:
- a中可能有重复元素,需要去重
- 如何算出a[i]离散化后的值是多少(二分)
需要先将所有要进行操作的数存进数组,在进行离散化
值域跨度很大,但是非常稀疏
将所有会用到的下标离散化映射到一个稠密数组内,在稠密数组内进行前缀和,考虑的是相对关系
注意点:
- 一定要注意映射到0开始还是1开始的序列!!!
离散化算法模板:
vector<int> alls; // 存储所有待离散化的值
sort(alls.begin(), alls.end()); // 将所有值排序
alls.erase(unique(alls.begin(), alls.end()), alls.end()); // 去掉重复元素
// 二分求出x对应的离散化的值
int find(int x) // 找到第一个大于等于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; // 映射到1, 2, ...n
}
区间和
假定有一个无限长的数轴,数轴上每个坐标上的数都是 0。
首先进行 n 次操作,每次操作将某一位置 x 上的数加 c。
接下来,进行 m次询问,每个询问包含两个整数 l 和 r,你需要求出在区间[l,r] 之间的所有数的和。
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 1e5 + 10;
typedef pair<int, int> PII;
int q[N * 3], s[N * 3];
vector<PII> add, query;
vector<int> alls;
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;
}
int main()
{
int n, m, x, c, l, r;
scanf("%d%d", &n, &m);
for(int i = 0; i < n; i ++)
{
scanf("%d%d", &x, &c);
add.push_back({x, c});
alls.push_back(x);
}
for(int i = 0; i < m; i ++)
{
scanf("%d%d", &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 p : add)
{
q[find(p.first)] += p.second;
}
s[0] = q[0]; // 求前缀和
for(int i = 1; i < alls.size(); i ++)
{
s[i] = s[i - 1] + q[i];
}
for(auto p : query) // 查询操作
{
printf("%d\n", s[find(p.second)] - s[find(p.first) - 1]);
}
return 0;
}
unique函数
对于一个有序数列,如何判断不是重复的
- 是第一个元素
- 和前一个元素不一样, a[i] != a[i - 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];
//a[0] ~ a[j - 1]中所有不重复的数
return a.begin() + j;
}
区间合并
给很多区间,假如两个区间有交集,就合并为一个区间
-
按照所有区间的左端点排序
-
从左到右扫描所有的区间,再扫描的过程中进行处理
-
假如下一个起始点和终止点在当前区间内,就不变
假如下一个起始点在当前区间内,但是终止点在当前区间外,就更新当前区间的右端点
假如下一个起始点和终止点都在当前区间外,就将当前区间存入结果,继续扫描下一区间
-
或者进一步简化一下
假如下一起始点在当前区间内,就让当前区间的右端点变成 当前右端点和下一终止点的较大值
假如下一起始点在当前区间外,就将当前区间存入答案并继续扫描下一区间
区间合并算法模板:
// 将所有存在交集的区间合并
void merge(vector<PII> &segs)
{
vector<PII> res;
//首先对原数组进行排序
//pair是先对第一关键词排序,再对第二关键词排序
sort(segs.begin(), segs.end());
//先初始化区间为负无穷
int st = -2e9, ed = -2e9;
//遍历每一个区间,如果这个区间的右端点在下一个区间的起始点前面,就将该区间保存
//然后更新当前区间
//假如这个区间的右端点在下一区间的后面,就比较当前右端点和下一区间的右端点,取较大值作为区间右端点
for (auto seg : segs)
if (ed < seg.first)
{
if (st != -2e9) res.push_back({st, ed});
st = seg.first, ed = seg.second;
}
else ed = max(ed, seg.second);
if (st != -2e9) res.push_back({st, ed});
segs = res;
}