圖問題中動態規劃的應用
閱讀本文前要求讀者對每個問題的描述都有了解,這裏只提供實現方法
一.多段圖最短路徑問題
那麼這裏呢我們就不寫輸入了,數據存儲到數組裏面就可以了
代碼:
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);
}
}
打印結果:
上面 中添加了回溯路徑,可以參考下面的圖更好理解: