图论基础

图论

相关概念

图 (Graph) 是一个二元组\(G=(V(G),E(G))\) 。其中\(V(G)\) 是非空集,称为 点集 (Vertex set) ,对于 \(V\)中的每个元素,我们称其为 顶点 (Vertex)节点 (Node) ,简称 \(E(G)\)\(V(G)\) 各结点之间边的集合,称为 边集 (Edge set)

常用\(G=(V,E)\) 表示图。

\(V,E\) 都是有限集合时,称\(G\)有限图 。当 \(V\)\(E\) 是无限集合时,称 \(G\)无限图

图有多种,包括 无向图 (Undirected graph)有向图 (Directed graph)混合图 (Mixed graph) 等。

\(G\)为无向图,则\(E\) 中的每个元素为一个无序二元组\((u,v)\) ,称作 无向边 (Undirected edge) ,简称 边 (Edge) ,其中\(u,v\in V\) 。设 \(e=(u,v)\),则\(u\)\(v\) 称为 的 端点 (Endpoint)

\(G\) 为有向图,则\(E\) 中的每一个元素为一个有序二元组\((u,v)\),有时也写作\(u\rightarrow v\) ,称作 有向边 (Directed edge)弧 (Arc) ,在不引起混淆的情况下也可以称作 边 (Edge) 。设\(e=u\rightarrow v\) ,则此时 \(u\)称为 \(e\)起点 (Tail) , 称\(v\)\(e\)终点 (Head) ,起点和终点也称为 \(e\)端点 (Endpoint)

\(G\) 为混合图,则 \(E\)中既有向边,又有无向边。

\(G\)的每条边\(e_k=(u_k,v_k)\) 都被赋予一个数作为该边的 ,则\(G\)称为 赋权图 。如果这些权都是正实数,就称 \(G\)正权图

若一张图的边数远小于其点数的平方,那么它是一张 稀疏图 (Sparse graph)

若一张图的边数接近其点数的平方,那么它是一张 稠密图 (Dense graph)

连通:在一个图中,如果从顶点U到顶点V有路径,则称U和V是连通的;

连通图:如果一个无向图中,任意两个顶点之间都是连通的,则称该无向图为连通图。否则称为非连通图;

连通分量:一个无向图的连通分支定义为该图的最大连通子图,左图的连通分量是它本身。

强连通图:在一个有向图中,对于任意两个顶点U和V,都存在着一条从U到V的有向路径,同时也存在着一条从V到U的有向路径,则称该有向图为强连通图;右图不是一个强连通图。

强连通分量:一个有向图的强连通分量定义为该图的最大的强连通 ,右图含有两个强连通分量,一个是1和2构成的一个子图,一个是3独立构成的一个子图。

形象地说,图是由若干点以及连接点与点的边构成的。

图的存储

直接存边

方法

使用一个数组来存边,数组中的每个元素都包含一条边的起点与终点(带边权的图还包含边权)。(或者使用多个数组分别存起点,终点和边权。)

#include <iostream>
#include <vector>
using namespace std;
struct Edge {//存储一条边所连的两个节点
  int u, v;
};
int n, m;
vector<Edge> e;
vector<bool> vis;
bool find_edge(int u, int v) //判断u,v是否相连,即是否存在一边端点分别为u,v
{
    for(int i = 1; i <= m; ++i)
        if (e[i].u == u && e[i].v == v) return true;
    return false;//枚举所有边都没有一边分别连u,v,则返回false
}

void dfs(int u) 
{
  if (vis[u]) return;//访问过就不再访问,去重
  vis[u] = true;//标记为访问过
  for(int i = 1; i <= m; ++i) 
      if (e[i].u == u) dfs(e[i].v);//有以u为起点的边,则遍历这条边的终点
}

int main() 
{
  cin >> n >> m;
  vis.resize(n + 1, false);//resize() 重新指定容器有效的元素个数为n+1,令为false
  e.resize(m + 1);
  for(int i = 1; i <= m; i++)   cin >> e[i].u >> e[i].v;
  return 0;
}

复杂度

查询是否存在某条边:\(O(m)\)

遍历一个点的所有出边:\(O(m)\)

遍历整张图: \(O(nm)\)

空间复杂度:\(O(m)\)

应用

由于直接存边的遍历效率低下,一般不用于遍历图。

在 Kruskal 算法中,由于需要将边按边权排序,需要直接存边。

在有的题目中,需要多次建图(如建一遍原图,建一遍反图),此时既可以使用多个其它数据结构来同时存储多张图,也可以将边直接存下来,需要重新建图时利用直接存下的边来建图。


邻接矩阵

方法

使用一个二维数组 adj 来存边,其中 adj[u][v] 为 1 表示存在\(u\)\(v\) 的边,为 0 表示不存在。如果是带边权的图,可以在 adj[u][v] 中存储 \(u\)\(v\) 的边的边权。

STL

#include <iostream>
#include <vector>
using namespace std;
int n, m;
vector<bool> vis;
vector<vector<bool> > adj;//二维布尔数组
bool find_edge(int u, int v) { return adj[u][v]; }//判断u,v是否相连
void dfs(int u) 
{
  if (vis[u]) return;//访问过就不再访问,去重
  vis[u] = true;//标记为访问过
  for (int v = 1; v <= n; ++v) 
      if (adj[u][v]) dfs(v);//如果有以u为起点的边,则遍历这条边的终点
}

int main() 
{
  cin >> n >> m;
  vis.resize(n + 1, false);//resize() 重新指定容器有效的元素个数为n+1,令为false
  adj.resize(n + 1, vector<bool>(n + 1, false));//重新指定容器有效的元素个数为n+1,令为布尔数组为0
  for (int i = 1; i <= m; i++) 
  {
    int u, v;
    cin >> u >> v;
    adj[u][v] = true;
    adj[v][u] = true;//当为无向图时
  }
  return 0;
}
#include <iostream>
#include <vector>
using namespace std;
int a[1001],n,m;
void vis[1001];

void dfs(int u) 
{
  if (vis[u]) return;
  vis[u] = true;
  for(int v = 1; v <= n; ++v) 
      if (a[u][v]) dfs(v);//如果有以u为起点的边,则遍历这条边的终点
}

int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
       for(int j=1;j<=n;j++)
          a[i][j]=0x7fffffff;
    for(int k=1;k<=m;k++)
    {
      cin>>i>>j>>w;//i连到j的边,权值为W
      a[i][j]=w;
      a[j][i]=w;//如果是无向图,加上
    }
}

复杂度

查询是否存在某条边:\(O(1)\)

遍历一个点的所有出边: $ O(n) $

遍历整张图: \(O(n^2)\)

空间复杂度: \(O(n^2)\)

应用

邻接矩阵只适用于没有重边(或重边可以忽略)的情况。

其最显著的优点是可以\(O(1)\) 查询一条边是否存在。

由于邻接矩阵在稀疏图上效率很低(尤其是在点数较多的图上,空间无法承受),所以一般只会在稠密图上使用邻接矩阵


邻接表

方法

使用一个支持动态增加元素的数据结构构成的数组,如 vector adj[n + 1] 来存边,其中 adj[u] 存储的是点\(u\) 的所有出边的相关信息(终点、边权等)。

#include <iostream>
#include <vector>
using namespace std;
int n, m;
vector<bool> vis;
vector<vector<int> > adj;

bool find_edge(int u, int v) 
{
  for(int i = 0; i < adj[u].size();i++) //size()目前容器正拥有的元素个数
      if (adj[u][i] == v)   return true;
  return false;
}

void dfs(int u) 
{
  if (vis[u]) return;
  vis[u] = true;
  for (int i = 0; i < adj[u].size();i++)   dfs(adj[u][i]);
}

int main() 
{
  cin >> n >> m;
  vis.resize(n + 1, false);
  adj.resize(n + 1);
  for (int i = 1; i <= m; ++i) 
  {
    int u, v;
    cin >> u >> v;
    adj[u].push_back(v);//在最后添加元素
  }
  return 0;
}

复杂度

查询是否存在\(u\)\(v\)的边:\(O(d^+(u))\) (如果事先进行了排序就可以使用二分查找做到 )。

遍历点\(u\)的所有出边:\(O(d^+(u))\)

遍历整张图: \(O(n+m)\)

空间复杂度: \(O(m)\)

应用

存各种图都很适合,除非有特殊需求(如需要快速查询一条边是否存在,且点数较少,可以使用邻接矩阵)。

尤其适用于需要对一个点的所有出边进行排序的场合。


数组模拟邻接表(推荐)

#include <iostream>
using namespace std;
const int maxn=1001,maxm=100001;
struct Edge
{
	int next;       //下一条边的编号 
	int to;         //这条边到达的点
    int dis;        //这条边的长度	                           
}edge[maxm];
int head[maxn],num_edge,n,m,u,v,dis
void add_edge(int from,int to,int dis)      //加入一条从from到to的单向边 
{
    edge[++num_edge].next=head[from];//head[i]代表点i边集中的第一条边编号,对应链表的head指针
    edge[num_edge].to=to;            //next[i]代表边i的下一条边编号,对应链表中的next指针
    edge[num_edge].dis=dis;          //to[i]代表边i到达的结点
    head[from]=num_edge;             //dis[i]代表边i的权值
}
int main()
{
      num_edge=0;
      scanf("%d %d",&n,&m);      //读入点数和边数
      for(int i=1;i<=m;i++)
	{
	    scanf("%d %d",&u,&v,&dis)//u、v之间有一条边 
	    add_edge(u,v,dis);          //u到v连一条边 
	}
 }

链式前向星

方法

本质上是用链表实现的邻接表,代码如下:

#include <iostream>
#include <vector>
using namespace std;
int n, m;
vector<bool> vis;
vector<int> head, nxt, to;
void add(int u, int v) 
{
  nxt.push_back(head[u]);
  head[u] = to.size();
  to.push_back(v);
}

bool find_edge(int u, int v)
{
  for (int i = head[u]; ~i; i = nxt[i]) 
  {  // ~i 表示 i != -1
    if (to[i] == v) return true;
  }
  return false;
}

void dfs(int u) 
{
  if (vis[u]) return;
  vis[u] = true;
  for (int i = head[u]; ~i; i = nxt[i]) dfs(to[i]);
}

int main() 
{
  cin >> n >> m;
  vis.resize(n + 1, false);
  head.resize(n + 1, -1);
  for (int i = 1; i <= m; ++i) 
  {
    int u, v;
    cin >> u >> v;
    add(u, v);
  }
  return 0;
}

复杂度

查询是否存在\(u\)\(v\) 的边:\(O(d^+(u))\)

遍历点 的所有出边: \(O(d^+(u))\)

遍历整张图: \(O(n+m)\)

空间复杂度: $ O (m)$

应用

存各种图都很适合,但不能快速查询一条边是否存在,也不能方便地对一个点的出边进行排序。

优点是边是带编号的,有时会非常有用,而且如果 cnt 的初始值为奇数,存双向边时 i ^ 1 即是 i 的反边


图的遍历

DFS

首先访问一个邻接结点,然后以这个被访问的邻接结点作为初始结点,访问它的第一个邻接结点。

先纵向深入,再横向访问。

void dfs(int u) 
{
  vis[u] = 1;
  for (int i = head[u]; i; i = edge[i].next)
    if (!vis[edge[i].to]) dfs(v);
}

BFS

图论中常用的遍历方法,先将一个邻接点放入队列,弹出后将其邻接结点放入队列。

void bfs(int u) 
{
  while (!Q.empty()) Q.pop();//清空队列
  Q.push(u);//放入点u
  vis[u] = 1;//u点被访问过
  d[u] = 0;//u点路径为0
  p[u] = -1;//u点的上一个点没有
  while (!Q.empty()) 
  {
    u = Q.front();//取出队头
    Q.pop();//弹出队头
    for (int i = head[u]; i; i = edge[i].next) //遍历
    {
      if (!vis[edge[i].to]) //邻接结点没有访问过
      {
        Q.push(edge[i].to);//入队
        vis[edge[i].to] = 1;//标记为访问过
        d[edge[i].to] = d[u] + 1;//路径加1
        p[edge[i].to] = u;//上一个点是u
      }
    }
  }
}

最短路问题

定义:在图中,每一条边或点有一定的权值(可能是正,也可能为负),找出一条从A到B的路径,使权值和最小

性质:对于边权为正的图,任意两个结点之间的最短路,不会经过重复的结点。

​ 对于边权为正的图,任意两个结点之间的最短路,不会经过重复的边。

​ 对于边权为正的图,任意两个结点之间的最短路,任意一条的结点数不会超过 \(n\),边数不会超过 \(n-1\)

Floyd算法 \(O(N^3)\)

用来求任意两个结点之间的最短路的。

复杂度比较高,但是常数小,容易实现。(我会说只有三个 for 吗?)

适用于任何图,不管有向无向,边权正负,但是最短路必须存在。(不能有个负环)

我们定义一个数组 f[k][x][y] ,表示只允许经过结点\(1\)\(k\) ,结点\(x\) 到结点\(y\) 的最短路长度。

很显然, f[n][x][y] 就是结点\(x\)到结点\(y\)的最短路长度。

\[f[k][x][y] = min(f[k-1][x][y], f[k-1][x][k]+f[k-1][k][y]) \]

上面两行都显然是对的,然而这个做法空间是 \(O(N^3)\)

但我们发现数组的第一维k是没有用的,于是可以直接改成\(f[x][y] = min(f[x][y], f[x][k]+f[k][y])\)

时间复杂度是\(O(N^3)\) ,空间复杂度是 \(O(N^2)\)

for(k = 1; k <= n; k++) //循环中间点
  for(i = 1; i <= n; i++) //起点
    for(j = 1; j <= n; j++)//终点
       f[i][j] = min(f[i][j], f[i][k] + f[k][j]);//如果有中间点k,使i——>j更优,则更换

Bellman-Ford算法 \(O(NM)\)

给定一个有向图,若对图中每一条边(x,y,z),有dis[y]<=dis[x]+z成立

枚举每条边(x,y,z),若dis[y]>dis[x]+z,则用dis[x]+z更新dis[y]

有向图

#include<iostream>
#include<cstdio>
using namespace std;
#define MAX 0x3f3f3f3f
#define N 1010
int n, m, from; //点,边,起点
typedef struct Edge //边
{
    int u, v;
    int cost;
}Edge;
Edge edge[N];
int dis[N], pre[N];
bool Bellman_Ford()
{
    for(int i = 1; i <= n; ++i) //初始化
	dis[i] = (i == from ? 0 : MAX);
    for(int i = 1; i <= n - 1; ++i)
	for(int j = 1; j <= m; ++j)
	    if(dis[edge[j].v] > dis[edge[j].u] + edge[j].cost) //松弛(顺序一定不能反~)
		{
		     dis[edge[j].v] = dis[edge[j].u] + edge[j].cost;
		     pre[edge[j].v] = edge[j].u;
		}
     bool flag = 1; //判断是否含有负权回路
     for(int i = 1; i <= m; ++i)
	if(dis[edge[i].v] > dis[edge[i].u] + edge[i].cost)
          {
	       flag = 0;
	       break;
          }
     return flag;
}
 
void print_path(int root) //打印最短路的路径(反向)
{
     while(root != pre[root]) //前驱
     {
	printf("%d-->", root);
	root = pre[root];
     }
     if(root == pre[root])
     printf("%d\n", root);
}
 
int main()
{
     scanf("%d%d%d", &n, &m, &from);
     pre[from] = from;
     for(int i = 1; i <= m; ++i)
	{
	    scanf("%d%d%d", &edge[i].u, &edge[i].v, &edge[i].cost);
	}
     if(Bellman_Ford())
          for(int i = 1; i <= n; ++i) //每个点最短路
		{
			printf("%d\n", dis[i]);
			printf("Path:");
			print_path(i);
		}
     else
	printf("have negative circle\n");
	return 0;
}

SPFA算法 \(O(kE)\)

使用一个队列,将源点s入队,对队列中每个元素,BFS其出边进行松弛操作,如果可以松弛,加入该出边所到点

这个算法,简单的说就是队列优化的bellman-ford,利用了每个点不会更新次数太多的特点发明的此算法。

这种算法常用于处理稀疏图和负权图,时间复杂度不稳定,容易被卡

void SPFA()
{
    int inq[maxn],d[maxn];
    queue<int> q;
    memset(inq,0,sizeof(inq));//初始化
    for(int i=1;i<=n;i++) d[i]= 0x3f;//初始化
    d[s]=0; inq[s]=1; q.push(s);//源点路径长为0,标记在队列中,插入队列
    while(!q.empty)
    {
        int u=q.front; q.pop() ; inq[u]=0;//将队头弹出,标记不在队列
        for(int i=head[u];i;i=edge[i].next)
        {
            int to=edge[i].to,from=edge[i].from,dis=edge[i].dis;
            if(d[to]>d[from]+dis)//松弛操作
            {
                d[to]=d[from]+dis;
                if(!inq[to]) //如果可以松弛,加入该出边所到点(前提此点未被访问过)
                {
                    inq[to]=1;q.push(to);
                }
            }
        }
    }
    printf("%d\n",d[t]);
}

Dijkstra算法 \(O ( N^2)\) ---》 \(O(m\log n)\)

此算法可以求解不带负权的图,求从源点s到图中其他所有点的最短路。

将所有节点分为两部分,已知最短路径的顶点集合 P 和未知最短路的集合 Q 。

用dis数组记录目前已找到每个点的最短路径,初始化dis[s]=0,进行遍历。

在集合中放入源点 s ,找到一个离 s 最近的点 u ,使得 dis[u]最小,加入集合 P

考察所有以 u 为起点的边,进行松弛操作。假如有一条 u 到 v 的边,且从 s 到 v 的长度 < dis[v],就更新dis[v]

struct HeapNode//建立小根堆
{
    int u,d;
    bool operator < (const HeapNode &rhs) const//重载运算符
    {
        return d > rhs.d;
    }
};
void Dijkstra()
{
    int d[maxn];
    priority_queue<HeapNode> q;
    for(int i=1;i<=n;i++) d[i]= 0x3f;//初始化
    d[s]=0;
    q.push((HeapNode){s,d[s]});//加入源点
    while(!q.empty)
    {
        HeapNode x=q.top(); q.pop();//弹出队头
        int u=x.u;
        if(x.d!=d[u]) continue;
        for(int i=head[u];i>=0;i=edge[i].next)
        {
            int to=edge[i].to,from=edge[i].from,dis=edge[i].dis;
            if(d[to]>d[from]+dis)//松弛操作
            {
                d[to]=d[from]+dis;
                q.push((HeapNode){to,d[to]});
             }
        }
    }
    printf("%d\n",d[t]);
}

最小生成树

最小生成树:对于带权图,权值和最小的生成树
最小瓶颈生成树:对于带权图,最大权值最小的生成树
最小生成树一定是最小瓶颈生成树
可以使用Prim算法或者Kruskal算法,复杂度均为O(mlogn)

prim算法 \(O (N^2)\)

随意选取一个点作为已访问集合的第一个点,并将所有相连的边加入堆中
从堆中找到最小的连接集合内和集合外点的边,将边加入最小生成树中
将集合外点标记为已访问,并将相连边加入堆
重复以上过程直到所有点都在访问集合中

#include <bits/stdc++.h>
using namespace std;
int n, m;
int head[200001] ,ecnt,d[200001];
struct Edge
{
	int to;
	int next;
	int dis;
}edge[1000001];
void add(int u, int v, int w)
{
    edge[++ecnt].next=head[u];
	edge[ecnt].to=v;
    edge[ecnt].dis=w;
	head[u]=ecnt;
}
bool vis[200001];
int cnt;
long long ans;
struct node
{
    int x, d;
    node(int x, int d) : x(x), d(d) {}
};
bool operator<(node a, node b)//重载运算符
{
    return a.d > b.d;
}
priority_queue<node> q;
int main()
{
    cin >> n >> m;
    for (int i = 1; i <= m; i++)
    {
        int u, v, w;
        cin >> u >> v >> w;
        add(u, v, w);
        add(v, u, w);//无向图
    }
    for (int i = 1; i <= n; i++)
        d[i] = 1000000005;//初始化
    q.push(node(1, 0));
    d[1] = 0;//第一个结点到自己为0
    cnt = -1;//初始化
    while (!q.empty() && cnt < n - 1)
    {
        node h = q.top();
        q.pop();
        if (vis[h.x])
            continue;
        vis[h.x] = 1;
        d[h.x] = 0;
        cnt++;
        ans += h.d;
        for (int e = head[h.x]; e; e =  edge[e].next)
        {
            if (d[edge[e].to] > edge[e].dis)
            {
                d[edge[e].to] = edge[e].dis;
                q.push(node(edge[e].to, d[edge[e].to]));
            }
        }
    }
    if (cnt == n - 1)
    {
        cout << ans << endl;
    }
    else
    {
        cout << "orz" << endl;
    }
}

Kruskal算法流程

将边按照权值排序
依次枚举每一条边,若连接的两点不连通则加入最小生成树中
使用并查集维护连通性

#include<bits/stdc++.h>
using namespace std;
struct edge{
    int from,to,dis;
}g[500001];
bool cmp(edge a,edge b)
{
    return a.dis<b.dis;
}
int fa[200001];
int getf(int x)
{
    if(fa[x]==x)return x;
    fa[x]=getf(fa[x]);//状态压缩
    return fa[x];
}
int n,m,cnt;
long long ans;
int main()
{
    cin>>n>>m;
    for(int i=1;i<=m;i++)
    {
        cin>>g[i].from>>g[i].to>>g[i].dis;
    }
    for(int i=1;i<=n;i++)
        fa[i]=i;
    sort(g+1,g+m+1,cmp);
    for(int i=1;i<=m&&cnt<n-1;i++)
    {
        int fu=getf(g[i].from),fv=getf(g[i].to);
        if(fu==fv)continue;
        fa[fu]=fv;
        cnt++;
        ans+=g[i].dis;
    }
    if(cnt==n-1){
        cout<<ans<<endl;
    }else{
        cout<<"orz";
    }
}

拓扑排序

与数组排序无关,倾向于一种逻辑关系,类似一种访问顺序,这种顺序往往不唯一

问题一般是随着排序顺序处理节点信息,不需要打印顺序

拓扑排序适用于有向无环图(无向则没有意义,有环得不出结果)

算法流程

首先在建图时记录每个点的入度

建立一个队列,把接下来需要访问的点加入队列

最开始时所有入度为0的点都可以访问,加入队列

依次从队列中取出每个点u,枚举其出边,边的终点设为v 。 此处进行各种u->v的信息更新

因为u信息已经计算过了,相当于从图中删去u,将其v入度-1

此时v若入度为0则说明前置信息处理完成,加入队列

拓扑排序源代码
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
struct edge
{
	int x,y,next;
}a[200010];

int first[100010],len;
queue<int> z;
int id[100010];
int p[100010],l;

void ins(int x,int y)
{
	len++;
	a[len].x=x;a[len].y=y;
	a[len].next=first[x];first[x]=len;
}

int main()
{
	int n,m,i,j,x,k,y;
	scanf("%d%d",&n,&m);
	for(i=1;i<=m;i++)
	{
		scanf("%d%d",&x,&y);
		ins(x,y);
		id[y]++;
	}
	for(i=1;i<=n;i++)
	  if(id[i]==0)z.push(i);
	while(!z.empty())
	{
	  x=z.front();
	  p[++l]=x;
	  z.pop();
	  for(k=first[x];k;k=a[k].next)
	  {
	  	y=a[k].y;
	  	id[y]--;
	  	if(id[y]==0)z.push(y);
	  }
	}
	for(i=1;i<=n;i++)printf("%d ",p[i]);
	printf("\n");
} 
拓扑排序求最大路径
#include <iostream>
using namespace std;
const int maxn=1001,maxm=100001;
int ind[maxn];
int d[maxn];
int q[maxn], qhead = 0, qtail = 0;
struct Edge
{
	int next;       //下一条边的编号 
	int to;         //这条边到达的点
    int dis;        //这条边的长度	                           
}edge[maxm];
int head[maxn],num_edge,n,m,u,v,dis;
void add_edge(int from,int to,int dis)      //加入一条从from到to的单向边 
{
	edge[++num_edge].next=head[from];
	edge[num_edge].to=to;
    edge[num_edge].dis=dis;
	head[from]=num_edge;
}
void topo() 
{
    for (int i = 1; i <= n; i++)
      if (!ind[i]) q[qtail++] = i;//入度为0先入队
    while (qhead != qtail)
    {
        int now = q[qhead++];//取队头
        for (int i = head[now]; i; i = edge[i].next)//head每一个点连的第一条边,next对应下一条边
        {
            d[edge[i].to] = max(d[edge[i].to], d[now] + 1);
            if (!--ind[edge[i].to]) q[qtail++] = edge[i].to;
        }
    }
}
int main()
{
	num_edge=0;
	scanf("%d %d",&n,&m);      //读入点数和边数
	for(int i=1;i<=m;i++)
	{
	    scanf("%d %d",&u,&v,&dis);//u、v之间有一条边 
	    ind[v]++;
		add_edge(u,v,dis);          //u到v连一条边 
	}
	topo();
	for(int i=1;i<=n;i++)
	   cout<<d[i]<<endl;

}
posted @ 2020-04-25 07:41  蒟蒻WZY  阅读(461)  评论(1)    收藏  举报