四、 图
0. 基本定义
度
无向图中,顶点的度是该点上连接的边的数量;有向图中,顶点的度为出度和入度之和。
出度
对于一个点来说,箭头向外指向别的点的边总数为出度。
入度
指向自己的边的总数为入度。
无论是有向图还是无向图,顶点数n,边数e和度数D(v)之间的关系为:
路径
从v1到vn长为Vn-1的路径用顶点序列(v1,...,vn)表示。
1. 存储
邻接矩阵
二维数组存放顶点间的距离。自然支持有向图。
实现:
#include <iostream>
#include <vector>
using namespace std;
int main() {
int n; // 顶点数
cin >> n;
vector<vector<int>> adj_matrix(n, vector<int>(n, 0)); // 初始化邻接矩阵
int m; // 边数
cin >> m;
for (int i = 0; i < m; i++) {
int u, v; // 边的两个顶点
cin >> u >> v;
adj_matrix[u][v] = 1; // 在邻接矩阵中标记该边
adj_matrix[v][u] = 1; // 若是无向图,也要标记反向的边
}
// 输出邻接矩阵
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
cout << adj_matrix[i][j] << " ";
}
cout << endl;
}
return 0;
}
时间复杂度:
该程序的主要时间复杂度在于邻接矩阵的初始化和标记输入的边。初始化邻接矩阵的时间复杂度为O(n^2),标记边的时间复杂度为O(m),其中n为顶点数,m为边数。因此,该程序的总时间复杂度为O(n^2 + m)。
需要注意的是,由于邻接矩阵表示法会在矩阵中标记每一条边,因此当图的边数m较大时,邻接矩阵表示法不太适合用来存储图。此时可以考虑使用邻接表或邻接多重表等更适合存储稀疏图的数据结构。
邻接表
对有向图,链表只存储节点的出度。求入度的时候很不方便。
可以定义一个“逆”邻接表,存储入度。
实现:
#include <iostream>
#include <vector>
using namespace std;
int main() {
int n, m;
cin >> n >> m;
vector<vector<int>> adj(n+1);
for (int i = 0; i < m; i++) {
int u, v;
cin >> u >> v;
adj[u].push_back(v);
adj[v].push_back(u);
}
for (int u = 1; u <= n; u++) {
cout << u << ": ";
for (int v : adj[u])
cout << v << " ";
cout << endl;
}
return 0;
}
时间复杂度分析:
- 邻接表的初始化需要O(n)的时间;
- 读入每条边并将其插入邻接表需要O(m)的时间;
- 遍历邻接表并输出每个节点的邻居列表需要O(n + m)的时间。
因此,总时间复杂度为O(n + m)。
十字链表
将有向图的正邻接表和逆邻接表连接起来,得到十字链表。
2. 图的搜索
深度优先搜索
对应在树中的先序遍历。区别是DFS有一个标志标记节点是否访问过,先序遍历不存在这种标记。
具体描述:设无向图G,G中所有的顶点都设置为未访问过。选定一个节点G,访问所有与G相邻的节点。这之后,如果仍然存在未被访问过的节点,在这些未被访问过的节点中任意选择一个,递归地进行上述操作,直到所有节点都被访问。
#include <iostream>
#include <vector>
#include <stack>
using namespace std;
void dfs(vector<vector<int>>& graph, int start) {
vector<bool> visited(graph.size(), false);
stack<int> s;
s.push(start);
while (!s.empty()) {
int cur = s.top();
s.pop();
if (!visited[cur]) {
visited[cur] = true;
cout << cur << " ";
for (int neighbor : graph[cur]) {
if (!visited[neighbor]) {
s.push(neighbor);
}
}
}
}
}
int main() {
vector<vector<int>> graph = {{1, 2}, {0, 2, 3}, {0, 1, 3}, {1, 2}};
int start = 0;
dfs(graph, start);
return 0;
}
时间复杂度分析:
在深度优先搜索中,每个节点最多被访问一次,因此时间复杂度为O(V+E),其中V为节点数,E为边数。具体地,每个节点需要入栈和出栈一次,时间复杂度为O(2V);对于每个节点的邻居节点,最多被访问一次,时间复杂度为O(E)。因此总时间复杂度为O(2V+E),即O(V+E)。
值得注意的是,由于深度优先搜索使用了递归或者栈来保存需要访问的节点,在最坏情况下可能需要使用O(V)的空间。如果要搜索图中的所有节点,建议使用邻接表实现,以减少空间占用。
广度优先搜索
对应在树中的层序遍历。区别是BFS有一个标志标记节点是否访问过,层序遍历不存在这种标记。
- 从起始节点开始遍历,将其加入队列中,并标记为已访问。
- 从队列中取出队头节点u,遍历u的所有邻居节点v,并检查v是否已被访问过。
- 如果v未被访问过,将其添加到队列中,并标记为已访问。
- 重复步骤2和3,直到队列为空为止。
在BFS遍历过程中,队列的作用是存储已经访问过但邻居节点未被访问过的节点。当遍历某一节点时,将其邻居节点加入队列,遍历完邻居节点后再从队列中取出下一个节点进行遍历,这样可以保证每个节点被访问的顺序符合BFS的规则:先访问距离起始节点近的节点,再访问距离起始节点较远的节点。
#include <queue>
#include <vector>
using namespace std;
void bfs(vector<vector<int>>& graph, int start) {
queue<int> q;
vector<bool> visited(graph.size(), false);
q.push(start);
visited[start] = true;
while (!q.empty()) {
int node = q.front();
q.pop();
// 处理当前节点
for (int i = 0; i < graph[node].size(); i++) {
int neighbor = graph[node][i];
if (!visited[neighbor]) {
q.push(neighbor);
visited[neighbor] = true;
}
}
}
}
时间复杂度:
该程序使用广度优先搜索遍历一个无向图,其时间复杂度为 O(|V|+|E|),其中 |V| 是图中节点数,|E| 是边数。具体分析如下:
- 首先,对于每个节点,只会被访问一次,因此时间复杂度为 O(|V|)。
- 其次,对于每条边 (u,v),都会被访问一次,因为可以从 u 到达 v,也可以从 v 到达 u,因此时间复杂度为 O(|E|)。
- 综上,广度优先搜索的时间复杂度为 O(|V|+|E|)。
需要注意的是,在稠密图(即 $|E|$ 接近 $|V|^2$)中,$|E|$ 的复杂度项会主导时间复杂度,而在稀疏图(即 $|E|$ 远小于 $|V|^2$)中,$|V|$ 的复杂度项会主导时间复杂度。因此,在不同的场景下需选择不同的算法来处理图形问题。
连通图与连通分量
按照深度优先或广度优先的顺序对图G中的顶点遍历,得到的若干个子树成为深度/广度优先生成森林。森林中每个树对应了G的一个连通子图,称为G的一个联通分量。
如果图是连通图,那么森林中只能得到一棵树。
对无向图进行深度优先搜索的结果,可以把边分为树边和回退边两大类。树边就是遍历时候经过的边。有向图中,树边由遍历编号小的点指向大号点,回退边由大号点指向小号点。
强连通分量
在有向图G中,如果两个顶点u,v间有一条从u到v的有向路径,同时还有一条从v到u的有向路径,则称两个顶点强连通。如果有向图G的每两个顶点都强连通,称G是一个强连通图。有向非强连通图的极大强连通子图,称为强连通分量。
联通而无环的无向图称为开放树。
性质:
1. 具有n>=1个顶点的开放树包含n-1个边。
2. 如果在开放树中任意增加一条边,将构成一个环路。
3. 应用问题
1)最小生成树(无向图)
CS61B 数据结构与算法笔记 20 最小生成树-CSDN博客
kruskal
主要步骤如下
- 将边按照权重从小到大排列
- 枚举第一个边,加入MST里,判断是否成环
- 如果成环则跳过,否则确定这条边为MST里的
- 继续枚举下一条边,直到所有的边都枚举完
数据结构:
1. 存储边。
2. 判断环的时候可以采用并查集(DisjointSet)。
并查集:
cs61b数据结构与算法学习笔记 9. DisjointSets(并查集)-CSDN博客
原理
(书上描述如下)
起初,令图T由G中的n个点构成,没有边。这样,T的每个顶点自身构成一个连通分量。然后,按照边的权递增顺序,递归遍历E中每个边,如果边连接两个属于不同分量的顶点,则把两个分量合并在一起;如果边连接两个相同分量的顶点,则说明连上后会形成环,则跳过这条边继续遍历。当T中的连通分量为1时,说明遍历完毕。
实现:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
struct Edge {
int from, to, weight;
Edge(int f, int t, int w) : from(f), to(t), weight(w) {}
bool operator<(const Edge& other) const {
return weight < other.weight;
}
};
vector<int> parent;
int find(int x) {
if (parent[x] == x) return x;
return parent[x] = find(parent[x]); // 路径压缩
}
void unionSet(int x, int y) {
int px = find(x);
int py = find(y);
parent[px] = py;
}
int kruskal(vector<Edge>& edges) {
int n = parent.size();
sort(edges.begin(), edges.end());
int sum = 0;
for (int i = 0; i < edges.size(); i++) {
int f = edges[i].from;
int t = edges[i].to;
int w = edges[i].weight;
if (find(f) == find(t)) continue;
unionSet(f, t);
sum += w;
if (find(0) == find(n - 1)) break; // 最小生成树已经包含所有节点,提前结束
}
return sum;
}
int main() {
int n, m;
cin >> n >> m;
parent.resize(n);
for (int i = 0; i < n; i++) parent[i] = i;
vector<Edge> edges;
for (int i = 0; i < m; i++) {
int u, v, w;
cin >> u >> v >> w;
edges.push_back(Edge(u, v, w));
}
int ans = kruskal(edges);
cout << ans << endl;
return 0;
}
时间复杂度
- 初始化 parent 数组的循环,时间复杂度为 O(|V|),其中 |V| 是图中节点数。
- 排序函数 sort,时间复杂度为 O(|E|\log|E|),其中 |E| 是图中边数。
- Kruskal 算法的主循环,需要遍历所有的边,时间复杂度为 O(|E|\alpha(|E|)),其中 \alpha(x) 是反着来看Ackermann函数的一个非常慢的增长函数,可以看作是 O(1)。因此,这一部分的时间复杂度可以看作是 O(|E|\log|E|)。
- 因此,整个程序的时间复杂度为 O(|E|\log|E|)。需要注意的是,在稀疏图中(即 |E| 远小于 |V|^2),Kruskal 算法的时间复杂度相对较低,而在稠密图中(即 |E| 接近 |V|^2),Kruskal 算法的时间复杂度可能会比 Prim 算法更高。
prim
相当于广度优先搜索(?)
原理
步骤如下:
1. 从某个任意的起始节点开始。
2. 重复添加连接在已遍历子图和未遍历子图之间的最短边。
3. 重复,直到有 V-1 条边。
实现:
例:
1. 链式前量星存储图。
2. 优先队列
括号里第一个元素是节点标号,第二个是距开始节点的最短距离(一开始都为无穷)。
3. 数组:
其中distTo为该点距开始节点的最短距离,edgeTo为在目前的最小生成树中指向该节点的上一个节点。
void prim(MGraph g,int v)
{
int lowcost[MAXV],min,n=g.vexnum;
int closest[MAXV],i,j,k;
for (i=0;i<n;i++) //给lowcost[]和closest[]置初值
{
lowcost[i]=g.edges[v][i];
closest[i]=v;
}
for (i=1;i<n;i++) //找出n-1个顶点
{
min=INF;
for (j=0;j<n;j++) //在(V-U)中找出离U最近的顶点k
if (lowcost[j]!=0 && lowcost[j]<min)
{
min=lowcost[j];k=j;
}
printf(" 边(%d,%d)权为:%d\n",closest[k],k,min);
lowcost[k]=0; //标记k已经加入U
for (j=0;j<n;j++) //修改数组lowcost和closest
if (g.edges[k][j]!=0 && g.edges[k][j]<lowcost[j])
{
lowcost[j]=g.edges[k][j];closest[j]=k;
}
}
}
时间复杂度分析:
- Prim算法的总时间复杂度为 O(m²),m为节点数。(重点)
注:
O(log n)是指以2为底的对数计算,输入规模翻倍,操作次数只增加一。即每操作一次,需要处理的规模就小一半。如二分查找。
推广:最大生成树
最大生成树和最小生成树几乎一样。当你用kruskal算最小生成树的时候,每次选取最小的边。那么每次选最大的边就是最大生成树。
2. 最短路径
Dijkstra:单元最短路
CS61B 19 最短路:Dijkstra与A*-CSDN博客
算法实现:
#include <iostream>
#include <vector>
#include <queue>
#include <limits>
using namespace std;
const int INF = numeric_limits<int>::max();
void dijkstra(vector<vector<pair<int, int>>>& graph, int start, vector<int>& dist) {
int n = graph.size();
vector<bool> visited(n, false);
dist.resize(n, INF);
dist[start] = 0;
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;
pq.push({0, start});
while (!pq.empty()) {
int u = pq.top().second;
pq.pop();
if (visited[u]) continue;
visited[u] = true;
for (auto e : graph[u]) {
int v = e.first;
int w = e.second;
if (dist[u] + w < dist[v]) {
dist[v] = dist[u] + w;
pq.push({dist[v], v});
}
}
}
}
int main() {
int n, m;
cin >> n >> m;
vector<vector<pair<int, int>>> graph(n);
for (int i = 0; i < m; i++) {
int u, v, w;
cin >> u >> v >> w;
graph[u].push_back({v, w});
graph[v].push_back({u, w}); // 如果是无向图,要加入反向边
}
vector<int> dist;
dijkstra(graph, 0, dist);
for (int i = 0; i < n; i++) {
cout << "Distance from 0 to " << i << ": " << dist[i] << endl;
}
return 0;
}
时间复杂度:
上述代码使用了 Dijkstra 算法来求解单源最短路径,时间复杂度为 O(|E|+|V|\log|V|),其中 |E| 表示边数,|V| 表示顶点数。具体时间复杂度分析如下:
1. 初始化 dist 数组,时间复杂度为 O(|V|)。
2. 初始化 priority_queue,加入起点,时间复杂度为 O(\log|V|)。
3. 进入 while 循环,最多会执行 |V| 次,因为每个顶点最多被访问一次。所以 while 循环的时间复杂度为 O(|V|)。
4. 在 while 循环内部,会遍历当前顶点的邻接表,遍历的次数不超过 |E|,因为每条边只会被遍历一次。所以遍历邻接表的时间复杂度为 O(|E|)。
5. 在遍历邻接表的过程中,会对 priority_queue 进行插入和弹出操作,时间复杂度为 O(\log|V|)。
综上所述,Dijkstra 算法的时间复杂度为 O(|E|+|V|\log|V|)。
书上: Dijkstra计算两点之间最短路时间复杂度为O(n^2)
Floyd:每一对点的最短路
实现:
1.图G的邻接矩阵C[ ][ ]:
0 若i=j;
无穷 若i,j不相邻且i不等于j
边ij的权重(长度) 若ij相邻
2. 矩阵A[ ][ ]: A[i][j]是i到j的最短路径长度。它的生成如下:
初始时,矩阵A和矩阵C相同。接着,对矩阵A进行N次更新。当第k遍处理时,A[i][j]由下式得出:
其中Ak和Ak-1分别表示第k次和第k-1次处理时A的值。
允许有负权,不允许有负回路。
代码实现:
#include <iostream>
#include <vector>
#include <limits>
using namespace std;
const int INF = numeric_limits<int>::max();
void floyd(vector<vector<int>>& graph) {
int n = graph.size();
for (int k = 0; k < n; k++) {
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (graph[i][k] != INF && graph[k][j] != INF) {
graph[i][j] = min(graph[i][j], graph[i][k] + graph[k][j]);
}
}
}
}
}
int main() {
int n, m;
cin >> n >> m;
vector<vector<int>> graph(n, vector<int>(n, INF));
for (int i = 0; i < m; i++) {
int u, v, w;
cin >> u >> v >> w;
graph[u][v] = w;
graph[v][u] = w; // 如果是无向图,要加入反向边
}
floyd(graph);
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (graph[i][j] == INF) {
cout << "INF" << " ";
} else {
cout << graph[i][j] << " ";
}
}
cout << endl;
}
return 0;
}
Floyd的时间复杂度为
可求出有向图的中心点。
warshall算法
warshall算法计算矩阵A:当且仅当有一条路径从[i]到[j]时,A[i][j]=1; 没有则为0。该矩阵A称为邻接矩阵C的传递闭包。
#include <iostream>
#include <vector>
#include <limits>
using namespace std;
const int INF = numeric_limits<int>::max();
void floyd(vector<vector<int>>& graph) {
int n = graph.size();
for (int k = 0; k < n; k++) {
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (graph[i][k] != INF && graph[k][j] != INF) {
graph[i][j] = min(graph[i][j], graph[i][k] + graph[k][j]);
}
}
}
}
}
int main() {
int n, m;
cin >> n >> m;
vector<vector<int>> graph(n, vector<int>(n, INF));
for (int i = 0; i < m; i++) {
int u, v, w;
cin >> u >> v >> w;
graph[u][v] = w;
graph[v][u] = w; // 如果是无向图,要加入反向边
}
floyd(graph);
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (graph[i][j] == INF) {
cout << "INF" << " ";
} else {
cout << graph[i][j] << " ";
}
}
cout << endl;
}
return 0;
}
warshall算法的时间复杂度为。
求有向图的中心点
1. 偏心度:对于任意一个顶点k,称E(k)=max{d[i][k] | i in V}为顶点k的偏心度。
2. 中心点:称具有最小偏心度的顶点称为图的中心点。
用Floyd求有向图中心点的方法:
1. 对加权有向图,调用Floyd算法,求每对顶点间最短路径的矩阵D。
2. 对矩阵D的每列j求最大值:
即为各个顶点j的偏心度。
3. 求出具有最小偏心度的顶点k:
第k个点即为图G的中心点,偏心度为E(k)。
prim和Dijkstra区别:
- prim: 边的最小值
- Dijkstra: 路径总长度的最小值
两者的区别在于,每次更新路径的不一样:
- prim更新的是未标记集合到已标记集合之间的距离
- Dijkstra更新的是源点到未标记集合之间的距离
3. 拓扑排序(有向图)
相当于顶点约束
讨论范围是无环有向图。
拓扑排序算法由广度优先算法演变得到。
拓扑序列必须满足下列两个条件:
- 每个顶点出现且仅出现一次。
- 若存在从A到B的路径,则在序列中A在B的前面。
通过无环有向图表达这种优先关系,这样的图叫做AOV(activity on vertex)网。
top排序实现:
定义两个辅助数组结构分别用来存放各顶点入度和记录拓扑排序的顶点序号。
从无入度的顶点开始,将所有无入度的顶点依次输出并从已有图中摘除,同时将该顶点指向其他顶点的边删除(将被指向的点的入度-1),递归进行上述步骤,最终输出的序列即为拓扑排序序列。
怎样算各个点的入度:
用邻接表作为图的表示方式,并在拓扑排序前先计算出每个节点的入度。
具体做法如下:
-
定义一个数组 in-degree,长度为总结点数,初始值全部为 0。
-
遍历图中所有的边,对于一条边 (u,v),将节点 v 的入度加 1。
-
最终得到 in-degree 数组,in-degree[i] 表示节点 i 的入度。
代码实现:
#include<iostream>
#include<vector>
#include<queue>
using namespace std;
const int MAXN = 10005;
vector<int> G[MAXN];
int in_degree[MAXN];
void top_sort(int n) {
queue<int> q;
for(int i=1; i<=n; i++) {
if(in_degree[i] == 0) {
q.push(i);
}
}
while(!q.empty()) {
int u = q.front();
q.pop();
cout << u << " ";
for(int i=0; i<G[u].size(); i++) {
int v = G[u][i];
in_degree[v]--;
if(in_degree[v] == 0) {
q.push(v);
}
}
}
}
int main() {
int n, m;
cin >> n >> m;
for(int i=0; i<m; i++) {
int u, v;
cin >> u >> v;
G[u].push_back(v);
in_degree[v]++;
}
top_sort(n);
return 0;
}
时间复杂度:O(n+m)
是否存在唯一的拓扑排序?
有些有向无环图的拓扑排序序列结果并不唯一。
4. 关键路径(有向图)
相当于在边上加权+顶点约束 最长路
定义:边的活动网(AOE网):在带权的有向图中,用顶点表示事件,边表示活动,权表示活动的持续时间。
- 每个事件表示在它之前的活动已经完成,在它之后的活动可以开始。
- 由于活动只有一个开始点和一个结束点,所以在正常情况下,AOE网只有一个入度为0的点,称为起始点(源点);一个出度为0的点,称为结束点(汇点)。
对于AOE网来说,重要问题有:
- 完成整个工程需要多长时间?
- 哪些活动时影响工程进度的关键活动?
定义1:关键路径:在AOE网中,由于有些活动可以同时进行,所以完成工程的最少时间是从起始点到结束点最长路径的长度(这条路径上所有活动持续时间之和),把从起始点到结束点具有最大长度的路径称为关键路径。
关键路径可能不止一条。
定义2:最早发生时间:从起始点到某点的最长路径长度,称为该事件的最早发生时间。
定义3:最早开始时间E(i):点Vi的最早发生时间 = 以vi为起点的所有边所表示的活动的最早开始时间。
定义4:最迟开始时间L(i):不使整个工程完成时间拖延的最晚开始时间,计算:
边Ai的最迟开始时间 = 关键路径长度 - 边Ai的权 - 从边Ai指向节点到结束点的所有边的权之和
定义5:关键活动:把满足等式E(i)=L(i)的所有活动称为关键活动。
定义6:时间余量:L(i) - E(i) ,即在不增加完成工程所需总时间的情况下,允许活动ai延缓的时间。可见,缩短非关键活动的持续时间并不能缩短整个工程的完成时间。
分析关键路径的目的是识别哪些活动是关键活动。如果一个关键活动不在所有的关键路径上,那么缩短这个关键活动的持续时间们,并不能缩短整个工程的完成时间。如果一个活动在所有的关键路径上,那么缩短这一活动的持续时间就能缩短整个工程的完成时间。
实现:
利用拓扑排序 + 邻接表很容易就可以实现关键路径。
/*---------关键路径算法---------*/
Status CriticalPath(ALGraph& G) {
//G为邻接表存储的有向图,输出G的各项关键活动
if (!TopologicalSort(G, topo)) return ERROR;
//调用拓扑排序算法,使拓扑序列保存在topo中,若调用失败,则存在有向环,返回ERROR
int n = G.vexnum;//n为顶点个数
for (int i = 0; i < n; i++)//给每个事件的最早发生时间置初值为0
ve[i] = 0;
/*-------------按拓扑序列求每个事件的最早发生时间---------------*/
for (int i = 0; i < n; i++) {
int k = topo[i];//取得拓扑序列中的顶点序号k
ArcNode* p = new ArcNode;
p = G.vertices[k].firstarc;//p指向k的第一个邻接顶点
while (p != NULL) {
int j = p->adjvex;//j为邻接顶点的序号
if (ve[j] < ve[k] + p->weight)//更新顶点j的最早发生时间ve[j]
ve[j] = ve[k] + p->weight;
p = p->nextarc;//p指向k的下一个邻接顶点
}
}
for (int i = 0; i < n; i++)
vl[i] = ve[n - 1];//给每个事件的最迟发生时间置初值ve[n-1]
/*-----------按拓扑次序求每个事件的最迟发生时间-----------*/
for (int i = n - 1; i >= 0; i--) {
int k = topo[i];//取得拓扑序列中的顶点序号k
ArcNode* p = new ArcNode;
p = G.vertices[k].firstarc;//p指向k的第一个邻接顶点
while (p != NULL) {//根据k的邻接点,更新k的最迟发生时间
int j = p->adjvex;//j为邻接顶点的序号
if (vl[k] > vl[j] - p->weight)//更新顶点k的最迟发生时间vl[k]
vl[k] = vl[j] - p->weight;
p = p->nextarc;//p指向k的下一个邻接顶点
}
}
/*-----------判断每一个活动是否为关键活动--------------*/
for (int i = 0; i < n; i++) {
ArcNode* p = new ArcNode;
p = G.vertices[i].firstarc;//p指向i的第一个邻接顶点
while (p != NULL) {
int j = p->adjvex;//j为邻接顶点的序号
int e = ve[i];//计算活动<vi,vj>的最早开始时间
int l = vl[j] - p->weight;//计算活动<vi,vj>的最迟开始时间
if (e == l)
cout << G.vertices[i].data << G.vertices[j].data << endl;
p = p->nextarc;//p指向i的下一个邻接顶点
}
}
}