图
图的表示
邻接矩阵
不适合存储稀疏图
邻接矩阵构造
#define Maxlnt 32767 //表示极大值,即 ∞
#define MVNum 100 //最大顶点数
typedef char VerTexType; //设顶点的数据类型为字符型
typedef int ArcType; //假设边的权值类型为整型
typedef struct {
VerTexType vexs[MVNum]; //顶点表
ArcType arcs[MVNum][MVNum]; //邻接矩阵
int vexnum, arcnum; //图的当前点数和边数
}ALGraph; //Adjacency Matrix Graph
邻接表
无向图的邻接表表示示例
邻接表构造
#define MaxVex 255
#define TRUE 1
#define FALSE 0
typedef char VertexType; //顶点类型
typedef int Bool;
Bool visited[MaxVex]; //记录图中节点访问状态
typedef struct EdgeNode { //边表节点
int adjvex; //该邻接点在顶点数组中的下标
struct EdgeNode *next; //链域 指向下一个邻接点
}EdgeNode;
typedef struct VertexNode { //头节点
VertexType data; //顶点信息
EdgeNode *firstedge; //边表头指针
}VertexNode,AdjList[MaxVex]; //顶点数组
typedef struct Graph{
AdjList adjList;
int numVertexes,numEdges; //图中当前的结点数以及边数
}Graph,*GraphAdjList;
逆邻接表
在有向图中,邻接表可以表示出顶点v的出度,而入度需要用逆邻接表表示
图的搜索
深度优先搜索(DFS)
用dfs求最大连通块
//定义方向数组
int dr[4]={0,-1,0,1};
int dc[4]={1,0,-1,0};
//记录是否访问过的数组
int idx[m][n];
//将要遍历的m×n的矩阵
int pic[m][n];
void dfs(int r, int c, int id){//id用于记录连通分量的个数
if(r<0 || r>=m || c<0 || c>=n)
return;//出界
if(idx[r][c]>0 || pic[r][c] != '@')
return;//不是'@'或已经访问过
idx[r][c]=id;//连通分量编号
for(int i=0;i<4;++i){
dfs(r+dr[i], c+dc[i], id);
}
}
宽度(广度)优先搜索(BFS)
用于求解无权或权值相同的图的最短路径
char mp[n][m];//1为空地,0为障碍物
int dist[maxn][maxn];//距离矩阵,记录各个点到终点的最短路长度
int sx, sy, tx, ty;//记录起止点的横纵坐标
int dx[4] = {1, 0, 0, -1}, dy[4] = {0, -1, 1, 0};//方向数组
void bfs()
{
dist[tx][ty] = 0;//终点到终点距离为零
queue<pair<int, int> > q;//bfs用队列实现,pair记录点的横纵坐标
pair<int, int> t;//临时变量
t.first = tx;
t.second = ty;
q.push(t);//终点入队
while(q.size()){//从终点开始逆方向遍历
t = q.front();//每次取出队头元素
q.pop();//队头元素出队
for(int i = 0; i < 4; i ++){//遍历队头元素可以往哪走
int nx = t.first + dx[i];
int ny = t.second + dy[i];//这两句话实现了往一个方向走,想不明白可以在纸上画一下
if(nx >= 0 && nx < n && ny >= 0 && ny < n && dist[nx][ny] == 0 && mp[nx][ny] != '1'){//是否越界,是否走过,是否可走
dist[nx][ny] = dist[t.first][t.second] + 1;//满足条件则为上一个点多走一步,离终点的距离也就多1
q.push(make_pair(nx, ny));//将该点入队,方便下次从该点出发遍历
}
}
}
cout<<dist[sx][sy];//输出起点到终点的最短距离
}
最小生成树
prim(普里姆算法)
贪心算法
将图分成两个集合{vi},{ui},{vi}表示已经记录的点,{ui}表示未记录的点
lowc[p]中记录了{ui}中的每一个点分别到{vi}中每一个点的最短距离
将点到点模糊处理为集合到集合
int graph[n][n];//邻接矩阵
bool vis[n];//判断是否遍历过
int lowc[n];//一个动态更新的数组,记录未遍历过的点到已遍历过的点中的最短距离
int prim(int index)
{
int ans = 0;
memset(vis, false, sizeof(vis));//初始表示均未遍历过
vis[index] = true;//遍历起点
for(int i = 1; i < n; ++i)//lowc中第一次记录index到其他各点的距离
lowc[i] = cost[index][i];
for(int i = 1; i < n; ++i)
{
int minc = INF;
int p = -1;
for(int j = 0; j < n; ++j)//找到lowc中的最小值,并记录其对应的点
{
if(!vis[j] && minc > lowc[j])
{
minc = lowc[j];
p = j;
}
}
if(minc == INF)//表示无法生成连通图
return -1;
ans += minc;
vis[p] = true;
for(int j = 0; j < n; ++j)//更新lowc数组,
{//要将点p到其他点的数据加到lowc中,故只需要遍历map[p][j]即可,无需重复遍历已经遍历过的点
if(!vis[j] && lowc[j] > graph[p][j])
lowc[j] = cost[p][j];
}
}
return ans;
}
kruskal (克鲁斯卡尔算法)
贪心+并查集
算法开始时,认为每一个点都是孤立的,分属于n个独立的连通块。
Kruskal算法将一个连通块当做一个集合。
首先将所有的边按从小到大顺序排序,然后按顺序枚举每一条边。如果这条边连接着两个不同的集合,那么就把这条边加入最小生成树,这两个不同的集合就合并成了一个集合(即选择一条最小的边,而且这条边的两个顶点分属于两个不同的集合);如果这条边连接的两个点属于同一集合,就跳过。直到选取了n-1条边为止。
typedef struct
{
int begin;
int end;
int weight;
} Edge;
/* 查找连线顶点的尾部下标 */
int Find( int *parent, int f)
{
while (parent[f] > 0)
f = parent[f];
return f;
}
/* 生成最小生成树 */
void MiniSpanTree_Kruskal(MGraph G)
{
int i, j, n , m;
int k = 0;
int parent[MAXVEX]; /* 定义一数组用来判断边与边是否形成环路 */
Edge edges[MAXEDGE]; /* 定义边集数组,edge的结构为begin,end,weight,均为整型 */
/* 此处省略将邻接矩阵G转换为边集数组edges并按权由小到大排列的代码*/
for (i = 0; i < G.numVertexes; i++)
parent[i] = 0;
cout << "打印最小生成树:" << endl;
for (i = 0; i < G.numEdges; i++) /* 循环每一条边 */
{
n = Find(parent, edges[i].begin);
m = Find(parent, edges[i].end);
if (n != m) /* 假如n与m不等,说明此边没有与现有的生成树形成环路 */
{
parent[n] = m; /* 将此边的结尾顶点放入下标为起点的parent中。 */
/* 表示此顶点已经在生成树集合中 */
cout << "(" << edges[i].begin << ", " << edges[i].end << ") "
<< edges[i].weight << endl;
}
}
}
单源最短路径
Dijkstra(迪杰斯特拉算法)
贪心
int dis[n+1];//当前各点到源点的距离
int vis[n+1];//各点到源点是否为最小值
int map[n][n];
void Dijkstra(int u)
{
memset(vis,0,sizeof(vis)); //初始化
for(int t=1;t<=n;t++)
{
dis[t]=map[u][t];
}
vis[u]=1;
for(int t=1;t<n;t++)
{//第一次循环到点1只有一条边的且距离最短的点,该点到1的距离一定最短,不可能再通过其他点作为中转点对距离进行松弛操作
int minn=Inf; //当前未确定最短路径的点,到源点的最小距离大小
int temp;
for(int i=1;i<=n;i++)//从上一次循环更新的dis数组中找到最小值作为新的中转点
{
if(!vis[i]&&dis[i]<minn)
{
minn=dis[i];
temp=i;
}
}
//每轮迭代都可以确定一个点到源点的最短距离,因此只要迭代n-1轮
vis[temp]=1;
//通过已经确定的点,对dis数组进行更新
//(未确定的点通过点u,缩小了到源点的距离)
for(int i=1;i<=n;i++)//通过中转点更新dis数组
{//该循环只根据最新找到的中转点对到其他点的距离进行更新,因为之前找到的点已经更新过距离,没有必要重复计算
if(map[temp][i]+dis[temp]<dis[i])
{
dis[i]=map[temp][i]+dis[temp];
}
}
}
}
多元最短路径
floyd(弗洛伊德算法)
插点法的动态规划
计算任意两点间的最短路径
从最开始的只允许经过1号顶点进行中转,接下来只允许1,2进行中转。。。。。允许经过1~n 号所有的顶点进行中转,求任意两点的最短路径。
for(int k = 1 ; k <= n ; k ++)//最外层循环遍历顶点
{
for(int i = 1 ; i <= n ; i ++)
{
for(int j = 1 ; j <= n ; j ++)
{
if(e[i][j] > e[i][k] + e[k][j])
e[i][j] = e[i][k] + e[k][j];
}
}
}
拓扑排序
”有向无环图"(Directed Acyclic Graph DAG)中
率先输出入度为零的点
void TopologicalSort(){
queue<int>q;
vector<int>edge[n];//记录每一个顶点的指向顶点有些哪些
for(int i=0;i<n;i++) //n 节点的总数
if(in[i]==0)
q.push(i); //将入度为0的点入队列
vector<int>ans; //ans 为拓扑序列
while(!q.empty())
{
int p=q.front();
q.pop(); // 选一个入度为0的点,出队列
ans.push_back(p);
for(int i=0;i<edge[p].size();i++)//遍历所有p的指向顶点
{
int y=edge[p][i];
in[y]--;
if(in[y]==0)
q.push(y);
}
}
if(ans.size()==n)
{
for(int i=0;i<ans.size();i++)
printf( "%d ",ans[i] );
printf("\n");
}
else printf("No Answer!\n"); // ans 中的长度与n不相等,就说明含环,无拓扑排序
}
求AOV网络的关键路径
AOV(Activity On Vertex)是一个有向图G
V(G)表示活动
E(G)表示领先关系
关键活动:延迟会影响整个工期的活动
关键路径:由关键活动构成的从开始到完成的路径,即最长路径
Earliest(v):
v的最早完成时间
是起点到点v的最长路径长度
表示所有v的前提任务都完成以后v才可以进行
Earliest(w)=max{ Earlist(vi)+edge(vi,w) }
Latest(v):
在不延迟整个工期下v的最晚完成时间
从终点逆向递推
Latest(v)=min{ latest(wi)-edge(v,wi) }
Delay(v):
Delay(v)=Latest(v)-Earliest(v)
Delay(v)=0的活动就是关键活动
由关键活动构成的最长路径叫关键路径