基础算法(概念、模板、练习)
一、枚举算法
枚举算法是一种通过穷举所有可能情况来解决问题的基础算法思想。它的基本思想是将问题的解空间 中的每个可能的解都枚举出来,并进行验证 和比较 ,找到满足问题条件的最优解或者所有解。
适用场景:
- 问题规模较小,解空间可被完整穷举。
- 不适合问题规模过大的场景,否则时间复杂度极高、效率低下。
注意
解空间:
解空间可以是一个范围内的所有数字(或二元组、字符串等数据),或者满足某个条件的所有数字。也可以是解空间树,一般可分为子集树和排列树,针对解空间树,需要使用回溯法进行枚举。通常使用循环去暴力枚举解空间。
循环枚举解空间:
根据问题需求确定枚举变量个数(如单变量用单层循环,二元组用双重循环或者用到STL)。
通过问题约束条件确定每个变量可能的取值范围,在循环体内进行条件验证(if...)、数值计算或结果输出操作
相关练习
二、模拟
模拟算法通过模拟实际情况来解决问题,一般不涉及太难的算法,由较多的简单但是不好处理的部分组成的,考察细心程度和整体的逻辑思维。在解决模拟题时通常会编写多个辅助函数。例如 int 和 string 的相互转换、回文串的判断、日期的转换、各种特殊条件的判断等等。
相关练习
注意
- 扫雷表格上下左右边界外没有网格,在遍历周围九宫格有无雷时,需要进行边界范围约束。
<1>
_i和_j的含义
i和j:当前要计算的位置(比如第 2 行第 2 列,记为(2,2));
_i和_j:遍历「当前位置周围九宫格」的位置。<2> 理解循环中 i 从
max(1, i-1)到min(n, i+1)遍历这两个函数是为了避免越界(比如第一行的位置,没有 "上一行";最后一行的位置,没有 "下一行"),用具体例子说明:
场景 i-1max(1, i-1)i+1min(n, i+1)最终遍历的行范围 当前在第 1 行(i=1) 0 1 2 2(假设 n≥2) 1~2 行 当前在中间行(i=2) 1 1 3 3(假设 n≥3) 1~3 行 当前在最后一行(i=n) n-1 n-1 n+1 n n-1~n 行 列方向的
max(1, j-1)和min(m, j+1)逻辑完全一样,比如:
- 第 1 列(j=1):列范围是 1~2;
- 中间列(j=2):列范围是 1~3;
- 最后一列(j=m):列范围是 m-1~m。
<3> 举例理解:
假设输入是 3 行 3 列(n=3, m=3),当前要计算的位置是
(2,2)(第二行第二列):
- 计算
_i的范围:max(1,2-1)=1→min(3,2+1)=3→_i遍历 1、2、3;- 计算
_j的范围:max(1,2-1)=1→min(3,2+1)=3→_j遍历 1、2、3;- 内层循环会遍历这 9 个位置:
(1,1)、(1,2)、(1,3)、(2,1)、(2,2)、(2,3)、(3,1)、(3,2)、(3,3);- 每遍历一个位置,若
a[_i][_j]=1(地雷),ans[2][2]就加 1。再举一个边界例子:当前位置是
(1,1)(第一行第一列):
_i范围:max(1,1-1)=1→min(3,1+1)=2→ 遍历 1、2;_j范围:max(1,1-1)=1→min(3,1+1)=2→ 遍历 1、2;- 内层循环只遍历 4 个位置:
(1,1)、(1,2)、(2,1)、(2,2)(避免了访问(0,0)这种不存在的位置)。
农田灌溉问题解题思路
- 初始化 :用数组
a记录初始出水点(标记为 1),数组b作为临时数组存储下一分钟灌溉状态;- 模拟扩散 :循环 k 次(对应 k 分钟),每轮遍历
a数组,将所有已灌溉格子的上下左右及自身在b中标记为 1,再将b的状态同步到a(保证每轮扩散基于上一轮状态);- 统计结果 :遍历
a数组,统计值为 1 的格子数量,即为 k 分钟后灌溉总面积。
3.回文日期 - 蓝桥云课(复杂)
三、进制转换
<1> 进制
进制是数值的表示规则,核心是「逢 N 进 1」(N为进制数,也就是基数)。最常用的是二进制、八进制、十进制、十六进制,进制转换则是不同进制间数值表示形式的转换。
| 进制名称 | 基数 | 进位规则 | 数字范围 | 位权规则(第 n 位) | 编程标识 | 示例 → 十进制 |
|---|---|---|---|---|---|---|
| 十进制 | 10 | 逢 10 进 1 | 0-9 | 10^(n-1) | 无 | 123 → 123 |
| 二进制 | 2 | 逢 2 进 1 | 0、1 | 2^(n-1) | 0b | 0b101 → 5 |
| 八进制 | 8 | 逢 8 进 1 | 0-7 | 8^(n-1) | 0 | 012 → 10 |
| 十六进制 | 16 | 逢 16 进 1 | 0-9、A-F(A=10,F=15) | 16^(n-1) | 0x | 0x1A → 26 |
<2> 进制转换
所有进制转换都以十进制为中间桥梁:
1.任意进制 → 十进制:按权展开求和
从右往左数,第 n 位的权重 = 基数ⁿ⁻¹,无论哪种进制转十进制,本质都是**"每一位数字 × 这一位的权重(基数的幂次),再求和"**。
| 进制类型 | 基数 | 位权计算公式(第 n 位) | 典型数值示例(十进制 153) |
|---|---|---|---|
| 十进制 | 10 | 10ⁿ⁻¹ | 153 = 1 ×10²+5 ×10¹+3×10⁰ |
| 二进制 | 2 | 2ⁿ⁻¹ | 10011001 = 1 ×2⁷+0 ×2⁶+⋯+1×2⁰ |
| 八进制 | 8 | 8ⁿ⁻¹ | 231 = 2 ×8²+3 ×8¹+1×8⁰ |
| 十六进制 | 16 | 16ⁿ⁻¹ | 99 = 9 ×16¹+9×16⁰ |
举例
二进制 10011001 → 十进制:1×2⁷+0×2⁶+⋯+1×2⁰ = 128+16+8+1 = 153
八进制 231 → 十进制:2×8²+3×8¹+1×8⁰ = 128+24+1 = 153
十六进制 99 → 十进制:9×16¹+9×16⁰ = 144+9 = 153
2.十进制 → 任意进制:除基数,取余数,逆序排列
-
用十进制数除以基数 k,记录余数(因为按照权重求和公式,取模得到的是最后一位数字)
-
商继续除以 k,重复记录余数
-
商为 0 时停止,将余数逆序排列得到结果
153 ÷ 2 = 76 余 1
76 ÷ 2 = 38 余 0
38 ÷ 2 = 19 余 0
19 ÷ 2 = 9 余 1
9 ÷ 2 = 4 余 1
4 ÷ 2 = 2 余 0
2 ÷ 2 = 1 余 0
1 ÷ 2 = 0 余 1
余数逆序:10011001 → 153=10011001
3.非十进制之间互转:先转十进制,再转目标进制(或用二进制分组法快速转换↓)
| 转换方向 | 分组规则 | 示例 |
|---|---|---|
| 二进制 → 八进制 | 从右往左每 3 位 一组,不足补前导 0 | 10011001 → 010 011 001 → 231 |
| 二进制 → 十六进制 | 从右往左每 4 位 一组,不足补前导 0 | 10011001 → 1001 1001 → 99 |
| 八进制 → 二进制 | 每 1 位八进制 → 3 位二进制 | 231 → 2(010) 3(011) 1(001) → 10011001 |
| 十六进制 → 二进制 | 每 1 位十六进制 → 4 位二进制 | 99 → 9(1001) 9(1001) → 100110012 |
模板
1.任意进制转十进制:
#include<bits/stdc++.h>
using namespace std;
using ll=long long;
int main()
{
//任意进制转换为十进制
ll x=0;
int k;cin>>k;//k表示k进制
int n;cin>>n;//数组有效长度
int a[]={0,1,3,10,5,7};//a[]表示一个k进制的整数
//得到a[]的十进制数
for(int i=1;i<=n;i++)
{
x=x*k+a[i];
}
cout<<x<<'\n';
return 0;
}
循环次数 i |
执行语句 | 循环后 x 的值 |
|---|---|---|
| 初始状态 | - | x=0 |
i=1 |
x=0×k + a[1] |
x = 1 |
i=2 |
x=1×k + a[2] |
x = 1×k + 3 |
i=3 |
x=(1×k+3)×k +10 |
x = 1×k² + 3×k +10 |
i=4 |
x=(1×k²+3k+10)×k +5 |
x = 1×k³ + 3×k² +10×k +5 |
i=5 |
x=(1×k³+3k²+10k+5)×k +7 |
x = 1×k⁴ + 3×k³ +10×k² +5×k +7 |
2.十进制转任意进制:
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int main() {
ll x;
int k;
cin >> x >> k;
ll a[100]; //定义数组 a 用于存储每一位的余数
int cnt = 0; // 记录有效数字的个数(下标从1开始)
// 除基取余
while (x) {
// 先自增 cnt,再存余数,确保数组从下标 1 开始存储
a[++cnt] = x % k;
x = x / k;
}
// 反转数组包含下标 1 到 cnt 的所有元素
reverse(a + 1, a + 1 + cnt);
for (int i = 1; i <= cnt; i++) {
// 处理大于9的数字,如 10->A, 11->B...
if (a[i] >= 10) {
cout << (char)('A' + a[i] - 10);
} else {
cout << a[i];
}
}
cout << endl;
return 0;
}
相关练习
前缀和
前缀和是编程中预处理数组、快速求解区间和的算法,思想是通过提前计算 "前 n 个元素的累加和",将多次区间和查询的时间复杂度从 O(n) 降至 O(1)。
- 前缀和数组 prefix[] 是基于用户输入数组 a[](下标从 1 开始)生成的。其中每个元素 prefix[i] 表示从 a[1] 到 a[i] 的累加和,即: prefix[i] = a[1] + a[2] + ... + a[i]
- 前缀和数组具有递推性质,可通过以下方式快速计算: prefix[i] = prefix[i-1] + a[i]
- 利用前缀和数组,我们可以在 O(1) 计算任意区间 [l, r] 的和:sum(l, r) = prefix[r] - prefix[l-1]
注意
prefix 是一种预处理算法,只适用于 a 数组为静态数组的情况,即 a 数组中的元素在区间和查询过程中不会进行修改。如果需要实现 "先区间修改,再区间查询" 可以使用差分数组,如果需要 "一边修改,一边查询" 需要使用树状数组或线段树等数据结构。
模板
#include <iostream>
#include <vector>
using namespace std;
int main() {
// 1. 输入数组长度n和查询次数q
int n, q;
cin >> n >> q;
// 2. 定义原数组a,下标从1开始,a[0] = 0(占位/边界值)
vector<int> a(n + 1, 0);
for (int i = 1; i <= n; ++i) {
cin >> a[i];
}
// 3. 定义前缀和数组prefix,prefix[0] = 0(边界值,方便计算)
vector<int> prefix(n + 1, 0);
// 核心循环:从前往后递推计算前缀和
for (int i = 1; i <= n; ++i) {
prefix[i] = prefix[i - 1] + a[i];
}
// 4. 处理多次区间和查询
while (q--) {
int L, R;
cin >> L >> R;
// 区间和公式:sum(L,R) = prefix[R] - prefix[L-1]
cout << prefix[R] - prefix[L - 1] << endl;
}
return 0;
}
输入:
5 2 // 数组长度5,2次查询
1 2 3 4 5 // 原数组a[1]~a[5]
1 3 // 第一次查询区间[1,3]
2 5 // 第二次查询区间[2,5]
输出:
6 // 1+2+3=6
14 // 2+3+4+5=14
相关练习
差分
差分算法是针对静态数组批量区间修改的算法,核心思想是通过 "记录数组相邻元素的差值",将原本需要 O(n) 时间的区间修改操作优化为 O(1),最终通过一次前缀和还原得到修改后的数组,是与前缀和互补的基础算法。
- 对于原数组
a,其差分数组diff满足:
diff[i] = a[i] - a[i-1](其中规定 a[0] = 0,避免边界特判)
- 前缀和还原:对差分数组做前缀和运算可还原原数组。
即:a[i] = diff[1] + diff[2] + ... + diff[i] = Σ(diff[1..i])
- 区间修改原理 :在差分数组上执行 diff[l] += x 且 diff[r+1] -= x
等价于在原数组的区间 [l, r] 内所有元素加 x(若 r+1 超出数组长度则无需执行 diff[r+1] -= x)
注意
差分数组仅支持批量修改后批量查询,实时查询需使用树状数组或线段树等数据结构。
模板
#include <iostream>
#include <vector>
using namespace std;
int main() {
// 1. 输入数组长度n
int n;
cin >> n;
// 2. 定义原数组a,下标从1开始,a[0] = 0(占位/边界值)
vector<int> a(n + 1, 0);
for (int i = 1; i <= n; ++i) {
cin >> a[i];
}
// 3. 定义差分数组diff,diff[0] = 0
vector<int> diff(n + 2, 0); // 多开1位,避免r+1越界
// 核心循环:构建差分数组,时间复杂度O(n)
for (int i = 1; i <= n; ++i) {
diff[i] = a[i] - a[i - 1];
}
// 4. 输入区间修改操作:将区间[l, r]所有元素加x
int l, r, x;
cin >> l >> r >> x;
// 差分数组区间修改核心操作:O(1)时间完成区间加
diff[l] += x;
diff[r + 1] -= x;
// 5. 还原修改后的原数组(前缀和还原)
vector<int> new_a(n + 1, 0);
for (int i = 1; i <= n; ++i) {
new_a[i] = new_a[i - 1] + diff[i];
}
// 6. 输出修改后的数组
for (int i = 1; i <= n; ++i) {
cout << new_a[i] << " ";
}
cout << endl;
return 0;
}
输入:
5 // 数组长度5
1 2 3 4 5 // 原数组a[1]~a[5]
2 4 3 // 区间[2,4]加3
输出:
1 5 6 7 5 // 修改后的数组:[1, 2+3, 3+3, 4+3, 5] = [1,5,6,7,5]
相关练习
前缀和与差分
| 维度 | 前缀和(Prefix Sum) | 差分(Difference Array) |
|---|---|---|
| 核心目标 | 快速查询任意区间 [l,r] 的元素和 | 快速对任意区间 [l,r] 做统一修改(如加 x) |
| 数组定义 | 原数组 a [1~n](下标从 1 开始),前缀和数组 s [0~n]:s[0] = 0``s[i] = a[1]+a[2]+...+a[i] |
原数组 a [1~n](下标从 1 开始),差分数组 d [0~n+1]:d[0] = 0``d[i] = a[i] - a[i-1] |
| 核心公式 | 1. 构建:s[i] = s[i-1] + a[i](递推累加)2. 查询:sum(l,r) = s[r] - s[l-1] |
1. 构建:d[i] = a[i] - a[i-1](求差值)2. 修改:d[l] += x + d[r+1] -= x |
| 逆运算 | - | 还原原数组:a[i] = a[i-1] + d[i](前缀和还原) |
| 时间复杂度 | 构建 O (n),查询 O (1) | 构建 O (n),修改 O (1),还原 O (n) |
贪心
贪心算法的核心逻辑是每一步都做出当前看来最优的选择(局部最优),试图通过一系列局部最优决策得到全局最优解。它不像动态规划那样考虑所有可能的情况,而是 "走一步看一步",追求当下的最优,因此实现简单、效率高,但并非所有问题都能通过贪心得到正确的全局最优解。
"贪心选择性质" + "最优子结构" 是贪心算法能生效的两个关键前提:
- 贪心选择性质:每一步的局部最优选择,无需回溯,直接导向全局最优;
- 最优子结构:问题的全局最优解可以由若干子问题的最优解组合而成。
贪心算法的解题步骤
- 确定问题的最优子结构 (贪心往往和**++排序、优先队列++**等一起出现)。
- 构建贪心选择的策略,可能通过 "分类讨论"、"最小代价"、"最大价值" 等方式来思考贪心策略。简单验证贪心的正确性,采用句式一般是:这样做一定不会使得结果变差、不存在比当前方案更好的方案等等。
- 通过贪心选择逐步求解问题,直到得到最终解。
相关练习
双指针
双指针是一种常用的数组 / 字符串优化算法思想,通过使用两个指针在同一序列上以不同方式移动,快速完成查找、匹配、排序、移动等操作。把原本需要多重循环的 O(n2) 复杂度问题降为 O(n),实现高效求解。
双指针并非 C++ 中真正的指针类型,而是用两个整型变量表示数组 / 字符串的下标,通过控制两个下标的移动逻辑来实现遍历优化,后续算法描述中统一用 "指针" 代指下标变量。
双指针往往也和单调性、排序联系在一起,在数组的区间问题上,暴力法的时间复杂度往往是 O (n^2) 的,但双指针利用 "单调性" 可以优化到 O (n)。
常见双指针模型
1. 对撞指针(左右指针)
- 两个指针分别从数组最左端和最右端开始。
- 一个向右走,一个向左走,向中间对撞,直到相遇。
- 适合有序数组的查找、求和、反转等问题。
对撞指针求解步骤
- 使用两个指针 left,right。left 指向序列第一个元素,即:left = 1,right 指向序列最后一个元素,即:right = n。
- 在循环体中将左右指针相向移动,当满足一定条件时,将左指针右移,left ++。当满足另外一定条件时,将右指针左移,right --。
- 直到两指针相撞(即 left == right),或者满足其他要求的特殊条件时,跳出循环体。
2. 快慢指针(龟兔指针)
- 两个指针都从数组左端 出发,同向移动。
- 移动的步长一个快一个慢。
- 适合链表的找环、找中点、以及有序数组去重等问题。
快慢指针求解步骤
- 使用两个指针 l、r。l 一般指向序列第一个元素,即:l = 1,r 一般指向序列第零个元素,即:r = 0。即初始时区间 [l, r] = [1, 0] 表示为空区间。
- 在循环体中将左右指针向右移动。当满足一定条件时,将慢指针右移,即 l ++。当满足另外一定条件时(也可能不需要满足条件),将快指针右移,即 r ++,保持 [l, r] 为合法区间。
- 到指针移动到数组尾端(即 l == n 且 r == n),或者两指针相交,或者满足其他特殊条件时跳出循环体。
3.滑动窗口
用两个指针 i 和 j 表示一个区间 [i, j],j往右走扩大窗口,i 往右缩小窗口,一遍遍历解决子数组 / 子串问题,时间复杂度 O (n)。
相关练习
二分
二分算法是一种在有序数据上进行快速查找的高效算法,核心思想是每次将查找范围缩小一半,把时间复杂度从暴力的 O(n) 降到 (O (log n),是算法中最经典的 "减治" 思想之一。
二分算法必须作用在有序序列上(升序或降序),无序数组无法使用二分,这是二分成立的根本条件。
解题步骤:
1. 确认单调性
**2. 确定搜索区间
[l, r]:**设定初始左右边界,必须保证目标答案 / 分界点在区间内若最终答案以
r为基准,答案区间为[l+1, r];若最终答案以
l为基准,答案区间为[l, r-1];整数二分建议区间从
1开始,避免越界;二分答案需根据问题设定合理上下界(如最大可能值、最小可能值)。
3. 设计
check函数: 传入中间值mid,判断mid属于「满足条件的区间」还是「不满足条件的区间」,从而决定指针移动方向;**4. 计算中间点
mid:**mid = (l + r) / 2整数二分:向下取整;
浮点二分:直接取中点,控制精度循环。
5. 根据
check结果和单调性,移动指针,缩小区间若
check(mid)为真(满足条件):根据需求更新边界(如找左边界则r=mid,找右边界则l=mid);若
check(mid)为假(不满足条件):更新另一侧边界(如l=mid+1或r=mid-1);整数二分需注意避免死循环,统一循环条件(如
while(l < r)或while(l <= r))。6. 输出最终答案
循环结束后,根据题意返回
l或r:找第一个≥目标值 的左边界:返回
r;找最后一个≤目标值 的右边界:返回
l;二分答案:
l与r最终重合,返回任意一个即可。
类型
1. 整数二分
在已有的有序整数数组上进行二分查找,核心是找元素位置、分界点(如第一个≥x、最后一个≤x 的位置),是最基础的二分类型。模板如下:
// 找到升序数组a中的x第一次出现的位置
int l = 0, r = 1e9;
// 注意这里的判断条件,这样可以保证l,r最终一定收敛到分界点
while(l + 1 != r) // l,r相邻退出
{
int mid = (l + r) / 2;
// 如果a为升序,说明mid偏大了,需要减小mid,就只能将r变小,即r = mid
if(a[mid] >= x)r = mid;
else l = mid;
}
cout << r << '\n';
2. 浮点二分
在实数(浮点数)区间上进行二分,因为实数域本身是单调的,所以也满足单调性,和整数二分的主要区别在于使用的变量类型、退出的判断条件不同。模板如下:
// 计算单调函数f(x)的零点
double l = 0, r = 1e9, eps = 1e-6;
// 注意这里的判断条件,这样可以保证l,r最终一定收敛到分界点
while(r - l >= eps) // eps是一个极小量,设置为1e-6较合适
{
double mid = (l + r) / 2;
// f(x)单调递增,f(mid) >= 0,说明mid偏大了,需要减小mid,就只能将r变小,即r = mid
if(f(mid) >= 0)r = mid;
else l = mid;
}
// 最后返回l,r差别不大
cout << r << '\n';
3. 二分答案
不直接在数组上二分,而是将答案本身作为二分变量,通过验证「mid 是否满足题目条件」,利用答案的单调性缩小区间,最终找到最优答案,是最常用的二分类型。
bool check(int mid)
{
bool res = true;
//do something to check the authority of mid...
return res;
}
int main()
{
int l = 0, r = 1e9; //范围
while(l + 1 != r)
{
int mid = (l + r) / 2;
//具体写法需要根据题意修改
if(check(mid))
l = mid;
else
r = mid;
}
cout << l << '\n';//具体输出的内容需要根据题意判断
}
相关练习
位运算
位运算是直接对二进制的位进行操作的运算方式,位运算中每一位都相互独立 ,各自运算得出结果(左右移除外)。在计算机科学和编程中,位运算常用于优化算法、位掩码操作、位字段处理等领域。在竞赛中,经常考察异或的性质、状态压缩、与位运算有关的特殊数据结构、构造题等。仅适用于非负整数,不可用于字符、浮点数。
<1> 整数的二进制表示
在计算机中,整数是通过补码表示的,一般情况下,对负数进行位运算意义不大,大多数都是对正整数进行处理,而正数的原码 = 补码,所以我们直接考虑二进制数的原码,也就是直接地表示二进制数。例如整数 10,在计算机中存储如下(按照书写习惯,一般认为右边为低位,在左右移时尤为重要):
| val(二进制) | 0 | 0 | 0 | 0 | ... | 1 | 0 | 1 | 0 |
|---|---|---|---|---|---|---|---|---|---|
| idx(下标) | 31 | 30 | 29 | 28 | ... | 3 | 2 | 1 | 0 |
<2> 常见位运算
1. 按位与 (&)
按位与运算符(&)用于对两个操作数的对应位进行逻辑与操作。
- 运算规则:只有当两个位都为 1 时,结果位才为 1,否则为 0。
- 特点:两个数字做与运算,结果不会变大。
| x | y | x&y |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 0 |
| 1 | 0 | 0 |
| 1 | 1 | 1 |
| 数值 | 二进制 |
|---|---|
| 6 | 0 1 1 0 |
| 11 | 1 0 1 1 |
| 6 & 11 = 2 | 0 0 1 0(十进制 2) |
2. 按位或 (|)
按位或运算符(|)用于对两个操作数的对应位进行逻辑或操作。
- 运算规则:只要两个位中有一个为 1,结果位就为 1,否则为 0。
- 特点:两个数字做或运算,结果不会变小。
| x | y | x|y |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 1 |
| 数值 | 二进制 |
|---|---|
| 6 | 0 1 1 0 |
| 11 | 1 0 1 1 |
| 6 | 11 = 15 | 1 1 1 1(十进制 15) |
3. 按位异或 (^)
按位异或运算符(^)用于对两个操作数的对应位进行逻辑异或操作。
- 运算规则:当两个位不同时,结果位为 1,否则为 0。
- 特点:两个数字做异或运算,结果可能变大也可能变小,也可能不变,不会进位。
| x | y | x^y |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
| 数值 | 二进制 |
|---|---|
| 6 | 0 1 1 0 |
| 11 | 1 0 1 1 |
| 6 ^ 11 = 13 | 1 1 0 1(十进制 13) |
异或的性质:
- 交换律:
x ^ y = y ^ x- 结合律:
x ^ (y ^ z) = (x ^ y) ^ z- 自反性:
x ^ x = 0- 零元素:
x ^ 0 = x- 逆运算:
x ^ y = z,则有z ^ y = x(两边同时异或 y,抵消掉)
4. 按位取反(~)
用于对操作数的每一位进行取反操作,即将 0 变为 1,将 1 变为 0。
按位取反操作通常用于无符号整数(unsigned int/long long),这是为了避免符号位取反造成干扰。假设某个无符号整数只有 4 位,结果如下(长度不同结果也会不同,需要具体情况具体分析):
| x | ~x |
|---|---|
| 0 | 1 |
| 1 | 0 |
| 数值 | 二进制 |
|---|---|
| 6 | 0 1 1 0 |
| ~6 = 9 (4 位无符号整数) | 1 0 0 1(十进制 9) |
5. 按位左移(<<)
左移(<<)操作将一个数的二进制表示向左移动指定的位数。
- 移动后,低位补 0 ,如果数据类型为有符号整型,注意移动的时候不要移动到符号位上,或者干脆使用无符号整型(unsigned int)(1 会移动到符号位上)。
- 特点:左移操作相当于对原数进行乘以 2 的幂次方的操作。
示例:整数 5(二进制 00000101)左移 3 位,等价于 5 * (2^3) = 40
| 阶段 | 二进制 |
|---|---|
| 原数 5 | 0 0 0 0 0 1 0 1 |
| 5 << 3 | 0 0 1 0 1 0 0 0(十进制 40) |
6. 按位右移(>>)
右移(>>)操作将一个数的二进制表示向右移动指定的位数。
- 移动后,一般情况高位补 0 ,如果数据类型为有符号整型,注意移动的时候让符号位为 0,或者干脆使用无符号整型(unsigned int)。如果符号位上有 1 不会被移走。
- 特点:右移操作相当于对原数进行除以 2 的幂次方的操作(向下取整)。
示例:整数 13(二进制 00001101)右移 2 位,等价于 13 / 4 = 3(向下取整)
| 阶段 | 二进制 |
|---|---|
| 原数 13 | 0 0 0 0 1 1 0 1 |
| 13 >> 2 | 0 0 0 0 0 0 1 1(十进制 3) |
| 运算类型 | 运算符 | 核心规则 | 特点 | 等价操作 |
|---|---|---|---|---|
| 按位与 | & | 两位都为 1 则为 1,否则为 0 | 结果不会变大 | 清零指定位、判断奇偶 |
| 按位或 | | | 有一位为 1 则为 1,否则为 0 | 结果不会变小 | 置 1 指定位 |
| 按位异或 | ^ | 两位不同则为 1,相同则为 0 | 结果可大可小,无进位 | 交换数、消重、翻转位 |
| 按位取反 | ~ | 0 变 1,1 变 0 | 符号位敏感,建议无符号使用 | 构造掩码、取反操作 |
| 左移 | << | 左移指定位,低位补 0 | 等价 ×2ⁿ | 快速乘 2 的幂 |
| 右移 | >> | 右移指定位,高位补 0 | 等价 ÷2ⁿ(向下取整) | 快速除 2 的幂 |
<3> 位运算技巧
1. 判断数字奇偶 :x & 1 结果为 1 则为奇数,否则为偶数。
2. 获取二进制数的某一位 :(x >> i) & 1 可获取第 i 位值(0 或 1)。
3. 修改二进制中的某一位为 1 :x | (1 << i) 将第 i 位置 1。清零操作:x & ~(1 << i) 将第 i 位置 0。
4. 快速判断一个数字是否为 2 的幂次方 :若 x & (x - 1) == 0 且 x ≠ 0,则 x 为 2 的幂。
如果 x 为 2 的幂次方,则 x 的二进制表示中只有一个 1,x - 1 就有很多个连续的 1 并且和 x 的 1 没有交集,两者与运算一定为 0,可以证明其他情况必然不为 0。
5. 获取二进制位中最低位的 1 :公式 lowbit(x) = x & -x,如果 x = (0100010),则 lowbit (x) = (0000010),常用于树状数组。
相关练习
递归
递归是一种通过函数调用自身来解决问题的算法思想,核心是将复杂的大问题拆解为结构相同的小问题,直到小问题简化为可直接求解的 "基线条件(终止条件)",再通过逐层返回结果,最终解决原问题。
返回类型 函数名(参数列表) {
// 基本情况(递归终止条件)
if (满足终止条件) {
// 返回终止条件下的结果
}
// 递归表达式(递归调用)
else {
// 将问题分解为规模更小的子问题
// 使用递归调用解决子问题
// 返回子问题的结果
}
}
斐波那契数列
已知 F (1) = F (2) = 1;n > 3 时 F (n) = F (n - 1) + F (n - 2)
输入 n,求 F (n),n ≤ 100000,结果对 1e9 + 7 取模
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1e5 + 9; // 数组最大长度,适配题目n<=1e5的输入范围,+9是防止边界越界
const ll p = 1e9 + 7; // 模数:1e9+7是竞赛常用大质数,用于取模防止数值溢出
ll dp[N]; // 全局备忘录数组:存储已计算的斐波那契值,全局变量默认初始值为0
ll fib(int n)
{
if(dp[n]) return dp[n]; // 如果dp[n]非0,说明该位置已计算过,直接返回结果
if(n <= 2) return 1; // 递归终止条件:F(1)=1,F(2)=1,避免无限递归
// 递归计算:F(n) = F(n-1) + F(n-2)
// 取模操作:每一步都%p,防止数值溢出。结果存入dp数组:下次调用直接读取,消除重复计算
return dp[n] = (fib(n - 1) + fib(n - 2)) % p;
}
int main()
{
int n; cin >> n; // 定义变量n,存储输入的项数
// 循环输出1~n项的斐波那契值,每行一个结果
for(int i = 1; i <= n; ++i)cout << fib(i) << '\n'; // 调用fib函数计算第i项并输出
return 0;
}
1 // F(1)
1 // F(2)
2 // F(3)=F(2)+F(1)
3 // F(4)=F(3)+F(2)
5 // F(5)=F(4)+F(3)
相关练习
离散化
离散化是一种针对值域大但数量少 的数组的算法,核心是将「大范围、稀疏分布」的数值映射为「小范围、连续」的整数。将值域大、元素少、有重复 的原数组,压缩为有序、去重的小数组,保留数值相对大小。一般不会单独考察,通常搭配树状数组、线段树、二维平面的计算几何考察。
离散化数组要求必须有序 ,一般是去重的。
双向映射:
下标→值:直接通过离散化数组下标访问对应值;
值→下标:通过
lower_bound二分查找实现映射。
步骤:原数组复制 → 排序 → 去重,生成离散化数组;再用二分完成原数值到下标的映射。
|---------------------|--------------------------------------------|
| 原数组 | [3, 1000, 2, 99999, 2] |
| 离散化数组 L(排序去重后) | [2, 3, 1000, 99999] |
| 下标→值映射 | L[0]=2,L[1]=3,L[2]=1000,L[3]=99999 |
| 值→下标映射(lower_bound) | 2→0,3→1,1000→2,99999→3 |
模板
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 9;
int a[N]; // 原数组:存储原始输入数据
vector<int> L; // 离散化数组:存储排序、去重后的元素
// 映射函数:返回原数值x在离散化数组L中的下标
int getidx(int x)
{
return lower_bound(L.begin(), L.end(), x) - L.begin();
}
int main()
{
int n; cin >> n;
for(int i = 1; i <= n; ++i)cin >> a[i]; //存入原数组
for(int i = 1; i <= n; ++i)L.push_back(a[i]);// 将原数组元素存入离散化数组L
sort(L.begin(), L.end()); //排序
L.erase(unique(L.begin(), L.end()), L.end());//去重
cout << "离散化数组为:"; //输出离散化数组
for(const auto &i : L)cout << i << ' ';
cout << '\n';
int val; cin >> val;
cout << getidx(val) << '\n'; //输入原数值,输出其离散化下标
return 0;
}
构造
构造算法是直接根据问题条件,设计特定规则 / 结构来求解的算法,不依赖通用模板(如贪心、动态规划),而是通过分析问题特征,手动设计满足条件的解。