1 双指针算法
(一)两类双指针算法:
- 有两个指针分别指向两个序列;
- 有两个指针指向一个序列;
(二)双指针算法通用模板:
for(i = 0,j = 0;i < n;i++)
{
while(j < i && check(i,j))
j ++;
// 每道题目的具体逻辑
}
(三)核心思想:
将如下算法,时间复杂度是O(n^2)
for(i = 0;i < n;i++)
for(j = 0;j < n;j++)
优化到O(n)
(三)模板题:最长连续不重复子序列
题目:
给定一个长度为 n的整数序列,请找出最长的不包含重复的数的连续区间,输出它的长度。
输入格式
第一行包含整数 n。
第二行包含 n个整数(均在 0∼10^5范围内),表示整数序列。
输出格式
共一行,包含一个整数,表示最长的不包含重复的数的连续区间的长度。
输入样例:
5
1 2 2 3 5
输出样例:
3
【思路】:使用i,j两个指针,i指针从左到右遍历“ 1 2 2 3 5 ”,j指针尽可能指向最左端,使得 j ~ i 这段序列没有重复数字。 比如 i 指向 第一个数“1”,j最左可以指向第一个数“1”;i指向第二个数“2”,j最左可以指向第一个数“1”;i指向第三个数“2”,j最左可以指向第三个数“2”。随着 i 指针往后(右)走,j指针也往后(右)走,j 指针不会往前(左)走。
for(int i = 0,j = 0;i < n;i++)
{
while(j <= i && check(j,i))
j++;
// check(j,i)是判断j和i之间有没有重复元素,有的话返回1
res = max(res,i - j + 1)
// i - j + 1 是从j到i这段子序列的长度
// res 是连续不重复子序列最大长度
}
时间复杂度:O(n),i虽多从0走到n-1,j 同 i 。
check(j,i) 如何实现?开一个10万的数组s[N] , N = 100000 ,动态记录当前区间中每个数出现的次数。每次 i 往后移动一格相当于在区间(j,i)中加入了一个新的数,s[ a[i] ] ++; 每次 j 往后移动一格,相当于有一个数从区间(j,i)中出来了,s[ a[j] ] --;这样就可以动态统计出来区间中有多少个数。也可以用hash来实现。
【具体实现】
#include<iostream>
using namespace std;
const int N = 100010;
int a[N],s[N];
int 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]] ++ ; // 区间(j,i)中新加入一个数
while(s[a[i]] > 1) // j 处于尽可能左端
{
s[a[j]] --; // s 数组中记录着区间a (j,i)中有哪些数(对应s[x]不为0), 这些数分别有多少个(对应s[x]的值为多少)
j++;
}
res = max(res,i-j+1);
}
cout << res;
return 0;
}
2 位运算
(一)n的二进制表示
#include<iostream>
using namespace std;
int main()
{
int n = 10; // 1 0 1 0
for(int k = 3;k >= 0;k--)
cout << (n >> k & 1); // 输出1010
return 0;
}
(二)lowbit(x)
举例:
x = 1010 lowbit(x) = 10 (保留x的最后一位1)
x = 101000 lowbit(x) = 1000
实现:
lowbit(x) = x & (-x) ; x 与负x
-x = ~x + 1
lowbit(x) = x & ( ~ x + 1)
应用:统计x中1的个数
每次把x最后一个1去掉,计算减到0减了多少次,x中1的个数就有多少个。
题目:二进制中1的个数
给定一个长度为 n 的数列,请你求出数列中每个数的二进制表示中 1 的个数。
输入格式
第一行包含整数 n。
第二行包含 n个整数,表示整个数列。
输出格式
共一行,包含 n个整数,其中的第 i 个数表示数列中的第 i 个数的
二进制表示中 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的最后一位1
{
x -= lowbit(x);
res++;
}
cout << res << " ";
}
return 0;
}
3 离散化
题目:区间和
假定有一个无限长的数轴,数轴上每个坐标上的数都是 0。
现在,我们首先进行 n 次操作,每次操作将某一位置 x 上的数加 c。
接下来,进行 m 次询问,每个询问包含两个整数 l 和 r,你需要求出在区间 [l,r] 之间的所有数的和。
输入格式
第一行包含两个整数 n 和 m。
接下来 n 行,每行包含两个整数 x 和 c。
再接下来 m 行,每行包含两个整数 l 和 r。
输出格式
共 m 行,每行输出一个询问中所求的区间内数字和。
数据范围
−10^9 ≤ x ≤ 10^9,
1 ≤ n,m≤ 10^5,
−10^9 ≤ l ≤ r ≤ 10^9,
−10000 ≤ c ≤10000
【分析】最多会用到 n + 2 * m(3 * 10^5)个坐标。这就是离散化问题的经典性质:整个值域跨度很大,但是非常稀疏,总共的范围是2 * 10^9,但是我们只用到了3 * 10^5,解决方法:把我们用到的下标映射到从1开始的自然数。比如我们现在想让x位置上的数加c,把x映射到k,a[k] += c,在求 l ~ r 之间的和时,先将l,r映射到kl,kr,再求 a[kl] ~ a[kr] 之间的和。
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
typedef pair<int,int> Pair;
const int N = 300010;
int n,m;
int a[N],s[N];
vector<int> alls; // alls 存的是所有要离散化的值
vector<Pair> add,query;
int find(int x) // find 函数:求 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;
} // 这个 while 就是求 x 在alls中的下标
return r + 1;
// 把所有的数映射到从1开始的数,因为下标是从0开始的,所以要加1
}
/*
unique 在 java 中没有,它可以怎样实现
对于一个排好序的序列,怎样保证这个序列没有重复的数
要使每个数都满足 ①或 ②
①它是这个序列的第一个数
②它与它前一个数不相等
*/
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[i]不是第一个数(i不是0)或者a[i] 与 a[i-1] 相等
a[j++] = a[i];
}
// a[0] ~ a[j-1] 是所有a中不重复的数 ,j就像一个新数组的指标一样
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);
}
// all 去重:①排序 ②将重复元素去掉
sort(alls.begin(),alls.end());
// alls.erase(unique(alls.begin(),alls.end()),alls.end());
alls.erase(unique(alls),alls.end());
for(auto i:add) // 给x位置处的值加上c
{
int x = find(i.first);
a[x] += i.second;
// item.first 是位置, item.second是要加上的值
}
// 预处理前缀和
for(int i = 1;i <= alls.size();i++)
s[i] = s[i-1] + a[i];
for(auto i:query)
{
int l = find(i.first),r = find(i.second);
cout << s[r] - s[l - 1] << endl;
}
return 0;
}
4 区间合并
题目:
给定 n 个区间 [li,ri],要求合并所有有交集的区间。
注意如果在端点处相交,也算有交集。
输出合并完成后的区间个数。
例如:[1,3]和 [2,6] 可以合并为一个区间 [1,6]。
输入格式
第一行包含整数 n。
接下来 n行,每行包含两个整数 l 和 r。
输出格式
共一行,包含一个整数,表示合并区间完成后的区间个数。
输入样例:
5
1 2
2 4
5 6
7 8
7 9
输出样例:
3
解决方法:
(1)按区间左端点进行排序;
(2)因为已经按左端点排序了,如果当前维护的区间和下一个区间没有交集,就可以将当前维护的区间放到答案中去了。
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
typedef pair<int,int> Pair;
const int N = 100010;
int n;
vector<Pair> segs;
void merge(vector<Pair> &segs)
{
vector<Pair> res;
sort(segs.begin(),segs.end()); // pair 排序优先按第一个数排
int st = -2e9,ed = -2e9; // st 和 en 是 当前维护的区间的左右端点
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; // 更新区间
}
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;
}