图
来源:互联网 发布:手机上编程 编辑:程序博客网 时间:2024/04/29 12:11
一个图
图中的路径是一个顶点序列
如果无向图中每个顶点到其他顶点都存在一条路径,则称为连通的。对应地有向图称为强连通的,如果有向图不是强连通的,但去掉边的方向形成的基础图是连通的,则有向图是弱连通的。完全图是每一对顶点间都存在一条边的图。
图可以用二维数组表示,称为邻接矩阵表示法。对每条边
对于稀疏图,更好的方法是用邻接表表示,它也是图的标准表示方法。对每个顶点,使用一个表保存所有邻接的顶点,如果边有权,可以保存在表示邻接顶点的元素中。这种表示空间需求为
下面是使用邻接表方式实现的图的类型。
/* graph.h */struct vertex { int v; int c; struct vertex *next;};struct edge { int v; int w; int c;};struct graph { int vn; int en; struct vertex **varray;};typedef struct vertex vertex_t;typedef struct edge edge_t;typedef struct graph graph_t;/* graph.c */static vertex_t *vertex_create(int v, int c){ vertex_t *u; u = malloc(sizeof(vertex_t)); if (u == NULL) err_quit("malloc error"); u->v = v; u->c = c; u->next = NULL; return u;}static void vertex_destroy(vertex_t *u){ free(u);}static void vertex_add(vertex_t *u, vertex_t *l){ u->next = l->next; l->next = u;}static edge_t *edge_create(int v, int w, int c){ edge_t *e; e = malloc(sizeof(edge_t)); if (e == NULL) err_quit("malloc error"); e->v = v; e->w = w; e->c = c; return e;}static void edge_destroy(edge_t *e){ free(e);}graph_t *graph_create(int earray[][2], int vn, int en){ graph_t *g; vertex_t *u; int v, w; int i, j; g = malloc(sizeof(graph_t)); if (g == NULL) err_quit("malloc error"); g->vn = vn; g->en = en; g->varray = malloc(sizeof(vertex_t *)*(vn+1)); if (g->varray == NULL) err_quit("malloc error"); for (i = 1; i <= vn; i++) g->varray[i] = vertex_create(i, 0); for (j = 0; j < en; j++) { v = earray[j][0]; w = earray[j][1]; u = vertex_create(w, 1); vertex_add(u, g->varray[v]); g->varray[w]->c++; } return g;}graph_t *graph_create_weighted(int earray[][3], int vn, int en){ graph_t *g; vertex_t *u; int v, w; int i, j; g = malloc(sizeof(graph_t)); if (g == NULL) err_quit("malloc error"); g->vn = vn; g->en = en; g->varray = malloc(sizeof(vertex_t *)*(vn+1)); if (g->varray == NULL) err_quit("malloc error"); for (i = 1; i <= vn; i++) g->varray[i] = vertex_create(i, 0); for (j = 0; j < en; j++) { v = earray[j][0]; w = earray[j][1]; u = vertex_create(w, earray[j][2]); vertex_add(u, g->varray[v]); g->varray[w]->c++; } return g;}void graph_destroy(graph_t *g){ vertex_t *u, *l; int i; for (i = 1; i <= g->vn; i++) { l = g->varray[i]; while (l) { u = l; l = l->next; vertex_destroy(u); } } free(g->varray); free(g);}void graph_print(graph_t *g){ vertex_t *u; int i; for (i = 1; i <= g->vn; i++) { printf("(%d,%d):", g->varray[i]->v, g->varray[i]->c); u = g->varray[i]->next; while (u) { printf(" (%d,%d)", u->v, u->c); u = u->next; } printf("\n"); }}
实际应用中,顶点的名字可能不是数字,可以再使用一个散列表来完成名字到数字的映射。
拓扑排序
拓扑排序是对有向无圈图的顶点的排序,使如果存在
定义顶点
void TopSort(Graph G){ int Counter; Vertex V, W; for (Counter = 0; Counter < VertexNum; Counter++) { V = FindNewVertexOfIndegreeZero(); if (V == NotAVertex) { Error("Graph has a cycle"); break; } TopNum[V] = Counter; for each W adjacent to V Indegree[W]--; }}
注意到并不需要每次查看所有顶点的入度,可以改进上述算法,将入度为0的顶点放入一个栈或队列中,每次从中取出顶点时,降低邻接顶点的入度,如果降为0则加入栈或队列。如果使用邻接表,算法的运行时间为
void TopSort(Graph G){ Queue Q; int Counter = 0; Vertex V, W; Q = Create(VertexNum); for each vertex V if (Indegree[V] == 0) Enqueue(V, Q); while (!Empty(Q)) { V = Dequeue(Q); TopNum[V] = ++Counter; for each W adjacent to V if (--Indegree[W] == 0) Enqueue(W, Q); } if (Counter != VertexNum) Error("Graph has a cycle"); Destroy(Q);}
下面是使用了队列的拓扑算法的一个实现。
void graph_top_sort(graph_t *g, int top_a[]){ int indegree_array[g->vn]; int v, w; queue_t *q; vertex_t *u; int i = 0; for (v = 1; v <= g->vn; v++) indegree_array[v] = g->varray[v]->c; q = queue_create(g->vn); for (v = 1; v <= g->vn; v++) if (indegree_array[v] == 0) queue_enqueue(v, q); while (!queue_empty(q)) { v = queue_dequeue(q); top_a[v] = ++i; u = g->varray[v]->next; while (u) { w = u->v; if (--indegree_array[w] == 0) queue_enqueue(w, q); u = u->next; } } if (i != g->vn) err_quit("graph is not a DAG"); queue_destroy(q);}
最短路径算法
每条边
单源最短路径问题:给定一个赋权图
图中也可能出现负边,这时可能出现称为负值圈的循环,如果出现了负值圈,最短路径问题就是不确定的。
无权最短路径
可以用广度优先搜索解决无权最短路径问题。广度优先搜索按层处理顶点,距开始点最近的顶点首先被赋值,最远的顶点最后被赋值,类似于树的层序遍历。
一种简单的算法是每次特定距离的顶点时遍历所有的顶点,这样运行时间为
void Unweighted(Table T){ int CurrDist; Vertex V, W; for (CurrDist = 0; CurrDist < VertexNum; CurrDist++) for each vertex V if (!T[V].Known && T[V].Dist == CurrDist) { T[V].Known = True; for each W adjacent to V if (T[W].Dist == Infinity) { T[W].Dist = CurrDist + 1; T[W].Path = V; } }}
和拓扑排序类似,可以简化该算法,考虑到正查看的顶点只有相距特定距离和特定距离加1两种情况,可以用队列来临时保存这些顶点,并且使用队列会在处理完特定距离的顶点后再处理特定距离加1的顶点,正好满足需要。如果使用邻接表,算法的运行时间为
void Unweighted(Table T){ Queue Q; Vertex V, W; Q = Create(VertexNum); Enqueue(S, Q); while (!Empty(Q)) { V = Dequeue(Q); T[V].Known = True; for each W adjacent to V if (T[W].Dist == Infinity) { T[W].Dist = T[V].Dist + 1; T[W].Path = V; Enqueue(W, Q); } } Destroy(Q);}
下面是使用了队列的拓扑算法的一个实现。
void graph_unweighted_path_length(graph_t *g, int s, pathlen_t plen_a[]){ int v, w; queue_t *q; vertex_t *u; for (v = 1; v <= g->vn; v++) { plen_a[v].dist = INT_MAX; plen_a[v].path = -1; } plen_a[s].dist = 0; q = queue_create(g->vn); queue_enqueue(s, q); while (!queue_empty(q)) { v = queue_dequeue(q); u = g->varray[v]->next; while (u) { w = u->v; if (plen_a[w].dist == INT_MAX) { plen_a[w].dist = plen_a[v].dist + 1; plen_a[w].path = v; queue_enqueue(w, q); } u = u->next; } } queue_destroy(q);}
Dijkstra算法
解决单源最短路径的一般算法称为Dijkstra算法,它是个典型的贪婪算法。贪婪算法分阶段地求解一个问题,在每个阶段把当前出现的当作最好的去处理。贪婪算法的问题是不能总是成功。
Dijkstra算法分阶段进行,每个阶段从所有未知顶点中选择
使用反证法可以证明,只要没有负边,算法总能顺利完成。
如果通过遍历表来找到最小的
void Dijkstra(Table T){ Vertex V, W; for (;;) { V = smallest unknown distance vertex; if (V == NotAVertex) break; T[V].Known = True; for each W adjacent to V if (!T[W].Known) if (T[V].Dist + Cvw < T[W].Dist) { T[W].Dist = T[V].Dist + Cvw; T[W].Path = V; } }}
下面用优先队列实现Dijkstra算法。
void graph_dijkstra(graph_t *g, int s, pathlen_t plen_a[]){ int v, w; heap_t *h; vertex_t *u; vdist_t *vd; int c; for (v = 1; v <= g->vn; v++) { plen_a[v].known = 0; plen_a[v].dist = INT_MAX; plen_a[v].path = -1; } plen_a[s].dist = 0; h = heap_create(g->en, dist_lt); for (v = 1; v <= g->vn; v++) { vd = vdist_create(v, plen_a[v].dist); heap_insert(h, vd); } while (1) { do { vd = heap_delete(h); if (vd == NULL) { heap_destroy(h); return; } v = vd->v; vdist_destroy(vd); } while (plen_a[v].known); plen_a[v].known = 1; u = g->varray[v]->next; while (u) { w = u->v; c = u->c; if (!plen_a[w].known && plen_a[v].dist + c < plen_a[w].dist) { vd = vdist_create(w, plen_a[v].dist+c); heap_insert(h, vd); plen_a[w].path = v; } u = u->next; } } heap_destroy(h);}
有负边值的图
如果图有负边值,就不能用Dijkstra算法了,可以将无权和赋权的算法结合起来解决。
void WeightedNegative(Table T){ Queue Q; Vertex V, W; Q = Create(VertexNum); Enqueue(S, Q); while (!Empty(Q)) { V = Dequeue(Q); for each W adjacent to V if (T[V].Dist + Cvw < T[W].Dist) { T[W].Dist = T[V].Dist + Cvw; T[W].Path = V; if (W is not already in Q) Enqueue(W, Q); } } Destroy(Q);}
下面是该算法的一个实现。
void graph_weighted_negative_path_length(graph_t *g, int s, pathlen_t plen_a[]){ int v, w; queue_t *q; vertex_t *u; int c; for (v = 1; v <= g->vn; v++) { plen_a[v].dist = INT_MAX; plen_a[v].path = -1; } plen_a[s].dist = 0; q = queue_create(g->vn); queue_enqueue(s, q); while (!queue_empty(q)) { v = queue_dequeue(q); u = g->varray[v]->next; while (u) { w = u->v; c = u->c; if (plen_a[v].dist + c < plen_a[w].dist) { plen_a[w].dist = plen_a[v].dist + c; plen_a[w].path = v; if (!queue_inqueue(w, q)) queue_enqueue(w, q); } u = u->next; } } queue_destroy(q);}
这个算法对于赋权图每条边可能不只执行一次,每个顶点最多出队
无圈图
如果图是无圈的,可以以拓扑顺序来选择顶点,改进Dijkstra算法。当顶点
无圈图可以模拟下坡滑雪、不可逆化学反应等。还有一个重要用途是关键路径分析法,用来分析动作节点图,松弛时间为0的动作为关键动作,由关键动作组成的路径为关键路径。
网络流问题
设有向图
这类问题一般都要分阶段解决。由图
上面的算法有个问题是,路径选择的不同可能出现不同的结果,选择有些路径会在达到最大流量前就找不到新的路径。这是贪婪算法行不通的一个例子。
解决该问题可以通过在
现在还有一个问题,如果存在流特别大的通路,因为路径是任意选择的,可能出现多次运行运行时间差距特别大的情况。避免这个问题的方法是总是选择使流增长最大的路径,可以通过修改Dijkstra算法来完成。如果最大的边容量为
最小生成树
一个无向图
对任一生成树
Prim算法
Prim算法维护一个已添加到树上的顶点集,在每一阶段都从所有
Kruskal算法
Kruskal算法连续地按照最小的权选择边,当所选的边不产生圈时就把它作为选定的边。开始时存在
对边的选择通过不相交集来完成。需要能够按顺序选取最小边,比较好的办法是使用堆,以线性时间建立。通常只有一部分的边需要测试。该算法的最坏运行时间为
void Kruskal(Graph G){ int EdgesAccepted; DisjSet S; PriorityQueue H; Vertex U, V; SetType Uset, Vset; Edge E; Initialize(S); ReadGraphIntoHeapArray(G, H); BuildHeap(H); EdgesAccepted = 0; while (EdgesAccepted < VertexNum - 1) { E = DeleteMin(H); Uset = Find(U, S); Vset = Find(V, S); if (Uset != Vset) { EdgesAccepted++; SetUnion(S, Uset, Vset); } }}
下面是该算法的一个实现。
void graph_kruskal(graph_t *g){ dset_t *s; heap_t *h; vertex_t *u; edge_t *e; int v; int vs, ws; int edge_added = 0; s = dset_create(g->vn); h = heap_create(g->en, cost_lt); for (v = 1; v <= g->vn; v++) { u = g->varray[v]->next; while (u) { e = edge_create(v, u->v, u->c); heap_insert(h, e); u = u->next; } } while (edge_added < g->vn - 1) { e = heap_delete(h); vs = dset_find(e->v, s); ws = dset_find(e->w, s); if (vs != ws) { edge_added++; printf("(%d,%d) ", e->v, e->w); dset_union(s, vs, ws); } edge_destroy(e); } printf("\n"); heap_destroy(h); dset_destroy(s);}
深度优先搜索
深度优先搜索是对前序遍历的推广。为了避免圈,每访问一个顶点,需要标记该顶点为访问过的,并对未被标记的所有邻接顶点递归调用深度优先搜索。对于无向图,每条边在邻接表中出现两次。如果图是无向的且不连通,或是有向的但非强连通,则可能会访问不到某些顶点,此时搜索未标记的顶点来继续。每条边只被访问一次,所以使用邻接表时,遍历的运行时间为
当且仅当从任一顶点开始的深度优先搜索能访问到每一个顶点,无向图是连通的。如果一个连通的无向图去掉任一顶点后仍连通,则称为双连通的。删除后使图不再连通的顶点称为割点。
深度优先搜索可以在线性时间找出连通图中的所有割点。首先进行深度优先搜索给顶点编号
NP-完全性
一般来说,运行时间不能比线性更好,运行时间更小的情况往往是对数据进行了预处理。另一方面,存在不可能解出的问题,称为不可判定问题。如停机问题:是否能让C编译器能检查所有的无限循环。
NP表示非确定型多项式时间,NP类在难度上小于不可判定问题的类。确定型机器在每一时刻都在执行一条指令,再根据它执行接下来的指令,这是确定的。非确定型机器则对其后的步骤是有选择的,并总是选择能得到解的正确的步骤。NP类包括所有具有多项式时间解的问题。但不是所有可判定问题都属于NP。
NP-完全问题是NP问题的一个子集,它满足NP中的任一问题都能够多项式地归约成NP-完全问题。NP-完全问题是最难的NP问题。
证明问题是NP-完全的,需要从另一个NP-完全问题变换到它。第一个被证明是NP-完全的问题是可满足性问题,它是被直接证明的。
哈密尔顿回路问题、巡回售货员问题、最长路径问题、装箱问题、背包问题、图的着色问题和团的问题等都是NP-完全问题。
- 图
- 图
- 图
- 图
- 图
- 图
- 图
- 图
- 图
- 图
- 图
- 图
- 图
- 图
- 图
- 图
- 图
- 图
- Android NDK开发总结
- JNI初步接触--认识、简单配置
- 【hdu】4685 Prince and Princess【二分匹配+tarjan】
- Android 向右滑动销毁(finish)Activity, 随着手势的滑动而滑动的效果
- oj:进制转换
- 图
- 如何在java中调用js方法
- hdu 5795 A Simple Nim (sg函数)
- 启发式搜索1
- leetcode Word Break
- 灰度级和像素值
- Android 使用Scroller实现绚丽的ListView左右滑动删除Item效果
- C51数据类型扩充定义
- Android手机获取GPS卫星数量问题