演算法設計與分析第四周練習(圖論)
Network Delay Time
1. 題目
There are N network nodes, labelled 1 to N.
Given times, a list of travel times as directed edges times[i] = (u, v, w), where u is the source node, v is the target node, and w is the time it takes for a signal to travel from source to target.
Now, we send a signal from a certain node K. How long will it take for all nodes to receive the signal? If it is impossible, return -1.
Note:
N will be in the range [1, 100]. K will be in the range [1, N]. The length of times will be in the range [1, 6000]. All edges times[i] = (u, v, w) will have 1 <= u, v <= N and 1 <= w <= 100.
2. 基礎知識
解決這題需要圖的相關方面的知識。尤其是圖的廣度遍歷的知識。 具體的遍歷過程如下
- 從某個頂點V出發,訪問該頂點的所有鄰接點V1,V2…VN。
- 從鄰接點V1,V2…VN出發,再訪問他們各自的所有鄰接點。
- 重複上述步驟,直到所有的頂點都被訪問過。 此時還有一種特殊得情況就是,當圖不是連通圖的時候,我們需要從另一個源點出發去遍歷。整個演算法過程如下: 我們以這個基本的遍歷為基礎,引入圖的非常重要的演算法,Dijkstra演算法和Bellman-Ford演算法。
- Dijkstra演算法
這個演算法是求點對所有點的最短路徑的演算法,其適用與圖沒有負邊的情況。
Dijkstra演算法採用的是一種貪心的策略,宣告一個數組dis來儲存源點到各個頂點的最短距離和一個儲存已經找到了最短路徑的頂點的集合:T,
- 初始時,原點 s 的路徑權重被賦為 0 (dis[s] = 0)。若對於頂點 s 存在能直接到達的邊(s,m),則把dis[m]設為w(s, m),同時把所有其他(s不能直接到達的)頂點的路徑長度設為無窮大。初始時,集合T只有頂點s。
- 從dis陣列選擇最小值,則該值就是源點s到該值對應的頂點的最短路徑,並且把該點加入到T中,OK,此時完成一個頂點,
- 我們需要看看新加入的頂點是否可以到達其他頂點並且看看通過該頂點到達其他點的路徑長度是否比源點直接到達短,如果是,那麼就替換這些頂點在dis中的值。
- 又從dis中找出最小值,重複上述動作,直到T中包含了圖的所有頂點。
- Bellman-Ford演算法
- 初始化:將除源點外的所有頂點的最短距離估計值 dist[v] ← +∞, dist[s] ←0;
- 迭代求解:反覆對邊集E中的每條邊進行鬆弛操作,使得頂點集V中的每個頂點v的最短距離估計值逐步逼近其最短距離;(執行|v|-1次)
- 檢驗負權迴路:判斷邊集E中的每一條邊的兩個端點是否收斂。如果存在未收斂的頂點,則演算法返回false,表明問題無解;否則演算法返回true,並且從源點可達的頂點v的最短距離儲存在 dist[v]中。
3. 題目分析
這裡的目的是求出網路從一個源點發布資訊到這個圖中,問至少需要多長的時間。這裡我們將網路抽象成一個圖的結構,從源點到某個點資訊的傳播可以看成是圖的一條路徑。那麼何時才會有當所有的點都收到資訊的時候時間確最短呢?為了時間最少,我們必須確保源點到每一個點的路徑是最短的,同時,為了讓所有的點都能收到的話,我們必須在這些最短的路徑中取需要的時間最長的。那麼問題就轉化為求圖某個點到所有點的最短路徑,並取出其中的最大值。再看題目資料的特點,路徑的權值是正數,說明Dijkstra演算法是合適的,那麼Bellman-Ford演算法也必定合適。
4. 演算法實現
首先,我們需要對圖用合適的資料結構表示出來,由於在找最小路徑的時候需要遍歷同一個起點的所有邊,那麼我們需要將同一起點的邊表示在一起,同時還需要儲存權重,所以我最後決定用map<int, vector<vector > > graph;作為本題圖 的資料結構。
- 4.1 Dijkstra演算法
- 建圖:將圖用我們決定的資料結構表示出來,map<int, vector<vector > > graph,其中key是邊的起點,vector的第一個元素是邊的終點,第二個元素是邊的權值。
- 確定優先佇列的形式,我們這裡採用最簡單的形式,也就是陣列,但為了模擬優先佇列,取出佇列中的最小的未訪問的元素,我們需要一個bool陣列。
- 初始化dist和優先佇列。
- 從優先佇列選擇最小值,則該值就是源點s到該值對應的頂點的最短路徑,並且把該點加入到T中,OK,此時完成一個頂點。
- 新加入的頂點是否可以到達其他頂點並且看看通過該頂點到達其他點的路徑長度是否比源點直接到達短,如果是,那麼就替換這些頂點在dis中的值。
- 重複。
- 4.2 利用佇列優化Bellman-Ford演算法 由於我們這裡的資料結構不是鄰接矩陣的形式,對同一個點遍歷所有的頂點(包括沒有邊相連的點)比較困難,並且這步沒有實際的意義,所以我們這裡用佇列優化的Bellman-Ford演算法來實現。這樣的話,我們只需要遍歷那行存在邊的頂點。 具體的做法如下
5. 原始碼
Dijkstra演算法
class Solution {
public:
int networkDelayTime(vector<vector<int>>& times, int N, int K) {
/*build a graph
* the key of map is the source
* the first element of vector is the target
* the second is the distance
*/
int count = 0;
map<int, vector<vector<int> > > graph;
for(auto a : times) {
vector<int> temp;
temp.push_back(a[1]);
temp.push_back(a[2]);
graph[a[0]].push_back(temp);
}
//the min distance
vector<int> dist(N+1, INT_MAX);
dist[K] = 0;
//array to tag weather the element is visited
vector<int> array_queue(N+1, INT_MAX);
vector<bool> bool_(N+1, false);
array_queue[K] = 0;
int index = 0;
while(count < N) {
int min = INT_MAX;
for(int i = 1; i < array_queue.size(); i++) {
if(bool_[i] == false) {
if(array_queue[i] < min) {
min = array_queue[i];
index = i;
}
}
}
bool_[index] = true;
count++;
//cout << "index " << index << endl;
//find the distance
for(int i = 0; i < graph[index].size(); i++) {
//cout << "graph[index][i][0] " << graph[index][i][0] << endl;
if(dist[graph[index][i][0]] > dist[index] + graph[index][i][1]) {
dist[graph[index][i][0]] = dist[index] + graph[index][i][1];
array_queue[graph[index][i][0]] = dist[graph[index][i][0]];
}
}
}
int result = -1;
for(int i = 1; i < N+1; i++) {
if(dist[i] == INT_MAX) {
return -1;
}
if(dist[i] > result) {
result = dist[i];
}
}
return result;
}
};
利用佇列優化Bellman-Ford演算法
class Solution {
public:
int networkDelayTime(vector<vector<int>>& times, int N, int K) {
/*build a graph
* the key of map is the source
* the first element of vector is the target
* the second is the distance
*/
int count = 0;
map<int, vector<vector<int> > > graph;
for(auto a : times) {
vector<int> temp;
temp.push_back(a[1]);
temp.push_back(a[2]);
graph[a[0]].push_back(temp);
}
//the min distance
vector<int> dist(N+1, INT_MAX);
vector<bool> visited(N+1, false);
dist[K] = 0;
queue<int> q;
set<int> s;
q.push(K);
s.insert(K);
while(!q.empty()) {
int index = q.front();
s.erase(index);
visited[index] = true;
//cout << index << endl;
q.pop();
for(int i = 0; i < graph[index].size(); i++) {
if(dist[graph[index][i][0]] > dist[index] + graph[index][i][1]) {
dist[graph[index][i][0]] = dist[index] + graph[index][i][1];
if(s.find(graph[index][i][0]) == s.end())
q.push(graph[index][i][0]);
}
}
}
int result = -1;
for(int i = 1; i < N+1; i++) {
if(dist[i] == INT_MAX) {
return -1;
}
if(dist[i] > result) {
result = dist[i];
}
}
return result;
}
};
6. 演算法複雜度分析
- 6.1 Dijkstra演算法 這裡外層迴圈的複雜度是確定的,為O(n),整個演算法的複雜度取決於內層優先佇列的實現,我們這裡採用簡單的陣列實現,也就是說,在我們尋找佇列的最小值的時候,我們需要遍歷陣列,內層也是O(n),最後整個演算法的演算法複雜度為O(n*n)。
- 6.2 利用佇列優化Bellman-Ford演算法 本來用Bellman-Ford的演算法,由於每個點與其他頂點都需要遍歷一遍,總的複雜度為O(n*n),現在優化了以後,只有那行有邊存在的才需要遍歷,複雜度是降低了。