五、二维前缀和数组/差分数组/前缀和数组
1.二维前缀和数组
// 初始化扩展的二维前缀和数组
void initPrefixSum(vector<vector<int>>& prefixSum, const vector<vector<int>>& matrix) {
int rows = matrix.size();
int cols = matrix[0].size();
prefixSum.resize(rows + 1, vector<int>(cols + 1, 0));
for (int i = 1; i <= rows; ++i) {
for (int j = 1; j <= cols; ++j) {
prefixSum[i][j] = matrix[i - 1][j - 1]
+ prefixSum[i - 1][j]
+ prefixSum[i][j - 1]
- prefixSum[i - 1][j - 1];
}
}
}
// 查询扩展的二维前缀和数组中子矩阵的和
int queryPrefixSum(const vector<vector<int>>& prefixSum, int x1, int y1, int x2, int y2) {
int sum = prefixSum[x2 + 1][y2 + 1]
- prefixSum[x1][y2 + 1]
- prefixSum[x2 + 1][y1]
+ prefixSum[x1][y1];
return sum;
}
// 从扩展的二维前缀和数组还原原始数组
vector<vector<int>> restoreOriginalArray(const vector<vector<int>>& prefixSum) {
int rows = prefixSum.size() - 1;
int cols = prefixSum[0].size() - 1;
vector<vector<int>> originalArray(rows, vector<int>(cols, 0));
for (int i = 1; i <= rows; ++i) {
for (int j = 1; j <= cols; ++j) {
originalArray[i - 1][j - 1] = prefixSum[i][j]
- prefixSum[i - 1][j]
- prefixSum[i][j - 1]
+ prefixSum[i - 1][j - 1];
}
}
return originalArray;
}
int main() {
vector<vector<int>> matrix = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
vector<vector<int>> prefixSum;
initPrefixSum(prefixSum, matrix);
cout << "Prefix Sum Array:" << endl;
for (const auto& row : prefixSum) {
for (int val : row) {
cout << val << " ";
}
cout << endl;
}
int sum = queryPrefixSum(prefixSum, 1, 1, 2, 2); // 查询从(1, 1)到(2, 2)的子矩阵的和
cout << "Queried sum: " << sum << endl;
vector<vector<int>> restoredArray = restoreOriginalArray(prefixSum);
cout << "Restored Original Array:" << endl;
for (const auto& row : restoredArray) {
for (int val : row) {
cout << val << " ";
}
cout << endl;
}
return 0;
}
2.差分数组
差分数组是高效处理数组区间修改问题的方法,适用于对原始数组的连续区间频繁地进行增加或减少操作的情形。
-
差分数组,记为 d i f f diff diff,是根据原数组 n u m s nums nums 构建的一个新数组,其中 d i f f [ i ] diff[i] diff[i] 表示 n u m s [ i ] nums[i] nums[i] 与 n u m s [ i − 1 ] nums[i - 1] nums[i−1] 的差值。对于数组的第一个元素, d i f f [ 0 ] = n u m s [ 0 ] diff[0] = nums[0] diff[0]=nums[0],因为没有前一个元素与之比较。差分数组的主要优势在于它可以快速、高效地对原始数组的任意连续区间进行修改。
-
差分数组的构造,构建差分数组 d i f f diff diff,遵循以下步骤:
- 初始化 d i f f diff diff 数组,使其长度与原数组 n u m s nums nums 相同。
- 设定 d i f f [ 0 ] = n u m s [ 0 ] diff[0] = nums[0] diff[0]=nums[0]。
- 对于 d i f f diff diff 数组中的其他元素,执行 d i f f [ i ] = n u m s [ i ] − n u m s [ i − 1 ] diff[i] = nums[i] - nums[i - 1] diff[i]=nums[i]−nums[i−1]。
int[] diff = new int[nums.length];
diff[0] = nums[0];
for (int i = 1; i < nums.length; i++) {
diff[i] = nums[i] - nums[i - 1];
}
-
使用差分数组进行区间修改:当需要对原数组 n u m s nums nums 的连续区间 [ i . . j ] [i..j] [i..j] 进行修改,即对该区间内所有元素增加或减少某个值 v a l val val 时,我们只需要更新差分数组 d i f f diff diff:
- 令 d i f f [ i ] + = v a l diff[i] += val diff[i]+=val。
- 如果
j
+
1
<
n
u
m
s
.
l
e
n
g
t
h
j + 1 < nums.length
j+1<nums.length,则
d
i
f
f
[
j
+
1
]
−
=
v
a
l
diff[j + 1] -= val
diff[j+1]−=val。
这样,我们就在 O ( 1 ) O(1) O(1) 的时间复杂度内完成了区间的修改,而不需要遍历整个区间。原理很简单,回想 d i f f diff diff 数组反推 n u m s nums nums 数组的过程, d i f f [ i ] + = v a l diff[i] += val diff[i]+=val 意味着给 n u m s [ i . . ] nums[i..] nums[i..] 所有的元素都加了 v a l val val,然后 d i f f [ j + 1 ] − = v a l diff[j+1] -= val diff[j+1]−=val 又意味着对于 n u m s [ j + 1.. ] nums[j+1..] nums[j+1..] 所有元素再减 v a l val val。综合起来,就是对 n u m s [ i . . j ] nums[i..j] nums[i..j] 中的所有元素都加 v a l val val 了
-
从差分数组还原原数组:修改完差分数组后,可以通过以下步骤还原修改后的原数组 n u m s nums nums:
- 初始化结果数组 r e s res res,使其长度与 d i f f diff diff 相同。
- 设定 r e s [ 0 ] = d i f f [ 0 ] res[0] = diff[0] res[0]=diff[0]。
- 对于 r e s res res 数组中的其他元素,执行 r e s [ i ] = r e s [ i − 1 ] + d i f f [ i ] res[i] = res[i - 1] + diff[i] res[i]=res[i−1]+diff[i]。
int[] res = new int[diff.length];
res[0] = diff[0];
for (int i = 1; i < diff.length; i++) {
res[i] = res[i - 1] + diff[i];
}
3.前缀和数组
vector<int> buildPrefixSum(const vector<int>& nums) { // 构建前缀和数组
int n = nums.size();
vector<int> prefixSum(n + 1, 0);
for (int i = 1; i <= n; ++i) {
prefixSum[i] = prefixSum[i - 1] + nums[i - 1];
}
return prefixSum;
}
vector<int> restoreOriginalArray(const vector<int>& prefixSum) { // 使用前缀和数组还原原数组
int n = prefixSum.size() - 1; // 原数组的长度
vector<int> originalArray(n);
for (int i = 0; i < n; ++i) {
originalArray[i] = prefixSum[i + 1] - prefixSum[i];
}
return originalArray;
}
int queryRangeSum(const vector<int>& prefixSum, int l, int r) {
return prefixSum[r + 1] - prefixSum[l];
}
六、矩阵运算
void mul(vector<vector<long long>>& t1, vector<vector<long long>>& t2, vector<vector<long long>>& res) {
for (size_t i = 0; i < t1.size(); i++) {
for (size_t j = 0; j < t2[0].size(); j++) {
for (size_t k = 0; k < t1[0].size(); k++) {
res[i][j] += t1[i][k] * t2[k][j];
}
}
}
}
七、质数
bool isPrime(int num) {
if (num <= 1) return false; // 小于等于1的数不是质数
if (num == 2) return true; // 2是唯一的偶数质数
if (num % 2 == 0) return false; // 排除偶数
int sqrtNum = sqrt(num);
for (int i = 3; i <= sqrtNum; i += 2) {
if (num % i == 0) return false;
}
return true;
}
八、判断闰年
bool isLeap(int y) {
if ((y % 4 == 0 && y % 100 != 0) || (y % 400 == 0)) return true;
return false;
}
九、并查集
1.并查集操作
-
查找(Find):确定特定元素属于哪个子集。此操作可帮助确定两个元素是否属于同一子集。
-
合并(Union 或 Merge):将两个子集合并成一个单独的子集。
2.实现方法
方法1:快速查找
快速查找的思想是利用一个数组来跟踪每个元素所属的集合。在这种方法中,数组的每个位置对应一个元素,数组中存储的值代表该元素所属的集合标识符。这里的“集合标识符”通常是该集合中某个元素的标识,通常是这个集合中最小的元素,但实际上可以是任何标识。(假设有一个名为
Set
的数组,其大小是N
(元素的数量)。初始时,每个元素都属于只包含其自身的集合,因此Set[i] = i
对于所有0 <= i < N
。)
i.查找操作 (Find)
查找操作非常简单且直接:给定元素x
,函数find1
只需返回数组中x
位置上的值。这个值就是x
所属的集合的标识符。因为直接通过索引访问数组,所以这个操作的时间复杂度是O(1)。
// 查找操作
int find1(int x) {
return Set[x];
}
ii.合并操作 (Union or Merge)
合并操作涉及到将两个集合合并为一个。这需要将这两个集合中的所有元素更新为拥有相同的集合标识符。在快速查找策略中,这通常涉及到遍历整个数组,将所有属于这两个集合之一的元素的集合标识符更新为统一的值(通常是两个集合标识符中的最小值)。因为这涉及到遍历整个数组,所以时间复杂度为O(N)。
// 合并操作
void merge1(int a, int b) {
int setA = find1(a); // 找到a的集合标识符
int setB = find1(b); // 找到b的集合标识符
if (setA != setB) { // 如果a和b不在同一集合,则合并
for (int i = 0; i < N; ++i) {
if (Set[i] == setB) { // 将所有属于集合B的元素的集合标识符更新为A的集合标识符
Set[i] = setA;
}
}
}
}
方法2:快速合并
快速合并优化了合并集合的操作,但是这使得查找操作可能变慢。在快速合并的实现中,每个集合都由一个树来表示,集合中的每个元素都是树的一个节点。每个节点都指向另一个节点(除了根节点,它指向自己),这个指向表示了元素之间的关联。集合的“根”节点是该集合的代表。
i.查找操作 (Find)
查找操作find2
需要找到元素所在集合的根节点,因为根节点是该集合的代表。开始时,find2
函数从指定的节点开始,沿着指向父节点的指针向上遍历,直到找到一个指向自己的节点,即根节点。这个过程可能需要遍历整个树的高度,所以在最坏的情况下时间复杂度为O(N)。
int find(int x) {
while (parent[x] != x) { // 当x不是自己的父亲(即不是根元素)时
parent[x] = parent[parent[x]]; // 进行路径压缩,将x的父亲设置为x的爷爷节点
x = parent[x]; // x上移至其父亲节点
}
return x; // 返回x的根元素,即集合的标识
}
ii.合并操作 (Union or Merge)
合并操作merge2
简单到只需要将一个集合的根节点指向另一个集合的根节点。因为这只需要更改一个节点的父指针,所以这个操作的时间复杂度是O(1)。
// 合并操作
void merge2(int a, int b) {
int rootA = find2(a); // 找到a的根
int rootB = find2(b); // 找到b的根
if (rootA != rootB) { // 如果a和b不在同一集合,将一个集合的根指向另一个集合的根
Set[rootA] = rootB;
}
}
iii.路径压缩优化
为了改善查找操作的效率,可以应用路径压缩技术。当执行查找操作以找到一个元素的根节点时,路径压缩将所有遍历过的节点直接连接到根节点上,这样它们在下一次查找时将直接指向根节点,大大减少了遍历的路径长度。通过这种方式,查找操作的平均时间复杂度可以显著减少。
// 查找操作,带路径压缩
int find2(int x) {
if (Set[x] != x) {
Set[x] = find2(Set[x]); // 路径压缩
}
return Set[x];
}
iv.基于秩的合并
基于秩的合并是一种优化并查集性能的策略,尤其是在
merge
和find
操作上。在这种策略中,我们通过保持树的高度尽可能低来提高效率。这里的“秩”可以理解为树的高度或深度的一个估计值,并不一定是实际的高度。
每个元素都有一个秩,初始时秩都设置为0。当两个集合合并时,如果它们的秩相同,则选择一个作为新的根,并将其秩增加1;如果它们的秩不同,则将秩较小的树合并到秩较大的树下,而不改变秩较大树的秩。这样做可以避免树变得太高,保证了find
操作的效率。
class UnionFind {
vector<int> parent; // 存储每个元素的父节点
vector<int> rank; // 存储每个根节点的秩
public:
UnionFind(int size) : parent(size), rank(size, 0) {
// 初始时,每个元素的父节点是它们自己
for (int i = 0; i < size; ++i) {
parent[i] = i;
}
}
// 查找元素x所在集合的根节点
int find(int x) {
if (parent[x] != x) {
// 路径压缩
parent[x] = find(parent[x]);
}
return parent[x];
}
// 合并元素x和元素y所在的集合
void merge(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX != rootY) {
// 将秩较小的树的根连接到秩较大的树的根
if (rank[rootX] < rank[rootY]) {
parent[rootX] = rootY;
} else if (rank[rootX] > rank[rootY]) {
parent[rootY] = rootX;
} else {
// 如果秩相等,则任选一个作为根,并增加其秩
parent[rootY] = rootX;
rank[rootX] += 1;
}
}
}
};
十、后缀表达式计算/中缀表达式转后缀表达式
1.后缀表达式计算
后缀表达式,也被称为逆波兰表示法(Reverse Polish Notation,简称RPN),是一种不需要括号来标识操作符优先级的数学表达式。在后缀表达式中,所有的操作符都跟随在它们的操作数之后,这使得它的结构比标准的算术表达式更为简洁明了,尤其是在进行复杂计算时。其优点主要在于:
- 消除了表达式中的括号:后缀表达式不需要括号来指定操作的顺序,因为操作符的位置已经明确了操作的顺序。
- 易于计算机计算:后缀表达式可以很容易地用栈结构在计算机程序中实现,这使得它在编程语言的解释器和编译器中特别有用。
- 消除了优先级规则的需求:由于计算顺序完全由操作符的位置决定,因此不需要考虑运算符优先级规则。
-
基本结构:后缀表达式的基本规则是:
操作数 操作数 操作符
。例如,传统算术表达式 3 + 4 3 + 4 3+4 在后缀表达式中表示为 34 + 3 4 + 34+。 -
计算方式:要计算后缀表达式的值,需要使用一个栈(Stack):
- 从左至右扫描表达式。
- 遇到数字时,将其推入栈中。
- 遇到操作符时,从栈中弹出两个元素(注意:第一个弹出的元素是右操作数,第二个是左操作数),执行相应的运算,并将结果推回栈中。
- 继续执行,直到整个表达式被处理完。
- 最后,栈中剩余的元素就是表达式的结果。
-
中缀(传统)表达式: ( 3 + 4 ) × 5 (3 + 4) \times 5 (3+4)×5
-
后缀表达式: 34 + 5 × 3 4 + 5 \times 34+5×
2.中缀表达式转后缀表达式
中缀表达式是大多数人习惯的数学表达式写法,其中运算符位于两个操作数之间,如 A + B A + B A+B。相比之下,后缀表达式(也称为逆波兰表示法)中,运算符位于操作数之后,例如 A B + A B + AB+。
将中缀表达式转换为后缀表达式可以使表达式更易于计算机程序处理,因为后缀表达式不需要括号来指定运算顺序。转换过程通常使用一个栈来辅助.
- 具体步骤:
- 创建一个空栈:用于存放运算符(包括括号),以及一个输出列表用于构建后缀表达式。
- 从左到右扫描中缀表达式:
- 如果遇到操作数,直接将其添加到输出列表。
- 如果遇到左括号,将其压入栈中。
- 如果遇到右括号,则从栈中弹出运算符并添加到输出列表,直到遇到左括号(左括号弹出但不添加到输出列表)。
- 如果遇到运算符,从栈中弹出所有优先级更高或相等的运算符,并将它们添加到输出列表中,然后将当前运算符压入栈中。
- 扫描完成后,从栈中弹出所有剩余的运算符并添加到输出列表。
优先级和结合性规则决定了操作符从栈中弹出的时机。在比较运算符优先级时,乘法和除法通常优先于加法和减法。当有多个运算符具有相同的优先级时,它们的结合性决定了它们的
- 示例:将中缀表达式
A * (B + C) / D
转换为后缀表达式:- 扫描
A
,因为它是操作数,直接输出A
。 - 扫描
*
,将其压入栈中。 - 扫描
(
,将其压入栈中。 - 扫描
B
,因为它是操作数,直接输出B
。 - 扫描
+
,将其压入栈中(注意,尽管+
的优先级低于*
,但由于+
在括号内,我们会暂时忽略外部的运算符)。 - 扫描
C
,因为它是操作数,直接输出C
。 - 扫描
)
,开始弹出栈中的运算符直到遇到(
,因此输出+
,然后丢弃(
。 - 最后,扫描
/
,弹出并输出栈顶的*
(因为*
的优先级与/
相等),然后将/
压入栈。
- 扫描
此时,输出列表为
A
B
C
+
∗
A B C + *
ABC+∗,然后把 /
添加到输出列表(因为栈内不再有其他运算符),所以最终的后缀表达式为
A
B
C
+
∗
/
A B C + * /
ABC+∗/。
bool isOperator(char c) { // 检查字符是否是操作符
return c == '+' || c == '-' || c == '*' || c == '/';
}
int getPriority(char c) { // 检查运算符的优先级
if (c == '*' || c == '/') return 2;
if (c == '+' || c == '-') return 1;
return 0;
}
string infixToPostfix(const string& infix) { // 中缀表达式转后缀表达式
string postfix;
stack<char> opStack;
for (char c : infix) {
if (c == ' ') continue; // 忽略空格
if (isdigit(c)) {
postfix += c;
}
else if (c == '(') {
opStack.push(c);
}
else if (c == ')') {
while (!opStack.empty() && opStack.top() != '(') {
postfix += opStack.top();
opStack.pop();
}
opStack.pop(); // 弹出 '('
}
else if (isOperator(c)) {
while (!opStack.empty() && getPriority(opStack.top()) >= getPriority(c)) {
postfix += opStack.top();
opStack.pop();
}
opStack.push(c);
}
}
while (!opStack.empty()) { // 将栈内剩余的操作符添加到后缀表达式
postfix += opStack.top();
opStack.pop();
}
return postfix;
}
// 计算后缀表达式
int calculatePostfix(const string& postfix) {
stack<int> valStack;
for (char c : postfix) {
if (isdigit(c)) {
valStack.push(c - '0'); // 将字符转换为整数
}
else {
int right = valStack.top(); valStack.pop();
int left = valStack.top(); valStack.pop();
switch (c) {
case '+': valStack.push(left + right); break;
case '-': valStack.push(left - right); break;
case '*': valStack.push(left * right); break;
case '/': valStack.push(left / right); break;
}
}
}
return valStack.top();
}