动态规划最短路算法总结
——以【Layout】差分约束为例
一.差分约束的概念
如果一个系统由 个变量和 个不等式组成,并且这 个不等式对应的系数矩阵每一行有且仅有一个1和-1,其它的都为0,这样的系统成为差分约束系统。
将不等式 变形为 ,再令 ,令 , ,再将数组名改为 ,不等式即可变形为: 。这就使人联想到SPFA中的一个松弛操作:
if(d[u] + w(u,v) <= d[v])
d[v] = d[u] + w(u,v);
虽然两个式子符号不同,但想想看,差分约束受到多个值的约束,这些值里面的最小值就是差分约束能取到的最大数值,而这也正是最短路的结果。所以:
对于每个不等式 ,对结点 和 建立一条 的有向边,边权为 ,求 的最大值,就是求0到n-1的最短路。
二.最短路算法详解
最短路可分为全点对最短路,代表算法Floyd-Warshall 和单源最短路,代表算法Dijkstra和Bell-Ford 。
1.全点对最短路及Floyd-Warshall算法
全点对最短路的决策量为 ,表示 与 之间的最短距离
子结构为 ,表示经过的节点数
表示从 到 ,中间只经历的最大结点不超过K
其最优子结构性质可以描述为:
=
min{ , }
表示中间经过的最大结点为 , 将 作为源点所以一定经过 。
代码实现:
struct node{
int vertex[maxn];//存储顶点数
int edges[maxn][maxn];//邻接矩阵
int n,e;//顶点数和边数
}MGraph;
void floyd(MGraph g)
{
int A[maxn][maxn];
int path[maxn][maxn];
int k,i,j;
for(i=0;i<n;i++){
for(j=0;j<n;j++){
A[i][j]=g.edges[i][j];
path[i][j]=i;
}
}
for(k=0;k<n;k++){
for(i=0;i<n;i++){
for(j=0;j<n;j++){
if(A[i][j]>(A[i][k]+A[k][j])){//关键步骤
A[i][j]=A[i][k]+A[k][j];
path[i][j]=k;
}
}
}
}
}
2.图的存储方式
- 邻接矩阵,优点是实现简单,缺点是容易造成空间浪费,当点数过多时,无法实现矩阵。
- 邻接链表,优点是不会有空间浪费,缺点是实现相对麻烦
- 前向星,存入(起点,终点,边长)三元组,并对起点进行排序。优点是实现简单,容易理解,缺点是需要读入所有边后,对边进行一次排序。时间开销大,实用性差。
- 链式前向星,存入(起点,终点,边长,下一条边)四元组,用head[i]数组来记录边。
struct node{
int u, v, w;
int next;
}A[10000];
void init()
{
node = 0;
memset(head, -1, sizeof(head));
}
void add(int u, int v, int w)
{
A[node].u = u;
A[node].v = v;
A[node].w = w;
A[node].next = head[u]; //指向同一个结点的前一条边
head[u] = node++; //把这次的边放进这个数组中去,head里存放的是起点为u的最后输入的
//的边对应的结点,可以通过这个结点的next,求得之前边的结点
}
单源最短路:
3.Dijkstra算法(松弛最小 周围边)
对于正权图,在可达的情况下最短路一定存在,最长路不一定存在。最短路具有最优子结构性质,所以是动态规划问题,其最优子结构的性质可以描述为:
表示从 到 的最短路,其中 和 是这条路径上的两个中间节点,那么 一定是 到 的最短路。
Dijkstra算法是最经典的最短路算法,用于计算正权图的单源最短路。它是基于这样的一个事实:
如果源点到 点的最短路已经求出,并且保存在 中,则可以利用 去更新 能够直接到达的点的最短路。 即:
具体算法描述如下:
输入: ,源点为 , 表示 到 的最短路, 表示 是否已经确定(布尔值)。
- 初始化,所有顶点 , , 。
- 在 的所有点中,选择 最小的点(贪心选择性质),并令 。如果不存在,则算法结束。
- 标记 ,更新和 直接相邻的所有顶点 的最短路:
时间复杂度:
当Q非空
找到最小的
枚举法找到与 相连的
松弛
Q非空,使整个循环进行|V|次,每次找出最小的 ,又要进行|V|次。对于所有的节点,枚举法枚举与 相连的 进行松弛需要2*|E|次。所以总的时间复杂度为 。
//使用链式前向星
queue<int>Q;
memset(vis,false,sizeof(vis));
memset(dis,inf,sizeof(dis));
dis[s] = 0; vis[s] = true;
while(true){//如果队列不为空
int min = inf; int i;
int j = 0;
for(i = 1; i <= n; i++){//选出最小的dis[i]
if(vis[i] == false && min > dis[i]){
min = dis[i];
j = i;
}
}
if(j == 0) break;//如果所有顶点都已经成为最小值了,算法结束
vis[j] = true;//标记vis
for(int k = head[j]; k != -1;k = A[k].next){//松弛j周围的每一条边
v = A[k].v;
if(dis[u] + A[k].w < dis[v]){
dis[v] = dis[u] + A[k].w;
}
}
}
4.Dijkstra算法 + 优先队列(小顶堆)
堆是用数组表示二叉树,小顶堆(大根堆)是指每个数都大于等于自己的父结点。可在 时间内插入或者删除数据,并且可以在 时间内得到当前数据最小值。
算法思路:
priority_queue <int,vector<int>,greater<int> > p;
void Dijkstra_Heap(s){
memset(dis, inf, sizeof(dis));
dis[s] = 0;
q.push(s);//在队列中放入源点
while(!q.empty()){
u = q.top();
q.pop();
for(int k = head[u]; e != inf; k = A[k].next){//对于每一个结点找其相邻的边
v = A[k].v;
if(dis[u] + A[k].w < dis[v]){
dis[v] = dis[u] + A[k].w;//进行松弛
q.push(v);//松弛完了放进去
}
}
使用两者结合的算法,时间复杂度为:
堆中的元素共有O(V)个,取出并更新O(E)次,所以时间复杂度为
5.Bellman-Ford算法(处理负权路)
Bellman-Ford算法可以在最短路存在的情况下求出最短路,并且存在负权圈的情况下判断出最短路不存在。它基于这样一个事实:一个图的最短路如果存在,那么最短路中必定不存在圈,所以最短路的顶点数除起点外只有n-1个 。
具体的算法描述如下:
- 输入:
- 初始化, ,
- 对于
- 对每条边 进行松弛: 若 则
- 再对于每条边判断,若 则存在负权圈
- 否则不存在
理解:如果没有负权圈,那么V-1遍已经完成松弛。如果有负权圈,松弛无法停止。
6.SPFA算法(松弛所有起始点相邻边,并计算一个顶点周围边的松弛次数)
SPFA(Shortest Path Faster Algorithm)是基于Bellman-Ford的思想,采用先进先出队列进行优化的一个计算但源最短路的快速算法。一般用来解决带负权圈的最短路问题。
算法思想:
建立一个队列,初始时队列只有一个起始点即源点,然后松弛与起始点相邻的边,并则将边的终点放入队列最后作为起始点。重复执行直到队列为空。
判断有无负环:如果一个顶点加入队列的次数超过n次则说明有负环存在。
具体算法代码可参考下文Layout的代码分析
三.Layout分析及代码
1.题意分析:
编号1到n的牛排队,有些牛比较友好,他们相隔的距离有最大值。有些牛很不友好,他们的相隔距离有最小值。可以看出题目中给出了差分约束限制,对于有最大值的情况,将其作为正权路,对于有最小值的情况,将其作为负权路,最终目的即求解最短路。
因为有负权有正权,所以采用SPFA算法。
2.代码分析:
#include<cstdio>
#include<cstdlib>
#include<queue>
#define max 1000000
using namespace std;
struct node{
int u,v,w;
int next
}A[maxn];//建立链式前向星
int cnt[maxn];//用于判断是否有负权圈
int dis[maxn];//用于计算最短距离
bool vis[maxn];//用于判断是否被放入过队列中!
int head[maxn];//存储起点为u的结点最大值
int node;
void init()
{
node = 0;
memset(head, -1, sizeof(head));
memset(vis, false, sizeof(vis));
memset(dis, max, sizeof(dis));
memset(cnt, 0, sizeof(cnt));
}
void add(int u,int v,int w)
{
A[node].u = u;
A[node].v = v;
A[node].w = w;
A[node].next = head[u];
head[u] = node++;
}
int spfa(int n)
{
int u = 1;
queue<int>Q;
dis[u] = 0; vis[u] = true;
Q.push(u); cnt[u] = 1;
while(!Q.empty()){
u = Q.front();//按照先进先出的顺序对队列中结点周围的边进行松弛
Q.pop();
vis[u] = false;
for(int k = head[u]; k != -1; k = A[k].next){
int v = A[k].v;
if(d[u] + A[k].w < d[v]){//如果发生了松弛
d[v] = d[u] + A[k].w;
if(!vis[v]){//如果v不在队列里,如果v在队列里就不把它再放进去一次了
cnt[v]++;
vis[v] = true;
Q.push(v);
}
}
}
}
return dis[n];
}
int main()
{
int n,ml,md;
int i;
while(scanf("%d%d%d",&n,&ml,&md)!=EOF){
int a,b,c;
init();
for(i = 0; i < ml; i++){
scanf("%d%d%d",&a,&b,&c);
add(a,b,c);
}
for(i = 0;i < md; i++){
scanf("%d%d%d",&a,&b,&c);
add(b,a,-c);//负权
}
int ans = spfa(n);
if(ans == inf){//没有更新,则可以取任意远,没有约束
printf("%d\n", -2);
}
else if(ans == -1){
printf("%d\n", -1);
}
else
printf("%d\n",ans);
}
return 0;
}
代码参考链接:https://blog.csdn.net/r1986799047/article/details/50444805
算法分析参考链接:http://www.cppblog.com/menjitianya/archive/2015/11/19/212292.html
https://blog.csdn.net/scythe666/article/details/50938123