图 基础
图的存储(两种简单存储)
邻接矩阵
#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);
}
}
}