省市縣三級聯動問題相信大家都耳熟能詳了,選擇市下拉選項依賴於省,同樣的選擇縣下拉選項依賴於市。把省市縣抽象成三個節點A(省),B(市),C(縣),它們的關系如下圖(1)。假如把這個聯動問題復雜化一點如圖(2)所示,現在隨便改變一個節點的值,其余節點的值會發生什么變化,你還能直接說出來嗎?這個問題就是本篇將要介紹的動態聯動問題。


閱讀目錄
動態聯動問題分析
動態聯動相對於普通的聯動體現在關系事先不可知,省市縣聯動改變什么相應聯動什么都是事先知道的,所以代碼實現是相對很簡單的。前台寫三個Select標簽,每個Select標簽綁定onchange事件實現相應的邏輯。
<div> <select id="selProvince" onchange="changeCity()"> <option></option> </select> <select id="selCity" onchange="changeArea()"> <option></option> </select> <select id="selArea"> <option></option> </select> </div> <script> function changeCity() { var Province = $("#selProvince").val(); //取數,取出該省對應的市 var data = getData(Province); //綁定選擇城市下拉選擇項 bindCity(data); //清空縣下拉選擇項和值 clearArea(); } function changeArea() { var City = $("#selCity").val(); //取數,取出該市對應的縣 var data = getData(City); //綁定選擇縣下拉選擇項 bindArea(data); } </script>
上面的兩個函數代碼是類似的,總結一下會發現以下步驟:
1.獲取當前改變項的值(省)
2.找出其直接影響的項(市),從后台獲取對應數據,進行綁定。
3.找出其間接影響的項(縣),將其下拉選擇項清空,值清空
動態聯動問題的難點在於第二步和第三步,怎么找當前改變項的直接影響節點和間接影響節點。
問題轉化
我們用圖來描述聯動,上圖2中改變A節點的值,哪些是直接影響節點和間接影響節點呢。直接節點:B,間接節點C F。這里可能存在一個疑惑點C節點為什也算是間接節點呢,它不是也可以直接由A->C嗎。從實際應用來考慮,從A節點到C節點有兩條路徑A->C,A->B->C。也就是說C是依賴於A,B兩個節點的,改變了A的值,我們可以獲取到B的下拉選項的值,注意了這個時候用戶是沒有選擇B的值的,也是就說B是空的,所以是算不出來C的下拉選項的值的。不明白的可以從省市縣聯動來考慮,改變了省是求不出來縣的值的,只能求出市。到這里可以給上面說了很多次的直接影響節點和間接影響節點下定義了
直接影響節點:改變節點到該節點不存在中轉節點
間接影響節點:改變節點到該節點存在中轉節點
無影響節點 :改變節點不能到達的節點
定義圖的邊路徑長度為1,上面三種節點的定義可以改為
直接影響節點:最遠距離=1
間接影響節點:最遠距離>1
無影響節點 :最遠距離 =無窮大


最短路徑算法實現
經過分析我們把動態聯動問題轉換成了最遠路徑問題,這個時候解決方案就很明確了,圖的最短路徑算法(最遠路徑可以先把路徑值變成相反值,再求最短路徑)。當然要求最短路徑就得要求圖是無閉環的,如何判斷圖存在閉環可以參考我的另一篇文章拓撲排序及其實際應用。
最短路徑算法經典的有Dijkstra and Floyd算法,Dijkstra算法適合求單個節點到其它節點的最短路徑問題,Floyd算法適合求每個節點到其它節點最短路徑問題。
Floyd算法的基本思想如下:從任意節點A到任意節點B的最短路徑不外乎2種可能,1是直接從A到B,2是從A經過若干個節點到B,所以,我們假設dist(AB)為節點A到節點B的最短路徑的距離,對於每一個節點K,我們檢查dist(AK) + dist(KB) < dist(AB)是否成立,如果成立,證明從A到K再到B的路徑比A直接到B的路徑短,我們便設置 dist(AB) = dist(AK) + dist(KB),這樣一來,當我們遍歷完所有節點K,dist(AB)中記錄的便是A到B的最短路徑的距離。
上圖的鄰接矩陣表示方法:

代碼實現
static int M = 6; static void Main(string[] args) { int[,] dist = new int[6, 6]{ {int.MaxValue,1,1,int.MaxValue,int.MaxValue,int.MaxValue}, {int.MaxValue,int.MaxValue,1,int.MaxValue,int.MaxValue,int.MaxValue}, {int.MaxValue,int.MaxValue,int.MaxValue,int.MaxValue,int.MaxValue,1}, {int.MaxValue,int.MaxValue,1,int.MaxValue,int.MaxValue,int.MaxValue}, {int.MaxValue,int.MaxValue,int.MaxValue,int.MaxValue,int.MaxValue,int.MaxValue}, {int.MaxValue,int.MaxValue,int.MaxValue,int.MaxValue,int.MaxValue,int.MaxValue} }; int[,] g = new int[6, 6]; Floyd(dist, g); PrintGraph(g, M); Console.Read(); } /// <summary> /// 輸出最短路徑矩陣 /// </summary> /// <param name="g"></param> /// <param name="v"></param> public static void PrintGraph(int[,] g, int v) { for (int i = 0; i < v; i++) { for (int j = 0; j < v; j++) { if (g[i, j] == int.MaxValue) Console.Write("*" + " "); else Console.Write(g[i, j] + " "); } Console.WriteLine(); } } /// <summary> /// Floyd最短路徑算法 /// </summary> /// <param name="dist">鄰接矩陣</param> /// <param name="D">最短路徑</param> public static void Floyd(int[,] dist, int[,] D) { //復制一份dist for (int i = 0; i < M; i++) { for (int j = 0; j < M; j++) { D[i, j] = dist[i, j]; } } for (int k = 0; k < M; k++) { for (int i = 0; i < M; i++) { for (int j = 0; j < M; j++) { D[i, j] = Math.Min(D[i, j], (D[i, k] == int.MaxValue || D[k, j] == int.MaxValue) ? int.MaxValue : D[i, k] + D[k, j]); } } } }
其中最外層循環K 表示經過K節點,i,j循環表示從i->j ,合起來的意思就是求min(dist(ij) ,dist(ik) + dist(kj)),可以看到實現很簡單,時間復雜性為O(n3)

要求最遠路徑,只要將路徑值變為相反值就行了
int[,] dest = new int[6, 6]; dist = new int[6, 6]{ {0,-1,-1,0,0,0}, {0,0,-1,0,0,0}, {0,0,0,0,0,-1}, {0,0,-1,0,0,0}, {0,0,0,0,0,0}, {0,0,0,0,0,0} }; Floyd_OneNode(dist, 0, dest); PrintGraph_OneNode(dest, M, 0); /// <summary> /// Floyd最短路徑算法,求指定節點到其它節點距離 /// </summary> /// <param name="dist">鄰接矩陣</param> /// <param name="index">指定節點在鄰接矩陣下標</param> /// <param name="D">最短路徑</param> public static void Floyd_OneNode(int[,] dist, int index, int[,] D) { //復制一份dist for (int i = 0; i < M; i++) { for (int j = 0; j < M; j++) { D[i, j] = dist[i, j]; } } for (int k = 0; k < M; k++) { //i為指定節點 int i = index; for (int j = 0; j < M; j++) { D[i, j] = Math.Min(D[i, j], (D[i, k] == 0 || D[k, j] ==0) ? 0 : D[i, k] + D[k, j]); } } }

總結
經過上一篇和這一篇的分析,你會發現聯動問題是圖論里面的相關知識,涉及到拓撲排序和最短路徑算法。實際代碼中還會涉及到遞歸,在這次開發中我感受最深的一點遇到復雜問題,一定要分析和規划清楚找到問題的本質,偏離了問題本質就可能用很復雜的代碼實現了。
動態聯動問題的經過總結我給出的步驟
1.計算每個節點到主節點的最遠距離,(這個其實是圖的最短路徑的變種)。
2.找出所有最遠距離是1的節點,這些節點是需要聯動的,而其它最遠距離不為無窮大的節點是需要清空的。
3.循環這些距離為1的節點,找這些節點直接依賴的父級節點連同主節點一起收集,傳到后台進行解析。
理清楚這幾個步驟你也可以實現自己的動態聯動功能了!
