虽说是再谈
然而我好像在blog中从来没有谈过最短路的问题
Dijkstra
简单的dijkstra就是n^2的效率
其中一个很重要的操作是找到dis最小的点
由此进行下一次的转移
如果我们在这里加上堆,那么复杂度就可以降为nlogn
这是目前最好的复杂度
在代码中,我用结构体和priority_queue完成的堆操作
(写完我就要疯了)
tip
在省选中,就不要用map了,
刚才和yhzq求证了一下,表示priority_queue还是可以用的
struct heapnode{ //dijkstra用到的优先队列的结点
int d,u; //d 距离,u 结点编号
bool operator <(const heapnode &a) const
{
return d>a.d;
}
//在优先队列中,是从大到小排序的,所以我们反向定义一下<号
};
struct node{
int x,y,v; //起点 终点 边权
};
//定义边的结构体
struct Dijkstra{
int n,m; //点和边的数目
vector<node> e; //存储边
vector<int> G[N]; //存储每个点所连的边的编号
bool p[N]; //已用标记
int dis[N]; //最短距离
int pre[N]; //每个点的转移边
void init(int n)
{
this->n=n;
e.clear();
for (int i=1;i<=n;i++) G[i].clear();
}
void add(int u,int w,int z) //加边
{
e.push_back((node){u,w,z}); //直接把边作为结构体塞进去
m=e.size();
G[u].push_back(m-1); //size是m,e中边是从0开始编号的
}
void dijkstra(int s)
{
memset(dis,0x33,sizeof(dis));
memset(pre,0,sizeof(pre));
memset(p,1,sizeof(p));
dis[s]=0;
priority_queue<heapnode> Q;
Q.push((heapnode){0,s}); //距离和点的编号作为一个结构体直接塞进优先队列
while (!Q.empty())
{
heapnode now=Q.front(); Q.pop();
int u=now.u;
if (!p[u]) continue; //打过标记的不能作为转移点
p[u]=0; //千万不要忘了打标记
for (int i=0;i<G[u].size();i++) //循环和u相连的所有边
{
node way=e[G[u][i]];
if (dis[way.y]>dis[u]+way.v)
{
dis[way.y]=dis[u]+way.v;
pre[way.y]=G[u][i];
Q.push((heapnode){dis[way.y],way.y});
}
}
}
}
};
dijkstra不仅可以用来求单源最短路
还可以枚举两点之间的所有最短路,以及统计最短路条数
枚举最短路
找出所有点到目标点的最短路d[i]
然后从起点开始出发
每次只能走d[j]=d[i]+w(i,j)的边
这样得到的一定是起点到终点的最短路
统计最短路条数
有dp的意味
令f[i]表示从i到终点的最短路条数
则有f[i]=sum{f[j]|d[i]=d[j]+w(i,j)}
从后往前推,边界条件是f[end]=1
最短路树
用Dijkstra可以求出单源最短路树,方法是在发现dis[j]>dis[i]+w(i,j)时,
除了更新dis之外,还要记录一下p[j]=i
这样把p看作是父指针,所有点就形成了一棵树
这样要从起点出发沿最短路走到任意其他结点,只要沿着最短路树的边走即可
在Dijkstra的模板中,我们实际上已经求出了p数组
Bellman-Ford
Bellmax-Ford算法一个很重要的应用就是判断负环
在迭代n-1次后如果还可以进行松弛操作说明一定存在负环
如果用队列实现,那么当某个点入队n次时就可以判断存在负环
在代码中有这样一段:
for (int i=1;i<=n;i++)
{
dis[i]=0;
in[i]=1;
Q.push(i);
}
因为Bellman求的也是单源最短路,所以这就相当于建立了一个虚拟源点
并且把所有与虚拟原点相连的结点都更新了dis
struct node{
int x,y,v;
};
struct Bellman{
int n,m;
vector<node> e; //边列表
vector<int> G[N]; //每个结点所连边的编号
bool in[N]; //在对列中的标记
int pre[N]; //转移边
int dis[N]; //最短路
int cnt[N]; //入队次数
void init(int n)
{
this->n=n;
e.clear();
for (int i=1;i<=n;i++) G[i].clear();
}
void add(int u,int w,int z)
{
e.push_back((node){u,w,z});
m=e.size();
G[u].push_back(m-1);
}
bool fuhuan(int s)
{
queue<int> Q;
memset(in,0,sizeof(in));
memset(cnt,0,sizeof(cnt));
for (int i=1;i<=n;i++)
{
dis[i]=0;
in[i]=1;
Q.push(i);
}
while (!Q.empty())
{
int now=Q.front(); Q.pop();
in[now]=0;
for (int i=0;i<G[now].size();i++)
{
node way=e[G[now][i]];
int v=way.y;
if (dis[v]>dis[now]+way.v)
{
dis[v]=dis[now]+way.v;
pre[v]=G[now][i];
if (!in[v])
{
Q.push(v);
in[v]=1;
if (++cnt[v]>n) return 1;
}
}
}
}
return 0;
}
};
差分约束
这是图论中很重要的一部分
ta的一般形式就是
不等式组,每个不等式满足形式:xj-xi<=bk
差分约束系统的经典解法就是最短路
xj-xi<=bk,移项可得xj<=bk+xi
符合最短路的形式
设dis表示源点到达每一个点的最短距离
则对于任一条边(i—>j)都有dis[j]<=dis[i]+w(i,j)
这样我们针对每一个不等式xj-xi<=bk建图,
单向连接i—>j,边权为bk
在图上运行Bellman,得到的dis即为一组可行解
如果Bellman运行失败(存在环),则原差分约束系统无解
下面是一些前辈对于差分约束的总结(也有我自己的感悟):
差分约束的作用
求值:它可以求解不等式的一组解
判环:判正负环
差分约束中的松弛操作
举个最短路的例子:
1->2 ,v=3
1->3 ,v=1
3->2 ,v=1
即以1为起点,d[2]>d[3]+w(3,2),可以进行松弛
而三条边本身表示的是:a(2)-a(1)<=3,a(3)-a(1)<=1,a(2)-a(3)<=1,
用1->3->2这条边更新1->2,
实际上表示的是后两个不等式之和a(2)-a(1)<=2比第一个不等式的约束条件更强最长路与最短路的区别
两者不仅是在三角不等式的表现形式上不同,具体求解的值也不同
在求解时,我们一般加入超级源点S,并建立权值为0的边
对于一个不等式组来说,只要有一组解,那么同时加上定值k,仍然满足约束条件。最短路:d[v] < d[u]+w(u,v)
求解出来的是不等式的最大值
解释一下:
若确定其中一个的值,并非能得到每个数的定值,因为是不等式,所以每个值有其取值范围
这里的最大值就是在源点S的限定条件内,所能够取值的上界
仔细思考,这里的最短路只是满足不等式组条件所找到的解,
由于最短路是由大到小做松弛操作,找到的最短路即为“最大”。最长路:d[v] > d[u]+w(u,v)
求的是最小值环的判定
在判环过程中,Bellman的判定条件为入队n次,
其实质是最短路最长经过(n-1)条边(一共有n个点,这n个点全在一个大环内),
所以入队n次队列仍不为空,即可以判定存在正负环
当点的总数+1(加入了一个超级源点),对应的判环条件也就变更为cnt>n。关于图的连通性
无论是求值,还是判环,原图不构成强连通的前提下都是不能搜索到每一个点的,
所以才出现设立一个源点S的方法
事实上,最实用的做法是在初始化时,就把所有点加入队列,并把dis[i]全部初始化为0
不仅仅是解决了连通的问题,更是避免了点数计算上的错误不等式的变形
差分约束解决的是>=和<=的问题,若题目给出的是 > k(或 < k),需要变形
比如若k是整数,> k等价于>=k+1。