小世界現象(又稱小世界效應),也稱六度分隔理論(英文:Six Degrees of Separation)。
假設世界上全部互不相識的人只須要不多中間人就能創建起聯繫。後來1967年哈佛大學的心理學教授斯坦利·米爾格拉姆根據這概念作過一次連鎖信實驗,嘗試證實平均只須要5箇中間人就能夠聯繫任何兩個互不相識的美國人。
NowCoder最近得到了社交網站Footbook的好友關係資料,請你幫忙分析一下某兩個用戶之間至
少須要幾個中間人才能創建聯繫?java
輸入第一行是一個整數t,表示緊接着有t組數據。
每組數據包含兩部分:第一部分是好友關係資料;第二部分是待分析的用戶數據。
好友資料部分第一行包含一個整數n (5≤n≤50),表示有n個用戶,用戶id用1->n表示。
緊接着是一個只包含0和1的n×n矩陣,其中第y行第x列的值表示id是y的用戶是不是id爲x的用戶的好友(1表明是,0表明不是)。假設好友關係是相互的,即A是B的好友意味着B也是A的好友。
待分析的用戶數據第一行包含一個整數m,緊接着有m行用戶組數據。
每組有兩個用戶ID,A和B (1≤A, B≤n; A != B)。算法
對於每組待分析的用戶,輸出用戶A至少須要經過幾箇中間人才能認識用戶B。
若是A不管如何也沒法認識B,輸出「Sorry」。數組
2 5 1 0 1 0 1 0 1 1 1 0 1 1 1 0 0 0 1 0 1 0 1 0 0 0 1 3 1 2 2 4 3 5 6 1 1 0 0 1 0 1 1 0 1 0 1 0 0 1 0 0 1 0 1 0 1 0 1 1 0 0 0 1 0 0 1 1 1 0 1 4 2 3 3 6 5 1 4 2
1 0 1 1 0 0 0
題目要求某兩我的之間最少經過多少箇中間人才能創建聯繫,人與人之間的關係用一個圖進行表示,有直接關係的使用1表示,沒有關係的使用0表示。能夠對這個關係矩陣進行改進,將自身與身的關係計爲1,<v,w>存在直接關係記爲1,不存在直接關係的記爲+∞。要求,<x,y>最少經過多少箇中間人能夠取得聯繫,能夠先計算,<x,y>之間的最短路徑,由於邊的權權重都是1,因此最短路徑就是,<x,y>所通過的最少的邊的數目e,而,<x,y>最少的聯繫人數目就是,<x,y>最少邊所在線段中間的頂點數,即e-1。
通過分析能夠得,該題能夠經過Dijkstra、Bellman-Ford或者Floyd算法進行處理。本題分析過程講解Floyd。Dijkstra方法見【016-回家過年】算法實現。測試
問題的提出:已知一個有向網(或無向網),對每一對頂點vi≠vj,要求求出vi與vj之間的最短路徑和最短路徑長度。
解決該問題的方法有:
1) 輪流以每一個頂點爲源點,重複執行Dijkstra算法(或Bellman-Ford算法)n次,就可求出每一對頂點之間的最短路徑和最短路徑長度,總的時間複雜度是O(n3)(或O(n2+ne))。
2) 採用Floyd(弗洛伊德)算法。Floyd 算法的時間複雜度也是O(n3),但Floyd算法形式更直接。網站
Floyd(弗洛伊德)算法的基本思想是:對一個頂點個數爲n的有向網(或無向網),設置一個n×n的方陣A(k),其中除對角線的矩陣元素都等於0外,其餘元素A(k)[i][j](i≠j)表示從頂點vi到頂點vj的有向路徑長度,k表示運算步驟,k=-一、0、一、二、…、n-1。
初始時:A(-1)= Edge(圖的鄰接矩陣),即初始時,以任意兩個頂點之間的直接有向邊的權值做爲最短路徑長度:
1) 對於任意兩個頂點vi和vj,若它們之間存在有向邊,則以此邊上的權值做爲它們之間的最短路徑長度;
2) 若它們之間不存在有向邊,則以MAX做爲它們之間的最短路徑。
之後逐步嘗試在原路徑中加入其餘頂點做爲中間頂點,若是增長中間頂點後,獲得的路徑比原來的最短路徑長度減小了,則以此新路徑代替原路徑,修改矩陣元素,更新爲新的更短的路徑長度。
例如,在圖1所示的有向網中,初始時,從頂點v2到頂點v1的最短路徑距離爲直接有向邊<v2,v1>上的權值(=5)。加入中間頂點v0以後,邊<v2,v0>和<v0,v1>上的權值之和(=4)小於原來的最短路徑長度,則以此新路徑<v2,v0,v1>的長度做爲從頂點v2到頂點v1的最短路徑距離A[2][1]。
圖1 Floyd算法:有向網及其鄰接矩陣
將v0做爲中間頂點可能還會改變其餘頂點之間的距離。例如,路徑<v2,v0,v3>的長度(=7)小於原來的直接有向邊<v2,v3>上的權值(=8),矩陣元素A[2][3]也要修改。
在下一步中又增長頂點v1做爲中間頂點,對於圖中的每一條有向邊<vi,vj>,要比較從vi到v1的最短路徑長度加上從v1到vj的最短路徑長度是否小於原來從vi到vj的最短路徑長度,即判斷A[i][1]+A[1][j]< A[i][j]是否成立。若是成立,則須要用A[i][1]+A[1][j]的值代替A[i][j]的值。這時,從vi到v1的最短路徑長度,以及從v1到vj的最短路徑長度已經因爲v0做爲中間頂點而修改過了,因此最新的A[i][j]其實是包含了頂點vi,v0, v1, vj的路徑的長度。
如圖1所示,A[2][3]在引入中間頂點v0後,其值減爲7,再引入中間頂點v1後,其值又減到6。固然,有時加入中間頂點後的路徑較原路徑更長,這時就維持原來相應的矩陣元素的值不變。依此類推,可獲得Floyd算法。
Floyd算法的描述以下。
定義一個n階方陣序列:A(−1),A(0),A(1), …,A(n−1),其中:
A(−1)[i][j]表示頂點vi到頂點vj的直接邊的長度,A(−1) 就是鄰接矩陣Edge[n][n]。
A(0)[i][j]表示從頂點vi 到頂點vj,中間頂點(若是有,則)是v0 的最短路徑長度。
A(1)[i][j]表示從頂點vi 到頂點vj,中間頂點序號不大於1 的最短路徑長度。
……
A(k)[i][j]表示從頂點vi 到頂點vj 的,中間頂點序號不大於k的最短路徑長度。
……
A(n−1)[i][j]是最終求得的從頂點vi 到頂點vj的最短路徑長度。
採用遞推方式計算A(k)[i][j]:
增長頂點vk做爲中間頂點後,對於圖中的每一對頂點vi和vj,要比較從vi到vk的最短路徑長度加上從vk到vj的最短路徑長度是否小於原來從vi到vj的最短路徑長度,即比較A(k−1)[i][k]+A(k−1)[k][j]與A(k−1)[i][j]的大小,取較小者做爲的A(k)[i][j]值。
所以,Floyd 算法的遞推公式爲:
spa
A(k)[i][j]=⎧⎩⎨Edge[i][j]min{A(k−1)[i][j],A(k−1)[i][k]+A(k−1)[k][j]}k=−1k=0,1,2,…,n−1.net
Floyd 算法在實現時,須要使用兩個數組:
1) 數組A:使用同一個數組A[i][j]來存放一系列的A(k)[i][j],其中k=-1,0,1,…, n-1。初始時,A[i][j]=Edge[i][j],算法結束時A[i][j]中存放的是從頂點vi到頂點vj的最短路徑長度。
2) path數組:path[i][j]是從頂點vi到頂點vj的最短路徑上頂點j 的前一頂點的序號。
Floyd算法具體實現代碼詳見例2.1。
例2.1 利用Floyd算法求圖1(a)中各頂點間的最短路徑長度,並輸出對應的最短路徑。
假設數據輸入時採用以下的格式進行輸入:首先輸入頂點個數n,而後輸入每條邊的數據。每條邊的數據格式爲:u v w,分別表示這條邊的起點、終點和邊上的權值。頂點序號從0 開始計起。最後一行爲-1 -1 -1,表示輸入數據的結束。
分析:
如圖2所示,初始時,數組A實際上就是鄰接矩陣。path數組的初始值:若是頂點vi到頂點vj有直接路徑,則path[i][j]初始爲i;若是頂點vi到頂點vj沒有直接路徑,則path[i][j]初始爲-1。在Floyd 算法執行過程當中,數組A 和path各元素值的變化如圖2所示。在該圖中,若是數組元素的值有變化,則用粗體、下劃線標明。
以從A(−1)推導到A(0)解釋A(k)的推導。從A(−1)推導到A(0),其實是將v0做爲中間頂點。引入中間頂點v0後,由於A(−1)[2][0]+A(−1)[0][1]=4,小於A(−1)[2][1],因此要將A(0)[2][1]修改爲A(−1)[2][0]+A(−1)[0][1],爲4;一樣A(0)[2][3]的值也要更新成7。
當Floyd算法運算完畢,如何根據path 數組肯定頂點vi到頂點vj的最短路徑?方法與Dijkstra算法和Bellman-Ford算法相似。以頂點v1到頂點v0的最短路徑加以解釋。如圖2所示,從path(3)[1][0]=2可知,最短路徑上v0的前一個頂點是v2;從path(3)[1][2]=3可知,最短路徑上v2的前一個頂點是v3;從path(3)[1][3]=1可知,最短路徑上v3的前一個頂點是v1,就是最短路徑的起點;所以,從頂點1到頂點0的最短路徑爲:v1→v3→v2→v0,最短路徑長度爲A[1][0]=11。
code
import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Scanner; /** * Declaration: All Rights Reserved !!! */ public class Main { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); // Scanner scanner = new Scanner(Main.class.getClassLoader().getResourceAsStream("data3.txt")); int group = scanner.nextInt(); for (int i = 0; i < group; i++) { // 用戶個數 int n = scanner.nextInt(); int[][] edge = new int[n][n]; for (int j = 0; j < n; j++) { edge[j] = new int[n]; for (int k = 0; k < n; k++) { edge[j][k] = scanner.nextInt(); } } // 用戶組 int m = scanner.nextInt(); List<Integer> pairs = new ArrayList<>(m * 2); m *= 2; for (int j = 0; j < m; j++) { // 由於數組下標從0開始,而人的編號從1開始,將人的編號所有減1 pairs.add(scanner.nextInt() - 1); } // 對輸入的關係矩陣進行處理(v, v)設置爲1,(v, w)不直接可達的設置爲Integer.MAX_VALUE for (int j = 0; j < n; j++) { for (int k = 0; k < n; k++) { if (j == k) { edge[j][k] = 0; } else if (edge[j][k] == 0) { edge[j][k] = Integer.MAX_VALUE; } } } List<Integer> result = floyd(edge, pairs); // List<Integer> result = dijkstra(edge, pairs); // 輸入結果,因求出的是(v,w)以前的邊的數目,它們以前的頂點數就是最少的聯繫人數目 // 最少的聯繫人數目=(v, w)最少的邊數-1 for (Integer r : result) { if (r < Integer.MAX_VALUE) { System.out.println(r - 1); } else { System.out.println("Sorry"); } } } scanner.close(); } ///////////////////////////////////////////////////////////////////////////////////////////////////// // 解法一:Floyd方法求任意兩點間的距離 ///////////////////////////////////////////////////////////////////////////////////////////////////// /** * 使用Floyd算法求圖任意兩點之間的最短距離 * * @param edge 圖的鄰接矩陣 * @param pairs 所要求的(v, w)點的集合 * @return (v, w)的最短路路徑 */ private static List<Integer> floyd(int[][] edge, List<Integer> pairs) { int MAX = Integer.MAX_VALUE; // 頂點數 int N = edge.length; // 記錄任意兩點的最短路徑 int[][] A = new int[N][N]; // 記錄最短路徑的走法,在本題中能夠不使用 int[][] path = new int[N][N]; // 初始化A和path for (int i = 0; i < N; i++) { A[i] = new int[N]; path[i] = new int[N]; for (int j = 0; j < N; j++) { A[i][j] = edge[i][j]; // (i, j)有路徑 if (i != j && A[i][j] < MAX) { path[i][j] = i; } // 從i到j沒有路徑 else { path[i][j] = -1; } } } // /從A(-1)遞推到A(0), A(1), ..., A(n-1),或者理解成依次將v0,v1,...,v(n-1)做爲中間頂點 for (int k = 0; k < N; k++) { for (int i = 0; i < N; i++) { for (int j = 0; j < N; j++) { if (k == i || k == j) { continue; } if (A[i][k] < MAX && A[k][j] < MAX && A[i][k] + A[k][j] < A[i][j]) { A[i][j] = A[i][k] + A[k][j]; // path[i][j]是從頂點vi到頂點vj的最短路徑上頂點j的前一頂點的序號 // 如今path[i][j]中j的前一個頂點就是path[k][j]中j的前一個頂點 path[i][j] = path[k][j]; } } } } List<Integer> result = new LinkedList<>(); while (!pairs.isEmpty()) { int x = pairs.remove(0); int y = pairs.remove(0); result.add(A[x][y]); } return result; } ///////////////////////////////////////////////////////////////////////////////////////////////////// // 解法二:Dijkstra方法求任意兩點間的距離 ///////////////////////////////////////////////////////////////////////////////////////////////////// /** * 使用Dijkstra算法求圖任意兩點之間的最短距離 * * @param edge 圖的鄰接矩陣 * @param pairs 所要求的(v, w)點的集合 * @return (v, w)的最短路路徑 */ private static List<Integer> dijkstra(int[][] edge, List<Integer> pairs) { int N = edge.length; int MAX = Integer.MAX_VALUE; // 標記頂點是否已經訪問過 boolean[] S = new boolean[N]; // 記錄起點到各點的最短距離 int[][] DIST = new int[N][N]; // 記錄前驅頂點,經過找前驅能夠找到從(v, w)的最短路徑的走法,在本題中能夠不使用 int[][] PREV = new int[N][N]; List<Integer> result = new ArrayList<>(); // 處理每個(v, w) for (int v = 0; v < N; v++) { DIST[v] = new int[N]; PREV[v] = new int[N]; // 處理第一個點 for (int i = 0; i < N; i++) { S[i] = false; DIST[v][i] = edge[v][i]; // 若是是最大值,說明(0, i)不存在。因此PREV[i]不存在 if (DIST[v][i] == MAX) { PREV[v][i] = -1; } else { PREV[v][i] = 0; } } // 標記v號頂點已經處理過 S[v] = true; // 處理其他的點 for (int i = 1; i < N; i++) { int min = MAX; int u = 0; // 找未訪問過的頂點j,而且DIST[j]的值最小 for (int j = 0; j < N; j++) { if (!S[j] && DIST[v][j] < min) { u = j; min = DIST[v][j]; } } // 標記u已經被訪問過了 S[u] = true; for (int j = 0; j < N; j++) { // j沒有被訪問過,而且(u, j)可達 if (!S[j] && edge[u][j] < MAX) { int weight = DIST[v][u] + edge[u][j]; // 從0->...->u->j比0->...->j(其它路徑)短 if (DIST[v][u] < MAX && edge[u][j] < MAX && weight < DIST[v][j]) { DIST[v][j] = weight; // j是經過u訪問到的 PREV[v][j] = u; } } } } } for (int i = 0; i < pairs.size(); i += 2) { int v = pairs.get(i); int w = pairs.get(i + 1); result.add(DIST[v][w]); } return result; } private static void print(int[][] arr) { for (int[] line : arr) { print(line); } } private static void print(int[] arr) { for (int val : arr) { if (val != Integer.MAX_VALUE) { System.out.print(val + " "); } else { System.out.print("- "); } } System.out.println(); } }