第一章 基础算法(三)
一、双指针算法
① 两个指针分别指向两个序列; :归并
②两个指针分别指向同一个序列 :快排
for (i = 0, j = 0; i < n; ++i)
{
while (j > i && check(i, j)) j++;
// 具体逻辑
}
- 核心思想:可以将某些朴素(暴力)算法(O(n)^2),优化到O(n);
(1)双指针思想:解决将一组字符串的的单词提炼问题【初步】
#include <bits/stdc++.h>
using namespace std;
int main()
{
char str[1010];
gets(str);
int len = strlen(str);
for (int i = 0; i < len; ++i)
{
int j = i;
// 当j 指针,还没有走完数组且j指针指向的不是空格,就继续往下走
while (j < len && str[j] != ' ') ++j;
// 具体逻辑
for (int k = i; k < j; ++k)
{
printf ("%c", str[k]);
}
printf ("\n");
i = j; // 更新让i指针指向当前j指针
}
return 0;
}
-
其实就是掌握这种思想,不必特意当成模板
-
将原来枚举O(n^2)的状态,变成枚举O(n)的状态;
-
该算法是通过使用特定大小的子列表,在遍历完整列表的同时进行特定的操作,以达到降低了循环的嵌套深度;
-
往右增大窗口,寻找可行解,再把左指针缩写窗口,寻找最优解。。。;
-
重要是理解思想,双指针不是算法模板
(2)ACWING 799 最长连续不重复子序列
- 思考一下:滑动窗口
- 代码如下:
//理解一下滑动窗口,两个指针交替放大缩小窗口
#include <bits/stdc++.h>
#include <algorithm>
using namespace std;
const int N = 1e6 + 10;
int a[N]; // 序列
int s[N]; // 当前区间内,每一个数出现的次数
int n;
int main ()
{
scanf ("%d", &n);
//输入序列
for (int i = 0; i < n; i++) scanf ("%d", &a[i]);
int res = 0; // 最终结果
for (int i = 0, j = 0; i < n; i++)
{
s[a[i]]++; // 走过的数是什么数,对应位就加1
// i不断后移,当i移到的数,已经重复了,就把这个窗口缩小 (j 缩小到当前i) ,把前面走过的数字清除掉 ,直到这个区间内无刚才的元素计次
while (s[a[i]] > 1)
{
s[a[j]]--;
j++;
}
// 更新结果值
res = max(res, i - j + 1);
}
printf ("%d\n", res);
return 0;
}
二、位运算
(1)n的二进制表示中第k位是几
-
k是从图中0位开始算起;
-
解法:
- 1. 先把第k位移到最后一位,用右移运算符
- 2. 看个位是几用 &1
#include <bits/stdc++.h>
using namespace std;
int main ()
{
int n = 11;
// 从最高位开始输出
for (int k = 3; k >= 0; --k)
{
cout << ((n >> k) & 1) << ' ';
}
return 0;
}
// 1011
(2)lowbit(x),返回x的最后一位1
x & -x = x & (~x + 1);
-x = ~x + 1( x取反 + 1);
- 应用:可以统计x中1的个数【操作:每一次都把x最后一位1去掉,当x = 0时,x里面就没有1了,减了多少次,说明x里面有多少个1】
(3)ACWING 801 二进制中1的个数
#include <bits/stdc++.h>
using namespace std;
int lowbit(int x)
{
return x & (-x);
}
int main()
{
int n;
cin >> n;
int x;
for (int i = 0; i < n; ++i)
{
cin >> x;
int res = 0; // 记录每一轮 数,1的个数
// 当 x 不为0的时候(就是还有1)
while (x)
{
// 用lowbit 求出最后一位1, 然后x减去1,把最后一位1 消掉
x -= lowbit(x);
res++;
}
cout << res << ' ';
}
return 0;
}
(4)计算机中的原码,反码,补码
#include <bits/stdc++.h>
using namespace std;
int main ()
{
int n = 10; // 8 4 2 1 1010 二进制
unsigned int x = -n;
for (int i = 31; i >= 0; --i) cout <<((x >> i) & 1);
cout << endl;
}
三、离散化(特指,整数且有效的离散化)
- 总不能开一个很大的数组,要用映射;
- 1映射到0,3映射到1。。。
- 因为排序完,a是有序的,是保序的离散化,小的离散完在前,大的离散完在后;
- 离散化的过程就是把a的值,映射到下标
- 算出x在a中的下标,因为a有序,就可以用二分来查找;
- unique函数属于STL中比较常用函数,它的功能是元素去重。即”删除”序列中所有相邻的重复元素(只保留一个)。此处的删除,并不是真的删除,而是指重复元素的位置被不重复的元素给占领了(详细情况,下面会讲)。由于它”删除”的是相邻的重复元素,所以在使用unique函数之前,一般都会将目标序列进行排序。
- 它指向的是去重后容器中不重复序列的最后一个元素的下一个元素;
- 再结合erase就可以把这一部分重复的元素去除掉
- +1 就从1 开始映射,不加就是从0开始;
- 离散化,就是把一些很离散的点给重新分配。
- 举个例子,如果一个坐标轴很长(>1e10),给你1e4个坐标,询问某一个点,坐标比它小的点有多少。
- 很容易就知道,对于1e4个点,我们不必把他们在坐标轴上的位置都表示出来,因为我们比较有多少比它小的话,只需要知道他们之间的相对大小就可以,而不是绝对大小,这,就需要离散化。
- 离散化,就是当我们只关心数据的大小关系时,用排名代替原数据进行处理的一种预处理方法。离散化本质上是一种哈希,它在保持原序列大小关系的前提下把其映射成正整数。当原数据很大或含有负数、小数时,难以表示为数组下标,一些算法和数据结构(如BIT)无法运作,这时我们就可以考虑将其离散化。
(1)ACWING 802 区间和:过程(记录的有点乱)不太好理解的话,可以先看代码理解
-
所以,我们想给x位置上的数 + c的话,就先找到x离散化的值是多少,在他离散化后的值的位置上 + c即可
-
-
求某个区间所有数的和,先把l, r离散化到对应下标的位置;
-
-
求新区间所有数的和;
-
// 理解相对位置
#include <bits/stdc++.h>
#include <vector>
#include <algorithm>
using namespace std;
typedef pair<int, int> PII; // 利用pair存储一对操作
const int N = 300010; // 插入运算10万,查询操作两个左边20万 30万。。。
int n, m;
int a[N], s[N]; // a:存储的数
vector<int> alls;
vector<PII> add, query; // 插入与询问
int find(int x)
{
int l = 0;
int 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;
// 插入操作,下标x + c;
cin >> x >> c;
add.push_back({x, c});
// 然后要把下标x离散化,把x加入待离散化的数据中
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; // 在离散化后的坐标的位置上 + c
}
// 预处理前缀和 映射从1开始
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;
}
- 存储数轴坐标x l r的alls数组通过二分映射到a的下标
vector<int>::iterator unique(vector<int> &a)
{
// 把不重复的数,放到前面
int j = 0;
for (int i = 0; i < a.size(); ++i)
{
// i 是第一个
if (!i || a[i] != a[i - 1])
{
a[j++] = a[i];
}
}
// a[0] ~ a[j - 1] 所有a中不重复的数
return a.begin() + j; // 返回尾端点后一个元素
}
四、区间合并:快速地把有交集的区间进行合并
(1)ACWING 803 区间合并
- 返回合并后区间的个数
//pair排序会优先以左端点排序,不行的再以右端点排序
#include <bits/stdc++.h>
#include <vector>
#include <algorithm>
using namespace std;
const int N = 1e6 + 10;
typedef pair<int, int> PII;
int n; // n个区间
//把n个区间存到vector里面去,两个端点(结合pair) first左, second右
vector<PII> segs;
void merge (vector<PII> &segs)
{
vector<PII> res; // 合并完的结果区间
sort (segs.begin(), segs.end()) ;
// for (auto seg : segs)
// {
// cout << seg.first << ' ' << seg.second << endl;
// }
// 设定边界 (无穷)
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;
// cout << st << ' ' << ed << endl;
}
else
{
// 否则就说明,当前维护的区间与当前枚举区间是有交集的
// 取二者右端点的最大值, 左端点是不能超过当前维护区间的左端点,所以左端点保持
// 理解一下示意图的前两种情况
ed = max (ed, seg.second);
// cout << ed << endl;
}
}
// 把最后一个区间,加到答案中
// 为了防止传入的segs是空的
if (st != -2e9) res.push_back({st, ed});
// 将区间更新成res
segs = res;
// cout << res.size() << endl;
}
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;
}