11.28 图论 基础

图的存储(两种简单存储)

邻接矩阵

	#define N 100			 //顶点最大值为100
	
	typedef struct{
		int vex[N];	         //顶点表
		int Edge[N][N];	     //邻接矩阵,边表
		int vn, en;		     //图的当前顶点数和边数
	}MGraph;

邻接表

邻接表需要用到两种结点:顶点表结点和边表结点

无向图邻接表:
在这里插入图片描述
有向图邻接表:
在这里插入图片描述

	#define  N 100			//定义顶点最大值为100
	
	struct eNode{			//边表结点
		int adjvex;			//该弧所指向的顶点位置
		eNode *next;		//指向下一条弧的指针
	};
	
	struct vNode{			//顶点表结点
		int data;			//顶点信息
		eNode *first;		//指向第一条依附该结点的弧的指针
	}adjList[N];
	
	typedef struct{			//邻接表
		AdjList vertices;	//图的顶点数和弧数
		int vn,en;		    //ALGraph是以邻接表存储的图类型
	}

图的遍历

深度优先遍历

void MGrap::dfs(int v)
    {
        cout<<vx[v]<<" ";
        visit[v]=1;
        for(int j=0;j<vn;j++)
        {
            if(edge[v][j]==1&&visit[j]==0)
                dfs(j);
        }
    }

广度优先遍历(队列)

void MGrap::bfs(int v)
    {
        int w,j,Q[N];
        int front =-1;
        int rear=-1;
        cout<<vx[v]<<" ";
        visit[v]=1;
        Q[++rear]=v;
        while(front!=rear)
        {
            w=Q[++front];
        for(int j=0;j<vn;j++)
        {
            if(edge[w][j]==1&&visit[j]==0)
                cout<<vx[j]<<" ";
                visit[j]=1;
                Q[++rear]=j;
                bfs(j);
        }
    }

最小生成树

带权图分为有向和无向,构造连通网络最小代价(即连接各顶点路径权值之和最小)的最短路径又叫做最小生成树,有prime算法和kruskal算法,构造最小生成树一般使用贪心策略

有向图的最短路径算法有floyd算法和dijkstra算法。

1.Prime算法

一般来说,prim算法常用于网比较稠密的图。

对于一个点来说,找到权重最小的连接点。之后对于这两个点来说,找到这两个点中权重最小的另外一个点(即没有经过遍历的点)。之后以此类推,直到遍历完成。

可以把prim算法理解为点优先

题目要求:从顶点v1出发生成最小树
输入:

6 10
0 6 1 5 100 100
6 0 5 100 3 100
1 5 0 5 6 4
5 100 5 0 100 2
100 3 6 100 0 6
100 100 4 2 6 0

输出:
1 4 2 5 3
在这里插入图片描述

//最小生成树添加任意一条属于原图的边必定会产生回路
//减少任意一条边则必然成为非连通
const int N=105;
int adj[N],low[N];//候选最短边的邻接点和权值
int edge[N][N];//邻接矩阵
int vn,en;
void prime(int v)
{
    int i,j,k;
    for(i=0; i<vn; i++)
    {
        low[i]=edge[v][i];
        adj[i]=v;
    }
    low[v]=0;
    for(k=1; k<vn; k++)//迭代n-1次
    {
        j=mine(low,vn);//返回最短边的邻接点(自定义函数)
        cout<<low[j]<<" ";
        low[j]=0;//顶点j加入
        for(i=0; i<vn; i++)
        {
            if(edge[j][i]<low[i])
            {
                low[i]=edge[j][i];
                adj[i]=j;
            }
        }

    }
}

2.Kruskal算法(并查集)

kauskal适用于网比较稀疏的图。

先对权重由小到大排序,依次遍历权重两边的点,如果点已经被遍历过,则跳过这个权重,继续遍历。直到遍历完成。(并查集的思想)

可以把kruskal算法理解为边优先

对于是否遍历过 ,可以建立一个数组,用1或0来表示是否遍历成功;

题目:(同上图)
要求对一个图使用kruskal算法求最小生成树,依次输出选出的边所关联的顶点序列,要求下标较小者在前,如图所示,其顶点序列为1 3 4 6 2 5 3 6 2 3

输入:

6 10
0 6 1 5 100 100
6 0 5 100 3 100
1 5 0 5 6 4
5 100 5 0 100 2
100 3 6 100 0 6
100 100 4 2 6 0

输出:
1 3 4 6 2 5 3 6 2 3

一些注释是自己的做题过程,可以忽略。。。

//Kruskal算法求最小树
//按照边的权值由小到大的顺序,依次考察边集的各个边
//判断是否符合一个连通分量
//考虑到算法是对边进行操作
struct e
{
    int from,to,weight;
};
//为了便于在并查集中查找和合并操作,树采用双亲表示法储存
//int parent[N];//用于表示顶点i的双亲
//int v1,v2;//分别表示两个集合所在的根
//集合的合并只需要简单的合并,也再进一步优化
//初始化parent[i]=-1;没有双亲
const int N=102;
e edge[N];//存储两个边以及两边的权值
int vn,en;//顶点数和边数

记得排序

sort(edge,edge+p,cmp);

算法实现

void kruskal()
{
    int num=0,i,v1,v2;
    int parent[vn];
    for(i=0;i<vn;i++)
    {
        parent[i]=-1;
    }
    //依次考察最短边
    for(num=0,i=0;num<vn-1;i++)
    {
        v1=findroot(parent ,edge[i].from);
        v2=findroot(parent ,edge[i].to);//找两者的集合
        if(v1!=v2)
        {
            //cout<<"("<<edge[i].from<<","<<edge[i].to<<")"<<edge[i].weight<<endl;
            cout<<edge[i].from+1<<" "<<edge[i].to+1<<" ";
            parent[v1]=v2;//合并
            num++;
        }
    }
}

并查集

int findroot(int parent[],int v)
{
    int t=v;
    while(parent[t]!=-1)
    {
        t=parent[t];//求树的双亲一直到根
    }
    return t;
}

最短路径

1.Dijkstra算法

迪杰斯特拉,还首创结构化程序设计,贡献很了不起的一个人物。

Dijkstra(迪杰斯特拉)算法是典型的单源最短路径算法,用于计算一个节点到其他所有节点的最短路径。主要特点是以起始点为中心向外层层扩展,直到扩展到终点为止。Dijkstra算法是很有代表性的最短路径算法。注意该算法要求图中不存在负权边

题目:
若干行整数,第一行有2个数,分别为顶点数v和弧数a,第二行为起点编号s和终点编号e,接下来有a行,每一行有3个数,分别是该条弧所关联的两个顶点编号和弧的权值。(单源最短路径)

输入:

6 8
0 3
0 2 10
0 4 30
0 5 100
1 2 5
2 3 50
3 5 10
4 3 20
4 5 60

输出:
50
v0 v4 v3

//dijkstra求最短路径
//有向图的结构,求某个顶点到另一点的最短路径

int vn,en;//顶点数和边数
int s,e;
const int N=1005;
const int INF=10000000;
int edge[N][N];//邻接矩阵
int dis[N];//到源点的最短距离
string vx[N];//顶点
string path[N];//路径
bool se[N]={false};//标记是否已经放入集合中

//第二行为起点编号s和终点编号e,接下来有a行
//每行有三个数
//单源点最短距离问题

算法实现

void dijkstra(int v)//从源点出发
{
    int k,num;
    dis[v]=0;
    for(int i=0;i<vn;i++)
    {
        dis[i]=edge[v][i];
        if(dis[i]!=N)//假设N为边上权值最大的值
        {
            path[i]="v"+vx[v]+" "+"v"+vx[i]+" ";
        }
        else path[i]="";
    }
    //int flag=0;
    for(num=1;num<vn;num++)
    {
        k=minn(dis,vn);//在dia数组找到最小
       // if(path[k][0]==s&&path[k][2]==e)
       //if(dis[e]==0)
       //cout<<path[k]<< "  "<<dis[k]<<endl;
        
        //修改数组dis和path
        if(k==-1) return ;
        for(int i=0;i<vn;i++)
        {
            if(dis[i]>dis[k]+edge[k][i]&&dis[k]!=N&&se[k]==false)
            {
                dis[i]=dis[k]+edge[k][i];
                path[i]=path[k]+"v"+vx[i]+" ";
            }
        }
        se[k]=true;
//        int a=dis[k],ans;
//        dis[k]=0;//将顶点k加到集合中
//        if(dis[e]==0&&flag==0)
//        {
//            flag=1;
//            ans=a;
//           // cout<<ans<<endl<<path[k]<<endl;
//        }
    }
}

2.Floyd算法(简单动态规划)

Floyd算法适用于APSP(AllPairsShortestPaths),是一种动态规划算法,稠密图效果最佳,边权可正可负。此算法简单有效,由于三重循环结构紧凑,对于稠密图,效率要高于执行|V|次Dijkstra算法。

优点:容易理解,可以算出任意两个节点之间的最短距离,代码编写简单

缺点:时间复杂度比较高,不适合计算大量数据。

时间复杂度:O(n3) ;空间复杂度:O(n2);

任意节点i到j的最短路径两种可能:
1 . 直接从 i 到 j ;
2. 从i经过若干个节点 k 到 j 。

dis(i,j)表示节点i到j最短路径的距离,对于每一个节点k,检查dis(i,k)+dis(k,j)小于dis(i,j),如果成立,dis(i,j) = dis(i,k)+dis(k,j);遍历每个k,每次更新的是除第k行和第k列的数。

题目:

输入:

3 5
0 1 4
0 2 11
1 0 6
1 2 2
2 0 3

输出:
4 v0 v1
6 v0 v1 v2
5 v1 v2 v0
2 v1 v2
3 v2 v0
7 v2 v0 v1

//有向图,求出所有顶点间的最短路径
//每一对顶点之间的最短路径问题
//邻接矩阵
int vn,en;
const int N=102;
const int inf=100004;
int dis[N][N];
int edge[N][N];
string vx[N];
string path[N][N];

算法实现

void floyd()
{
    int i,j,k;
    for(i=0; i<vn; i++)
    {
        for(j=0; j<vn; j++)
        {
            dis[i][j]=edge[i][j];
            if(dis[i][j]!=inf)
            {
               path[i][j]="v"+vx[i]+" v"+vx[j]+" ";
            }
            else
                path[i][j]="";
        }
    }
    for(k=0; k<vn; k++)
    {
        for(i=0; i<vn; i++)
        {
            for(j=0; j<vn; j++)
            {
                if(dis[i][k]+dis[k][j]<dis[i][j])
                {
                    dis[i][j]=dis[i][k]+dis[k][j];
                    path[i][j]=lian(path[i][k],path[k][j]);
           //可以先尝试path[i][k]+path[k][j],,,
           //就知道这个土土的lian(string ,string)函数怎么写了,,,
                }
            }
        }
    }
    int flag=0;
    for(i=0; i<vn; i++)
    {
        for(j=0; j<vn; j++)
        {
            if(dis[i][j]!=inf&&dis[i][j]!=0)
            {
                cout<<dis[i][j]<<" ";
               cout<<path[i][j]<<endl;
               flag=1;
            }
        }
        //cout<<endl;
    }
    if(flag=0) cout<<"no answer"<<endl;
}

AOV网 拓扑排序(栈)

拓扑排序,是对有向无圈图的顶点的一种排序方式,使得如果存在一条从vi到vj的路径,那么在排序中vj就在vi的之后出现。课程先修问题,有向边(v,w)就表明课程v必须在课程w选修前修完。这些课程的拓扑排序是不破坏课程先修要求的任意的课程序列。如果图含有圈,那么进行拓扑排序是不可能的,因为对于圈上的两个顶点v和w, v先于w同时w又先于v。此外,拓扑排序不必是唯一的,任何合理的排序都是可以的。

题目:(邻接表存储)
给出一个图的结构,输出其拓扑排序序列,要求在同等条件下,编号小的顶点在前
若干行整数,第一行有2个数,分别为顶点数v和弧数a,接下来有a行,每一行有2个数,分别是该条弧所关联的两个顶点编号。。。

输入:

6 8
1 2
1 3
1 4
3 2
3 5
4 5
6 4
6 5

输出:
v1 v3 v2 v6 v4 v5

//拓扑排序
const int N=100000;
int s[N];//初始化栈
int vn,en;
struct enode//定义边表节点
{
    int adj;
    enode *next;
};
struct vnode//定义定点表节点
{
    int in;
    int vx;
    enode *first;
};
vnode ad[N];//存放顶点表的数组

邻接表建立

for(int i=1;i<=vn;i++)
    {
        ad[i].vx=i;
        ad[i].in=0;
        ad[i].first=NULL;
    }
    enode *ss=NULL;
    for(int a=0;a<en;a++)
    {
        int i,j;
        cin>>i>>j;
        ad[j].in++;
        ss= new enode;
        ss->adj=j;//生成一个边表结点s
        ss->next=ad[i].first;//将结点s插入到第i个
        ad[i].first=ss;
    }

算法实现

void topsort()
{
    int i,j,k,cot=0;
    int top=-1;
    enode *p=NULL;
   // sort(ad+1,ad+vn+1,cmp);
    for(int i=vn;i>=1;i--)//为什么用倒叙呢
    //可以先正序一下,发现并不对,再看题目要求就懂啦 哈哈
    {
     if(ad[i].in==0)
     s[++top]=i;//将入度为0的顶点入栈
    }
    while(top!=-1)
    {
        j=s[top--];
        cout<<"v"<<ad[j].vx<<" ";
        cot++;
        p=ad[j].first;
        while(p!=NULL)
        {
            k=p->adj;
            ad[k].in--;
            if(ad[k].in==0) s[++top]=k;//将入度为0的顶点入栈
            p=p->next;
        }
    }
    if(cot<vn) cout<<"有回路"<<endl;
}

AOE网 关键路径

/*
对于边来说:
1.事件最早发生的时间ve[k]
该事件前面的活动最晚结束时间

2.事件最迟的发生时间vl[k]
在不耽误整个工期的情况下,时间vk允许的最晚发生时间
倒序计算

对于顶点活动来说:
3.活动ai发生的最早时间ee[i]
ee[i]=ve[k]
4.活动ai的最早开始时间el[i]
el[i]=vl[k]

对于所有的边来说,如果它的最早时间等于最晚时间
称这条边代表的活动为关键活动,组成的路径为关键路径
*/

/*算法
1.输入边数m
2.初始化权重d[i][j]为max
3.输入每条边的 起点终点权重,设置d[u][v]=w;
4.输入要查询的两个点first and last
5.初始化el[i]=0,el[i]=max,
6.从k=first开始(earliest[first]=0),递归查找下一个节点i,然后计算各个点最早开始的时间earliest[i],
如果earliest[k]+d[k][i]>earliest[i]则更新earliest[i];
如果发现没有任何节点i与k相连,则输出“路径不存在”;
7. 从k=last开始(latest[last]=earliest[last]),递归查找上一个节点i,然后计算各个点最晚开始的时间latest[i],
如果latest[k]-d[i][k]<latest[i]则更新latest[i];
8. 从k=first开始,循环查找下一个节点i,
如果latest[i]-d[k][i]==earliest[k],则k->i为关键路径,把i加入到path中。
9.打印输出path的内容
*/

#include<iostream>
using namespace std;
#define max 1000000
const int N=100;
int d[N][N];
int earliest[N];
int latest[N];
int path[N];
void findEarliest(int k,int last);
void findLatest(int k,int first);
bool flag=false;//用于判断是否存在关键路径
int n;
int main(){
	int i,j,k,m;
	int u,v,w;
	int first,last,count=0;
	int next;
	cin>>n;
	//cout<<"请输入边数:";
	cin>>m;

	for(i=1;i<=m;i++){
		for(j=1;j<=m;j++)
			d[i][j]=max;
	}
	//cout<<"请输入每条边的顶点及其权重"<<endl;
	for(i=1;i<=m;i++){
		cin>>u>>v>>w;
		d[u][v]=w;
	}
	//cout<<"请输入要查询的两点:";
	first=1;
	last=n;

	for(i=1;i<=n;i++){
		earliest[i]=0;
		latest[i]=max;
		path[i]=max;
	}
	k=first;path[1]=k;count=2;
	findEarliest(k,last);
	if(!flag){
		cout<<"路径不存在";
		return 0;
	}

	k=last;latest[k]=earliest[k];
	findLatest(k,first);
	k=first;
	while(k!=last){
		for(i=1;i<=n;i++){
			if(d[k][i]!=max&&(latest[i]-d[k][i]==earliest[k])){
				path[count++]=i;
				k=i;
				break;
			}
		}
	}
	for(i=1;path[i]!=last;i++){
		cout<<"v"<<path[i]<<" ";
	}
	cout<<"v"<<path[i]<<endl;

}

void findEarliest(int k,int last){
	if(k==last)
		return;
	flag=false;
	for(int i=1;i<=n;i++){
		if(d[k][i]<max){
			flag=true;
			if((earliest[k]+d[k][i])>earliest[i])
				earliest[i]=earliest[k]+d[k][i];
			findEarliest(i,last);
		}
	}
        //如果flag没有在循环中被修改为ture,则说明没有节点与k相连(即没有进入if语句内,函数返回不再进行递归),然后在主函数中判断flag的值来决定是否继续
}
void findLatest(int k,int first){
	if(k==first)
		return;
	for(int i=1;i<=n;i++){
		if(d[i][k]<max){
			if(latest[k]-d[i][k]<latest[i])
				latest[i]=latest[k]-d[i][k];
			findLatest(i,first);
		}
	}
}
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值