对于需要枚举路径上两点的,可以考虑建反图,双向来考虑,只要枚举中间的分割点即可。如《NOIP2009最优贸易》。
默认
1,图为一张 n n n个点、 m m m条边的有向图(无向图两次加边即可)。
存边
邻接矩阵
空间复杂度: O ( n 2 ) \Omicron(n^2) O(n2)。
链式前向星
用head[i]保存i节点最后一条边的编号。
nex[x]表示与x这条边共起点的上一条边。(next是关键字所以使用nex)
ver[x]表示x这条边的终点。
当你遍历一个节点的所有边,就从head开始一个个找,直到指向为空。
for(int i = head[now]; i; i = nex[i])//nex的初始值为0
加边:
记录这条边的终点,把它的nex(上一条边)这个指针指向原来的最后一条边,再把它登记为最后一条边。
void add(int u,int v)
{
ver[++tot] = v,nex[tot] = head[u],head[u] = tot;
}
空间复杂度: O ( n + m ) \Omicron(n+m) O(n+m)。
单源最短路
贪心
即Dijkstra。
适用场合:
1,所有边长非负。
流程:
原理:
边权为正→找到的y的d已经最小
普通版
void dij(int x)//Dijkstra算法。
{
fill(d+1,d+n+1,INT_MAX);//将初始距离设为无穷大。
fill(vis+1,vis+n+1,false);//将初始标记设为未访问。
d[x] = 0;//起点距离为0。
u(i,1,n)//一共只需要更新n-1次,因为同一个点不会被重复更新。
{
int now = 0;
u(j,1,n+1)//找当前距离最小的点。
{
if(!vis[j] && (d[now] > d[j] || now == 0))
now = j;
}
vis[now] = true;
for(int y = head[now]; y; y = nex[y])
{
int des = ver[y];//目的地destination。
if(d[des] > d[now] + edge[y])
d[des] = d[now] + edge[y];//更新距离。
}
}
}
这个时间复杂度 O ( n 2 + m ) \Omicron(n^2+m) O(n2+m)。
堆优化版
时间复杂度应该是 O ( ( m + n ) log n ) \Omicron((m+n)\log n) O((m+n)logn)。
class node
{
public:
int num;
int dis;
node(int _num,int _dis)
{
num = _num;
dis = _dis;
}
bool operator < (node y) const
{
//return d[num] > d[y.num];
//一定不要贪图省空间写成这样!会错而且会变慢。可能是因为这样的话改变了d中的内容就会影响q的堆性质。
//因为默认大顶堆,所以这里要反过来。
return dis > y.dis;
}
};
priority_queue<node> q;
void dij(int x)//Dijkstra算法。
{
fill(d+1,d+n+1,INT_MAX);//将初始距离设为无穷大。
fill(vis+1,vis+n+1,false);//将初始标记设为未访问。
d[x] = 0;//起点距离为0。
q.push(node(x,0));
while(!q.empty())
{
node now = q.top();//利用堆在log时间内维护当前距离最小的点。
q.pop();
if(vis[now.num]) continue;//这句不可省略,因为可能压入了多个相同的节点。
vis[now.num] = true;
for(int y = head[now.num]; y; y = nex[y])
{
int des = ver[y];//目的地destination。
if(d[des] > d[now.num] + edge[y])
{
d[des] = d[now.num] + edge[y];//更新距离。
if(!vis[des]) q.push(node(des,d[des]));
}
}
}
}
注意到在 m ≫ n m\gg n m≫n时,不用堆优化会更快。
有向图最小环
枚举起点,用堆优化的dijkstra,扫描完所有出边后将起点的 d = ∞ d=\infin d=∞,起点第二次被取出时就是经过此起点的最小环长度。
迭代
普通版
即Bellman-Ford。
适用场合:
1,可以找环。
2,可以处理负边权,但是有负边权的时候复杂度会增加。有SLF LLL DFS优化等。
SLF(Small Label First) 用双端队列,每次更新d[y]后与队头比较,若小,从队头入队。
流程:
原理:
若对所有边 ( x , y , z ) (x,y,z) (x,y,z)都满足三角不等式 d [ y ] ⩽ d [ x ] + z d[y]\leqslant d[x]+z d[y]⩽d[x]+z,则 d d d数组就是所要的最短路。
复杂度
时间: O ( n × m ) . \Omicron(n\times m). O(n×m).
队列优化版
即SPFA。
适用场合:
同普通版。
原理:
避免了普通版对不需要扩展节点的冗余扫描。
复杂度
时间: O ( k m ) , k \Omicron(km),k O(km),k 是一个小常数,但是你知道大家都能把它卡回 O ( n m ) \Omicron(nm) O(nm)。
代码:
void spfa()
{
queue<int> q;
memset(dis,0x3f,sizeof(dis));
dis[1] = 0, v[1] = 1;//v记录是否在队列中。
q.push(1);
while(!q.empty())
{
tem = q.front();
q.pop();
v[tem] = 0;
for(int i = head[tem]; i; i = nex[i])
{
if(dis[ver[i]] > dis[tem] + edg[i])
{
cnt[ver[i]]++;
if(cnt[ver[i]] >= n)//就算每个点都更新它,也就n-1回。多了就是有环。
{
printf("有环");
exit(0);
}
dis[ver[i]] = dis[tem] + edg[i];
if(!v[ver[i]]) v[ver[i]] = true, q.push(ver[i]);
}
}
}
}
任意两点最短/长路
一般用Floyd。
原理:
动态规划,用 d [ k , i , j ] d[k,i,j] d[k,i,j]表示经过若干个编号不超过 k k k的结点从 i i i到 j j j的最短路。
状态转移方程:
d
[
k
,
i
,
j
]
=
min
(
d
[
k
−
1
,
i
,
j
]
,
d
[
k
−
1
,
i
,
k
]
+
d
[
k
−
1
,
k
,
j
]
)
.
d[k,i,j]=\min(d[k-1,i,j],d[k-1,i,k]+d[k-1,k,j]).
d[k,i,j]=min(d[k−1,i,j],d[k−1,i,k]+d[k−1,k,j]).
与背包问题同理,最外一维可以省略。
d
[
i
,
j
]
=
min
(
d
[
i
,
j
]
,
d
[
i
,
k
]
+
d
[
k
,
j
]
)
.
d[i,j]=\min(d[i,j],d[i,k]+d[k,j]).
d[i,j]=min(d[i,j],d[i,k]+d[k,j]).
复杂度:
空间: O ( n 2 ) \Omicron(n^2) O(n2)。
时间: O ( n 3 ) \Omicron(n^3) O(n3)。
传递闭包
给定若干个元素和若干对二元关系,且关系具有传递性,通过传递性推导出尽量多元素之间的关系的问题被称为传递闭包。
通过连边构建关系,Floyd算法可以解决传递闭包问题。
如POJ1094
代码:
#define u(i,a,b) for(int i = a; i < b; i++)
#define d(i,a,b) for(int i = a; i >= b; i--)
#define sci(a) scanf("%d",&a)
#define LL long long
using namespace std;
char gc()
{
char c = getchar();
while(c == '\n' || c == ' ') c = getchar();
return c;
}
int big_letter_to_number(char x)
{
return x - 'A';
}
int d[30][30],road[30][30];
string rout[30][30],ori_rout[30][30];
void solve(int n, int m)
{
string tem;
int left = 0;
if(m == 0)
{
if(n == 1)
{
printf("Sorted sequence determined after 0 relations: A.\n");
return;
}
else
{
printf("Sorted sequence cannot be determined.\n");
return;
}
}
u(t,0,m)
{
cin>>tem;
road[big_letter_to_number(tem[0])][big_letter_to_number(tem[2])] = 1;
ori_rout[big_letter_to_number(tem[0])][big_letter_to_number(tem[2])] = tem;
ori_rout[big_letter_to_number(tem[0])][big_letter_to_number(tem[2])].erase(1,1);
u(i,0,n)
{
u(j,0,n)
{
d[i][j] = road[i][j];
rout[i][j] = ori_rout[i][j];
}
}
u(k,0,n)
{
u(i,0,n)
{
u(j,0,n)
{
if(d[i][k] != 0 && d[k][j] != 0)
{
if(d[i][k] + d[k][j] > d[i][j])
{
d[i][j] = d[i][k] + d[k][j];
rout[i][j] = rout[i][k] + rout[k][j];
rout[i][j].erase(rout[i][k].length(),1);
}
}
if(j == i && d[i][j] != 0)
{
printf("Inconsistency found after %d relations.\n",t+1);
left = m-t;
goto end;
}
if(d[i][j] == n-1)
{
printf("Sorted sequence determined after %d relations: ",t+1);
cout<<rout[i][j]<<"."<<endl;
left = m-t;
goto end;
}
}
}
}
}
printf("Sorted sequence cannot be determined.\n");
end:
u(i,0,left-1)
cin>>tem;
return;
}
void init(int n)
{
u(i,0,n)
{
u(j,0,n)
{
road[i][j] = 0;
ori_rout[i][j] = "";
}
}
}
int main()
{
int n,m;
sci(n);
sci(m);
while(n != 0)
{
init(n);
solve(n,m);
sci(n);
sci(m);
}
}
另外此题还要记录路径,在更新长度的同时记录路径即可。( O ( n 3 ) \Omicron(n^3) O(n3)能维护的就是多!)
无向图最小环
考虑Floyd算法的过程当,外层循环
k
k
k刚开始时的
d
[
i
,
j
]
d[i,j]
d[i,j]保存着经过编号不超过
k
−
1
k-1
k−1的结点从
i
i
i到
j
j
j的最短路长度。
min
1
⩽
i
<
j
<
k
{
d
[
i
,
j
]
+
a
[
j
,
k
]
+
a
[
j
,
i
]
}
\min_{1\leqslant i<j<k}\{d[i,j] + a[j,k]+a[j,i]\}
1⩽i<j<kmin{d[i,j]+a[j,k]+a[j,i]}
表示了节点编号最大为
k
k
k的最小环长度。