十.左程云算法基础班整理(十)(c++)
并查集
1.有若干个样本a、b、c、d…类型假设是V
2在并查集中一开始认为每个样本都在单独的集合里
3.用户可以在任何时候调用如下两个方法:
1.bool isSameSet(V& x,V& y):查询样本x和y是否属于同一个集合。其实就是问x和y联通没有。
2.void union(V& x,V& y):把x和y各自所在集合的所有样本合并成一个集合。把x背后所有的东西(集团)和y背后的所有东西(集团)连到一起。
4.isSameSet和union方法的代价越低越好
10.1.1isSameSet的算法逻辑
一开始是这种结构
同时建立一个map表
怎么确定两个节点是否在同一个集合?找代表点!!!如下,a往上到不能再往上,得到代表点a圈。c往上到不能再往上,得到代表点c圈。a圈和c圈不是同一个,因此,a和c不连通。
代表点一开始都是自己指向自己的,但是慢慢地代表点就不是了,可以把代表点看成根节点。。。共根节点肯定联通。没有公共根节点的肯定不连通。
10.1.2union的算法逻辑
例如我们要联通a和e,union(a,e);
找到a的代表点,再找到e的代表点。然后将集合内元素较少的代表点挂到较多的代表点的下面。
如果集合个数相同,谁挂谁无所谓。
小集合的头结点直接连到大集合的头结点上。
parent表是这样的:
如果代表点是一个,就不用union了。
findfather是一个扁平化的操作,之前是下面那样
扁平化之后
a,b,c下面的链都不用调整,只把从x出发的直链给弄扁平。
调用频繁时,时间复杂度O(1).
不用关心怎么证,直接用结论。
以后只要是连通性的题,全都这么干。
并查集的优化有两个,一个是扁平化优化,另一个是小集合绑上大集合,也叫做秩优化。
并查集代码
#include<iostream>
#include<vector>
#include<list>
#include<stack>
#include<unordered_map>
using namespace std;
struct V
{
int v;
};
class Node
{
public:
int val;
Node(V va)
{
val = va.v;
}
//bool operator!=(Node a)
//{
//return this!=&a;
//}
};
class UnionSet
{
public:
unordered_map<V, Node> nodes;
unordered_map<Node, Node> parents;
unordered_map<Node, int> sizeMap;
//初始化
UnionSet(list<V> values)
{
for (V cur : values)
{
Node node(cur);
nodes.insert({ cur,node });
parents.insert({ node, node });
sizeMap.insert({ node, 1 });
}
}
//从点cur开始,一直往上找,找到不能再往上的代表点,返回
Node findFather(Node& cur)
{
stack<Node> path;
while (&cur != &parents[cur])
{
path.push(cur);
cur = parents.at(cur);
}
// cur头节点,这是一个扁平化的过程,一个优化,可以降低以后的时间复杂度
while (!path.empty())
{
parents.insert({ path.top(), cur });
path.pop();
}
return cur;
}
bool isSameSet(V& a, V& b)
{
if (!nodes.count(a) || !nodes.count(b))
{
return false;
}
return &findFather(nodes.at(a)) == &findFather(nodes.at(b));
}
void unionSet(V& a, V& b)
{
if (!nodes.count(a) || !nodes.count(b))
{
return;
}
Node aHead = findFather(nodes[a]);
Node bHead = findFather(nodes[b]);
if (&aHead != &bHead)
{
int aSetSize = sizeMap[aHead];
int bSetSize = sizeMap[bHead];
Node big = aSetSize >= bSetSize ? aHead : bHead;
Node small = aSetSize < bSetSize ? aHead : bHead;
parents.insert({ small,big });
sizeMap.insert({ big, aSetSize + bSetSize });
sizeMap.erase(small);
}
}
};
10.2合并用户
每个学生实例有3个id,身份证ID,B站ID,githubID。只要任意两个实例有某项id相同,都可以视为是同一个人(只要一个id相同就可以)。求问一堆实例中,一共有几个人。
这是今年的一个原题。
三张表,桥联,每一个新来的样本都看之前表里有没有,有的话就直接连上,数据选哪个无所谓,因为并查集会把所有的连到一块的。
不断地桥联,看最后到底有几个集合,其实就是sizemap的大小。里面放着的是最后的代表点,而一个代表点代表一个集合。
#include<iostream>
#include<vector>
#include<list>
#include<stack>
#include<unordered_map>
using namespace std;
struct User
{
string a;
string b;
string c;
User(string aa,string bb,string cc):a(aa),b(bb),c(cc){}
};
class Node
{
public:
User val;
Node(User a):val(a){}
};
class UnionSet
{
public:
unordered_map<User, Node> nodes;
unordered_map<Node, Node> parents;
unordered_map<Node, int> sizeMap;
//初始化
UnionSet(list<User> values)
{
for (User cur : values)
{
Node node(cur);
nodes.insert({ cur,node });
parents.insert({ node, node });
sizeMap.insert({ node, 1 });
}
}
//从点cur开始,一直往上找,找到不能再往上的代表点,返回
Node findFather(Node& cur)
{
stack<Node> path;
while (&cur != &parents[cur])
{
path.push(cur);
cur = parents.at(cur);
}
// cur头节点,这是一个扁平化的过程,一个优化,可以降低以后的时间复杂度
while (!path.empty())
{
parents.insert({ path.top(), cur });
path.pop();
}
return cur;
}
bool isSameSet(User& a, User& b)
{
if (!nodes.count(a) || !nodes.count(b))
{
return false;
}
return &findFather(nodes.at(a)) == &findFather(nodes.at(b));
}
void unionSet(User& a, User& b)
{
if (!nodes.count(a) || !nodes.count(b))
{
return;
}
Node aHead = findFather(nodes[a]);
Node bHead = findFather(nodes[b]);
if (&aHead != &bHead)
{
int aSetSize = sizeMap[aHead];
int bSetSize = sizeMap[bHead];
Node big = aSetSize >= bSetSize ? aHead : bHead;
Node small = aSetSize < bSetSize ? aHead : bHead;
parents.insert({ small,big });
sizeMap.insert({ big, aSetSize + bSetSize });
sizeMap.erase(small);
}
}
int getSetNum()
{
return sizeMap.size();
}
};
// (1,10,13) (2,10,37) (400,500,37)
// 如果两个user,a字段一样、或者b字段一样、或者c字段一样,就认为是一个人
// 请合并users,返回合并之后的用户数量
int mergeUsers(list<User> users)
{
UnionSet unionFind(users);
unordered_map<string, User> mapA;
unordered_map<string, User> mapB;
unordered_map<string, User> mapC;
for (User user : users)
{
if (mapA.count(user.a))
{
unionFind.unionSet(user, mapA[user.a]);
}
else{
mapA.insert({ user.a,user });
}
if (mapB.count(user.b))
{
unionFind.unionSet(user, mapB[user.b]);
}
else {
mapB.insert({ user.b,user });
}
if (mapC.count(user.c))
{
unionFind.unionSet(user, mapC[user.c]);
}
else {
mapC.insert({ user.c,user });
}
}
return unionFind.getSetNum();
}
10.3图的基本性质和结构表示
10.3.1
1.由点的集合和边的集合构成
2.虽然存在有向图和无向图的概念,但是实际上都可以用有向图来表达
3.边上可能带有权值
无向图也可以看成双边有向图,所以可以看成,所有的图都是有向图。
图的结构表示主流两种方法:
1.邻接表法,2.邻接矩阵法
邻接表法:
邻接矩阵法:
图的算法本身没什么难度,最大的难度是图的表示方式比较多,不可能每个都来一遍。
下面也是一种面试时常用的图的结构
图的算法都不算难,只不过coding的代价比较高。
1、先用自己的最熟练的方式,实现图结构的表达
2、在自己熟悉的结构上,实现所有常用的图算法作为模板
3、把面试题提供的图结构转化为自己熟悉的图结构,再调用模板或改写即可。
#include<iostream>
#include<vector>
#include<unordered_set>
#include<unordered_map>
using namespace std;
//点结构的描述, A,0
struct Node
{
int value;//该点的值,可以是string,可以是int,也可以是其他的。
int in;//入度,进来的边有多少
int out;//出度,出去的边有多少,out也是next的size
vector<Node*> next;//该节点的直接的邻居
vector<Edge*> edges;//该节点的直接的边
Node(int value)
{
this->value = value;
in = 0;
out = 0;
}
};
struct Edge//这个边是有向边
{
int weight;
Node* from;
Node* to;
Edge(int weight, Node* from, Node* to)
{
this->weight = weight;
this->from = from;
this->to = to;
}
};
struct Graph//图就是点集和边集
{
unordered_map<int, Node*> nodes;
unordered_set<Edge*> edges;
Graph(){}
};
//进行转换
class GraphGenerator
{
public:
// matrix 所有的边
// N*3 的矩阵
// [weight, from节点上面的值,to节点上面的值]
//难点在于怎么转化成我们知道的那种图的结构
Graph createGraph(vector<vector<int>>& matrix)
{
Graph graph;
for (int i = 0; i < matrix.size(); i++)
{
// matrix[0][0], matrix[0][1] matrix[0][2]
int weight = matrix[i][0];
int from = matrix[i][1];
int to = matrix[i][2];
if (!graph.nodes.count(from))
{
graph.nodes.insert(make_pair(from,new Node(from)));
}
if (!graph.nodes.count(to))
{
graph.nodes.insert({ to, new Node(to) });
}
Node* fromNode = graph.nodes[from];
Node* toNode = graph.nodes[to];
Edge* newEdge = new Edge(weight, fromNode, toNode);//创建新边
fromNode->next.push_back(toNode);
fromNode->out++;
toNode->in++;
fromNode->edges.push_back(newEdge);
graph.edges.insert(newEdge);
}
}
};
10.4图的广(宽)度优先遍历,BFS
宽度优先遍历
1.利用队列实现
2.从源节点开始依次按照宽度入队,然后弹出
3.每弹出一个点,把该节点所有没进入队列的邻接点放入队列
4.直到队列变空
图的宽度优先遍历要建立一个unordered_set,作用是防止之前遍历过得一些节点再次遍历,防止出现环的问题
#include<iostream>
#include<queue>
#include<unordered_set>
#include<unordered_map>
using namespace std;
//点结构的描述, A,0
struct Node
{
int value;//该点的值,可以是string,可以是int,也可以是其他的。
int in;//入度,进来的边有多少
int out;//出度,出去的边有多少,out也是next的size
vector<Node*> next;//该节点的直接的邻居
vector<Edge*> edges;//该节点的直接的边
Node() :value(0), in(0), out(0) {}
Node(int value)
{
this->value = value;
in = 0;
out = 0;
}
};
struct Edge//这个边是有向边
{
int weight;
Node* from;
Node* to;
Edge(int weight, Node* from, Node* to)
{
this->weight = weight;
this->from = from;
this->to = to;
}
};
struct Graph//图就是点集和边集
{
unordered_map<int, Node*> nodes;
unordered_set<Edge*> edges;
};
class BFS
{
public:
void bfs(Node* node)
{
if (node == nullptr)
{
return;
}
queue<Node*> queue1;
unordered_set<Node*> set1;
queue1.push(node);
set1.insert(node);
while (!queue1.empty())
{
Node* cur = queue1.front();
queue1.pop();
cout << cur->value << endl;
for (Node* next : cur->next)
{
if (!set1.count(next))
{
set1.insert(next);//set和add是同步加的
queue1.push(next);
}
}
}
}
};
10.5图的深度优先遍历,DFS
深度优先遍历
1.利用栈实现
2.从源节点开始把节点按照深度放入栈,然后弹出
3.每弹出一个栈,把该节点下一个没有进过栈的邻接点放入栈
4.直到栈变空
深度优先遍历就是,一条路,需要走到不能再走了,再往上返回。
#include<iostream>
#include<stack>
#include<unordered_set>
#include<unordered_map>
using namespace std;
//点结构的描述, A,0
struct Node
{
int value;//该点的值,可以是string,可以是int,也可以是其他的。
int in;//入度,进来的边有多少
int out;//出度,出去的边有多少,out也是next的size
vector<Node*> next;//该节点的直接的邻居
vector<Edge*> edges;//该节点的直接的边
Node() :value(0), in(0), out(0) {}
Node(int value)
{
this->value = value;
in = 0;
out = 0;
}
};
struct Edge//这个边是有向边
{
int weight;
Node* from;
Node* to;
Edge(int weight, Node* from, Node* to)
{
this->weight = weight;
this->from = from;
this->to = to;
}
};
struct Graph//图就是点集和边集
{
unordered_map<int, Node*> nodes;
unordered_set<Edge*> edges;
};
class DFS
{
public:
void dfs(Node* node)
{
if (node == nullptr)
{
return;
}
stack<Node*> stack1;
unordered_set<Node*> set1;
stack1.push(node);//先把头入栈
//____________________//进栈的时候打印处理某些事情
set1.insert(node);//标记头结点
cout << node->value << endl;
//___________________//
while (!stack1.empty())
{
Node* cur = stack1.top();
stack1.pop();
for (Node* next : cur->next)
{
if (!set1.count(next))//如果还未入栈
{
//再将刚才弹出来的那个压入栈
stack1.push(cur);
stack1.push(next);
//____________________//进栈的时候打印处理某些事情
set1.insert(next);
cout << next->value << endl;
//___________________//
break;//break是因为路要一条一条的走,直接回到最初栈位置
}
}
}
}
};
10.6拓扑排序
1.在图中找到所有入度为0的点输出
2.把所有入度为0的点在图中删掉,继续找入度为0的点输出,周而复始
3.图的所有点都被删除后,一次输出的顺序就是拓扑排序
要求:有向图且其中没有环
应用:事件安排,编译顺序
有环的图是没有拓扑排序的概念,无向图也不可以
所以拓扑排序一定是有向无环图。
拓扑排序可以看成依赖关系,完成一件事情的前置条件完成顺序。
最适用的场景是编译的时候。
各头文件相互包含相互依赖,其实就是一张有向无环图。
依赖包就是从最底层的东西开始编译,最后把自己的项目给编译出来。
在一张图中先找到入度为0的点,删掉该点及该点的影响,依次进行此类操作。
#include<iostream>
#include<vector>
#include<list>
#include<queue>
#include<unordered_set>
#include<unordered_map>
using namespace std;
//点结构的描述, A,0
struct Node
{
int value;//该点的值,可以是string,可以是int,也可以是其他的。
int in;//入度,进来的边有多少
int out;//出度,出去的边有多少,out也是next的size
vector<Node*> next;//该节点的直接的邻居
vector<Edge*> edges;//该节点的直接的边
Node(int value)
{
this->value = value;
in = 0;
out = 0;
}
};
struct Edge//这个边是有向边
{
int weight;
Node* from;
Node* to;
Edge(int weight, Node* from, Node* to)
{
this->weight = weight;
this->from = from;
this->to = to;
}
};
struct Graph//图就是点集和边集
{
unordered_map<int, Node*> nodes;
unordered_set<Edge*> edges;
Graph() {}
};
// directed graph and no loop
list<Node*> sortedTopology(Graph* graph)
{
// key:某一个node
// value:剩余的入度
unordered_map<Node*, int> inMap;
// 剩余入度为0的点,才能进这个队列
queue<Node*> zeroInQueue;
for (auto it = graph->nodes.begin(); it != graph->nodes.end(); it++)
{
Node* cur = it->second;
inMap.insert({ cur,cur->in });//在所有点集中,先把inmap初始化一下
if (cur->in == 0)//第一批入度为0的点,率先放到zeroinqueue里面
{
zeroInQueue.push(cur);
}
}
// 拓扑排序的结果,依次加入result
list<Node*> result;
while (!zeroInQueue.empty())
{
Node* cur = zeroInQueue.front();
zeroInQueue.pop();
result.push_back(cur);
for (Node* next : cur->next)
{
inMap.insert({ next, inMap[next] - 1 });//所有的邻居,入度-1;
if (inMap[next] == 0)
{
zeroInQueue.push(next);
}
}
}
return result;
}
10.7克鲁斯卡尔算法(最小生成树)
1.总是从权值最小的边开始考虑,依次考察权值依次变大的边
2.当前的边要么进入最小生成树的集合,要么丢弃。
3.如果当前的边进入最小生成树的集合中不会形成环,就要当前边
4.如果当前的边进入最小生成树的集合中也会形成环,就不要当前边
5.考察完所有边之后,最小生成树的集合也就找到了。
理解并查集之后,最小生成树十分容易,有向图和无向图之间没有明显界限。
只要不破坏连通性的话,可以删掉某些边。
最小生成树,是在保留连通性的前提下,最省的权值组合。
K算法,先是对边从小到大进行排序。
之后从点集角度触发,看当前点的两侧是否为同一个连通区域,如果是同一连通区域,则此边不会加入进去。如果不是同一连通域,则该边会被加进去。
并查集做这个有天然的优势,可以查找两片区域的连通性问题。
#include<iostream>
#include<unordered_map>
#include<list>
#include<unordered_set>
#include<numeric>
using namespace std;
//点结构的描述, A,0
struct Node
{
int value;//该点的值,可以是string,可以是int,也可以是其他的。
int in;//入度,进来的边有多少
int out;//出度,出去的边有多少,out也是next的size
vector<Node*> next;//该节点的直接的邻居
vector<Edge*> edges;//该节点的直接的边
Node(int value)
{
this->value = value;
in = 0;
out = 0;
}
};
struct Edge//这个边是有向边
{
int weight;
Node* from;
Node* to;
Edge(int weight, Node* from, Node* to)
{
this->weight = weight;
this->from = from;
this->to = to;
}
};
struct Graph//图就是点集和边集
{
unordered_map<int, Node*> nodes;//点的集合
unordered_set<Edge*> edges;//边的集合
Graph() {}
};
unordered_map<Node*, list<Node*>> setMap;//第二个参数是包含第一个参数的集合
//设置集合
void mySet(list<Node*>nodes) {
for (Node* cur : nodes) {
list<Node*>set;
set.push_back(cur);
setMap[cur] = set;//当前就只将自己单独放入一个集合
}
}
//判断是否一条边的两个端点在同一个集合
bool isSameSet(Node* from, Node* to) {
list<Node*>fromSet = setMap[from];
list<Node*>toSet = setMap[to];
return fromSet == toSet;//只要在同一个集合里面,地址就相同
}
//将from所在集合和to所在集合合并在一起
void merge(Node* from, Node* to) {
list<Node*>fromSet = setMap[from];
list<Node*>toSet = setMap[to];
for (Node* curTo : toSet) {
fromSet.push_back(curTo);
setMap.insert(make_pair(curTo, fromSet));//此时from和to所在setMap属于同一内存地址
}
}
//生成最小树:克鲁斯卡尔算法
list<Node*> smallestTreeKruskal(Graph* graph) {
//先将graph的边排序
unordered_set<Edge*> edg;
for (Edge* curEdge : graph->edges)
edg.insert(curEdge);//ed会自动按照key值排序
for (Edge* curEdge : edg) {
Node* from = curEdge->from;//拿到边的两个端点
Node* to = curEdge->to;
if (!isSameSet(from, to)) {//不在同一容器就合并
merge(from, to);
}
}
return setMap.begin()->second;//返回点集,既有点值的信息,又有边的信息
}
10.8普利姆算法实现最小生成树
随意指定一个出发点,被解锁的点和被解锁的边。
先从一个点出发,解锁最短的边,得到新的点,再从新的点触发,解锁最短的边…依次周而复始,知道最后所有的点都被包括了进来,注意在解锁边的时候,要注意之前的结点有没有被加进来。
普利姆算法用不到并查集,因为每次都是加进来一个点,而不是两个集合合并,因此用不到并查集,一个正常的unordered_set就足够了。
#include<iostream>
#include<unordered_map>
#include<queue>
#include<unordered_set>
using namespace std;
//点结构的描述, A,0
struct Node
{
int value;//该点的值,可以是string,可以是int,也可以是其他的。
int in;//入度,进来的边有多少
int out;//出度,出去的边有多少,out也是next的size
vector<Node*> next;//该节点的直接的邻居
vector<Edge*> edges;//该节点的直接的边
Node(int value)
{
this->value = value;
in = 0;
out = 0;
}
};
struct Edge//这个边是有向边
{
int weight;
Node* from;
Node* to;
Edge(int weight, Node* from, Node* to)
{
this->weight = weight;
this->from = from;
this->to = to;
}
};
struct Graph//图就是点集和边集
{
unordered_map<int, Node*> nodes;//点的集合
unordered_set<Edge*> edges;//边的集合
Graph() {}
};
//比较器
class EdgeComparator {
public:
int compare(Edge* o1, Edge* o2) {//排序方式,从小往大
return o1->weight - o2->weight;
}
};
unordered_set<Edge*> primMST(Graph* graph) {
//解锁的边进入小根堆
priority_queue<Edge*> priorityQueue;
unordered_set<Node*> set;//标记点
unordered_set<Edge*> result;//依次挑选的边在result里面
//for (Node* node : graph->nodes.begin()->second) {//随便找一个点就行
for(auto it = graph->nodes.begin();it!=graph->nodes.end();it++)
{
Node* node = it->second;
//node是开始点
if (set.find(node) == set.end()) {//如果还没遍历过该点
set.insert(node);
for (Edge* edge : node->edges) {//由一个点解锁所有相连的边
priorityQueue.push(edge);
}
while (!priorityQueue.empty()) {
Edge* edge = priorityQueue.top();//弹出解锁边中的最小的边
Node* toNode = edge->to;//可能是新的点
if (set.find(toNode) != set.end()) {//如果不含有就是新的点
set.insert(toNode);
result.insert(edge);//将边装进结果里面
for (Edge* nextEdge : toNode->edges)
priorityQueue.push(nextEdge);//将新的点的相邻的边解锁
}
}
}
}
return result;
}
10.9路径规划(迪杰斯特拉)
要求各边权值都没有负数,返回一张最短路径表。
迪杰斯特拉算法和商旅问题TSP问题是完全不一样的。迪杰斯塔拉不是全连接的。是针对某一个顶点,可以实现到达各个顶点。
商旅问题是所有节点都全连接,类似一个邮差,一定要走过所有的城市,并且回到当前的城市,每个城市值许经过一次,请问总距离如何最短。
TPS问题不指定出发点,因为是个环。而迪杰斯特拉一定要指定出发点。
迪杰斯特拉本质上是贪心思想。
迪杰斯特拉的改进:每次都遍历很多距离,选择一个最近的点。多次遍历不够快,可以使用一个小根堆来解决问题。
#include<iostream>
#include<unordered_map>
#include<queue>
#include<unordered_set>
using namespace std;
//点结构的描述, A,0
struct Node
{
int value;//该点的值,可以是string,可以是int,也可以是其他的。
int in;//入度,进来的边有多少
int out;//出度,出去的边有多少,out也是next的size
vector<Node*> next;//该节点的直接的邻居
vector<Edge*> edges;//该节点的直接的边
Node(int value)
{
this->value = value;
in = 0;
out = 0;
}
};
struct Edge//这个边是有向边
{
int weight;
Node* from;
Node* to;
Edge(int weight, Node* from, Node* to)
{
this->weight = weight;
this->from = from;
this->to = to;
}
};
struct Graph//图就是点集和边集
{
unordered_map<int, Node*> nodes;//点的集合
unordered_set<Edge*> edges;//边的集合
Graph() {}
};
unordered_map<Node*, int> dijkstral(Node* head) {
//返回值的第一个参数:从head出发到达key,
//返回值第二个参数:从head出发到达key的最小长度
//如果在表中不含T,则说明从head到达T的距离为正无穷,不能到达
unordered_map<Node*, int> distanceMap;
distanceMap.insert(make_pair(head, 0));//自己到自己距离为0
//锁定的结点放在selectNode中
unordered_set<Node*>selectedNode;
//从distanceMap中选择没有选过的最小距离的结点
Node* minNode = geiMinDistanceAndUnselected(distanceMap, selectedNode);
while (minNode != NULL) {
int distance = distanceMap[minNode];
for (Edge* edge : minNode->edges) {
Node* toNode = edge->to;
if (distanceMap.find(toNode) != distanceMap.end()) {
distanceMap.insert(make_pair(toNode, distance + edge->weight));
}
//更新
distanceMap.insert(make_pair(edge->to,
min(distanceMap[toNode], distance + edge->weight)));
}
selectedNode.insert(minNode);
minNode = geiMinDistanceAndUnselected(distanceMap, selectedNode);
}
return distanceMap;
}
//找最小路径对应的节点
Node* geiMinDistanceAndUnselected(unordered_map<Node*, int>distanceMap,
unordered_set<Node*> selectedNode) {
Node* minNode;
int minValue = INT_MAX;
for (pair<Node*, int> cur : distanceMap) {//获得对组
Node* node = cur.first;
int distance = cur.second;
if (selectedNode.find(node) != selectedNode.end() && minValue > distance) {
minNode = node;
minValue = distance;
}
}
return minNode;
}