挑战程序设计(算法和数据结构)—图论(最小生成树、单源最短路径和多元最短路径、拓扑排序)

文章目录

          • 最小生成树
            • 采用Prim算法
            • 采用Kruskal算法
          • 单源最短路径
            • 使用Bellman-Ford算法
            • SFPA算法(Bellman-Ford算法的改进)
            • 使用dijstra算法
          • 多源最短路径和拓扑排序

最小生成树

题目13.2链接Minimum Spanning Tree

采用Prim算法

二者均采用邻接矩阵
第一个采用STL简化了代码(思路直接来源于Prim):

#include 
#include 
using namespace std;

const int Max = 105;
const int INF = (1<<29);
int n, G[Max][Max], Min;
set<int> T;
set<int> V;
//T为MST的顶点,V为剩余的顶点

void Pime0(int u)
{
    T.insert(u);
    V.erase(u);
    set<int>::iterator i, j;
    while(T.size()<n)
    {
        int minv = INF;
        int index = -1;
        for(i=T.begin(); i!=T.end(); i++)//寻找T到V的最短边
        {
            for(j=V.begin(); j!=V.end(); j++)
            {
                if(G[*i][*j]!=INF && minv>G[*i][*j])
                {
                    minv = G[*i][*j];
                    index = *j;
                }
            }
        }
        T.insert(index);
        V.erase(index);
        Min += minv;
    }
}
int main()
{
    cin >> n;
    Min = 0;
    for(int i=0; i<n; i++)
    {
        V.insert(i);
    }
    for(int i=0; i<n; i++)
    {
        for(int j=0; j<n; j++)
        {
            cin >> G[i][j];
            if(G[i][j]==-1)
                G[i][j] = INF;
        }
    }
    Pime0(0);
    cout << Min << endl;
    return 0;
}

第二种方法:变量如下设置
以下表示中T表示已经成为MST的结点集合,V表示所有结点集合。

变量 含义
color[n] color[v]用于记录v的访问状态,0表示结点在MST中,-1表示不在
G[n][n] 邻接矩阵,G[u][v]中记录u到v的权值
d[n] d[v]用于记录连接T内顶点与V-T内顶点(v)的边中,权值最小的边的权值
p[n] p[v]用于记录MST中顶点v的父节点,方便重建MST

先找T到V-T的最短边(通过的d[n])及其顶点(v),将v加入T后,然后更新V-T中的最短边数组d[n]
算法复杂度O(N^2)

#include 
#include 
using namespace std;

const int Max = 105;
const int INF = (1<<29);
int n, G[Max][Max], Min, color[Max], d[Max], p[Max];

void Prim()
{
    memset(color, -1, sizeof(color));
    for(int i=0; i<n; i++)
    {
        d[i] = INF;
    }
    d[0] = 0;
    p[0] = -1;
    while(1)
    {
        int minv = INF, u;
        for(int i=0; i<n; i++)//找到T到V-T中的最短边的顶点
        {
            if(color[i]!=0 && d[i]<minv)
            {
                minv = d[i];
                u = i;
            }
        }
        if(minv==INF) break;
        color[u] = 0;//MST中加入u
        Min += minv;

        for(int j=0; j<n; j++)//当MST加入u后,更新d[v]
        {
            if(color[j]!=0 && G[u][j]!=INF && G[u][j]<d[j])
            {
                d[j] = G[u][j];
                p[j] = u;//j的父节点为u,有利于重建MST
            }
        }
    }
}

int main()
{
    cin >> n;
    Min = 0;
    for(int i=0; i<n; i++)
    {
        for(int j=0; j<n; j++)
        {
            cin >> G[i][j];
            if(G[i][j]==-1)
                G[i][j] = INF;
        }
    }
    Prim();
    cout << Min << endl;
    return 0;
}
采用Kruskal算法

题目15.5链接Minimum Spanning Tree
采用Kruskal算法,从排好序(升序)的边集合中选择不会产生回路的边,直到边的个数等于顶点个数减一。
这个方法主要采用了互质集合(Union-Find)的高等数据结构,具体讲解见acm之旅–高等数据结构。算法复杂度为O(|E|log|E|)。
代码如下:

#include 
#include 
#include 
#include 
using namespace std;

struct Node//边的数据结构体
{
    int source, target, value;
};
bool cmp(Node &a, Node &b)
{
    return a.value<b.value;
}
const int MAX = 100005;
int V, E, MIN = 0;
Node e[MAX];
vector<Node> Mst;//最小生成树

class DisjointSet
{
    public:
        vector<int> rank, p;//rank记录集合树高,rank[x],则x为集合根,p指向父节点

    DisjointSet(){}
    DisjointSet(int size)
    {
        rank.resize(size, 0);
        p.resize(size, 0);
        for(int i=0; i<size; i++)
        {
            makeSet(i);
        }
    }

    void makeSet(int x)//创建仅包含元素x的新集合
    {
        p[x] = x;
        rank[x] = 0;
    }

    bool same(int x, int y)//判断x和y是否在同一集合
    {
        return findSet(x) == findSet(y);
    }

    void unite(int x, int y)//合并指定的元素及其集合
    {
        link(findSet(x), findSet(y));
    }

    void link(int x, int y)//合并函数
    {
        if(rank[x]>rank[y])//根据树的高度来确定合并策略
        {
            p[y] = x;
        }
        else
        {
            p[x] = y;
            if(rank[x]==rank[y])
            {
                rank[y]++;
            }
        }
    }

    int findSet(int x)//寻找x的集合的根
    {
        if(x!=p[x])
        {
            p[x] = findSet(p[x]);
        }
        return p[x];
    }

};

void kruskal()
{
    DisjointSet ds = DisjointSet(V);//建立互质的集合
    sort(e, e+E, cmp);//为边排序
    for(int i=0; i<E; i++)
    {
        if(!ds.same(e[i].source, e[i].target))//如果两点不连通
        {
            ds.unite(e[i].source, e[i].target);
            MIN += e[i].value;
            Mst.push_back(e[i]);//记录最小生成树的边
            if(Mst.size()==V-1) break;//数的边个数等于顶点数减一
        }
    }
    cout << MIN << endl;
}
int main()
{
    scanf("%d %d", &V, &E);
    for(int i=0; i<E; i++)
    {
        cin >> e[i].source >> e[i].target >> e[i].value;
    }
    kruskal();

    return 0;
}
单源最短路径
使用Bellman-Ford算法

参考博文:算法(五):图解贝尔曼-福特算法
算法复杂度为O(VE),E和V分别为边数和点数。
可以解决的问题

  • 从A出发是否存在到达各个节点的路径(有计算出值当然就可以到达);
  • 从A出发到达各个节点最短路径(时间最少、或者路径最少等)
  • 图中是否存在负环路(权重之和为负数)

思想:使用一个松弛条件不断更新相应的最短距离,直到没有结点需要更新为止。

  • 松弛条件:d[i] = min{ d[j]+(从j到i的边的权值)| e=(j, i) ∈ \in E}
  • 对于求解各个结点的最短路径:需要将松弛条件更新到不再更新,且最多更新|V|-1次(V是结点个数)。如果不存在负环,则只需要从源节点不断扩展,且最短路径最多经过|V|-1个结点,且不会重复经过同一个结点,所以最多更新|V|-1次。
  • 对于判断是否存在负环:如果存在负环,则更新一定不会停止,只需要判断是否存在第|V|次更新即可。

代码:

typedef long long LL;
const int MAX = 100000;
const int INF = 1<<29;
struct Edge
{
    int from, to, cost;
};
//N是结点个数,E是边的个数
int N, E;
Edge e[MAX];
LL d[MAX];//存储最短路径长度

//找到各个结点的最短路径,并判断是否存在负环
bool Bellman_Ford()
{
    fill(d, d+MAX, INF);
    d[1] = 0;
    int k = 0;
    while(1)
    {
        bool updated = false;
        for(int i=0; i<E; i++)//遍历所有的边
        {
            Edge w = e[i];
            if(d[w.from]!=INF && d[w.to]>d[w.from]+w.cost)
            {
                d[w.to] = d[w.from]+w.cost;
                updated = true;
            }
        }
        if(!updated) break;//没有更新则立即停止
        k++;
        if(k>=N) return false;//判断是否有负环
    }
    return true;
}
//单纯判断是否存在负环
bool find_negative_loop()
{
    //memset(d, 0, sizeof(d));
    fill(d, d+MAX, INF);
    d[1] = 0;
    for(int i=1; i<=N; i++)
    {
        for(int j=0; j<E; j++)
        {
            Edge w = e[j];
            if(d[w.to]>d[w.from]+w.cost)
            {
                d[w.to]=d[w.from]+w.cost;
                if(i==N) return true;//第N次仍然更新
            }
        }
    }
    return false;
}
SFPA算法(Bellman-Ford算法的改进)

参考博文:SPFA 算法详解(最短路径)
实际上就是FIFO队列+Bellman-Ford算法,使得最差情况复杂度为O(VE)E和V分别为边数和点数。
核心代码:

bool SPFA(int s,int n)
{
	queue <int> q;
	memset(vis,inf,sizeof(vis));
	memset(ven,0,sizeof(ven));//标记一个结点是否在队列中
	memset(nums,0,sizeof(nums));//记录一个结点进入队列的次数
	vis[s]=0;//初始化距离 
	ven[s]=1,nums[s]++;//标记s节点在队列,队列次数+1 
	q.push(s);
	while(!q.empty())
	{
		int x=q.front();
		q.pop();//出队 
		ven[x]=0;//标记不在队列 
		for(int i=pre[x]; ~i; i=a[i].next)//遍历与x节点连通的点 
		{
			int y=a[i].y;
			if(vis[y]>vis[x]+a[i].time)//更新 
			{
				vis[y]=vis[x]+a[i].time;
				if(!ven[y])
				//由于更新了节点,所以后续以这个为基础的最短路,也要更新下
				//所以如果在队列就不用加入,不在的话加入更新后续节点 
				{
					q.push(y);
					ven[y]=1;//标记这个节点在队列中 
					nums[y]++;//记录加入次数 
					if(nums[y]>n)//如果这个点加入超过n次,说明存在负圈,直接返回 
						return false;
				}
			}
		}
	}
	return true;
}
使用dijstra算法

第一种与prim算法类似,相当于改变了d数组的含义,其他的都不变。该方法使用邻接矩阵,复杂度为O(N^2)。
Dijkstra算法为什么不能用于负权图
变量如下设置

变量 含义
color[n] color[v]用于记录v的访问状态,0表示该结点的最短路已经确定,-1表示还没有确定
G[n][n] 邻接矩阵,G[u][v]中记录u到v的权值
d[n] d[v]用于记录起点s到v的最短路径成本
p[n] p[v]用于记录最短路径中顶点v的父节点,方便重建最短路径
#include 
#include 
using namespace std;

const int Max = 105;
const int INF = (1<<29);
int n, G[Max][Max], Min, color[Max], d[Max], p[Max];
int u, k, v, c;

void dijstra()
{
    memset(color, -1, sizeof(color));
    for(int i=0; i<n; i++)
    {
        d[i] = INF;
    }
    d[0] = 0;
    p[0] = -1;
    while(1)
    {
        int minv = INF, u;
        for(int i=0; i<n; i++)//找到s到V-T中的最短路径的顶点
        {
            if(color[i]!=0 && d[i]<minv)
            {
                minv = d[i];
                u = i;
            }
        }
        if(minv==INF) break;
        color[u] = 0;

        for(int j=0; j<n; j++)
        {
            if(color[j]!=0 && G[u][j]!=INF && d[u]+G[u][j]<d[j])//使用新获得的结点更新最短路径
            {
                d[j] = G[u][j]+d[u];
                p[j] = u;//j的父节点为u,有利于重建最短路径
            }
        }
    }
    //打印输出
    for(int i=0; i<n; i++)
    {
        cout << i << " " << d[i] << endl;
    }
}

int main()
{
    cin >> n;
    Min = 0;
    for(int i=0; i<n; i++)
    {
        for(int j=0; j<n; j++)
        {
            G[i][j] = INF;
        }
    }
    for(int i=0; i<n; i++)
    {
         cin >> u >> k;
         for(int i=0; i<k; i++)
         {
             cin >> v >> c;
             G[u][v] = c;//化为邻接矩阵
         }
    }
    dijstra();
    return 0;
}

第二种方法:该方法采用优先级队列(堆)+邻接表来实现,使得复杂度降为O((|V|+|E|)log(|V|)),即选最短路径使用优先级队列(O(|V|log|V|),更新采用邻接表(O(|E|log|V|)。
思路与第一种一样,就是使用STL和邻接表优化了一下。

#include 
#include 
#include 
#include 
#include 
using namespace std;

const int Max = 10005;
const int INF = (1<<29);
struct Node
{
    int v, c;//v表示结点编号,c表示结点距离
    bool operator < (const Node& a) const{
        return c > a.c;
    }
};

int n, color[Max], d[Max], p[Max];
int u, k, v, c;
vector<Node> G[Max];
priority_queue<Node> pq;
Node pn;

void dijstra()
{
    memset(color, -1, sizeof(color));
    for(int i=0; i<n; i++)
    {
        d[i] = INF;
    }
    d[0] = 0;
    p[0] = -1;
    pn = {0, d[0]};
    pq.push(pn);

    while(!pq.empty())
    {
    	//找到最短路径的点
        pn = pq.top(); pq.pop();
        int u = pn.v;
        color[u] = 0;
        
        if(d[u]<pn.c) continue;//如果得到的最小值比目前的最短路径还大,则忽略//不是必须的
        for(int j=0; j<G[u].size(); j++)//更新与u相邻的结点的最短路径
        {
            int v = G[u][j].v;
            if(color[v]!=0 && d[u]+G[u][j].c<d[v])
            {
                d[v] = G[u][j].c+d[u];
                pn = {v, d[v]};
                pq.push(pn);
                p[j] = u;//j的父节点为u,有利于重建MST
            }
        }
    }
    //打印输出
    for(int i=0; i<n; i++)
    {
        if(d[i]==INF)
            cout << i << " " << -1 << endl;
        else
            cout << i << " " << d[i] << endl;
    }
}

int main()
{
    cin >> n;
    for(int i=0; i<n; i++)
    {
         cin >> u >> k;
         for(int i=0; i<k; i++)
         {
             cin >> v >> c;
             Node pn = {v, c};
             G[u].push_back(pn);
         }
    }

    dijstra();
    return 0;
}
多源最短路径和拓扑排序

链接:acm之旅–高等图算法

你可能感兴趣的:(挑战程序设计-算法和数据结构)