小世界現象

題目描述

  小世界現象(又稱小世界效應),也稱六度分隔理論(英文:Six Degrees of Separation)。
  假設世界上全部互不相識的人只須要不多中間人就能創建起聯繫。後來1967年哈佛大學的心理學教授斯坦利·米爾格拉姆根據這概念作過一次連鎖信實驗,嘗試證實平均只須要5箇中間人就能夠聯繫任何兩個互不相識的美國人。
  NowCoder最近得到了社交網站Footbook的好友關係資料,請你幫忙分析一下某兩個用戶之間至
  少須要幾個中間人才能創建聯繫?java

1.1 輸入描述:

  輸入第一行是一個整數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)。算法

1.2 輸出描述:

  對於每組待分析的用戶,輸出用戶A至少須要經過幾箇中間人才能認識用戶B。
  若是A不管如何也沒法認識B,輸出「Sorry」。數組

1.3 輸入例子:

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.4 輸出例子:

1
0
1
1
0
0
0

 

2 解題思路

  題目要求某兩我的之間最少經過多少箇中間人才能創建聯繫,人與人之間的關係用一個圖進行表示,有直接關係的使用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-回家過年】算法實現。測試

2.1 Floyd算法

  問題的提出:已知一個有向網(或無向網),對每一對頂點vivj,要求求出vivj之間的最短路徑和最短路徑長度。
解決該問題的方法有:
  1) 輪流以每一個頂點爲源點,重複執行Dijkstra算法(或Bellman-Ford算法)n次,就可求出每一對頂點之間的最短路徑和最短路徑長度,總的時間複雜度是O(n3)(或O(n2+ne))。
  2) 採用Floyd(弗洛伊德)算法。Floyd 算法的時間複雜度也是O(n3),但Floyd算法形式更直接。網站

2.2 算法思想

  Floyd(弗洛伊德)算法的基本思想是:對一個頂點個數爲n的有向網(或無向網),設置一個n×n的方陣A(k),其中除對角線的矩陣元素都等於0外,其餘元素A(k)[i][j](i≠j)表示從頂點vi到頂點vj的有向路徑長度,k表示運算步驟,k=-一、0、一、二、…、n-1。
  初始時:A(-1)= Edge(圖的鄰接矩陣),即初始時,以任意兩個頂點之間的直接有向邊的權值做爲最短路徑長度:
    1) 對於任意兩個頂點vivj,若它們之間存在有向邊,則以此邊上的權值做爲它們之間的最短路徑長度;
    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>,要比較從viv1的最短路徑長度加上從v1到vj的最短路徑長度是否小於原來從vivj的最短路徑長度,即判斷A[i][1]+A[1][j]< A[i][j]是否成立。若是成立,則須要用A[i][1]+A[1][j]的值代替A[i][j]的值。這時,從viv1的最短路徑長度,以及從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做爲中間頂點後,對於圖中的每一對頂點vivj,要比較從vi到vk的最短路徑長度加上從vk到vj的最短路徑長度是否小於原來從vivj的最短路徑長度,即比較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

 

2.3 算法實現

  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

3 算法實現

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();
    }
}

 

4 測試結果

這裏寫圖片描述

相關文章
相關標籤/搜索