邻接矩阵一般只适用于顶点数目不太大的题目(一般不超过1000)
邻接表 (顶点数大于1000的题目)
vector
实现开一个vector
数组Adj[N]
,其中N
为顶点个数
vector
中的元素类型可以直接定义为int
Node
注:结构体的初始化可以定义构造函数
strcut Node{
int v, w;
Node(int _v, int _w) : v(-v), w(_w) {}
}
//此时可以不定义临时变量即可实现加边操作
Adj[i].push_back(Node(3, 4));
DFS(u) { //访问顶点u
vis[u] = true; //设置u已被访问
for(从u出发能到达的所有顶点v) //枚举
if(vis[v] = false)
DFS(v);
}
DFSTrave(G) {
for(G的所有顶点u) //枚举
if (vis[u] == false
DFS(u); //访问u所在的连通块
}
BFS(u) { //遍历u所在的连通块
queue q; //定义队列
将u入队列
inq[u] = true; //设置u已入队列
while(q非空) {
取出q的队首元素u进行访问;
for(从u出发能到达的所有顶点v)
if(inq[v] == false){ //若未曾加入队列
将v入队列
inq[v] = true; //标记v加入队列
}
}//while
}
BFSTrave() { //遍历图
for(G的所有顶点u)
if(inq[u] == false)
BFS(u); //遍历u所在的连通块
}
int n, G[maxn][maxn]; //n为顶点数,maxn为最大顶点数
bool vis[maxn] = {false};
void DFS(int u, int depth) {
vis[u] = true; //设置u已被访问
//如果需要对u进行一些操作,在此处进行
for(int v = 0; v < n; v++) //枚举所有结点
if(vis[v] == false && G[u][v] != INF) //若未曾访问,且u可到达v
DFS(v, depth + 1); //访问i,深度+1
}
void DFSTrave() { //遍历图G
for(int u = 0; u < n; u++)
if(vis[u] == false)
DFS(u, 1); //初始为第一层
}
vector<int> Adj[maxn]; //图G的邻接表
int n; //n为顶点数
bool vis[maxn] = {false};
void DFS(int u, int depth) {
vis[u] = true; //设置u已被访问
//如果需要对u进行一些操作,在此处进行
for(int i = 0; i < Adj[u].size(); i++) { //枚举所有结点
int v = Adj[u][i]; //vector可按下标访问
if(vis[v] == false) //若未曾访问,且u可到达v
DFS(v, depth + 1); //访问i,深度+1
}//for
}
void DFSTrave() { //遍历图G
for(int u = 0; u < n; u++)
if(vis[u] == false)
DFS(u, 1); //初始为第一层
}
int n, G[maxn][maxn]; //n为顶点数
bool inq[maxn] = {false}; //标记结点是否入队列
void BFS(int u) {
queue<int> q;
q.push(u);
inq[u] = true; //标记已入队列
while(!q.empty()) {
int u = q.front();
q.pop();
for (int v = 0; v < n; v++) {
if(inq[v] == false && G[u][v] != INF) {//未曾入队列,且可达
q.push(v);
inq[v] = true; //标记v已经入队列
}
}
}//while
}
void BFSTrave() { //遍历图
for(int u = 0; u < n; u++)
if(inq[u] == false)
BFS(u); //遍历u所在的连通块
}
vector<int> Adj[maxn]; //图G的邻接表
int n; //n为顶点数
bool inq[maxn] = {false};
void BFS(int u) {
queue<int> q;
q.push(u);
inq[u] = true; //标记已入队列
while(!q.empty()) {
int u = q.front();
q.pop();
for (int i = 0; i < Adj[u].size(); i++) {
int v = Adj[u][i]; //vector可按下标访问
if(inq[v] == false) {//未曾入队列
q.push(v);
inq[v] = true; //标记v已经入队列
}
}
}//while
}
void BFSTrave() { //遍历图
for(int u = 0; u < n; u++)
if(inq[u] == false)
BFS(u); //遍历u所在的连通块
}
注:当题目要求输出结点的层号时,只需把vector<>
中存储的元素换成结构体即可,结构体中包括结点编号和层号信息。遍历的同时更新层号信息,子节点是父节点结点的层号+1
。结构体如下:
struct node {
int v;
int layer;
};
练习试题:PAT. A1003 || PAT. A1030 || PAT. A1018 || PAT. A1072 || PAT. A1087 ||
//G为图,一般设为全局变量,数组d为源点到达各个点的最短路径长度,s为起点
Dijkstra(G, d[], s) {
初始化;
for(循环n次) {
u = 使d[u]最小且还未被访问的顶点的标号; //暴力搜索 or 堆结构
标记u已被访问;
for(从u出发能到达的所有顶点v) {
if (v未被访问 && 以u为中介点 使 s到顶点v的最短距离d[v]更优) {
优化d[v];
//可以在此处保存路径 把u保存为v的前驱即可
pre[v] = u;
}
}
}//for
}//Dijkstra
实现差异: 主要区别主要在于如何枚举从u
出发到达的顶点v
上;邻接矩阵需要枚举所有结点查看顶点v
能否到达u
,而邻接表则可以直接得到这些顶点v
;
注: 若题目所给为无向图,把它转化为两条有向边即可!
const int maxn = 1010;
const int INF = 0x7fffffff;
int n; //结点数
int d[maxn]; //起点到各顶点的最短距离
bool vis[maxn] = {false}; //标记数组,标记是否已经访问
int pre[maxn]; //保存结点前驱,用于获取最短路径
int G[maxn][maxn]; //顶点数,图
//邻接矩阵版本
void Dijkstra(int s) {
fill(d, d + maxn, INF); //初始为不可达(慎用memset)
d[s] = 0;
for(int i = 0; i < n; i++) { //循环n次(第一次找到的肯定是起点本身,正好完成初始化)
//找到未访问结点中d[]最小的
int u = -1, MIN = INF;
for(int j = 0; j < n; j++) {
if(vis[j] == false && d[j] < MIN) {
u = j;
MIN = d[j];
}
}
if(u == -1) //找不到小于INF的d[u],说明剩下的顶点和起点s不连通
return;
vis[u] = true; //标记已访问
for(int v = 0; v < n; v++) { //更新
//如果v未访问 && u能到达v && 以u为中介点可以是d[v]更优
if(vis[v] == false && G[u][v] != INF && d[u] + G[u][v] < d[v]) {
d[v] = G[u][v] + d[u];
pre[v] = u;
}
}
}//for - i
}//Dijkstra
//时间复杂度:两层循环 ,O(n^2)
struct node{
int v, dis; //v为边的目标结点,dis为边权
};
vector<node> Adj[maxn]; //邻接表; Adj[u]保存从u出发能到达的所有顶点(结构体中还保存了其间的边权)
//邻接表版本
void Dijkstra(int s) {
fill(d, d + maxn, INF);
d[s] = 0;
for(int i = 0; i < n; i++) {
int u = -1, MIN = INF;
for(int j = 0; j < n; j++) {
if(vis[j] == false && d[j] < MIN) {
u = j;
MIN = d[j];
}
}
if(u == - 1) return;
vis[u] = true;
for(int j = 0; j < Adj[u].size(); j++){ //注意vector<>保存的是结构体
int v = Adj[u][j].v;
if(vis[v] == false && Adj[u][j].dis + d[u] < d[v]){
d[v] = d[u] + Adj[u][j].dis;
pre[v] = u;
}
}
}//for - i
}//Dijkstr
//时间复杂度:O(V^2 + E) 【双重循环遍历结点;每条边都遍历且只遍历了一遍】
pre[]
,pre[v]
表示最短路径上v
的前驱;u
保存为v
的前驱即可(见伪代码);Dijkstra()
结束后,从目标点DFS()
回溯即可得到最短路径;void DFS(int s, int v) {
if(v == s){ //如果已经到达起点,则输出并返回
printf("%d\n", s);
return;
}
DFS(s, pre[v]); //回溯
printf("%d\n", v); //等返回后在输出
}
Dijkstra
优化
最外层的循环O(V)
是无法避免的,但是寻找最小距离d[u]
可以用堆结构优化,是内部复杂度降到O(logV)
,整体复杂度可以到O(VlogV + E)
;
【堆结构可以直接用STL
的priority_queue
实现】
题目考法
很多时候最短路径不止一条,就需要题目所给的其他条件选择其中一条;一般有一下三种考法:
1、给每条边再增加一个边权(比如花费),然后要求最短路径有多条时,选择花费之和最小的;
2、给每个点增加一个点权,有多条最短路径时,选择点权之和最大(最小)的;
3、直接问有多少条最短路径;
这三种出法都只需增加一个数组,存放新增的边权或点权或最短路径条数然后在Dijkstra()
中修改 更新d[v]
的那一步操作即可;其他无需改变;
//考法一:边增加花费
int cost[maxn][maxn]; //存储边的额外信息
int c[maxn]; //存储到每个点最短路径的累计花费
for(int v = 0; v < n; v++) {
if(vis[v] == false && G[u][v] != INF) {
if(d[u] + G[u][v] < d[v]){
d[v] = G[u][v] + d[u];
c[v] = cost[u][v] + c[u];
} else if (d[u] + G[u][v] == d[v] && c[u] + cost[u][v] < c[v]) { //最短距离相同时,若花费更小,则更新c[v]
c[v] = c[u] + cost[u][v];
}
}
}
//考法二:顶点增加权值
int weight[maxn]; //存储每个点的权值
int w[maxn]; //存储到每个点最短路径的累计权重
for(int v = 0; v < n; v++) {
if(vis[v] == false && G[u][v] != INF) {
if(d[u] + G[u][v] < d[v]){
d[v] = G[u][v] + d[u];
w[v] = weight[v] + w[u];
} else if (d[u] + G[u][v] == d[v] && w[u] + weight[v] > w[v]) {
w[v] = w[u] + weight[v]; //最短距离相同时,若权重更大,则更新w[v]
}
}
}
//考法三:输出最短路径条数
int num[maxn]; //记录到每个点的最短路径条数
for(int v = 0; v < n; v++) {
//如果v未访问 && u能到达v && 以u为中介点可以是d[v]更优
if(vis[v] == false && G[u][v] != INF) {
if(d[u] + G[u][v] < d[v]){
d[v] = G[u][v] + d[u];
num[v] = num[u];
} else if (d[u] + G[u][v] == d[v]) {
num[v] += num[u]; //最短距离相同时,累加num!!!
}
}
}
练习题: HDU-1863 || HDU-1233 || HDU-1879 || HDU-1875 || HDU-3371 || HDU-1162 || HDU-4313 || POJ-1861 ||
Dijkstra
算法和Prime
算法实际上是相同的思路,不过是数组d[]
所表示的最小距离含义不同而已;【Dijkstra
表示到源点的最小距离,而Prime
表示到树的最短距离;】 【多用数稠密图(边多)】此为一道 最小生成树的母题,已用多种思路解决!
const int maxn = 10010;
vector<int> Adj[maxn]; //邻接表
int in_degree[maxn]; //入度
int N; // 顶点数
//拓扑排序
bool top_sort() {
int num = 0; //保存加入拓扑序列的顶点数
queue<int> Q;
for(int i = 0; i < N; i++) { //所有入度为0的结点入队
if(in_degree[i] == 0)
Q.push(i);
}
while(!Q.empty()) {
int u = Q.front(); //取队首结点
// printf("%d", u); //此处可输出顶点u,作为拓扑了序列
Q.pop();
for(int i = 0; i < Adj[u].size(); i++) {
int v = Adj[u][i]; //u的后集结点v
in_degree[v]--; //结点v的入度-1
if(in_degree[v] == 0) //入度减为0时入队
Q.push(v);
}
Adj[u].clear(); //清空结点u的所有出边(如无必要可不写)
num++; //顶点数累加
}//while
if(num == N) return true; //拓扑排序成功
else return false; //失败
}
0
的顶点时选择编号最小的顶点,那么把queue
改成priority_queue
,并保持队首元素(堆顶元素)是优先队列中最小的元素即可(当然用set
也是可以的)AOV
网(Activity On Vertex):顶点表示活动,边表示活动间的优先关系的有向无环图;
AOE
网(Activity On Edge):带权的边表示活动,而用顶点表示事件的有向无环图;边权表示完成活动需要的时间;
AOE
网中的最长路径被称为关键路径,关键路径上的活动称为关键活动,关键活动会影响整个工程的进度
理解:关键路径的定义是AOE
网中的最长路径,为什么其长度会等于整个工程的最短完成时间呢?
**A:**从时间的角度上看,不能拖延的活动严格按照时间表所达到的就是最短时间;而从路径长度的角度上看,关键路径选择的总是最长的道路。
对一个没有正环的图(指从源点可达的正环),如果需要求最长路径长度,则可以把所有边的边权乘以-1
,然后使用Bellman-Ford
算法或SPFA
算法求最短路径,将所得结果取反即可;
如果图中有正环,那么最长路径是不存在;但如果要求最长简单路径(每个顶点最多只经过一次),那么虽然最长简单路径存在,却无法通过Bellman-Ford
等算法得到,原因是最长路径问题是NP-Hard
问题(即没有多项式时间复杂度算法可解决);
如果求有向无环图的最长路径长度,关键路径的求法比上面取反用SPFA
等算法更快;
思路:即求解DAG
(有向无环图)中最长路径的方法 - 先求点,再夹边
1、按照拓扑序和逆拓扑序分别计算个顶点(事件)的最早发生时间和最迟发生时间:
ve[j] = max { ve[i] + length[i->j] }
(j
的所有入边)vl[i] = min { vl[j] - length[i->j] }
(i
的所有出边)2、用上面的计算结果计算各边(活动)的最早开始时间和最迟开始时间:
e[i->j] = ve[j]
l[i->j] = vl[j] - length[i->j]
3、e[i->j] == l[i->j]
的边(活动)即为关键活动
代码实现
适用于 汇点确定且唯一 的情况,以n-1
号顶点为汇点为例;
#include
#include
#include
#include
#include
#include
using namespace std;
const int maxn = 10010;
struct edge{
int v; //终点
int w; //边权
};
vector<edge> Adj[maxn]; //邻接表(边带权值)
int in_degree[maxn]; //入度
int N; // 顶点数
stack<int> topOrder; //栈 保存拓扑序列
int ve[maxn]; //结点的最早发生时间
int vl[maxn]; //结点的最迟发生时间
//拓扑排序 顺便求ve数组
bool topLocgicalSort() {
queue<int> Q;
for(int i = 0; i < N; i++) { //所有入度为0的结点入队
if(in_degree[i] == 0)
Q.push(i);
}
while(!Q.empty()) {
int u = Q.front(); //取队首结点
Q.pop();
topOrder.push(u); //保存拓扑序列
for(int i = 0; i < Adj[u].size(); i++) {
int v = Adj[u][i].v; //u的后集结点v
in_degree[v]--; //结点v的入度-1
if(in_degree[v] == 0) //入度减为0时入队
Q.push(v);
//用ve[u]来更新u的所有后继结点
if(ve[u] + Adj[u][i].w > ve[v]) { //选最大值
ve[v] = ve[u] + Adj[u][i].w;
}
}
}//while
if(topOrder.size() == N) return true; //拓扑排序成功
else return false; //失败
}
//颠倒拓扑序列得到一组合法的逆拓扑序列,求vl数组
void get_vl() {
while(!topOrder.empty()) {
int u = topOrder.top();
topOrder.pop();
for(int i = 0; i < Adj[u].size(); i++) {
int v = Adj[u][i].v;
//用u的所有后继结点v的vl来更新vl[u]
if(vl[v] - Adj[u][i].w < vl[u]) { //选最小值
vl[u] = vl[v] - Adj[u][i].w;
}
}//for-i
}//while
}//get_vl
//关键路径
//使用动态规划可以更简洁地求解关键路径(11.6节)
int CriticalPath() {
memset(ve, 0, sizeof(ve)); //ve数组初始化为0
if(topLocgicalSort() == false) { //计算ve[]值
return -1; //非有向无环图
}
fill(vl, vl + maxn, ve[N - 1]); //初始化vl数组,值为终点(汇点)的ve值
get_vl(); //计算vl[]值
//遍历邻接表的所有边(代表活动),计算活动的最早开始时间e和最迟开始是按l
for(int u = 0; u < N; u++) {
for(int i = 0; i < Adj[u].size(); i++) {
int v = Adj[u][i].v, w = Adj[u][i].w;
//计算活动(边)的最早开始时间e和最迟开始时间l
int e = ve[u], l = vl[v] - w;
if(e == l) { //若e==l,则u->v为关键活动
printf("%d->%d\n", u, v);
//如果需要完整输出关键路径,保存即可(建一个邻接表,保存u->v)
}
}//for - i
}//for - u
return ve[N -1]; //返回关键路径长度
}//CriticalPath
其他扩展
ve[]
数组中的最大值为汇点即可;ve[]
数组的含义是时间的最早开始时间,因此所有事件中最大的一定是最后一个(或多个)时间,即汇点;fill()
函数之前添加一段语句,改变vl[]
函数初始值即可; int maxLength = 0;
for (int i = 0; i < n; i++) { //找到ve[]中的最大值
if (ve[i] > maxLength) maxLength = ve[i];
}
fill (vl, vl + n, maxLength);
2、如果想输出完整路径,就需要把关键活动存下来;
方法是新建一个邻接表,当确定u->v
是关键活动时,将其加入邻接表,这样最后生成的就是所有关键路径合成的图,最后可以用DFS
遍历来获取所有关键路径。(即可能有多条路径)
3、使用动态规划的做法可以更简洁地求解关键路径,补充完成后会加入链接!!