上2篇详细分析了动态规划的一些理解,传统的教材上就大概说了下空间换时间,记忆以避免重复计算等。
然后我们在文章中深入的分析和解释了交叠子问题是怎么表现的,最优子结构的表现,多阶段决策(无后效性)的表现,递推式(状态转移方程),一个状态表示一个子
问题的解,动态规划就是将问题划分为规模不是太大的子问题,动态规划填矩阵的顺序(拓扑排序)以及空间复杂度的优化跟什么有关等等。
计算二项式系数其实可以看成是不是解决最优问题的一个动态规划的特例,这节我们再来介绍2个动态规划的算法:Warshall算法(也可以说是不是最优问题,但它也可
以表现为动态规划的过程)和Floyd算法。
----------------------------------------------------------------------------------------------------------------------------------------------------
1,Warshall算法求传递闭包
问题的定义:
有向图的传递闭包表达的就是每个顶点之间的可达性。当然,可以从每个起点开始深度或广度优先遍历,能遍历到的顶点就说明从这个点到它可达,这样来生成传递闭
包。这样做对图进行了多次遍历,我们希望找到更好一点的办法:
Warshall算法通过动态规划的形式,
以多阶段决策的方式来逐步构建一个有向图的传递闭包:
1)有向图的邻接矩阵表示了一个有向图,实际上有向图的邻接矩阵我们可以看作是
没有经过任何中间顶点(
即一个点仅到它的邻接点为可达)的图的传递闭包(传递
闭包的初始条件)
2)我们可以通过一次加入一个点的方式(一共n次,加入n个点,n步决策)来构造最终的传递闭包:
----
用R0表示邻接矩阵,以后每次加入一个顶点来构造R1,R2.......Rn。
---如果 r(i , j) 在Rk-1中为1,那么加入顶点k作为中间节点后,r(i , j) 在Rk中的值仍为1(如果可达了,加入点之后肯定还是可达)
--如果r(i , j)在Rk-1中不为1,仅当 r(i , k) = 1 且 r(k , j) = 1,r(i , j)在Rk中才为1(如果现在不可达,仅当加入的一个中间节点可以作为一个桥梁使之可达,才
可达)
即下面2条规则:
如果不想分2种情况,用与或式可以直接写成下式:
括号在”或“后面。
伪代码:
实现如下:
package
Section8;
/*
第八章 动态规划 有向图传递闭包的Warshall算法
*/
public
class
Warshall {
/**
*
@param
args
*/
public
static
void
main(String[] args) {
//
TODO Auto-generated method stub
int
[][] AdjMat
=
{
{
0
,
1
,
0
,
0
},
{
0
,
0
,
0
,
1
},
{
0
,
0
,
0
,
0
},
{
1
,
0
,
1
,
0
}
};
int
[][] BiBao
=
Warshall(AdjMat);
System.out.println(
"
输出表达传递闭包的矩阵:\n
"
);
for
(
int
i
=
0
;i
<
BiBao.length;i
++
)
{
for
(
int
j
=
0
;j
<
BiBao.length;j
++
)
System.out.print(BiBao[i][j]
+
"
"
);
System.out.println();
}
}
public
static
int
[][] Warshall(
int
[][] AdjMat){
//
接受一个图的邻接矩阵为参数,返回表达它的传递闭包的矩阵
int
[][] Rresult
=
AdjMat;
//
初始结果R0
int
n
=
AdjMat.length;
for
(
int
k
=
0
;k
<
n;k
++
)
//
R0到Rn,做n步变换
{
int
[][] Rtemp
=
new
int
[n][n];
//
每循环一下求下次结果
for
(
int
i
=
0
;i
<
n;i
++
)
for
(
int
j
=
0
;j
<
n;j
++
)
{
if
(Rresult[i][j]
==
1
)
Rtemp[i][j]
=
1
;
else
if
(Rresult[i][k]
==
1
&&
Rresult[k][j]
==
1
)
Rtemp[i][j]
=
1
;
else
Rtemp[i][j]
=
0
;
}
Rresult
=
Rtemp;
}
return
Rresult;
}
}
运行结果:
输出表达传递闭包的矩阵:
1 1 1 1
1 1 1 1
0 0 0 0
1 1 1 1
可以看出Warshall算法算法的时间复杂度是n^3,空间复杂度是n^2,根据我们前面2篇文章讲的,它的空间复杂度也是可以优化的,见习题(
我们可以在原矩阵上来
填,不需要开辟空间)
------------------------------------------------------------------------------------------------------------------------------------------------
计算完全最短路径的Floyd算法
Floyd算法可以用于构造无向或有向加权图(不包含长度为负的回路)的完全最短路径:
Floyd算法算法的构造过程非常类似于Warshall算法,所以放在一起讲:
Warshall算法是通过每次加入一个顶点,看把这个顶点作为中间顶点是否能改进传递闭包的矩阵(通过这个新加入的顶点作为中间桥梁,使得原来不可达的2个顶点可
达,以此逐步向传递闭包逼近)。
Floyd算法非常类似,通过初试的权重矩阵,
每次加入一个顶点,看这个顶点是否能作为中间顶点改变图的权重矩阵(加入这个中间顶点后,每两个点之间的最短距离
是否减小了)。
伪代码:
实现:
package
Section8;
/*
第八章 动态规划 完全最短路径的Floyd算法
*/
public
class
Floyd {
/**
*
@param
args
*/
public
static
void
main(String[] args) {
//
TODO Auto-generated method stub
int
[][] WeightMat
=
{
{
0
,
1000
,
3
,
1000
},
{
2
,
0
,
1000
,
1000
},
{
1000
,
7
,
0
,
1
},
{
6
,
1000
,
1000
,
0
}
};
int
[][] Result
=
Floyd(WeightMat);
System.out.println(
"
输出表达完全最短路径的矩阵:\n
"
);
for
(
int
i
=
0
;i
<
Result.length;i
++
)
{
for
(
int
j
=
0
;j
<
Result.length;j
++
)
System.out.print(Result[i][j]
+
"
"
);
System.out.println();
}
}
public
static
int
[][] Floyd(
int
[][] WeightMat){
//
接受一个图的权重矩阵,返回表达完全最短路径的矩阵
int
n
=
WeightMat.length;
int
[][] Result
=
WeightMat;
for
(
int
k
=
0
;k
<
n;k
++
)
//
进行n次变换,每次加入第k个点
{
int
[][] temp
=
new
int
[n][n];
for
(
int
i
=
0
;i
<
n;i
++
)
for
(
int
j
=
0
;j
<
n;j
++
)
temp[i][j]
=
min(Result[i][j],Result[i][k]
+
Result[k][j]);
//
加入第k个点后路径是否能缩短
Result
=
temp;
}
return
Result;
}
private
static
int
min(
int
m,
int
n){
if
(m
<
n)
return
m;
return
n;
}
}
运行结果:
输出表达完全最短路径的矩阵:
0 10 3 4
2 0 5 6
7 7 0 1
6 16 9 0
可以看出,Floyd算法的构造几乎和Warshall完全一样,在习题中可以看到,它也是可以优化空间的(
直接在原来的矩阵上填,来得到下一个矩阵)
---------------------------------------------------------------------------------------------------------------------------------------------------
1,证明Warshall算法和Floyd算法的时间复杂度都是n^3的
easy,
2,空间优化,说明Warshall算法和Floyd算法都可以直接从前驱矩阵得到下一个矩阵,不需要另开一个矩阵:
刚开始做这个题时,还感觉挺难的,现在有了前面2篇文章对动态规划的比较全面的剖析,应该比较简单了。
为什么可以优化?因为它填矩阵的顺序,或者说递推式的依赖关系使得我们可以使用一定的技巧。
无论从递推式的关系还是第 k 次加入点 k ,去改变构造下一个矩阵的意义上,都很容易看出来,
加入第 k 个点来转换到下一个矩阵时,第 k 行k列不会变(从加入第
k个点的意义就可以看出来,加入第k个点就是把它当中间点,它自己那一行一列原本就包含了自己,加不加都一样。或者从依赖式也可以分析)。
由于第k行k列不会变,所以往下一个矩阵构造时,先把k行k列填好,其他点用到时,不会被覆盖。(画一下,很容易就知道怎么做)
--------------------------------------------------------------------------------------------------------------------------------------------------
再来谈
动态规划里另一个方面的重要问题:例如Floyd算法,我们得到了它的最短路径的值,但具体的最短路径又是哪一条呢?
这也是动态规划问题的一个常见的关键点,
我们常见的是求出最优的那个代价,但有时候需要去求得到这个代价的具体路径。
于是有下面一道很好的思考题:
Q:加强Floyd算法,使得它能得到最短路径本身,而不仅仅是它的长度。
解答:
可以再弄一个矩阵p(大小为n*n),
p[i , j] = k,表明从 i 到 j 的最短路径要经过顶点 k (
注意不是只经过 k)。这样在原来的Floyd算法里,每次加入顶点 k 来改
变矩阵时,若 k 起到了作用(加入k后对某个最短路径起到了修正作用),那么就把进行把 k 记在 p 矩阵里:
相比原算法,就是多用了一个p矩阵,加了最后一行,每次加入k起到了作用的时候,记下。注意这个伪代码是空间优化了的Floyd算法,直接在原矩阵上填。
这样最终结果就得到了2个矩阵:D矩阵记录了所有 i 到 j 顶点的最短路径。
p矩阵间接表达了每条最短路径的具体路径。例如:最终可能得到下列p矩阵:
根据p[i , j] 的定义, p[1][2] = 3,表明从顶点1(即顶点a)到顶点2(即顶点b)的最短路路径至少要经过顶点3(即顶点c)----见最上面那幅图。
即顶点3是顶点1到2最短路径上的一个桥梁,那么从1到3以及从3到2之间还有没有别的桥梁呢?再看p[1][3] = 0,p[3][2] = 0,所以没有了,那么从1到2的最短
路径就是1,3,2。
下列递归算法可以表明上述过程,来从p矩阵中输出任意两个点之间的最短路径:
注意是递归的,每个k都将 i,j 分开了,
为什么可以递归?这里又要说到最优子结构,因为从 i 到 j是最短路径,那么从 i 到 k,以及从 k 到 j也是子问题的最短路
径。
这个问题很重要,注意怎么去记录一个动态规划产生的过程,
这样我们最终得到的就不仅仅是一个最优化的代价,还可以得到一个表达这个动态规划解是怎么来的矩
阵。注意这个矩阵
只是表达了生成的过程,要输出具体的解,
还需要一个算法从这个矩阵中去挖掘出这个答案。
--------------------------------------------------------------------------------------------------------------------------------------------------
总结:
1)Warshall算法和Floyd算法是非常明显的多阶段决策,构造思想非常相似,Floyd算法还表现了最优子结构性质(Warshall算法不是最优化问题)。
2)注意他们的空间优化,可以直接在原矩阵上本地构造下一个矩阵
3)加强的Floyd算法,很好,
注意怎么去记录解构造过程中的信息,以使得我们能从记录信息里得出那个最优代价的细节。
这一篇也很重要,特别是详细讲解了前2篇没有提到的一个动态规划的问题,就是上述第3点