第24章 單源最短路徑
24.1 Bellman-Ford算法
24.1-4
思路:
先做|V|-1遍松弛操作,然后再做一遍松弛操作,對於這次松弛操作中dist值被更新的點,必然包含了每個負環中的至少一個點。對於這些點做dfs查找它們能夠在圖中到達哪些點,所有被搜索到的點即為題目要求找的點
部分c++代碼:
#include <bits/stdc++.h>
using namespace std;
const int maxn = ...;
const int inf = 0x3f3f3f3f;//正無窮
struct E{
int x,y,z;//三元組(x,y,z)表示一條有向邊。從x出發到y,權值為z。
}
vector<E> es;//存邊
vector<int> e[maxn];//模擬鄰接鏈表
vector<int> vec;//存起始點
void bellman(int s){
for(int i = 1; i<=n; i++)d[i]=inf;
d[s] = 0;
for(int t = 1; t<n; t++){
for(auto e:es){
if(d[e.x]!=inf && d[e.x]+e.z<d[e.y])d[e.y] = d[e.x] + w;
}
}
for(auto e:es){
if(d[e.x]!=inf && d[e.x]+e.z<d[e.y]){
vec.push_back(y);
}
}
}
int v[maxn];
void dfs(int x){
v[x] = 1;
for(auto y: e){
if(!v[y]) dfs(y);
}
}
void solve(int s){
bellman(s);
for(auto x:vec){
if(!v[x]) dfs(x);
}
for(int i = 1; i<=n; i++){
if(v[i]) cout<<"負無窮"<<endl;
else if(d[i]==inf) cout<<"不可達"<<endl;
else cout<<d[i]<<endl;
}
}
24.1-5
思路:
跑一遍Bellman-Ford算法,具體做法如下:
1、初始化\(\forall v\in V ,d[v] = 0\)。
2、對於邊(x,y,z),如果d[y]>d[x]+z,更新d[y]。
證明:
首先,一個點對自身的距離為0,所以\(\forall v\in V,\delta^*(v)\leq 0\),可以將每個點d[v]初始化為0
接下來證明更行操作的正確性:
設\(\delta_i(u,v)\)為從u到v邊數不超過I的路徑的最小值\(u,v\in V\)
同時,設\(\delta_i^*(v) = min_{u\in V}\{\delta_i(u,v)\}\)
這樣我們便有 \(\delta^*(v)=\delta_{n-1}^*(v)\)
只要我們證明,對於\(0\leq i<n\),均有\(d[v]\leq \delta_i^*(v)\)且最后\(d[v] = \delta_{n-1}^*(v)\)即可
1、i==0時,\(d[v]\leq \delta^*(v) = 0\)
2、假設前i次迭代中\(d[v]\leq \delta_i^*\)成立
對於第i+1次迭代,
如果\(\delta_{i+1}^* = \delta_{i}^*\),
\(d[v] \leq \delta_{i+1}^*(v) = \delta_i^*(v)\)仍成立
否則,在此輪中邊(u,v)被松弛,有\(d[v] = min\{d[u]+w(u,v)\}\)\(\leq min\{\delta_i^*(u)+w(u,v)\} = \delta_{i+1}^*(v)\),仍成立。
綜上,所以有\(d[v]\leq \delta_{n-1}^*\)
因為d[v]為兩頂點路徑長,所以\(d[v] = \delta_{n-1}^*(v)\)
(以上證明前提是圖中無負環,實際程序中將負環判掉了)
部分c++代碼:
#include <bits/stdc++.h>
using namespace std;
const int maxn = ...;
struct E{
int x,y,z;//三元組(x,y,z)表示一條有向邊。從x出發到y,權值為z。
}
vector<E> es;//存邊
int d[maxn];
bool bellman(){
memset(d,0,sizeof(d));
for(int t = 1; t<n; t++){
for(auto e:es){
if(d[e.y]>d[e.x]+z) d[e.y] = d[e.x] + z;
}
}
for(auto e:es){
if(d[e.y]>d[e.x]+z) return false;
}
return true;
}
void solve(){
if(bellman()){
for(int i = 1; i<=n; i++)cout<<d[i]<<endl;
} else{
cout<<"有負環";
}
}
24.3 Dijkstra算法
24.3-6
思路;
做n遍Dijkstra算法。用數組d[x][y]表示從結點u到結點v最可靠通信鏈路的不失效概率。此外,因為題目要求找到這些通信鏈路,我們可以設一個pre數組,遞歸打印出道路。
時間復雜度:
加入堆優化后時間復雜度為O(nmlogn)
部分c++代碼:
#include <bits/stdc++.h>
#define mp make_pair
using namespace std;
const int maxn = ...;
double d[maxn][maxn];//存u->v最可靠通信鏈路不失效概率
int v[maxn];//結點是否訪問過
int n,m;//結點數、邊數
struct E{
int y; double p;
E(int a = 0, double b = 0){y = a; p = b;}
}
vector<E> es[maxn];
priority_queue<pair<double,int>> q;
int pre[maxn][maxn];//存前一個結點,用於打印
void dijkstra(int t){
memset(v,0,sizeof(v));
d[t][t] = 1.0; pre[t][t] = -1;
q.push(mp(1,0,t));
while(!q.empty()){
int x = q.top().second; q.pop();
if(v[x]) continue;
v[x] = 1;
for(auto e: es[x]){
int y = e.y; double z = e.p;
if(d[t][x]*z > d[t][y]){
pre[t][y] = x;
d[t][y] = d[t][x]*z;
q.push(mp(d[t][y],y));
}
}
}
}
void print(int t,int x){
if(pre[t][x] != -1) print(t,pre[t][x]);
printf("%d ",x);
}
void solve(){
for(int i = 1; i<=n; i++) for(int j = 1; j<=n; j++)d[i][j]=-1;
for(int i = 1; i<=n; i++){
dijkstra(i);
for(int j = 1; j<=n; j++){
if(d[i][j] == -1) continue;//兩點之間無路可走
printf("%d->%d:",i,j);
print(i,j);
printf("p:%lf\n",d[i][j]);
}
}
}
24.3-8
思路:
從優化“貪心尋找dist最小點”的操作入手。由於邊權\(\leq W\),那么一個點的dist值不會超過VW。基於這個條件,我們可以抽象出一個長度為VW的隊列數組。每個數組的隊列存儲着“dist值為該數組序號”的點。然后抽象出一個指針,這個指針指向的隊列,是我們貪心取點的隊列。由於一個點被取出后,被他更新的點只會被push進該隊列或者該隊列之后的隊列,所以指針只會從左到右掃描一遍數組。
時間復雜度:
最多掃描一遍數組,復雜度為O(VW)。此外,最多會有E次入隊、出隊操作,復雜度為O(E)。總時間復雜度為O(VW+E)。
部分c++代碼:
#include <bits/stdc++.h>
#define mp make_pair
using namespace std;
const int N = ..., M = ..., W = ...;
queue<int> qs[N*W];
int head[N], ver[M], edge[M], Next[M], tot = 0;//采用數組模擬鄰接鏈表的方式
void add(int x,int y,int z){
ver[++tot] = y, edge[tot] = z, Next[tot] = head[x], head[x] = tot;
}
int d[N], v[N];
int n, m, w;
void dijkstra(int s){
memset(d,0x3f,sizeof(d));//初始化為正無窮
memset(v,0,sizeof(v));
d[s] = 0; qs[0].push(s);
for(int k = 0; k<=n*w; k++){
while(!qs[k].empty()){
int x = qs[k].front(); qs[k].pop();
if(v[x]) continue;
v[x] = 1;
for(int i = head[x]; i; i = Next[i]){
int y = ver[i], z = edge[i];
if(d[y] > d[x] + z){
d[y] = d[x] + z;
qs[d[y]].push(y);
}
}
}
}
}
24.3-9
思路:
類比堆優化Dijkstra算法,我們可以將dist值丟入堆里,每次取堆頂的dist值。查詢那些dist值為該值且未被訪問的點,用這些點來更新。
時間復雜度:
假設起始堆滿,即0~W,共W+1個值,取0,它更新的結點的dist值在0~W之間,不會使堆變大。以此類推,任意時刻,堆的大小不會超過W+1,所以維護堆的復雜度為lgW。其他操作同普通堆優化的Dijkstra算法。復雜度為O((V+E)logW)。
部分c++代碼:
#include <bits/stdc++.h>
using namespace std;
const int N = ..., M = ..., W = ...;
queue<int> qs[N*W];
int head[N], ver[M], edge[M], Next[M], tot = 0;
void add(int x,int y,int z){
ver[++tot] = y, edge[tot] = z, Next[tot] = head[x], head[x] = tot;
}
int d[N], v[N];
priority_queue<int> q;
int inh[N*W];//記錄該值是否在堆中。
void dijkstra(int s){
memset(d,0x3f,sizeof(d));
memset(v,0,sizeof(v));
d[s] = 0; qs[0].push(s);
q.push(0);
while(!q.empty()){
int k = q.top(); q.pop(); inh[k] = 0;
while(!qs[k].empty()){
int x = qs[k].front(); qs[k].pop();
if(v[x]) continue;
v[x] = 1;
for(int i = head[x]; i; i = Next[i]){
int y = ver[i], z = edge[i];
if(d[y] = d[x] + z){
d[y] = d[x] + z;
qs[d[y]].push(y);
if(!inh[d[y]]){
q.push(d[y]);
inh[d[y]]=1;
}
}
}
}
}
}
24.3-10
證明:
首先,Dijkstra的算法思路是:對於任意一個從隊列取出來的點x,如果它沒有被標記過,那么d[x]一定是源結點s到x的最短路徑,然后我們不斷地進行貪心擴展,最終可以得到源結點s到每個結點的最短路徑。它的本質是一個貪心+BFS算法。
現在,題目中給出的限制是:“只有從源結點s出發的邊權重可以為負,且圖中無負環”。
源結點s一定是在一開始被取出,更新完之后被丟棄。因為只有源結點s出發的邊權重可以為負,所以我們在后面更新由“s以外的其他點”更新“s以外的其他點”時,仍舊是在一個無負邊權的圖中的更新,所以Dijkstra仍正確。而對於這些點,當他們嘗試更新s的時候,因為圖中無負環,所以無法更新s。此外,由於無負環,s也不會出現有一個權值為負的子環無限更新自身的情況。綜上,Dijkstra基於貪心的性質沒有發生改變,每次從隊列中取出的一個點更新完其他點被丟棄后,這個點在后面一定不會被更新。所以Dijkstra仍舊可以正確運行。
得證。
思考題
24-1
a.
對於\(G_f\)它的所有邊都是由一個具有較小索引的結點指向一個具有較大索引的結點。所以我們無法找到一個結點v,使得對於該點存在邊(u,v),u是“到v含有至少一條路徑的點組成的點集”中的點。因為這樣的點集中的點的索引必定小於v,而從v出發的邊所到達的點的索引均大於v,從而可以得知\(G_f\)是無環的。此外,因為\(G_f\)中不會存在由索引高的點指向索引低的點的邊,所以\(<v_1,v_2,...,v_{|V|}>\)一定是一個合法的拓撲序。
同理,\(G_b\)也是無環的,且其拓撲序為\(<v_{|V|},...,v_1>\)。
b.
對於s存在一條路徑到v,如果這個路徑是圖\(G_f\)中的,此時路徑結點的索引是上升的,那我們對\(G_f\)做一遍松弛松弛操作就得到了正確結果。如果最終結果中有一個“拐點”,即這條路徑結點索引原本是上升的后來又下降了,那我們一遍松弛操作也可以得到正確結果(因為我們在兩個圖\(G_f,G_b\)都做了松弛操作)。不難發現,按照題目的操作方式,如果發生了更新,必然會出現新的“拐點”。相應地,我們可以說,只有更新后出現我們的到路徑出現了新的“拐點”,這個松弛操作才會有效,否則,更新已經完成。我們知道,對於最終的結果,我們考慮一種最差的情況,應該是上升、下降、上升、下降……或者下降、上升、上升、下降……。
(索引\(s<v_1<v_2<...\))
對於這兩種最差的情況,只會有\(\lfloor |V|/2\rfloor\)。此外,還有個要考慮的是,可能開始\(s\)->\(v_1\)原本是上升的,后來在更新后又變成下降了,這樣應該需要做一次松弛操作,但是我們在最終結果圖中會忽略掉,所以實際上有\(\lceil |V|/2\rceil\)次操作。綜上,我們最差也只需做\(\lceil |V|/2\rceil\)遍松弛操作。
c.
直接做Bellman-Ford復雜度為O(V(V+E)),用該思路來做應該為V/2(2V+E)=V*(V+E/2)。由於往往\(V\leq E\),我們將其分別寫為\(O(VE),O(\frac{1}{2}VE)\)。可以看出這種方法只會使得前面的系數發生變化,所以漸進復雜度沒有發生改變。
24-2
a.
假設有盒子\(x = <x_1,...,x_d>\),\(y = <y_1,...,y_d>\),\(z = <z_1,...,z_d>\)。且x嵌套在y中,y嵌套在z中。我們有\(x_{\pi(1)}<y_1,...,x_{\pi(d)}<y_d\),\(y_{\delta(1)}<z_1,...,y_{\delta(d)}<z_d\)。那么,便有\(x_{\pi(\delta(1))}<z_1,...,x_{\pi(\delta(d))}<z_d\),即x嵌套在z里,所以嵌套關系是傳遞的。
b.
將兩個盒子里的元素排序沒,只要對應位置元素滿足偏序關系,即可得到兩個盒子有嵌套關系,復雜度為O(dlogd)(反證法可以證明)。
c.
將每個盒子的元素排序,然后對於每對有嵌套關系的盒子連邊,然后跑DFS即可,復雜度為$O(ndlogd+dn^2)。
此外,因為要找到這個序列,我用一個to數組來記錄這條鏈。
部分c++代碼:
#include <bits/stdc++.h>
using namespace std;
const int maxn = ..., maxd = ..., maxm=...;
int head[maxn], ver[maxm], edge[maxm], Next[maxm], tot = 0;
void add(int x,int y,int z){
ver[++tot] = y, edge[tot] = z, Next[tot] = head[x], head[x] = tot;
}
int box[maxn][maxd];
int n,d;
int dp[maxn], to[maxn];
bool judge(int a[maxd],int b[maxd]){
for(int i = 1; i<=d; i++){
if(a[i]<b[i]) continue;
return false;
}
return true;
}
void dfs(int x){
if(dp[x]) return ;
dp[x] = 1;
for(int i = head[x]; i; i = Next[i]){
int y = ver[i]; dfs(y);
if(dp[y]+1>d[x]) to[x] = y;
}
}
void solve(){
//盒子與盒子元素的索引均從1開始
for(int i = 1; i<=n; i++) sort(box[i]+1,box[i]+1+d,cmp);
for(int i = 1; i<=n; i++){
for(int j = 1; j<=n; j++){
if(judge(box[i],box[j]))add(i,j)
}
}
memset(dp,0,sizeof(dp));
int ans = 0, p;
for(int i = 1; i<=n; i++){
if(dp[i] == 0) dfs(i);
if(dp[i] > ans){
ans = dp[i];
p = i;
}
}
do {
printf("%d",p);
p = to[p];
}while(p);
}
24-3
思路:
用歸約的思想:
由於 \(R[i_1,i_2]*...*R[i_k,i_1] > 1\)
取對數,有\(lg(R[i_1,i_2]+ ... +lg(R[i_k,i_1]) > 0\)
取負,有\((-lg(R[i_1,i_2]) + ... + (-lg(R[i_k,i_1])) < 0\)
所以,我們將\(R[i_{k},i_{k+1}]\)這樣原來的邊權進行兩步處理后,問題即轉化為在圖中找負環。
問題a要求判斷是否存在負環,可以直接用bfs版的spfa,在隨機圖上運行效率為O(k|E|),k是一個較小的常數,在特殊圖上運行效率為O(|V||E|)。
問題b要求打印負環,由於已知存在負環,dfs版的spfa會在多數情況下更快一點,特殊圖上運行效率為O(|V||E|)。
a,部分c++代碼:
#include <bits/stdc++.h>
using namespace std;
const int maxn = ..., maxm = ...;
const int inf = 0x3f3f3f3f;//正無窮
int head[maxn], ver[maxm], Next[maxm], tot = 0;
double edge[maxm];
void add(int x,int y,int z){
ver[++tot] = y, edge[tot] = z, Next[tot] = head[x], head[x] = tot;
}
double d[maxn]; int n,m,s;
int vis[maxn], cnt[maxn];//vis記錄是否在隊列中,cnt記錄更新了幾遍。
bool spfa(int s){
queue<int> q;
for(int i = 1; i<=n; i++) d[i] = inf;
d[s] = 0; vis[s] = 1; cnt[s] = 1; q.push(s);
while(!q.empty()){
int x = q.front(); q.pop();
vis[x] = 0;
for(int i = head[x]; i; i=Next[i]){
int y = ver[i]; double w = edge[i];
if(d[y] > d[x] + w){
d[y] = d[x] + w;
cnt[y] = cnt[x] + 1;
if(cnt[y] > n) return false;
if(!vis[y]){
q.push(y);
vis[y] = 1;
}
}
}
}
return true;
}
void solve(int s){
for(int x = 1; x<=n; x++){
for(int i = head[x]; i; i = Next[i]){
edge[i] = -log(edge[i]);
}
}
if(spfa(s)) cout<<"不存在這樣的貨幣序列"<<endl;
else cout<<"存在這樣的貨幣序列"<<endl;
}
b,部分c++代碼:
#include <bits/stdc++.h>
using namespace std;
const int maxn = ..., maxm = ...;
const int inf = 0x3f3f3f3f;//正無窮
int head[maxn], ver[maxm], Next[maxm], tot = 0;
double edge[maxm];
void add(int x,int y,int z){
ver[++tot] = y, edge[tot] = z, Next[tot] = head[x], head[x] = tot;
}
double d[maxn]; int n,m,s;
int Stack[maxn], top = 0;
int ins[maxn];//記錄是否在棧中
int ans[maxn], cnt = 0;
bool spfa(int s){
ins[x] = 1; Stack[++top] = x;
for(int i = head[x]; i; i = Next[i]){
int y = ver[i]; double w = edge[i];
if(d[y] > d[x] + w){
if(ins[y]){
int t;
do{
t = Stack[top--];
ans[++cnt] = t;
}while(t!=y);
return false;
}
d[y] = d[x] + w;
}
if(!spfa(y)) return false;
}
ins[y] = 0; top--;
return true;
}
void solve(int s){
for(int x = 1; x<=n; x++){
for(int i = head[x]; i; i = Next[i]){
edge[i] = -log(edge[i]);
}
}
for(int i = 1; i<=n; i++) d[i] = inf;
d[s] = 0;
if(spfa(s)) cout<<"不存在"<<endl;
else {
while(cnt!=1)cout<<ans[cnt--]<<'->';
cout<<ans[cnt]<<endl;
}
24-6
思路:
該題與24-1思路相似,只是條件改為路徑上的邊權具有單調的性質。之前的結論在此仍有效,即每次松弛更新有效當且僅當產生了新的“拐點”。題目中給出限制最多只能有1個“拐點”,需要注意一點是在第一遍松弛操作可能掩蓋掉上一次的拐點(在24-1中也討論過這種情況),所以我們要做2遍操作,即4次松弛操作(因為每遍要做2次松弛操作)。
時間復雜度:
由於我們事先要對邊按權值排序,所以復雜度為O(ElogE)。(松弛操作的復雜度為O(E),被省略)。
部分c++代碼:
#include <bits/stdc++.h>
using namespace std;
const int maxn = ..., maxm = ...;
const int inf = 0x3f3f3f3f;//正無窮
struct E{int x,y,z;}e1[maxm],e2[maxm];
int m,n;
bool cmp1(E a,E b){return a.z < b.z;}
bool cmp2(E a,E b){return a.z > b.z;}
int d[maxn];
void update(E *e){
for(int j = 0; i<m; j++){
int x = e[j].x, y = e[j].y, z e[j].z;
dp[y] = min(dp[y],dp[x]+z);
}
}
void solve(){
memset(d,0x3f,sizoef(d));
sort(e1,e1+m,cmp1), sort(e2,e2+m,cmp2);//e1與e2開始時相同。
for(int i = 1; i<=2; i++){
update(e1), update(e2);
}
}