圖問題中動態規劃的應用

圖問題中動態規劃的應用

閱讀本文前要求讀者對每個問題的描述都有了解,這裏只提供實現方法

一.多段圖最短路徑問題

在這裏插入圖片描述
在這裏插入圖片描述
那麼這裏呢我們就不寫輸入了,數據存儲到數組裏面就可以了
代碼:

import org.junit.Test;

import java.util.ArrayList;
import java.util.List;

public class Main {
    int Max = 65535;
   @Test
    public void Test(){
        int arc[][] = {
                {Max,4,2,3,Max,Max,Max,Max,Max,Max},
                {Max,Max,Max,Max,9,8,Max,Max,Max,Max},
                {Max,Max,Max,Max,6,7,8,Max,Max,Max},
                {Max,Max,Max,Max,Max,4,7,Max,Max,Max},
                {Max,Max,Max,Max,Max,Max,Max,5,6,Max},
                {Max,Max,Max,Max,Max,Max,Max,8,6,Max},
                {Max,Max,Max,Max,Max,Max,Max,6,5,Max},
                {Max,Max,Max,Max,Max,Max,Max,Max,Max,7},
                {Max,Max,Max,Max,Max,Max,Max,Max,Max,3},
                {Max,Max,Max,Max,Max,Max,Max,Max,Max,Max}
        };
        int cost = BackPath(10,arc);
       System.out.println("最短路徑爲:"+cost);
   }
   //該函數返回最短路徑長度,同時打印最短路徑
    //arc是代價矩陣,n爲點的個數(注意我們傳入的矩陣是已經分好的多段圖)
    int BackPath(int n,int [][]arc){
        //path[i]用於記錄i頂點點的前一個頂點(當然是在我們最後求的最短路徑上的)
        int path[]=new int[n];
        //cost[i]表示0到i的路徑長度(當然是在我們最後求的最短路徑上的)
        int cost[]=new int[n];
        for (int i=0;i<n;i++){
            path[i]=-1;
            cost[i]= Max;
        }
        cost[0]=0;
        //cost[j]=min{cost[i]+arc[i][j]}
        for(int j=1;j<=n-1;j++)
            //因爲只有可能前面的點才肯=可能有指向它的路徑,所以從j-1開始
            for(int i=j-1;i>=0;i--){
                //通過入邊來更新cost與path
              if(cost[j]>cost[i]+arc[i][j]){
                  cost[j]=cost[i]+arc[i][j];
                  path[j]=i;
              }
            }
        List<Integer> list = new ArrayList<>();
        int parent=path[n-1];
        while(parent!=-1){
            list.add(0,parent);
            parent=path[parent];
        }
        for (Integer integer : list) {
            System.out.print(integer+"->");
        }
        System.out.println(n-1);
        return cost[n-1];

    }
}

在這裏插入圖片描述
假設有k條邊,m個段,這時間複雜度爲:O(k+m)

二.多源最短路徑算法—Floyd算法

在這裏插入圖片描述

import org.junit.Test;
public class Main {
    int Max = 65535;
   @Test
    public void Test(){
        int arc[][] = {
                {Max,5,7,Max,Max,Max,2},
                {5,Max,Max,9,Max,Max,3},
                {7,Max,Max,Max,8,Max,Max},
                {Max,9,Max,Max,Max,4,Max},
                {Max,Max,8,Max,Max,5,4},
                {Max,Max,Max,4,5,Max,6},
                {2,3,Max,Max,4,6,Max}
        };
        char[] vex = {'A','B','C','D','E','F','G'};
      int dist[][] = new int[vex.length][vex.length];
       String parent[][] = new String[vex.length][vex.length];//parent[i][j]表示i經過parent[i][j]到達j
       //parent[i][j]只包含了i到j的1中間節點及i,沒有j,因此可以打印時自行補上
       Floyd(parent,dist,7,arc,vex);
       show(parent,dist,7,vex);
   }
   //n代表點的個數,該方法利用動規的思想,遞推公式爲:
    //dist[i][j]=min{dist[i][k]+dist[k][j]}(0<=k<=n-1)
   void Floyd(String parent[][],int dist[][],int n,int arc[][],char vex[]){
       //先做初始化的工作
       for(int i=0;i<n;i++)
           for(int j=0;j<n;j++){
                parent[i][j]=""+vex[i];//初始化爲i經過i到達j()注意不要加上j
               if(i==j)
                   dist[i][j]=0;
               else
                    dist[i][j]=arc[i][j];
           }
       for(int k=0;k<n;k++)
           for(int i=0;i<n;i++)
               for(int j=0;j<n;j++){
                   if(dist[i][j]>dist[i][k]+dist[k][j]){
                     parent[i][j]=parent[i][k]+parent[k][j];//在這裏就可以看到parent[i][j]只包含了i到j的1中間節點及i,沒有j的用意是爲了防止重複
                       dist[i][j]=dist[i][k]+dist[k][j];
                   }
               }
   }
    void show(String parent[][],int dist[][],int n,char vex[]){
        for(int i=0;i<n;i++)
            for(int j=0;j<n;j++){
                System.out.println(vex[i]+"到"+vex[j]+"的最短路徑爲"+parent[i][j]+vex[j]+",對應長度爲"+dist[i][j]+" "+"對稱"+dist[i][j]+" "+dist[j][i]);
            }
    }
}

打印結果:(筆者已經檢查過了,讀者可以自行檢查一遍是沒有問題的)

A到A的最短路徑爲AA,對應長度爲0 對稱0 0
A到B的最短路徑爲AB,對應長度爲5 對稱5 5
A到C的最短路徑爲AC,對應長度爲7 對稱7 7
A到D的最短路徑爲AGFD,對應長度爲12 對稱12 12
A到E的最短路徑爲AGE,對應長度爲6 對稱6 6
A到F的最短路徑爲AGF,對應長度爲8 對稱8 8
A到G的最短路徑爲AG,對應長度爲2 對稱2 2
B到A的最短路徑爲BA,對應長度爲5 對稱5 5
B到B的最短路徑爲BB,對應長度爲0 對稱0 0
B到C的最短路徑爲BAC,對應長度爲12 對稱12 12
B到D的最短路徑爲BD,對應長度爲9 對稱9 9
B到E的最短路徑爲BGE,對應長度爲7 對稱7 7
B到F的最短路徑爲BGF,對應長度爲9 對稱9 9
B到G的最短路徑爲BG,對應長度爲3 對稱3 3
C到A的最短路徑爲CA,對應長度爲7 對稱7 7
C到B的最短路徑爲CAB,對應長度爲12 對稱12 12
C到C的最短路徑爲CC,對應長度爲0 對稱0 0
C到D的最短路徑爲CEFD,對應長度爲17 對稱17 17
C到E的最短路徑爲CE,對應長度爲8 對稱8 8
C到F的最短路徑爲CEF,對應長度爲13 對稱13 13
C到G的最短路徑爲CAG,對應長度爲9 對稱9 9
D到A的最短路徑爲DFGA,對應長度爲12 對稱12 12
D到B的最短路徑爲DB,對應長度爲9 對稱9 9
D到C的最短路徑爲DFEC,對應長度爲17 對稱17 17
D到D的最短路徑爲DD,對應長度爲0 對稱0 0
D到E的最短路徑爲DFE,對應長度爲9 對稱9 9
D到F的最短路徑爲DF,對應長度爲4 對稱4 4
D到G的最短路徑爲DFG,對應長度爲10 對稱10 10
E到A的最短路徑爲EGA,對應長度爲6 對稱6 6
E到B的最短路徑爲EGB,對應長度爲7 對稱7 7
E到C的最短路徑爲EC,對應長度爲8 對稱8 8
E到D的最短路徑爲EFD,對應長度爲9 對稱9 9
E到E的最短路徑爲EE,對應長度爲0 對稱0 0
E到F的最短路徑爲EF,對應長度爲5 對稱5 5
E到G的最短路徑爲EG,對應長度爲4 對稱4 4
F到A的最短路徑爲FGA,對應長度爲8 對稱8 8
F到B的最短路徑爲FGB,對應長度爲9 對稱9 9
F到C的最短路徑爲FEC,對應長度爲13 對稱13 13
F到D的最短路徑爲FD,對應長度爲4 對稱4 4
F到E的最短路徑爲FE,對應長度爲5 對稱5 5
F到F的最短路徑爲FF,對應長度爲0 對稱0 0
F到G的最短路徑爲FG,對應長度爲6 對稱6 6
G到A的最短路徑爲GA,對應長度爲2 對稱2 2
G到B的最短路徑爲GB,對應長度爲3 對稱3 3
G到C的最短路徑爲GAC,對應長度爲9 對稱9 9
G到D的最短路徑爲GFD,對應長度爲10 對稱10 10
G到E的最短路徑爲GE,對應長度爲4 對稱4 4
G到F的最短路徑爲GF,對應長度爲6 對稱6 6
G到G的最短路徑爲GG,對應長度爲0 對稱0 0

時間複雜度爲:O(n^3)

三.TSP問題

由於此問題比較複雜,我那下面的例子來詳細的講解。

1.狀態轉移方程

d(i,V’)表示由i頂點出發經過V’裏面的所有頂點僅僅一次然後回到出發點的最短路徑長度
在這裏插入圖片描述

2.例子講解

假設我們是以0爲出發點的,最後又要回到0
在這裏插入圖片描述
在這裏插入圖片描述
根據上面的狀態轉移方程不難畫出上面的狀態樹。很自然地,最下面的一層我們是知道的,

d(1,{})=5   發生狀態轉移(1-->0)
d(2,{})= 6   發生狀態轉移(2-->0)
d(3,{})=3 發生狀態轉移(3-->0)

接下來我們的工作就是逐層的向上最後把d(0,{1,2,3})求出來。
我們需要明白V’的個數爲2^(n-1),其中n爲總的頂點數,下面我們要定義數組
V[2^(n-1)],下面我們以上面的例子來看看V[i]的含義

n=4
V[0]={}
V[1]={1}
V[2]={2}
V[3]={1,2}
V[4]={3}
V[5]={1,3}
V[6]={2,3}
V[7]={1,2,3}
爲什麼是這樣我後面會來介紹

算法步驟如下:

//把0當做出發點與終點,在理解下面僞代碼時,應結合上面的狀態樹來看
//狀態樹最底層來看i不需要爲0
for(int i=1;i<2^(n-1);i++)
	d[i][0]=arc[i][0];
//相當於從V[1]~V[2^(n-1)]來遍歷一遍,在狀態樹裏面來看的話就是從第二層(也不是嚴格的,你可能會疑問爲
//什麼,在V[j]的排列中{1,2}排在了{3}的前面,但是{1,2}卻在{3}的前面,那這樣可行嗎?關於這個問題在代碼
//實現那裏我會解釋,但總體上我們仍然可以認爲他是由底向上的,只是順序有點不同了)開始往上走,這是合理的
//然後的話我們把最頂層(即d(0,V))是單獨到最後獨立於循環外求的,所以j不可以爲2^(n-1)-1
for(int j=1;j<2^(n-1)-1;j++)//j是V[j]裏面的j
	//從狀態樹來看d(i,V')中i是不爲0的(不看最頂層的話),因此i從1開始到n-1即可
	for(int i=1;i<n;i++){
		if(V[j]裏面沒有i的話){
			d[i][j]=min{arc[i][k]+d[i][j-1]};//這裏的d[i][j-1]
			//表示由i出發經過在V[j]中排除掉k後回到0的最短長度,後面代碼實現會用位運算來修正,
			//嚴格來講這裏不是寫的j-1
		}
	}
d[0][2^(n-1)-1]=min{arc[0][k]+d[k][2^(n-1)-2]};//這便是最後的結果

下面我們要說的是如何來具體實現呢?在實現之前我需要補充關於位運算的知識

1.’&’符號,x&y,會將兩個十進制數在二進制下進行與運算,然後
返回其十進制下的值。例如3(11)&2(10)=2(10)。
2.’|’符號,x|y,會將兩個十進制數在二進制下進行或運算,然後
返回其十進制下的值。例如3(11)|2(10)=3(11)。
3.’^’符號,x^y,會將兩個十進制數在二進制下進行異或運算,然
後返回其十進制下的值。例如3(11)^2(10)=1(01)。
4.’<<’符號,左移操作,x<<2,將x在二進制下的每一位向左移
動兩位,最右邊用0填充,x<<2相當於讓x乘以4。相應的,’>>’
是右移操作,x>>1相當於給x/2,去掉x二進制下的最右一位。

dp狀態壓縮一般都是與位運算聯繫緊密的,那麼下面我將爲大家介紹一些常用的位運算的公式(這些公式不用記憶,在實踐中敲代碼熟悉即可)

下面的運算我們常用的數據類型都是int(假設這裏int的爲c語言裏的2字節)
A|=1<<c;//表示將A的第(c+1)位變爲1(c=0~31)
A&=~(1<<c);//表示將A第(c+1)位變爲0(c=0~31)
A^=1<<c;//表示將A第(c+1)位變爲0(c=0~31)\
a&(-a)//lowbit操作
A=0//表示把集合置爲空集
A|B//表示把集合A,B取並集、
A&B//表示把集合A,B去交集
//現在假設我們需要一個大小爲15的全集,做法如下
size = 15
ALL = (1<<size)-1
ALL^A//求A的補集
(A&B)==B;//判斷B是否爲A的子集

下面介紹一些枚舉的方式:
//枚舉全集的所有子集
for(int i=0;i<=ALL;i++);
//枚舉集合A的所有子集,包括本身與空集
int subset = A;
do{
	subset=(subset-1)&A;
}while(subset!=A);
//清點集合A裏的元素個數,有下面兩種寫法
int count=0;for(int i=0;i<size;i++){
	if(A&(1<<i)==1) count++;
}for(int i=A;i!=0;i>>=1)
	count+=i&1;

下面來介紹一下lowbit操作
x&(-x)--->//舉例若x=0110 1100,那麼結果爲100,
//即找到最小的那個1的值,具體證明可以嘗試用補碼來證明

下面介紹highbit操作
int p = low_bit(x);
while(p!=x)x-=p,p=low_bit(x);
//最後p即爲所求

下面我們來介紹判斷一個數是否爲2的冪次
x&&!(x&(x-1))//最前面的x是爲了保證x不爲0,0的話那麼就不是2的冪次

下面我們實現一個求各個集合的元素個數的方法:

count[0]=0;
for(int i=1;i<2^(n-1);i++){
	count[i]=count[i>>1]+(i&1);
}

最後回到我們的TSP問題
下面我要解釋前面的一個的問題,在上面的僞代碼中我們介紹了,其實也比較簡單,只是那裏留了個引子給大家,我們的方式是從V[1]~V[2^(n-1)],會擔心一個問題就是由於我們的V[i]的排列順序並不是按照元素個數大小從小到大來排列的,可能導致在計算某一個狀態時他的轉移狀態並未有計算好從而出錯,那麼我們試想,加入我們再求某個狀態時是將他的每一個裏面的1的每次抽取一個出來來求最小值,那麼當你抽取一個1時很自然地他會變小,而我們的遍歷是從小到大來的,也就是說如果他前面的計算好了,那麼他就是沒有問題的,那麼說到這很自然地可以用數學歸納法就可以證明正確性,這裏我就不說了。
代碼實現:

import org.junit.Test;
public class Main {
    int Max = 65535;
   @Test
    public void Test(){
       int arc[][] = {
               {Max,3,6,7},
               {5,Max,2,3},
               {6,4,Max,2},
               {3,7,5,Max}
       };
        TSP_DP(arc,4);
   }
    //TSP問題,默認0爲起始點
    void TSP_DP(int arc[][],int n){
    //path[i][j][0]存放當前狀態的上一個狀態的i,path[i][j][1]存放當前狀態的上一個狀態的j
       int path[][][] = new int[n][1<<(n-1)][2];//路經保存
       int dp[][] = new int[n][1<<(n-1)];
       //下面我們來初始化
        for(int i=1;i<n;i++){
            dp[i][0]=arc[i][0];
            path[i][0][0]=-1;
        }
        for(int j=1;j<1<<(n-1);j++) {
            for (int i = 1; i <= n - 1; i++) {
                if ((j & (1 << (i - 1))) == 0) {
                    dp[i][j] = Max;
                    for (int k = 1; k <= n - 1; k++) {
                        if ((j & (1 << (k - 1))) != 0) {//判斷集合V[j]裏面是否有元素k
                            if(dp[i][j]>arc[i][k] + dp[k][j - (1 << (k - 1))]){
                                dp[i][j] = arc[i][k] + dp[k][j - (1 << (k - 1))];
                                path[i][j][0]=k;
                                path[i][j][1]=j - (1 << (k - 1));
                            }
                        }
                    }
                }
            }
        }
        dp[0][(1<<(n-1))-1]=Max;
        //最後我們對頂層元素來處理
        for(int i=1;i<n;i++){
            if(dp[0][(1<<(n-1))-1]>dp[i][((1<<(n-1))-1)^(1<<(i-1))]+arc[0][i]){
                dp[0][(1<<(n-1))-1]=dp[i][((1<<(n-1))-1)^(1<<(i-1))]+arc[0][i];
                path[0][(1<<(n-1))-1][0]=i;
                path[0][(1<<(n-1))-1][1]=((1<<(n-1))-1)^(1<<(i-1));
            }
        }
        System.out.println("最短長度爲:"+dp[0][(1<<(n-1))-1]);
        //下面來記錄路徑
        int i=0,j=(1<<(n-1))-1;
        int temp;
        System.out.print("對應路徑爲:");
        System.out.print(0);
        while(path[i][j][0]!=-1){
            temp=path[i][j][0];
            j=path[i][j][1];
            i=temp;
            System.out.print("->"+i);
        }
        System.out.print("->"+0);
    }
}

打印結果:
在這裏插入圖片描述
上面 中添加了回溯路徑,可以參考下面的圖更好理解:
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章