
**🎬 博主名称**:迷途之人不知返
🔥 个人专栏 : 《C语言》、《数据结构》、《C++》、《Linux》
🗂️ Gitee仓库 : 《C语言》、《数据结构》、《C++》、《Linux》
</> 算法专栏 : 《算法精选集》
位运算
- [1 ···> 一维前缀和(模板)](#1 ···> 一维前缀和(模板))
- [2 ···> 二维前缀和(模板)](#2 ···> 二维前缀和(模板))
- [3 ···> 寻找数组的中心下标](#3 ···> 寻找数组的中心下标)
- [4 ···> 除了自身以外数组的乘积](#4 ···> 除了自身以外数组的乘积)
- [5 ···> 和为 K 的子数组](#5 ···> 和为 K 的子数组)
- [6 ···> 和可被 K 整除的子数组](#6 ···> 和可被 K 整除的子数组)
- [7 ···> 连续数组](#7 ···> 连续数组)
- [8 ···> 矩阵区域和](#8 ···> 矩阵区域和)
位运算的算法思想,专门用于解决一段区间内子区间的快速求和。
1 ···> 一维前缀和(模板)
1.1、描述
比如题目给出的示例:
1, 2, 4
这个数组有3个元素,
- 第一次查询,是从1 ~ 2两个数的和,1 + 2 = 3,所以打印的是3;
- 第二次查询,是从2 ~ 3两个数的和,2 + 4 = 6,所以打印的是6。
1.2、解决
我们很容易想到的暴力解法就是遍历求和。但在这道题中,暴力解法的时间复杂度是O(m*n),不是最优,而且暴力解法肯定超时。
此时的数组是一维的,我们就可以采用一维前缀和的算法。
1、做出前缀和数组
利用动态规划,做出前缀和数组:
dpi = dpi - 1 + arri
2、使用前缀和数组
比如对于序列: 1, 2, 4 ,我们做出了如下的前缀和数组dp:
0, 1, 3, 7
- 要求1 ~ 2两个数的和,就用dp2 - dp0;
- 要求2 ~ 3两个数的和,就用dp3 - dp1;
- 要求l ~ r两个数的和,就用dpr - dpl - 1;
cpp
#include <iostream>
#include <vector>
using namespace std;
int main()
{
int n, m;
cin >> n >> m;
// 1. 读入数组
vector<int> arr(n + 1);
for (int i = 1; i <= n; ++i) cin >> arr[i];
// 2. 制作前缀和数组
vector<long long> dp(n + 1); // 防止溢出
for (int i = 1; i <= n; ++i)
dp[i] = dp[i - 1] + arr[i]; // 注意下标匹配
// 3. 使用前缀和
while (m--)
{
int l, r;
cin >> l >> r;
cout << dp[r] - dp[l - 1] << endl;
}
return 0;
}
2 ···> 二维前缀和(模板)
2.1、描述
分析题目给出的示例:
这是一个3行4列的二维数组:

第一次查询,求(1, 1)到(2, 2)矩阵范围内数值和,结果是:1 + 2 + 3 + 2 = 8。

第二次查询,求(1, 1)到(3, 3)矩阵范围内数值和,结果是:
1 + 2 + 3 + 3 + 2 + 1 + 1 + 5 + 7 = 25

第三次查询,求(1, 2)到(3, 4)矩阵范围内数值和,结果是:
2 + 3 + 4 + 2 + 1 + 0 + 5 + 7 + 8 = 32

2.2、解决
1、做出前缀和数组
我们先做出前缀和数组dp,
- dpij表示从(1, 1)到(i, j),所有数值的和。
关键是状态转移方程,我们似乎无法直接看出来。我们可以把问题抽象成一个矩阵图:

如图表示的是原二维数组,ABCD表示的是对应区域的和。要求dpij,就是求(1, 1)到(i, j)所有值,就是:
dpij = A + B + C + D
D我们知道是原数组(i, j)位置上的值,A是dpi - 1j - 1。而B和C似乎表示不出来。
我们对上面表达式做一个变形:
dpij = (A + B) + (A + C) + D - A
(A + B)和(A + C)就很好表示了:
- (A + B):dpi - 1j
- (A + C):dpij - 1
那么状态转移方程:
dpij = arrij + dpi - 1j + dpij - 1 - dpi - 1j - 1
注意我们为了使原数组与dp数组的位置匹配上,统一在题目给出的行列基础上再加一行一列,这样我们不需要调整下标,避免了很多麻烦。
2、使用前缀和数组
要求(x1, y1)到(x2, y2)的和,我们也无法直接写出式子。我们依旧借助一个抽象图分析:

ABCD依旧代表区域和。我们要求D,不妨做一步转化:
D = (A + B + C + D) - (A + B) - (A + C) + A
那么各部分就很好表示了:
- (A + B + C + D): dpx~2~y~2~
- (A + B): dpx~1~ - 1y~2~
- (A + C): dpx~2~y~1~ - 1
- A: dpx~1~ - 1y~1~ - 1
cpp
#include <iostream>
#include <vector>
using namespace std;
int main()
{
int n, m, q;
cin >> n >> m >> q;
// 1. 读入数组
vector<vector<int>> arr(n+1, vector<int>(m+1));
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j)
cin >> arr[i][j];
// 2. 创建前缀和数组
vector<vector<long long>> dp(n+1, vector<long long>(m+1));
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j)
dp[i][j] = arr[i][j] + dp[i-1][j] + dp[i][j-1] - dp[i-1][j-1];
// 3. 使用
while (q--)
{
int x1, y1, x2, y2;
cin >> x1 >> y1 >> x2 >> y2;
cout << dp[x2][y2] - dp[x1-1][y2] - dp[x2][y1-1] + dp[x1-1][y1-1] << endl;
}
return 0;
}
3 ···> 寻找数组的中心下标
"中心下标"满足的条件是,以中心下标为界,左边子数组之和与右边子数组之和相等。
暴力解法是遍历,每遍历到一个数,就再通过遍历分别将左边子数组和右边子数组做求和。但是时间复杂度为O(N2),提交代码一定会超时。
既然提到了子区间求和,那么我们可以借助"前缀和"的思想解题。

原来求中心下标的问题,就转化成比较左边之和与右边之和是否相等的问题。
对于左边之和,即0, i - 1区间求和,我们可以构造一个前缀和数组front:
- 由于对于原数组的每一位数,都有一个0, i - 1区间,所以前缀和数组的元素个数与原数组一样,也是n;
- 求0, i - 1区间和,即求fronti,由于0, i - 1区间和可以由0, i - 2区间和(fronti - 1)与numsi - 1求得,而fronti - 1又可以分为fronti - 2和numsi - 2......所以我们可以得出状态转移方程:
- fronti = fronti - 1 + numsi - 1
- front0应初始化成0,否则直接套用状态转移方程会越界;
- 填表顺序是从左往右。
同理,对于右边之和,即i + 1, n - 1区间求和,可以构造一个后缀和数组back:
- 前缀和数组的元素个数与原数组一样也是n;
- 求i + 1, n - 1区间和,即求backi,由于i + 1, n - 1区间和可以由i + 2, n - 1区间和(backi + 1)与numsi + 1求得,而backi + 1又可以分为backi + 2和numsi + 2......所以我们可以得出状态转移方程:
- backi = backi + 1 + numsi + 1
- backn - 1应初始化成0,否则直接套用状态转移方程会越界;
- 填表顺序是从右往左。
cpp
class Solution {
public:
int pivotIndex(vector<int>& nums) {
int n = nums.size();
vector<int> front(n), back(n);
for (int i = 1; i < n; ++i) front[i] = front[i-1] + nums[i-1];
for (int i = n-2; i >= 0; --i) back[i] = back[i+1] + nums[i+1];
for (int i = 0; i < n; ++i)
if (front[i] == back[i])
return i;
return -1;
}
};
其实我们也可以只用一个0, i的前缀和数组来解决问题,只不过判断相等的逻辑比较复杂:
cpp
class Solution {
public:
int pivotIndex(vector<int>& nums) {
int n = nums.size();
vector<int> dp(n + 1);
dp[0] = 0;
for (int i = 1; i <= n; ++i) dp[i] = dp[i-1] + nums[i-1];
for (int i = 1; i <= n; ++i)
if (dp[i-1] == dp[n] - dp[i])
return i - 1;
return -1;
}
};
4 ···> 除了自身以外数组的乘积
题目要求是返回一个与原数组长度相同的数组,每一个位置上的数,为除原数组对应位置数之外所有数的乘积。
暴力的解法是遍历,遍历原数组到每一个数,然后再遍历原数组将其它数乘起来。时间复杂度一定是O(N2),一定超时。
分析题目,我们就能发现题目需要求子区间的乘积。所以我们就可以采用前缀和思想,并进行一些改造。

对于任意位置i,要求除i位置的所有位置的乘积,我们就需要求:
- 0, i - 1区间的积,我们称为"前缀积";
- i + 1, n - 1区间的积,我们称为"后缀积"。
原数组每一个位置i,都有一个前缀积和后缀积,所以我们需要创建两个数组,数组与原数组长度一致,分别用来放前缀积、后缀积。
对于前缀积,
- 要求0, i - 1区间的积,我们设为fronti;求fronti,可以求0, i - 2区间的积与numsi - 1的乘积,即fronti - 1 * numsi - 1......所以前缀积的状态转移方程为:
- fronti = fronti - 1 * numsi - 1
- front0应初始化成1,以保证不会影响后续前缀积的计算;
- 要知道fronti必须先知道fronti - 1,所以填表顺序是从左往右。
对于后缀积,
- 设i + 1, n - 1区间的积为backi。状态转移方程为:
- backi = backi + 1 * numsi + 1
- backn - 1应初始化成1,以保证不会影响后续后缀积的计算;
- 填表顺序是从右往左。
cpp
class Solution {
public:
vector<int> productExceptSelf(vector<int>& nums) {
int n = nums.size();
vector<int> front(n), back(n);
front[0] = 1;
for (int i = 1; i < n; ++i)
front[i] = front[i-1] * nums[i-1];
back[n-1] = 1;
for (int i = n-2; i >= 0; --i)
back[i] = back[i+1] * nums[i+1];
vector<int> ret(n);
for (int i = 0; i < n; ++i)
ret[i] = front[i] * back[i];
return ret;
}
};
5 ···> 和为 K 的子数组
5.1、描述
例如,给出和 k = 2 ,数组为:
1, 1, 1
子数组有两个,分别为:

给出和 k = 3 ,数组为:
1, 2, 3
子数组有两个,分别为:

5.2、解决
乍一看,我们可能会想用"滑动窗口"思路做题。但是这道题不能用滑动窗口做,因为题目说明了数组里面可能有0或负值。如果使用同向的双指针,我们就会漏掉情况:

如图,假设此时left和right找到了一种情况。由于数组中可能有0或负数,那么left和right之间就很可能也有一种情况(紫色圈标注)。但是left, right指针式是同向的,紫圈标注的情况就不会被统计到。所以我们不能用滑动窗口做题。
既然题目要我们求和为k的子数组个数,那么我们就可以想一想如何用前缀和思想做题。
我们可以重新定义前缀和的状态表示:到i位置为止所有数的和,设为dpi。

状态转移方程还是不变:
dpi = dpi - 1 + numsi
然后dp0应初始化成0。
我们需要在0, i - 1区间内找一个子区间和为k。假设存在某一个位置x,使得x, i - 1之间和为k,那么0, x之间和就为dpi - k。

问题就变成了:前面用i在遍历得出前缀和数组,后面返回来求前缀和数组中值为dpi - k的个数,即原数组在i位置之前有多少个和为dpi - k的区间。
这里有四个细节。
1、哈希表
我们需要根据区间和,来统计区间个数。如果我们直接创建一个前缀和数组,处理就会变得非常麻烦。我们可以借助哈希表来建立前缀和与区间个数的关系。
2、不用创建一个真的前缀和数组
我们可以定义一个变量sum(i),然后每次加numsi,即可在每一次循环中迭代出前缀和。
3、哈希表入数据的时机
我们是先做好一个哈希表,然后求子区间的个数吗?
当然不是。我们使用i遍历到每一个位置时,得先统计 0, i - 1 区间内有多少个和为 sum(i) - k 的子区间,再将sum入哈希表。
如果我们直接先做好哈希表,或者先统计子区间个数再入哈希表,我们就既统计到 0, i - 1 区间内符合要求的子区间,又会统计到 i + 1, n - 1 区间内的子区间,就会重复统计。
4、整个区间和为k?
如果是先统计个数,再入哈希表,那么当整个区间和为k的时候,返回值应该是1,而此时sum - k = 0,所以我们要让hash0 = 1。
cpp
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
unordered_map<int, int> hash;
hash[0] = 1;
int sum = 0, ret = 0;
for (auto i : nums)
{
sum += i;
if (hash.count(sum - k)) ret += hash[sum-k];// 往后遍历时,sum可能会一样,但是位置一定不一样
// 后面sum相同的序列,是前面序列加上几个和为0的数而成的序列,所以后面求的时候就已经统计好了
hash[sum]++;
}
return ret;
}
};
6 ···> 和可被 K 整除的子数组
首先补充两个知识点。
6.1、补充知识
1、同余定理
对于等式:
(a - b) % p = 0
我们可以得出:
a % p = b % p
证明的方式也比较简单。等式(a - b) % p = 0可以表示成:
(a - b) / p = k ······ 0
移项得:
a = b + p*k
再等号两边同时%p。(b + p*k) % p的结果为b % p,证毕。
2、C++与java的 负数 % 正数 的结果修正
C++与java的 负数 % 正数 的结果都是负数。我们可以将负数的结果修正成正数:
a % p + p
而为了正负统一,即正数和负数都可以用修正式,我们对上面修正式做进一步修正:
(a % p + p) % p
6.2、解决
这道题与第五题思路非常类似。
定义sum(i)为原数组到i位置为止的前缀和。
我们要在0, i区间中找和能被5整除的子区间:

由同余定理,问题就转化为了在 0, i - 1 内求子区间的个数。子区间满足子区间本身和%k的结果,与sum(i)%k的结果相等。
由于子区间和,和sum可能为负数,所以我们要使用修正式修正取模的结果。同时我们也要注意和第五题一样需要处理的细节
- 哈希表的使用
- 不需要真的创建一个真的前缀和数组
- 哈希表入数据的时机
- 整个数组满足条件的情况
cpp
class Solution {
public:
int subarraysDivByK(vector<int>& nums, int k) {
unordered_map<int, int> hash;
hash[0 % k] = 1;
int ret = 0, sum = 0;
for (auto& i : nums)
{
sum += i;
int in = (sum % k + k) % k;
if (hash.count(in)) ret += hash[in];
hash[in]++;
}
return ret;
}
};
7 ···> 连续数组
初看此题,我们似乎无从下手。
我们不妨将数组中所有的0转变成-1 。例如示例3中的数组:

将所有的0变成-1,数组转化为:

而1 + -1 = 0,所以找一个0和1数量相同的最长子区间,我们就转化成了找和为0的最长子区间。
这类题我们之前是做过的:定义dpi为到i位置为止0, i之间的前缀和,然后在0, i - 1区间内找一个下标x,使得0, x区间和dpx等于dpi,接着比较得出最长下标。

但是这道题处理的细节,与"和为k的子数组"有所不同,
- 遇到
nums[i]为0,加的是-1; - 这里也使用哈希表,但是哈希表存的不再是
[前缀和]:[出现次数],而是[前缀和]:[出现位置]; - 不用真的创建前缀和数组,只需迭代sum(i);
- 入哈希表的时机也有差别。当遍历到i位置
- 如果当前存在位置x ∈ \in ∈ 0, i - 1,
hash.count(sum)不为0,更新最大长度(比谁大); - 如果不存在,
hash.count(sum)为0,那么可以入哈希表。
- 如果当前存在位置x ∈ \in ∈ 0, i - 1,
- 如果整个数组的和为0,此时x的位置应该为-1,所以我们要让前缀和结果0的hash值默认为位置-1,即
hash[0] = -1; - 子数组长度的计算。很明显,和为0的子区间的范围是x + 1, i,所以子数组长度的计算式是
i - x,即i - hash[sum]。
cpp
class Solution {
public:
int findMaxLength(vector<int>& nums) {
unordered_map<int, int> hash;
hash[0] = -1;
int sum = 0, ret = 0;
for (int i = 0; i < nums.size(); ++i)
{
sum += nums[i] == 0 ? -1 : 1;
if (hash.count(sum)) ret = max(ret, i - hash[sum]);
else hash[sum] = i;
}
return ret;
}
};
8 ···> 矩阵区域和
8.1、描述
以示例一为例。整数k == 1,矩阵mat为:

要返回的矩阵是answer,我们简称ans。ans[0][0]是以mat[0][0]为中心,边长为2 * k + 1 == 3的正方形区域内的和:

ans[0][1]是以mat[0][1]为中心,边长为3的正方形区域内的和:

ans[1][1]:

ans[2][2]:

8.2、解决
我们会发现,这道题似乎可以用"二维前缀和"的思路来做。所以我们可以先处理出来一个二维前缀和数组。

接下来的问题是:对于ans的每一个位置,都对应一个区域和,那么如何使用二维前缀和数组,以正确地表示每一个区域呢?
设ans上某一位置为(i, j)。由于制作前缀和数组时,为了防止越界,多开辟了一行一列,所以(i, j)映射到dp上应该是(i + 1, j + 1)。
要求区域和,根据"二维前缀和"一问,我们就需要求(x1, y1),(x2, y2),很明显:
x1 = i + 1 - ky1 = j + 1 - kx2 = i + 1 + ky2 = j + 1 + k
但是,x1和y1计算结果可能越界,即小于1。所以我们要将计算结果与1取一个最大:
x1 = max(1, i + 1 - k)y1 = max(1, j + 1 - k)
类似的,x2和y2计算结果可能越界,即x2可能大于mat的行数(row),y2可能大于mat的行数(col)。所以我们要将计算结果与mat的行、列数最大值取一个最小:
x2 = min(row, i + 1 + k)y2 = min(col, j + 1 + k)
cpp
class Solution {
public:
vector<vector<int>> matrixBlockSum(vector<vector<int>>& mat, int k) {
int row = mat.size(), col = mat[0].size();
vector<vector<int>> dp(row + 1, vector<int>(col + 1));
for (int i = 1; i <= row; ++i)
for (int j = 1; j <= col; ++j)
dp[i][j] = mat[i - 1][j - 1] + dp[i][j - 1] + dp[i - 1][j] - dp[i - 1][j - 1];
vector<vector<int>> ans(row, vector<int>(col));
for (int i = 0; i < row; ++i)
for (int j = 0; j < col; ++j)
{
int x1 = max(1, i + 1 - k), y1 = max(1, j + 1 - k);
int x2 = min(row, i + 1 + k), y2 = min(col, j + 1 + k);
ans[i][j] = dp[x2][y2] - dp[x2][y1 - 1] - dp[x1 - 1][y2] + dp[x1 - 1][y1 - 1];
}
return ans;
}
};
小贴士
千万不要乱取变量名!!!
如果col取成c,x1, x2, y1, y2取成a, b, c, d,就会出现命名冲突,就会发生意想不到的错误。