2`并查集,单调栈,字典树,A※
并查集
并查集使用树形数据结构,用于处理不相交集合的合并及查询问题,从而加快查询效率(比如pq元素是否属于同集合)
经典问题:亲戚/朋友问题:例如(a,b) 即a是b的朋友,且(b,c),则abc互为朋友关系
数据结构:通常由数组实现
算法:
此处元素整形,如果是string,应使用unordered_map<string, string>
- init():初始化
1
2
3
4
5
void init(int n)
{
for (int i = 1; i <= n; ++i)
parents[i] = i;//元素i的父节点为本身,每个元素单独为一个集合
}
展开
merge(p,q): 把两个不相交的集合合并为一个集合
每棵树表示一组集合,一旦两个元素所在的根节点组成父子关系,那么两者所在的集合将合并为一个集合
1
2
3
4
void merge(int i, int j)
{
parents[findRoot(i)] = findRoot(j);//i的根节点指向j的根节点,变为同一集合
}
展开
- findRoot(p): 查找p所在集合的根节点
1
2
3
4
int findRoot(int x)
{
return x == parents[x] ? x : findRoot(parents[x]);//如果x是根节点直接返回,否则递归查询父节点
}
展开
- isConnected(p,q):查询两个元素是否在同一个集合中
1
2
3
4
bool isConnected(p,q)
{
return findRoot(p) == findRoot(q);//如果两个元素根节点相同,则在一个集合中
}
展开
路径压缩:在查询节点p的根节点q时,如果pq路径很长,每次都要花费很多查询时间,如何保证查询时间复杂度只需要O(1)呢?
每次的findRoot都相当于对树的重构,所有以q为根树内的节点,全部重构为直接指向q,从而在以后的findRoot时,可以一步到位
1
2
3
4
int findRoot(int x)
{
return x == parents[x] ? x : (parents[x] = findRoot(parents[x]));
}
展开
单调栈
单调栈(Monotone Stack):一种特殊的栈。在栈的「先进后出」规则基础上,要求「从 栈底 到 栈顶 的元素是单调递增 / 单调递减
单调递增栈:从 栈底 到 栈顶 的元素是单调增的
- 入栈:只有比栈顶元素大的元素才能直接进栈,否则需要先将栈中比当前元素大的元素出栈,再将当前元素入栈 0 经典问题:
时间复杂度为 O(n) 一次遍历的情况下,求解出某个元素左边或者右边第一个比它大或者小的元素
问题:
对于给定的整数数组nums,找到每个元素右侧第一个比自己大的数的下标,如果没有,填-1
关注的是比自己大的元素下标,因此我们维护一个单调递减栈,反之如果要找到右侧比自己小的元素下标,维护一个单调递增栈
1
2
3
4
5
6
7
8
9
10
11
12
13
14
vector<int> solve(vector<int>& nums) {
int n = nums.size();
vector<int> res(n, -1);//元素初始为-1
stack<int> st;
for (int i = 0; i < n; ++i) {
while (!st.empty() && nums[i] > nums[st.top()]) {
res[st.top()] = i;//出栈时操作
st.pop();
}
st.push(i);
}
return res;
}
展开
如果是左侧查询,并不依赖还未遍历到的元素,因此可以遍历到哪个元素,直接求得这个元素的结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
vector<int> solve(vector<int>& nums) {
int n = nums.size();
vector<int> res(n, -1);
stack<int> st;
for (int i = 0; i < n; ++i) {
while (!st.empty() && nums[i] > nums[st.top()]) {
st.pop();
}
res[i] = st.empty() ? -1 : st.top();//入栈时操作
st.push(i);
}
return res;
}
展开
总结:
求右侧第一个比自己大的元素 | 从左向右遍历,单调递减栈,出栈时操作 |
---|---|
求右侧第一个比自己小的元素 | 从左向右遍历,单调递增栈,出栈时操作 |
求左侧第一个比自己大的元素 | 从左向右遍历,单调递减栈,入栈时操作 |
求左侧第一个比自己小的元素 | 从左向右遍历,单调递增栈,入栈时操作 |
字典树
字典树/前缀树:使用树形数据结构,保存大量的字符串,每个从根节点开始的路径(不一定到叶结点)表示一个字符串,从而加快查询效率(就像查字典一样)
数据结构:
第一种:
- 根节点为空,不存储任何字符串
- int N = 1000050;//数组数量
- int trie[N][26];26叉树,用2维数组,元素值为节点编号
- int cnt[N];节点编号n存储了多少个完整的单词
- int id;节点唯一编号(标识)
第2种:(数据大小不确定,动态分配内存时)
- calss有类成员 一维数组保存26个子节点,每个节点是一个class
- 包含是否存储单词,以便查询
算法:
- 插入:
1
2
3
4
5
6
7
8
9
10
11
void insert(string s)
{
int p = 0;//从根节点索引开始
for (int i = 0; i < s.size(); i++)
{
int x = s[i] - 'a';
if (trie[p][x] == 0) trie[p][x] = ++id;//除了根节点为0,其他不为0
p = trie[p][x];
}
cnt[p]++;
}
展开
- 查询:
1
2
3
4
5
6
7
8
9
10
11
int find(string s)
{
int p = 0;//从根节点索引开始
for (int i = 0; i < s.size(); i++)
{
int x = s[i] - 'a';
if (trie[p][x] == 0)return 0;//没有此节点
p = trie[p][x];
}
return cnt[p];//如果没有存储单词即0,返回没有找到,否则找到
}
展开
A*
作用:
可以在指定区域内 A到B 的多条可行路径中 快速找到最短路径
其他搜索算法:
- 深度优先搜索,会沿着一条路径一直深入探索,直到无法继续或达到目标节点才回溯(寻找AB最短路径效率很差)
- 广度优先搜索,会一层一层地向外扩展,先遍历完当前层的所有节点,再进入下一层(各个方向平等的搜索),直到找到叶节点 / 目标节点 停止
- Dijkstra,每个节点到起点的距离作为优先级,和广度一样,使用队列遍历节点,不过这里因为代价的关系使用优先队列(当每个节点代价相等,它和bfs 将花费一样的时间查询)
- 最佳优先搜索,每个节点到终点的距离作为优先级,每次考虑距离中点最近的那个节点,就可以沿着某方向的路径前进,可以大大加快路径的搜索速度(如果AB间有障碍物,很有可能搜索不到最短路径)
A*算法:
A* 实际上是综合上面这些算法的特点于一身的,像bfs一样每次向外搜索邻居节点加入代遍历列表,会选取代价最小的方向进行下一次搜索,它的代价结合了Dijkstra和最佳优先搜索
- gn是距离起点的代价,hn距离终点的估算(因为路径中有未知的障碍物,无法知道真正的代价,所以我们计算的只是忽略障碍物的代价)代价(启发函数),两者相加为fn节点代价
- hn可以控制算法的速度和精确度
- 如果hn == 0,则退化成了Dijkstra算法
- hn <= n到终点的代价,能保证找到最短路径, hn值越小,算法越慢
- hn == n到终点的代价,能保证找到最短路径,算法很快
- hn > n到终点的代价,不能保证找到最短路径, 算法很快
- 当hn比g(n)大很多,则退化成了最佳优先搜索
- 启发函数常见的有:
- 曼哈顿距离、只允许朝上下左右四个方向移动
- 对角距离、允许朝八个方向移动(对角代价更大)
- 欧几里得距离、允许朝任何方向移动
steps:
- 确立搜索区域,
- 简化搜索区域:划分网格 / 节点(可以用vector< vector< Node*»存储,每个节点有可走(非障碍物),不可走(障碍物)两种情况)
- 开始搜索算法:
- 每次选取优先级最高(值最小)的节点
- 两个集合open_set和close_set,表示待遍历的节点,与已遍历的节点,其他未遍历的不在两个集合中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
// 定义节点结构
struct Node {
int x, y; // 节点的坐标
float g; // 从起点到当前节点的实际代价
float h; // 从当前节点到终点的启发式估计代价
float f; // 优先级
Node* parent; // 父节点指针
bool isWalkable;//是否可走
Node(int x, int y, float g = 0, float h = 0, Node* parent = nullptr, bool isWalkable = true)//构造
: x(x), y(y), g(g), h(h), f(g + h), parent(parent), isWalkable(isWalkable) {}
// 重载比较运算符,用于优先队列
bool operator<(const Node& other) const {
return f > other.f; // 注意:优先队列默认是最大堆,所以这里用 > 来实现最小堆
}
};
// 计算启发式估计代价(曼哈顿距离)
float heuristic(int x1, int y1, int x2, int y2) {
return std::abs(x1 - x2) + std::abs(y1 - y2);
}
// 两节点的距离(曼哈顿距离)
float distance(Node* a, Node* b) {
return std::abs(a->x - b->x) + std::abs(a->y - b->y);
}
// 获取上下左右节点
std::vector<Node*> getNeighbors(Node* node) {
std::vector<Node*> neighbors;
//如果之前划分的网格是2维数组,则直接从数组查询就好,不用new创建
neighbors.push_back(new Node(node->x + 1, node->y));
neighbors.push_back(new Node(node->x - 1, node->y));
neighbors.push_back(new Node(node->x, node->y + 1));
neighbors.push_back(new Node(node->x, node->y - 1));
return neighbors;
}
// A*算法实现
std::vector<Node*> AStar(Node* start, Node* goal) {//接受起点和终点,返回最短路径
// 初始化open_set和close_set
std::priority_queue<Node*> open_set;
std::unordered_set<Node*> close_set;
// 将起点加入open_set中
open_set.push(start);
while (!open_set.empty()) {
// 每次从open_set中选取优先级最高的节点n
Node* current = open_set.top();
open_set.pop();
// 如果节点n为终点(找到了path)
if (current->x == goal->x && current->y == goal->y) {
// 从终点开始逐步追踪parent节点,一直达到起点
std::vector<Node*> path;
while (current != nullptr) {
path.push_back(current);
current = current->parent;
}
// 返回找到的结果路径,算法结束
return path;
}
// 将节点n从open_set中删除,并加入close_set中
close_set.insert(current);
// 遍历节点n所有的邻近节点
for (Node* neighbor : getNeighbors(current)) {
// 如果邻近节点m在close_set中,或者是不可走的,则跳过
if (close_set.find(neighbor) != close_set.end() || !isWalkable) {
continue;
}
// 计算 经由当前节点 到邻近节点的实际代价
float tentative_g = current->g + distance(current, neighbor);
// 如果邻近节点m不在open_set中,或者neighbor在open_set中但,是经由current节点的g值更小(需要更新父节点指向current,以及 重新计算fgh代价值)
if (open_set.find(neighbor) == open_set.end() || tentative_g < neighbor->g) {
// 设置节点m的parent为节点n
neighbor->parent = current;
// 更新节点m的g,h,f值
neighbor->g = tentative_g;
neighbor->h = heuristic(neighbor->x, neighbor->y, goal->x, goal->y);
neighbor->f = neighbor->g + neighbor->h;
// 如果邻近节点m不在open_set中,则将其加入open_set
if (open_set.find(neighbor) == open_set.end()) {
open_set.push(neighbor);
}
}
}
}
// 如果open_set为空且未找到路径,返回空路径
return std::vector<Node*>();
}
展开
runtime error: member access within null pointer of type ‘ListNode‘
链表的每个节点都是struct,我们通常会用指向struct的指针,在访问时很容易出现访问空指针(指针指向mullptr)的情况,因此需要在访问前判断一下,head == nullptr ,head->next == nullptr ,cur == nullptr , cur->next == nullptr
其他常见算法和数据结构:
- 暴力,循环,递归(深搜dfs,回溯),二分查找,动态规划dp,贪心,双指针,快慢指针,滚动数组,滑动窗口,广度bfs,分治,并查集(合并,查询),前缀和,排序(拓步,归并,计数,桶,基数)
- 字符串,队列(优先队列),栈,单调栈,哈希,集合,数组(1.2维),链表(双向),矩阵,树(N叉树),字典树,图,空间数据结构