图问题中动态规划的应用

图问题中动态规划的应用

阅读本文前要求读者对每个问题的描述都有了解,这里只提供实现方法

一.多段图最短路径问题

在这里插入图片描述
在这里插入图片描述
那么这里呢我们就不写输入了,数据存储到数组里面就可以了
代码:

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

打印结果:
在这里插入图片描述
上面 中添加了回溯路径,可以参考下面的图更好理解:
在这里插入图片描述

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