算法基础_基础算法【位运算 + 离散化 + 区间合并】
- ---------------位运算---------------
- 801.二进制中1的个数
- 题目介绍
- 方法一:
- ---------------离散化---------------
- 802.区间和
- 题目介绍
- 方法一:
- ---------------区间合并---------------
- 803.区间合并
- 题目介绍
- 方法一:
往期《算法基础》回顾:
算法基础_基础算法【快速排序 + 归并排序 + 二分查找】
算法基础_基础算法【高精度 + 前缀和 + 差分 + 双指针】
---------------位运算---------------
801.二进制中1的个数
题目介绍

方法一:
cpp
#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 -= lowbit(x);
res++; // 每次减去x的最后一位1
}
cout << res << " ";
}
return 0;
}
//位运算:
//求n的第k位数字:n >> k & 1
//返回n的最后一位1:lowbit(n) = n & -n
代码片段解释
片段一:
cpp
int lowbit(int x)
{
return x & -x;
}
lowbit(x):用于返回x的二进制表示中,最低位的1及其后面的所有0所组成的二进制数对应的十进制值。
- 如果
x = 6(二进制110),lowbit(x)返回2(二进制10)- 如果
x = 12(二进制1100),lowbit(x)返回4(二进制100)
x & -x的原理:
-x是x的补码表示 。在计算机中,负数的表示方式是取反加 1 (即:补码)
- 例如:
x = 6(二进制0000 0110),-x的计算过程:
- 取反:
1111 1001- 加 1:
1111 1010(即:-6的二进制表示)
x & -x的结果是x的二进制表示中最低位的1及其后面的所有0
- 例如:
x = 6(二进制0000 0110),-x = 1111 1010x & -x的结果是0000 0010(二进制10,即:2)
疑问:为什么x & -x能得到最低位的1?
x的二进制表示中,最低位的1右边的所有位都是0
-x是x的补码,它的二进制表示中,最低位的1及其右边的位与x相同 ,而左边的位都取反当
x和-x进行按位与运算时:
- 最低位的
1及其右边的0会被保留- 左边的位会因为取反操作而全部变为
0因此 :
x & -x的结果就是x的最低位的1及其后面的所有0
示例1:
x = 6(二进制0000 0110)
-x = -6(二进制1111 1010)
x & -x:
cpp0000 0110 (x) & 1111 1010 (-x) ------------ 0000 0010 (结果)结果是
2(二进制10)示例2:
x = 12(二进制0000 1100)
-x = -12(二进制1111 0100)
x & -x:
cpp0000 1100 (x) & 1111 0100 (-x) ------------ 0000 0100 (结果)结果是
4(二进制100)
片段二:
cpp
int res = 0;
while (x)
{
x -= lowbit(x);
res++; // 每次减去x的最后一位1
}
代码的作用:
- 这行代码的作用是:统计整数
x的二进制表示中1的个数- 通过不断去掉
x的二进制表示中最低位的1,并计数,直到x变为0
代码的组成:
while (x):
- 这是一个循环条件,表示只要
x不为0,循环就会继续执行- 当
x变为0时,循环结束
x -= lowbit(x):
lowbit(x)返回x的二进制表示中最低位的1所对应的值x -= lowbit(x)表示将x减去其最低位的1,相当于去掉了x的二进制表示中最低位的1
res++:
- 每次去掉一个
1,计数器res加 1- 最终
res的值就是x的二进制表示中1的个数
示例 :假设
x = 13,其二进制表示为1101
第一次循环:
x = 13(二进制1101)lowbit(13) = 1(二进制0001)x -= lowbit(x):x = 13 - 1 = 12(二进制1100)res++:res = 1第二次循环:
x = 12(二进制1100)lowbit(12) = 4(二进制0100)x -= lowbit(x):x = 12 - 4 = 8(二进制1000)res++:res = 2第三次循环:
x = 8(二进制1000)lowbit(8) = 8(二进制1000)x -= lowbit(x):x = 8 - 8 = 0(二进制0000)res++:res = 3循环结束:
x = 0,循环结束- 最终
res = 3,表示13的二进制表示中有3个1
---------------离散化---------------
802.区间和
题目介绍

方法一:
cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
typedef pair<int, int> PII;
const int N = 300010;
int n, m; //操作的次数n,访问的次数m
int a[N], s[N]; //a[N]用于存储离散化后的数轴上的值,s[N]是前缀和数组
vector<int> alls; // alls用于存储所有需要离散化的位置
vector<PII> add, query; //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()
{
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 <= a.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;
}
代码执行过程
输入样例:
cpp3 3 1 2 3 6 7 5 1 3 4 6 7 8解释:
- 操作部分 :
- 在位置
1上加2- 在位置
3上加6- 在位置
7上加5- 查询部分 :
- 查询区间
[1, 3]的和- 查询区间
[4, 6]的和- 查询区间
[7, 8]的和
1. 初始化
定义变量和数据结构:
n = 3(操作次数)m = 3(查询次数)add:存储操作。query:存储查询。alls:存储所有需要离散化的位置。2. 读取操作
读取
3个操作:
x = 1,c = 2:
- 将
{1, 2}加入add- 将
1加入allsx = 3,c = 6:
- 将
{3, 6}加入add- 将
3加入allsx = 7,c = 5:
- 将
{7, 5}加入add- 将
7加入alls此时:
add = [{1, 2}, {3, 6}, {7, 5}]alls = [1, 3, 7]3. 读取查询
读取
3个查询:
l = 1,r = 3:
- 将
{1, 3}加入query- 将
1和3加入allsl = 4,r = 6:
- 将
{4, 6}加入query- 将
4和6加入allsl = 7,r = 8:
- 将
{7, 8}加入query- 将
7和8加入alls此时:
query = [{1, 3}, {4, 6}, {7, 8}]alls = [1, 3, 7, 1, 3, 4, 6, 7, 8]4. 离散化
对
alls进行排序和去重:
- 排序后:
[1, 1, 3, 3, 4, 6, 7, 7, 8]- 去重后:
[1, 3, 4, 6, 7, 8]离散化后的映射关系:
1 -> 13 -> 24 -> 36 -> 47 -> 58 -> 65. 处理操作
遍历
add,将操作的值加到离散化后的位置上:
{1, 2}:
- 离散化后位置:
find(1) = 1a[1] += 2{3, 6}:
- 离散化后位置:
find(3) = 2a[2] += 6{7, 5}:
- 离散化后位置:
find(7) = 5a[5] += 5此时:
a = [0, 2, 6, 0, 0, 5, 0]6. 构造前缀和数组
计算前缀和数组
s:
s[0] = 0s[1] = s[0] + a[1] = 0 + 2 = 2s[2] = s[1] + a[2] = 2 + 6 = 8s[3] = s[2] + a[3] = 8 + 0 = 8s[4] = s[3] + a[4] = 8 + 0 = 8s[5] = s[4] + a[5] = 8 + 5 = 13s[6] = s[5] + a[6] = 13 + 0 = 13此时:
s = [0, 2, 8, 8, 8, 13, 13]7. 处理查询
- 遍历
query,计算每个查询的结果:
{1, 3}:
- 离散化后:
l = find(1) = 1,r = find(3) = 2- 结果:
s[2] - s[0] = 8 - 0 = 8{4, 6}:
- 离散化后:
l = find(4) = 3,r = find(6) = 4- 结果:
s[4] - s[2] = 8 - 8 = 0{7, 8}:
- 离散化后:
l = find(7) = 5,r = find(8) = 6- 结果:
s[6] - s[4] = 13 - 8 = 58. 输出结果
- 查询结果:
8(区间[1, 3]的和)0(区间[4, 6]的和)5(区间[7, 8]的和)
代码片段解释
片段一:
cpp
const int N = 300010;
在代码中,
N = 300010的定义是为了为离散化后的数组和前缀和数组分配足够的空间疑问:为什么将 N的值设置为300010呢?
- 题目中涉及的操作和查询次数最多为 (10^5) 次
- 每次操作会涉及一个位置 (x),每次查询会涉及两个位置 (l) 和 (r)
- 因此,所有需要离散化的位置的总数最多为:
- 操作次数:(n)(最多 (10^5))
- 查询次数:(m)(最多 (10^5)),每次查询涉及两个位置
- 总位置数:(n + 2m \leq 10^5 + 2 \times 10^5 = 3 \times 10^5)
片段二:
cpp
typedef pair<int, int> PII;
typedef pair<int, int> PII;这行代码的作用是为pair<int, int>定义一个别名PII,使得代码中可以更方便地使用PII来代替pair<int, int>
pair<int, int>的作用
pair是 C++ 标准库中的一个模板类,用于存储两个值(可以是相同类型或不同类型)pair<int, int>表示一个存储两个int类型值的对象
pair<int, int> p = {1, 2};定义了一个pair对象p- 其中
p.first = 1,p.second = 2
typedef的作用
typedef是 C++ 中的关键字,用于为已有的数据类型定义一个新的别名。
typedef int myInt;定义了一个别名myInt,myInt等价于int- 之后可以使用
myInt x = 10;来定义一个int类型的变量
typedef pair<int, int> PII;的含义
- 这行代码为
pair<int, int>定义了一个别名PII- 之后可以使用
PII来代替pair<int, int>,使代码更简洁
解题思路分析
离散化的问题的思路步骤:
第一步:将所有涉及这个无限长数组位置的变量存放在存储所有需要离散化的位置的数组中
第二步:对这个存储所有需要离散化的位置的数组中的元素按升序排序
第三步:对这个存储所有需要离散化的位置的数组中的元素去重操作
第四步:书写find函数(核心)(意义:每次调用find函数寻找原无限数组中的下标,它都会返回一个该位置在离散化后的数组中的下标)
第五步:处理加数操作
第六步:构造前缀和数组
第七步:处理访问操作
---------------区间合并---------------
803.区间合并
题目介绍

方法一:
cpp
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
int n;
vector<pair<int, int>> segs;
void merge(vector<pair<int, int>>& segs)
{
vector<pair<int, int>> res;
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;
}
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;
}
代码片段解释
片段一:
cpp
int st = -2e9, ed = -2e9;
if (st != -2e9) res.push_back({ st, ed });
在这段代码中,
int st = -2e9, ed = -2e9;和if (st != -2e9)的作用是初始化区间合并的起始和结束位置,并确保在合并区间时正确处理第一个区间
int st = -2e9, ed = -2e9;的作用:
初始化区间合并的起始和结束位置:
st和ed分别表示当前合并区间的起始和结束位置。- 初始值设置为
-2e9(即 (-2 \times 10^9)),这是一个极小的值,确保任何实际的区间都会大于这个初始值。为什么选择
-2e9:
- 题目中给定的区间范围是 ([-10^9, 10^9]),因此
-2e9是一个比最小可能值更小的值,确保不会与任何实际区间冲突。- 这样做的目的是在合并区间时,能够正确识别和处理第一个区间。
if (st != -2e9)的作用:
确保第一个区间被正确处理:
- 在合并区间时,
st和ed初始值为-2e9,表示还没有开始处理任何区间。- 当遇到第一个区间时,
st和ed会被更新为该区间的起始和结束位置。- 在后续的循环中,如果
st和ed仍然是-2e9,说明还没有处理任何区间,此时不需要将{st, ed}加入结果中。防止将无效区间加入结果:
- 如果
st和ed仍然是-2e9,说明还没有处理任何区间,此时不需要将{st, ed}加入结果中。- 只有当
st和ed被更新为实际的区间值时,才将{st, ed}加入结果中。
解题思路分析
代码的逻辑流程:
1. 初始化
st = -2e9,ed = -2e9:表示当前没有处理任何区间。2. 遍历区间
- 遍历所有区间
segs,逐个处理:
- 如果当前区间与合并区间无交集 :
- 将当前的合并区间
{st, ed}加入结果res(如果st和ed不是初始值)- 更新
st和ed为当前区间的起始和结束位置。- 如果当前区间与合并区间有交集 :
- 更新合并区间的结束位置
ed为当前区间和合并区间的最大值。3. 处理最后一个区间
- 在循环结束后,如果
st和ed不是初始值,将最后一个合并区间{st, ed}加入结果res
示例说明:
输入:
cpp3 1 3 2 6 5 7
初始化:
st = -2e9,ed = -2e9
res = []处理第一个区间
[1, 3]:
ed < seg.first(-2e9 < 1),说明当前没有处理任何区间。将
{st, ed}(无效区间)不加入结果。更新
st = 1,ed = 3处理第二个区间
[2, 6]:
ed >= seg.first(3 >= 2),说明当前区间与合并区间有交集。更新
ed = max(ed, seg.second) = max(3, 6) = 6处理第三个区间
[5, 7]:
ed >= seg.first(6 >= 5),说明当前区间与合并区间有交集。更新
ed = max(ed, seg.second) = max(6, 7) = 7处理结束:
- 将最后一个合并区间
{1, 7}加入结果res输出:
cpp1
区间合并步骤总结:
第一步:定义一个与要处理的容器一样的容器用于临时存放合并后的区间
第二步:将传入的容器中的元素按升序排列
第三步:定义两个变量作为初始区间的两端点,其值为区间不可能达到最小值
第四步:使用范围for循环遍历排好序的容器中的每一个元素
第五步:使用if分支语句判断新区间是否与旧区间有交集
- 第六步:无交集同时还是真实的区间的话,就将旧区间添加到临时容器
- 第七步:无交集更新维护的区间为新区间
第八步:有交集的话更新维护区间的右端点
第九步:使用if语句判断如果当前维护的区间是真实的区间的话,就将这个区间添加到临时容器
第十步:将临时容器中的元素添加到原容器中