数据结构与算法实战 8.图
*******图******* 有向图(undirected graph)<a,b> 无向图(directed graph)() 简单图 无重边和自回路 完全图 和完全树有点像,完全图就是图没有缺一条连续的边,就是不能再加入新的边了,比如完全有向图Cn2=n*(n-1)/2 * 2 两个方向 顶点的“邻接”(adjacent) 两个点有一条边连着就叫邻接,如果是无向图,叫互相邻接,有向图是有方向的邻接 子图:顶点集合是一个母图的顶点集合的子集,边集是母图边集的子集,并且也是一个图才行(因为你抽出来点和边不一定构成图) 路径:用顶点的序列表示 路径长度:路径点序列里面边的个数 简单路径:一个点只出现一次,Vi1,Vi2,Vi3,...Vin各不相同 (也就是不相交) 圈/环(circle):起点=终点的简单路径 两个顶点Vi和Vj连通(Connected):有路可达 图是连通的:每一对顶点连通 连通分量(component 部分):图的极大连通子图,就是图里面找一个子图,是连通的,然后点极大,边极大 树:连通无环图 DAG:有向无环图 度:跟一个顶点相连的边的数量 有向图分出去(出度) 进来(入度) 边数 = 入度之和 = 出度之和 = 度之和 / 2 稠密图、稀疏图 没有严格定义:就是边和点密点就稠密 稀疏就是。。。顾名思义 稀疏 权:给边带权 如果一个图带了权就叫做网络 例如一条公路的长度可以叫做权 这个路的图叫公路网络 *****图的表示***** 稠密图常用:因为没有稀疏图那么稀疏,利用空间较多,而且时间上遍历也没有那么无效 邻接矩阵(二维数组 用下标代替点的位置)(注意带权图的话,没有权要标记inf(最大值),因为0 负数可能都有特殊含义) 稀疏图常用: 邻接表(有几条边带几个格子) 常用实现就是用单链表 (带权图的话写个结构体把编号和权都弄进去) 例如A-C 0 1 2 3 4 | A C B E D B-E | D 0->1 2 1->0 2->0 3 4 3->2 4->2 如果是有向图用邻接表实现,要看一个点出发有几条边,比较容易,如果要看多少条边到达一个点就得遍历整个表了 这时候就出现了,逆邻接表,就是把边的出发点放在后面,而不是像邻接表把到达点放在后面。 如果两个操作(查点的入边,出边)都经常做,邻接表和逆邻接表一起用就完事了. 图的实现:创建 插入节点 插入边 查找边 删除边 删除点的话,如果用邻接表or邻接矩阵,后来都会变得特别麻烦,我们不推荐删除点,一定要删除的话,做个删除的标记就好了. ****拓扑排序**** 应用场景:比如大学里面课程有先修课,把课连起来会变成一个有向无环图,注意无环是必须的,因为有环的话, 先修的顺序会变成一个死循环...比如程序设计基础->C++->数据结构与算法分析,如果连回去, 程序设计基础->C++->数据结构与算法分析->, 那到底哪个是先修课... | <<<<<< | 所谓拓扑排序,从这个应用场景来看的话,就是得到一个序列,按照先修的顺序含所有的顶点. 例如c03 -> c04 -> c05 \ c06 / c01 -> c02 如何找出拓扑排序,我们一番试验后可以发现就是把没有入边的点输出一遍就行,每次输出一个没有入边的点,就把它的出边删掉 例如c03 c01 c04 c02 c05 c06 伪代码思路: TopSort{ for(i = 0; i < N; i ++){ 寻找未输出且入度为0的顶点v //如果运气不好的话要N的时间.每次都NE的时间,总的效率会变成n^2,效率很低 if(找不到v) 不存在拓扑排序序列(有环) 结束 输出v 对于每个邻接自v的顶点w:删除边<v, w> } } 改进版 TopSort{ 计算每个顶点的入度InDegree[] for(i = 0; i < N; i ++){ 寻找未输出且度为0的顶点v //线性查找 效率是O(N) if(找不到v)错误:不存在拓扑排序序列 结束 输出v 对于每个邻接自v的顶点w:InDegree[w]减1 } } //我们可以看到每次都要去寻找未输出且度为0的顶点v 度为0的点大部分是执行InDegree[w]-1之后产生的 //每次产生之后,我们没有保存,如果我们保存了,下一次循环,就可以直接从保存的容器去查找就好了,减少很多无效搜索 进一步改进 TopSort{ 计算每个顶点的入度InDegree[],若入度为0,入队 while(队不空){ 出队顶点v 输出v,计数count++ 对于每个邻接自v的顶点w:InDegree[w]减1,若变为0则入队 } if(count != N) 错误:不存在拓扑排序序列//因为存在的话肯定会把每个点都遍历一遍,不会多也不会少 } 实例:输入个数后输入先修关系,左边的为右边的先修课 输入 5 C03 C04 C04 C05 C01 C02 C05 C06 C02 C06 以下代码要结合下面图的实现代码使用 bool topSort(){ vector<int> inDegree(nv, 0);//入度初值默认0 for(auto vl : adjL){//vl指的是邻接表里的每一行 for(auto w : vl)//w指的是每一行邻接表右边的小节点. 防遗忘小tip:邻接表里面存的是<x,y>的y和他们两之间的长度 inDegree[w.first]++; } queue<int> q; for(int i = 0; i < nv; i ++){ if(inDegree[i] == 0) q.push(i); } int count = 0; int v; while(!q.empty()){ v = q.front(); q.pop(); cout << vertices[v] << " "; count ++; for(auto w : adjL[v]){ inDegree[w.first]--;//把拿出来的点的入边取消 if(inDegree[w.first] == 0) q.push(w.first); } } if(count != nv) return false;//点不够的话就是有环,输出错误 return true; } int main(){ LGraph g(true);//拓扑图是一个有向图 string n1, n2; int M; cin >> M; for(int i = 0; i < M; i ++){ cin >> n1 >> n2; g,insertE(n1, n2); } return 0; } *****最短路径***** 无权图的最短路径和带权图的最短路径(dijstra算法) 无权图的最短路径: shortest{ 顶点s入队 //起点入队 while(队不空){ 出队->顶点v 对于每个邻接自v的顶点w:{ if(d[w] == 无穷) d[w] = d[v] + 1; w入队 } } } 以下代码要结合下面图的实现代码使用 void shortest(string src){ if(iov.find(src) == iov.end()) return; vector<int> d(nv, INT_MAX);//距离数组 从src到d[w]中的w的距离 vector<int> from(nv, -1);//-1表示没有来源 用来源数组保存路径 方便打印从起点到任一点的最短路径 d[iov[src]] = 0;//起点和起点的距离为0 queue<int> q; q.push(iov[src]); int v; while(!q.empty()){ v = q.front(); q.pop(); for(auto w : adjL[v]){ if(d[w.first] == INT_MAX){ d[w.first] = d[v] + 1; from[w.first] = v; q.push(w.first); } } } for(int i = 0; i < nv; i ++){ cout << vertices[i] << "(" << d[i] << ", " << (from[i]>=0? vertices[from[i]] : "-") << ") ";//-1是起点,没有前一个点,所以要用运算判定是不是>=0 } //打印出来会是这样的结果 例如 A(1,C) B(2,A) C(0,-),解释:从C到A最短路径为1,从C到B最短路径为2,B是从A来,A呢?往前看,从C来 cout << endl; } 带权图的最短路径(求一个点到其他顶点的最短路径) dijstra算法:从src出发,选择一条当前到其他邻接顶点最短的路径(因为当前出发就只有那么几条路,当前最短的肯定是最短的), 如何证明是当前最短的?我们可以利用反证法,假设还存在一点,从src到这个dst最短,那么就会和当前最短矛盾,所以这个是最短的, 以下图为例,我们求从A到其他各个顶点的最短距离,先建立一个表格,标明距离和来源点,并创建已发现最短路径点集V 先从A出发,有两条路可以走,A-D是目前最短路径,加入后更新D的来源,并更新从D出发的四个点的距离,并把D加入点集V, 然后四个点的距离与表格中A-B的距离比较,发现A-B的距离更加短,把B加入点集V,从B到E的距离是10,比从A-D-E的距离更大, 就不更新,并且不把E加入,然后再从剩下的挑选最短的路径的点,就选了C,C加入点集V,因为A-D-C是3,从C出发又有两条边, 到F的距离比之前ADF的9就短,就把F的来源点重新设置为C,为什么这时候不能把F也加入点集V?因为这是个有向图,还有其他入边, 然后从表格中找最短路的点,E是最短的,最后选G,最后再选F.全部算完. ***dijstra算法实现*** 我们先把GRAPH类放进.h头文件(含所有函数的声明 和包含的头文件)还有GRAPH.CPP的源文件(函数的具体定义) 接着把MGRAPH类放进.h头文件和MGRAPH.CPP的源文件,还有把LGRAPH也一样这么干 输入样例: /* 12 A B 2 C A 4 A D 1 B D 3 B E 10 D C 2 D E 7 C F 5 D F 8 D G 4 E G 6 G F 1 */ #include<iostream> #include"mgraph.h" using namespace std; int main(){ MGraph g(true);//有向图 用邻接矩阵 string n1,n2; int weight; int M; cin >> M; for(int i = 0; i < M; i ++){ cin >> n1 >> n2 >> weight; g.insertE(n1, n2, weight); } g.dijkstra("A"); return 0; } //在mgraph.h里面public部分增加以下声明 void dijkstra(string src); //在mgraph.cpp源文件里面加入下面的具体实现部分 void MGraph::dijkstra(string src){ if(iov.find(src) == iov.end()){ return ; } vector<int> d(nv, INT_MAX);//一开始src到每个点都认为是无穷大距离 vector<int> from(nv, -1);//每个点,只需要记录上一个点是谁就行,完整路径可以追溯上一个点去看 vector<bool> known(nv, false);//标记到这个点的最短路径是不是确定了 d[iov[src]] = 0; int minDis, v;//minDis 含义:最小距离,v 含义:src到v的最小距离 for(int t = 0; t < nv; t ++){ minDis = INT_MAX; v = -1; //关于上面两个变量为什么那样设置可以去看一下课...不知道怎么用文字解释 for(int i = 0; i < nv; i ++){ if(known[i] == false && d[i] < minDis){ minDis = d[i]; v = i; } } if(v == -1) break;//找了一圈 点还是最初的值-1 说明找不到路 known[v] = true; //新加入一个点之后,判断从新加入的点出发到剩余的点会不会比原来直接到剩余的点的距离更近 for(int w = 0; w < nv; w ++){ if(adjM[v][w] != INT_MAX && !known[w] && d[v]+adjM[v][w] < d[w]){ //边存在,而且没有加入点集,而且比原来直接从起点到这个点的距离要更近 d[w] = d[v] + adjM[v][w]; from[w] = v; } } } for(int i = 0; i < nv; i ++) cout << i << '\t'; cout << endl; for(int i = 0; i < nv; i ++) cout << vertices[i] << '\t'; cout << endl; for(int i = 0; i < nv; i ++) cout << d[i] << '\t'; cout << endl; for(int i = 0; i < nv; i ++) cout << (from[i]>=0 ? vertices[from[i]] : "-") << '\t'; cout << endl; } ****有负边存在的最短路径算法**** 假如存在负回路,就是走几圈路还会变小了,不存在解决这种图的算法,因为会产生死循环. 只存在负边的图可以找出最短路径 函数如下: void weightedShortest(string src){ if(iov.find(src) == iov.end()){ return ; } vector<int> d(nv, INT_MAX);//一开始src到每个点都认为是无穷大距离 vector<int> from(nv, -1);//每个点,只需要记录上一个点是谁就行,完整路径可以追溯上一个点去看 queue<int> q; vector<bool> inqueue(nv, false);//标志是否入队 d[iov[src]] = 0; q.push(iov[src]); inqueue[iov[src]] = true; int v; while(!q.empty()){ v = q.front(); q.pop(); inqueue[v] = false; for(int w = 0; w < nv; w ++){ if(adjM[v][w] != INT_MAX && d[v] + adjM[v][w] < d[w]){ d[w] = d[v] + adjM[v][w]; from[w] = v; if(!inqueue[w]){ q.push(w); inqueue[w]=true; } } } } for(int i = 0; i < nv; i ++) cout << i << '\t'; cout << endl; for(int i = 0; i < nv; i ++) cout << vertices[i] << '\t'; cout << endl; for(int i = 0; i < nv; i ++) cout << d[i] << '\t'; cout << endl; for(int i = 0; i < nv; i ++) cout << (from[i]>=0 ? vertices[from[i]] : "-") << '\t'; cout << endl; } ********* ****C++实现图**** #include<iostream> #include<vector> #include<map> #include<queue> #include<cstring> using namespace std; class Graph{ protected: int nv;//顶点数 number of vertices //因为不想分成两个图来写,而且两个图的操作几乎无区别,为避免重复写代码,增加标记有向图还是无向图 bool directed;//true有向 false无向 vector<string> vertices;//顶点名字的集合 map<string, int> iov; //顶点名字到编号的映射 index of vertices /*例如 0 1 2 3 4 A B C D E <-vertices map:A-0 B-1 C-2 D-3 E-4 */ vector<bool> visited;//dfs中用到的访问标记数组 用来标记点是否访问过 virtual void dfs(int v) = 0;//因为深搜和图是用矩阵保存还是表保存有关,所以把具体实现放在子类 //至于为什么这个dfs放在protected里面,听课没听懂...2021.8.1 14:00 public: //C++支持默认参数,注意如果有多个默认参数,是从右边开始去确认 Graph(bool dir = false){//只输入有无向 directed = dir;//确认有向还是无向 nv = 0;//0个结点 } //overload 提供一个根据名字构造图的方法 Graph(vector<string> v, bool dir = false){//传入向量和方向有无 directed = dir; nv = (int)v.size();//v.size返回的是无符号长整数,所以要进行强制转换取消warning vertices = v;//点集拷贝 for(int i = 0; i < nv; i ++){ iov[vertices[i]] = i;//iov[key] = value,因为顶点的名字作为key,所以是vertices[i] } } //直接根据总的定点数来构造图 编号就是顶点的名字 0号顶点名字就是0 1号的就是1 以此类推 Graph(int n, bool dir = false){ directed = dir; nv = n; vertices.resize(n);//n个点 就 n个元素 for(int i = 0; i < n; i ++){ vertices[i] = to_string(i);//注意 to_string是C++11的新特性 iov[to_string(i)] = i; } } virtual void print() = 0; //通过把这个函数设置为纯虚函数,废置掉GRAPH这个类产生对象的作用,因为GRAPH这个类不完整. //定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。 //故 我们在下面的MGRAPH中要实现test这个函数 //设置为虚函数,免得被子类重写;ps: JAVA中继承是动态绑定,C++中继承是静态绑定 virtual void insertV(){ insertV(to_string(nv));//0 - nv-1 } virtual bool insertV(string v){//插入一个名字为v的节点 //不允许点的名字重名 //如果点的名字存在,就失败;另外在iov这个map查比在v里面直接查要快得多 if(iov .find(v) != iov.end()) return false;//如果find返回end,就是找到了,判断一个点不存在 //没找到同名的,可以插入 vertices.push_back(v); //增加新的映射 iov[v] = nv; nv++; return true; } //插入边:插入重复的边我们在这里视作失败,那插入不存在的点和已存在的点的边呢? //后期我们可能会想去直接通过插入边来构造图 //比如A B C ,我们插入 A-X,算不算插入成功?算,这时候就需要我们在插入边里面的函数插入一个新增的点就行 //插入函数的参数呢? 应该提供可以插入编号,插入名字的,两个,还有带权图的,可能有权值,不提供就用默认的 //一条边需要两个点构成 virtual bool insertE(string src, string dst, int weight = 1){//string 提供点的名字 //起点 终点 默认权值为1 //如果边存在,失败,如果两个点不存在,还要创建点 //我们无脑插入两个点,不管存在不存在,反正在调用insertV之后肯定存在就完事了 insertV(src); insertV(dst); //为什么提供名字就能去插入...而插入点的编号不行?因为插入名字的话,编号会确定 //反过来的话会变得???麻烦???我也不懂(2021.7.27) return insertE(iov[src], iov[dst], weight); } //提供点的编号 能够去插入吗? 会让操作非常麻烦...(2021.7.27 迷惑不解) virtual bool insertE(int src, int dst, int weight = 1) = 0;//纯虚函数 //删除边 virtual bool removeE(string src, string dst){//判断点不存在可以放在子类去写,但是要重复写两遍,不太好,所以放在父类这里做 if(iov.find(src) == iov.end() || iov.find(dst) == iov.end()) return false;//有一个点不存在 return removeE(iov[src], iov[dst]); } virtual bool removeE(int src, int dst) = 0;//因为这函数和父类没关系,所以弄成虚函数 //图的遍历 virtual void dfs(string v){//深度优先搜索 if(iov.find(v) == iov.end()) return; visited.resize(nv); for(int i = 0; i < nv; i ++) visited[i] = false;//设置缺省值 全部都是没未访问 dfs(iov[v]);//从编号开始递归 } }; //邻接矩阵 //真正的C++多文件工程 应该把每个类写成.h引入 这里是为了方便 全部在同一个文件里面写 class MGraph : public Graph{//比一般的图多了个邻接矩阵 protected: //C++最好不用数组,因为你不知道那个数组元素要填入多少... 如果你填一个定值,后期又要重新全部改 //C++用vector JAVA用Arraylist //实际上邻接矩阵 不就是整数向量的向量嘛 //可以想象 一个向量是一行 多行数据 实际上就是向量嵌套向量 vector嵌套vector,arraylist嵌套arraylist //vector套vector,只要对着邻接矩阵看就能明白了 vector<vector<int>> adjM;//嵌套向量可以代替二维数组,每一行是一个向量元素,实际和C也是一样的,C的二维数组是数组的数组 void setAdjM(){//根据nv构造好邻接矩阵 //不要循环一个一个去push,可能会引起扩容的操作,扩容是要很大的时间代价的 //所以我们直接调用了resize adjM.resize(nv);//nv是有值的,在子类进行构造的时候调用了父类的构造方法,已经把成员变量进行了一遍初始化 for(int i = 0; i < nv; i ++){ adjM[i].resize(nv);//adjM是一个嵌套向量,那么它的第i行就是一个向量,因为有4个点,所以也是resize(nv) for(int j = 0; j < nv; j ++){//我们用一个比较通用的规定去初始化邻接矩阵:填入一个INF(无穷大) adjM[i][j] = INT_MAX;//INT_MAX C语言的宏定义,整数最大值 //由此可以得到 如果权值为INT_MAX的时候,两点的边不存在 } } } void dfs(int v){ cout << vertices[v] << " ";//打印出来 表示访问过了 visited[v] = true; for(int i = 0; i < nv; i ++){ if(!visited[i] && adjM[v][i]!=INT_MAX) dfs(i);//没访问过,而且两点间存在边 } } public: MGraph(bool dir = false) : Graph(dir){ //父类有的东西,应该让父类去做,而不是在子类去做,不要拷贝代码!!!don't repeat yourself. //子类应该是做扩展的东西,所以这个函数直接丢给父类就完事了 } MGraph(vector<string> v, bool dir=false) : Graph(v, dir){ //先让父类去调用它的构造方法把成员变量都做个初始化 setAdjM(); } MGraph(int n, bool dir = false) : Graph(n, dir){ //先让父类去调用它的构造方法把成员变量都做个初始化 setAdjM(); } //"打印(名词)"/测试函数 //print必须有,因为父类中test是个纯虚函数,所以继承它的子类必须实现print void print(){ cout << '\t'; for(int i = 0; i < nv; i ++){//打印顶点的名字 cout << vertices[i] << '\t'; } cout << endl; for(int i = 0; i < nv; i ++){ cout << vertices[i] << ":\t"; for(int j = 0; j < nv; j ++){ //cout << adjM[i][j] << '\t'; 直接输出这个的话 打印会比较不符合格式化 //如果是没有边,是最大值的话,就不输出上面那个数了,直接输出'-\t' //记住,这样写纯粹是因为好看,不是因为上面那句话写错了 if(adjM[i][j] == INT_MAX) cout << "-\t"; else cout << adjM[i][j] << '\t'; } cout << endl; } } //插入节点 void insertV(){ //直接插入点的个数总数 //为什么基类里面有,这边还要重新写一遍? //因为如果这个被注释掉的话,在主函数里面调用.insertV,本来会去基类里面调用基类的无参insertV //但是在子类里面(也就是下一个函数)已经写了个带参的,那么编译器会默认这个函数是要有参数的,就会出现error //所以我们不得不在这里重新写一下这个函数 而且它和下面那个函数算是重载,没有冲突 insertV(to_string(nv)); } bool insertV(string v){ //先让基类(父类)去完成插入,如果父类的插入都失败,那子类更不用插入了,铁定失败 bool r = Graph::insertV(v);//在基类就插入失败的话就不用在派生类再插入了 if(!r){//!r <=> r==NULL return false; } //在父类插入成功,所以我们可以在子类进行节点插入.注意基类插入成功后,nv(点的个数)已经增加 //邻接矩阵插入的逻辑 //图插入一个节点以后,邻接矩阵是多了一行一列 //论行来看的话,就需要在原先的每一行后面追加一个元素 //然后又因为nv已经++了,(即属于这个节点自己的那一行),在最后要追加一行向量 //为什么不直接在for里面追加最后一行的向量,或者说直接每一行都是弄成INT_MAX? //那人家本来前面都有数据的,你把人家的数据初始化成INT_MAX?我人都傻了 //由于你插入节点只是做一个插入操作,还没进行边权赋值,所以追加的时候是追加全部是INT_MAX的一向量 for(int i = 0; i < nv - 1; i ++){//在运行第一句Graph:insertV之后 nv已经++了,所以要-1 adjM[i].push_back(INT_MAX);//初始化一行 } adjM.push_back(vector<int>(nv,INT_MAX));//因为nv已经++了,在最后一行追加一行向量,nv个点,每个点是INT_MAX return true; } //virtual标识符写不写这函数都是virtual,因为基类里面那个函数就是virtual. virtual bool insertE(string src, string dst, int weight = 1){ return Graph::insertE(src, dst, weight);//调用函数顺序有点蛋疼,在此略过笔记- -反正反正我又不学C++hhhh } virtual bool insertE(int src, int dst, int weight = 1){ if(src < 0 || dst < 0 || src >= nv || dst >= nv) return false; //重边失败 if(adjM[src][dst] != INT_MAX) return false; adjM[src][dst] = weight; //如果是无向图的话,是对称的,就可以在对称位置赋值weight,有向图的话就不用再加入了 if(!directed) adjM[dst][src] = weight; return true; } //删除边 模仿插入边写 virtual bool removeE(string src, string dst){ //派生类 调用 基类 基类中是纯虚的 又会跑来调用派生类里的 return Graph::removeE(src, dst); } virtual bool removeE(int src, int dst){ if(src < 0 || dst < 0 || src >= nv || dst >= nv) return false; if(adjM[src][dst] == INT_MAX) return false; adjM[src][dst] = INT_MAX;//INT_MAX就是不存在. 就相当于删除了 if(!directed) adjM[dst][src] = INT_MAX;//无向图是对称的,所以在对称位置也赋值为INT_MAX return true; }; void dfs(string v){ Graph::dfs(v);//由父类判断v是否合法,然后就会去调用这个MGraph里面protected的dfs } virtual void bfs(string v){//广度优先搜索 if(iov.find(v) == iov.end()) return; int iv = iov[v]; visited.resize(nv); for(int i = 0; i < nv; i ++) visited[i] = false;//设置缺省值 全部都是没未访问 queue<int> q; cout << vertices[v] << " "; visited[iv] = true; q.push(iv); //开始广度优先搜索 while(!q.empty()){ w = q.front(); q.pop(); for(int i = 0; i < nv; i ++){ if(!visited[i] && adjM[w][i]!=INT_MAX){//没访问过,且两点存在边 cout << vertices[i] << " ";//打印出来表示访问 visited[i] = true; q.push(i); } } } } }; //邻接表 //为什么稀疏图更适合用邻接表?因为每一行后面不是等长顺序表,元素多可以多放,元素少可以少放 //如果每一个标号后面的链表是等长的话,那和用邻接矩阵有什么区别呢? class LGraph: public Graph{ protected: vector<map<int,int>> adjL;//边的出发点(key)和另一个结点(value)做映射 void setAdjL(){ adjL.resize(nv); for(auto x : adjL){ x.clear();//clear 把每一行的映射清空 } } void dfs(int v){ cout << vertices[v] << " ";//打印出来 表示访问过了 visited[v] = true; for(auto x : adjL[v]){ if(!visited[x.first]){//因为邻接表的话,能拿出来点就肯定有这个边 dfs(x.first); } } } public: LGraph(bool dir=false) : Graph(dir) { } LGraph(vector<string>v, bool dir=false) : Graph(v, dir){ setAdjL();} LGraph(int n, bool dir=false) : Graph(n, dir){ setAdjL();} void test(){ //测试打印后面名字带编号 与下面的cout << vertices[x.first] 对应 adjL[0].insert(pair<int, int>(1, 90)); adjL[0].insert(pair<int, int>(2, 30)); // for(int i = 0; i < nv; i ++){ cout << vertices[i] << "[" << i << "]-->";//i是顶点的编号,因为可能用ABCD去初始化顶点名字 for(auto x : adjL[i]){ // cout << x.first << "(" << x.second << ") "; //x.first是点的序号 如果要打印名字 就打印vertices[x.first]就好了 cout << vertices[x.first] << "(" << x.second << ") "; } cout << endl; } } virtual void insertV(){ insertV(to_string(nv)); } virtual bool insertV(string v){ bool r = Graph::insertV(v);//在基类就插入失败的话就不用在派生类再插入了 if(!r){//!r <=> r==NULL return false; } //在基类没有增加失败 那么我们就需要在邻接表里面增加一行 adjL.push_back(map<int, int>());//加一行空的 return true; } bool insertE(string src, string dst, int weight = 1){ return Graph::insertE(src, dst, weight);//调用函数顺序有点蛋疼,在此略过笔记- -反正反正我又不学C++hhhh } bool insertE(int src, int dst, int weight = 1){ if(src < 0 || dst < 0 || src >= nv || dst >= nv) return false;//参数不合法 if(adjL[src].find(dst) != adjL[src].end()) return false; //重边,失败 adjL[src].insert(pair<int, int>(dst, weight));//插的是编号 权,key是编号,value是边的权 //如果是无向图的话,是对称的,就可以对称加入,有向图的话就不用再加入了 if(!directed) adjL[dst].insert(pair<int, int>(src, weight)); return true; } //删除边 模仿插入边写 virtual bool removeE(string src, string dst){ //派生类 调用 基类 基类中是纯虚的 又会跑来调用派生类里的 return Graph::removeE(src, dst); } virtual bool removeE(int src, int dst){ if(src < 0 || dst < 0 || src >= nv || dst >= nv) return false; if(adjL[src].find(dst) == adjL[src].end()) return false;//不存在 adjL[src].erase(dst); if(!directed) adjL[dst].erase(src); return true; } void dfs(string v){ Graph::dfs(v);//由父类判断v是否合法,然后就会去调用这个LGraph里面protected的dfs } virtual void bfs(string v){//广度优先搜索 if(iov.find(v) == iov.end()) return; int iv = iov[v]; visited.resize(nv); for(int i = 0; i < nv; i ++) visited[i] = false;//设置缺省值 全部都是没未访问 queue<int> q; cout << vertices[v] << " "; visited[iv] = true; q.push(iv); //开始广度优先搜索 while(!q.empty()){ w = q.front(); q.pop(); for(auto x : adjL[w]){ if(!visited[x.first]){//没访问过,且两点存在边 cout << vertices[x.first] << " ";//打印出来表示访问 visited[x.first] = true; q.push(x.first); } } } } }; int main(){ vector<string> v = {"AA", "BB", "CC", "DD", "EE"}; /* Graph g(v); 编号是A英文字母 */ //编号就是...就是编号- -hhhhhhhhhhh // Graph g(10); 已经变成了虚类(抽象类) MGraph g(v); g.insertE("AA","CC"); g.insertE(1,3); MGraph g2(6); // g.test(); // g2.test(); // g.insertV("XX"); // g.test(); // /*MGraphg(6); // g.insertV();//增加一个点*/ // LGraph g3(v); // g3.test(); // g3.insertV("x"); return 0; } //剩一个findEdge() 留作练习 自己实现 ******最小生成树***** 图G = (V,E) 则图G的生成树(spanning tree): G' = (V,E') 其中E'是E的子集 性质: 生成树是无环连通图(树的定义);生成树有N-1条边; 生成树增加一条边->产生环;减少一条边->不连通; 图G存在生成树等价于图G连通 生成树不唯一 当有权值的时候,各边权值之和最小的树为最小生成树 最小生成树的权值之和唯一,但是这棵树不唯一. ****求最小生成树**** Prim算法适用于稠密图 Kruskal适用于稀疏图(2021.8.3 没看完最小生成树) Prim法 从一个点开始去生成一棵树,将该点加入集合v,从原图剩下的点钟找另一点b,且有点b 到该点集v中的任意一点的权值最小,将b加入点集v,依次循环至原图n个点全部加入,n-1条边加入. 从1个点0条边 到最终 选N-1条边以及N个点 循环n-1次,如果循环过程中不通了,就说明没有最小生成树 #include<iostream> #include"mgraph.h" using namespace std; int main(){ MGraph g; string n1, n2; int weight; int M; cin >> M; for(int i = 0; i < M; i ++){ cin >> n1 >> n2 >> weight; g.insertE(n1, n2, weight); } g.prim("A"); return 0; } //mgraph.cpp中新增函数prim如下 因为和dijstra算法有点像 可以直接拷dijstra来修改 void MGraph::prim(string src){ if(iov.find(src) == iov.end()) return ; vector<int> d(nv, INT_MAX); vector<int> from(nv, -1);//来源 vector<bool> known(nv, false);//点是否加入生成树 d[iov[src]] = 0; int minDis, v; for(int t = 0; t < nv; t ++){ minDis = INT_MAX; v = -1; for(int i = 0; i < nv; i ++){ if(known[i] == false && d[i] < minDis){ minDis = d[i]; v = i; } } if(v == -1) break;//没找到最小的点 known[v] = true; for(int w = 0; w < nv; w ++){ //判断条件:已经有边不用动,不在树里面也不用动,而且这条边的边长比原来那个要短 if(adjM[v][w]!=INT_MAX && !known[w] && adjM[v][w] < d[w]){ d[w] = adjM[v][w]; from[w] = v; } } } }