0x07算法设计与分析复习(二):算法设计策略-动态规划法4
参考书籍:算法设计与分析——C++语言描述(第二版)
算法设计策略-动态规划法
最优二叉搜索树
问题描述
二叉搜索树运算有很好的平均时间复杂度(\(O(\log n\)),但可能会产生退化的树形,是搜索时间变坏。二叉平衡树限制了输的高度,使搜索运算的最坏情况时间位\(O(\log n)\)。
以上分析均假定二叉搜索树上搜索一个元素的概率是相等的。如果元素集合是固定的,并且已知搜索集合中每个元素的概率,包括不成功的概率,那么可以构造一颗最优二叉搜索树,使其具有最小的平均搜索时间。
设有元素集合\(\{a_1,a_2,\cdots,a_n\}\),其中,\(a_1<a_2<\cdots<a_n\),\(p(i)\)是在集合中成功查找\(a_i\)的概率,\(1\leq i\leq n\),\(q(i)\)是待查元素x值满足\(a_i<x<a_{i+1}\)的概率,\(0\leq i\leq n\)(假定\(a_0=-\infty,a_{n+1}=+\infty\))。显然:
最优二叉搜索树问题是指设法构造一颗具有最小平均搜索时间的二叉搜索树。
动态规划法求解
最优子结构
已知递增有序的元素集合\(\{a_1,a_2,\cdots,a_n\}\)(假定\(a_0=-\infty,a_{n+1}=+\infty\)),以及成功查找\(a_i\)的概率\(p(i)(i\leq i\leq n)\)和不成功查找的概率\(q(i)(0\leq i\leq n)\)。为了构造一颗最优二叉搜索树,首先应当确定根结点(\(a_k\))。根结点将原集合分成三部分:\(L\)、\(a_K\)和\(G\),其中,\(L=\{a_1,a_2,\cdots,a_{k-1}\}\),\(G=\{a_{k+1},\cdots,a_n\}\)。于是问题就分解成两个同类子问题,即分别构造根\(a_k\)的左子树和右子树,它们应当都是最优二叉搜索树。
与二分搜索的二叉判定树类似,每个内结点代表一次成功搜索可能的终止位置,每个外结点表示一次成功搜索可能的终止位置,每个外结点表示一次不成功搜索的终止位置。设\(level(a_i)\)是内结点\(a_i\)的层次,\(level(E_i)\)是外结点\(E_i\)的层次。若搜索在\(a_i\)终止,则需进行\(level(a_i)\)次元素值间的比较;若搜索在\(E_i\)处终止,则需进行\(level(E_i)-1\)次元素值间的比较。可以导出二叉搜索树T的平均搜索代价\(cost(T)\)的计算公式:
假定\(a_k\)位根结点,则该二叉搜索树分划位左子树L、根和右子树R三部分。设对左子树L和右子树R搜索的平均搜索代价分别为\(cost(L)\)和\(cost(R)\),则
式中,\(lev(a_i)\)和\(lev(E_i)\)是相应结点在其所在子树上的层次,对原树而言,有\(level(a_i)=lev(a_i)+1\)和\(level(E_i)=lev(E_i)+1\)。
定义\(w(i,j)\)如下:
二叉搜索树T的平均搜索代价\(cost(T)\)为:
从上式可以知道,如果T是最优二叉搜索树,必定要求其左右子树都是二叉搜索树,否则T就不是最优的。这表明,队医最优二叉搜索树问题,最优性原理成立。
设\(c(0,n)\)是由元素值集合\(\{a_1,a_2,\cdots,a_n\}\)所构造的最优二叉搜索树的代价,则
一般,\(c(i,j),i\leq j\)是元素值集合\((a_{i+1},a{i+2},\cdots,a_j)\)所构造的最优二叉搜索树的代价,设\(r(i,j)=k\)为该树的根,要求结点k满足下式:
式中,\(c(i,k-1)\)和\(c(k,j)\)分别是左右子树的平均搜索代价。上式建立了原问题最优解和子问题最优解之间的数值关系,建立这一关系是动态规划求解问题时必须的和关键的一步。
构造最优二叉搜索树
设w、c和r是上面定义的3个二维数组,计算此3个量,便可以从中得到最优二叉搜索树。运用动态规划法求解这三个量的递推算法如下:
-
计算主对角线的w、c和r的值
\(w(i,i)=q(i);c(i,i)=0;r(i,i)=0\quad i=0,1,\cdots,n\)
-
计算紧邻主对角线上面的那条对角线的w、c和r的值
\(w(i,i+1)=q(i)+q(i+1)+p(i+1)\)
\(c(i,i+1)=c(i,i)+c(i+1,i+1)+w(i,i+1)=w(i,i+1)\)
\(r(i,i+1)=i+1\)
-
根据下列公式,计算主对角线以上n-2条斜线的w、c和r的值
\[\begin{aligned} \]&c(i,j)=\underset{i+1\leq k\leq j}{\min}{c(i,k-1)+c(k,j)}+w(i,j)\
& r(i,j)=k
\end{aligned}\[ \]
一维数组p和q保存成功和不成功搜索的两种概率,n是数组长度,计算结果保存在二维数组w、c和r的值。函数Find计算满足\(\underset{i+1\leq k\leq j}{\min}\{c(i,k-1)+c(k,j)\}\)的k值。函数CreateOBST调用Find计算w、c和r。最后根据r的值可以构造所求得的最优二叉搜索树。
//构造最优二叉搜索树
int Find(int i,int j,int **r,float **c)
{
float min=INFTY;
int k;
for(int m = i+1;m<=j;m++){
if((c[i][m-1]+c[m][j])<min){
min=c[i][m-1]+c[m][j];
k=m;
}
}
return k;
}
void CreateOBST(float *p,float *q,float **c,int **r,float **w,int n)
{
for(int i=0;i<=n-1;i++){
//初始化
w[i][i]=q[i];c[i][i]=0.0;r[i][i]=0;
w[i][i+1]=q[i]+q[i+1]+p[i+1];
c[i][i+1]=q[i]+q[i+1]+p[i+1];
r[i][i+1]=i+1;
}
w[n][n]=q[n];c[n][n]=0.0;r[n][n]=0;
for(int m = 2;m<=n;m++){
//计算n-2条对角线元素
for(i=0;i<=n-m;i++){
int j = i+m;
w[i][j]=w[i][j-1]+p[j]+q[j];
int k=Find(i,j,r,c);
c[i][j]=w[i][j]+c[i][k-1]+c[k][j];
r[i][j]=k;
}
}
}
Find函数用于计算k,其计算时间为\(j-i=m\),忽略二维数组生成时间,CreateOBST函数总的计算时间为:
利用D.E.Kunth[^1]的结论,计算\(c(i,j)\)的式子中\(i+1\leq k\leq j\)的范围还可以进一步缩小为\(r(i,j-1)\leq k\leq r(i+1,j)\),从而得到下式:
0/1背包
问题描述
与使用贪心法求解的一般背包问题不同,在0/1背包问题中,物品不能分割只能作为一个整体或者装入或者不装入背包。
0/1背包问题可以描述为:已知一个载重为M的背包和n件物品,物品编号为\(0~n-1\)。第i件物品的重量为\(w_i\),如果将第i种物品装入背包将获益\(p_i\),这里\(w_i>0,p_i>0(0\leq i<n)\)。在物品不能分割、只能整件装入背包的情况下,求一种最佳装载方案使得总收益最大。
0/1背包问题可以形式化为:给定\(M>0,w_i>0,p_i>0(0\leq i<n)\),求一个n元向量\(X=(x_0,x_1,\cdots,x_{n-1}),x_i\in\{0,1\}(0\leq i<n)\),使得\(\sum_{0\leq i<n}w_ix_i\leq M\)且\(\sum_{0\leq i<n}p_ix_i\)最大。不妨使用KNAP(0,n-1,M)表示一个背包问题的实例。
动态规划法求解
判断一个问题是否适用于动态规划法求解,首先必须分析问题解的结构,考察它的最优解是否具有最优子结构特性。其次应当检查分解所得的子问题是否相互独立,是否存在重叠子问题现象。
最优子结构
0/1背包的最优解具有最优子结构特性。设\((x_0,x_1,\cdots,x_{n-1}),x\in \{0,1\}\)是0/1背包的最优解,那么\((x_1,x_2,\cdots,x_{n-1})\)必然是0/1背包子问题的最优解:背包载重\(M-w_0x_0\),共有n-1件物品,第i件物品的重量为\(w_i\),效益为\(p_i\),\(w_i>0,p_i>0,1\leq i<n\)。若不然,设\((z_1,z_2,\cdots,z_{n-1})\)是该子问题的一个最优解,而\((x_1,x_2,\cdots,x_{n-1})\)不是该子问题的最优解。由此可知:
因此,
显然\((x_0,z_1,z_2,\cdots,z_{n-1})\)是比\((x_0,x_1,\cdots,x_{n-1})\)收益更高的最优解,\((x_0,x_1,\cdots,x_{n-1})\)不是背包问题的最优解。这与假设矛盾,因此\((x_1,\cdots,x_{n-1})\)必然是相应子问题的一个最优解。
最优解的递归算法
给定一个0/1背包问题实例KNAP(0,n-1,M),可以通过对n个物品是否加入背包做出一系列决策进行求解,假定变量\(x_i\in\{0,1\},0\leq i<n\)表示对物品i是否加入背包的一个决策。假定对这些\(x_i\)做出决策的次序是\(X=(x_{n-1},x_{n-2},\cdots,x_0)\)。在对\(x_{n-1}\)做出决策后,存在两种情况:
- \(x_{n-1}=1\),将编号为n-1的物品加入背包,接着求解子问\(KNAP(0,n-2,M-w_{n-1})\);
- \(x_{n-1}=0\),编号为n-1的物品不放入背包,接着求解子问题\(KNAP(0,n-2,M)\)。
设\(f(j,X)\)是当背包载重为X,可供选择的物品为\(0,1,\cdots,j\)时的最优解,那么\(f(n-1,M)\)可表示为:
对于任意\(j,0\leq j<n\),有
如果物品\(w_j\)被加入背包,则\(f(j,X)=f(j-1,X-w_j)+p_j\)否则\(f(j,X)=f(j-1,X)\)。以上分析得到递推式:
//0/1背包的递归算法
template<class T>
class Knapsack
{
public:
Knapsack(int mSize,float cap,float *wei,T *prof);
T RKnap();
private:
T f(int j,float X);//递归函数f计算0/1背包的最大收益
float m,*w;
T *p;
int n;
};
template<class T>
T Knapsack<T>::f(int j,float X)
{
if(j<0)
return ((X<0)? -INFTY:0);
if(X<w[j])
return f(j-1,X);
else {
T a=f(j-1,X);
T b=f(j-1,X-w[j])+p[j];
if(a>b)
return a;
else
return b;
}
}
template<class T>
T Knapsack<T>::RKnap()
{
if(n>0)
return f(n-1,m);
else
return NoAns;//NoAns可定义为类型T的一个代表无收益的常量
}
上述0/1背包递归算法的时间复杂度在最坏情况下是指数级的(\(O(2^n)\))。
动态规划算法
如果X是满足\(0\leq X\leq M\)条件的整数,可以采用动态规划法,自底向上进行计算,已经计算的子问题的最优解值可以用二维数组f[j][k]保存(\(0\leq j<n,0\leq k\leq M\))。也可以采用备忘录方法,在递归计算中保存子问题的最优解值。在这种情况下,必须对X的不同值计算f[j][k],因此总的计算时间为\(\Theta(Mn)\),其中M是背包载重量,n是物品个数。
但是如果物品重量和背包载重为实数,那么子问题的最优解值\(f(j,X)\)是\(X(0\leq X\leq M)\)的连续函数,上述方法就无法解决问题,只能开发物品重量为 实数的0/1背包的动态规划算法。
0/1背包算法框架
求0/1背包的最优解值
0/1背包问题的图解法
用\(S^j\)表示函数曲线\(f(j,X)\)的全部阶跃点的集合,\(S^j={(X_i,P_i)|函数曲线f(j,X)的全部阶跃点},-1\leq j\leq n-1\),其中\(S^{-1}=\{(0,0)\}\)。用\(S_1^j\)表示函数曲线\(f(j-1,X-w_j)+p_j\)的全部阶跃点的集合,\(S_1^j={(X_i,P_i)|函数曲线f(j-1,X-w_j)+p_j的全部阶跃点},-1\leq j<n-1\)
计算所有\(S^j\)和\(S_1^j\)的步骤如下:
- \(S^{-1}=\{(0,0)\}\),函数\(f(-1,X)\)只有一个阶跃点;
- \(S_1^j=\{(X,P)|(X-w_j,P-p_j)\in S^{j-1}\}\),也就是说,由集合\(S^{j-1}\)中的每个阶跃点\((X,P)\),得到集合\(S_1^j\)中的一个阶跃点\((X+w_j,P+p_j)\);
- \(S^j\)是合并集合\(S^{j-1}\cup S_1^j\),并舍弃其中被支配的阶跃点和所有\(X>M\)的阶跃点得到的。
设\((X_1,P_1)\)和\((X_2,P_2)\)是两个阶跃点,如果\(X_1<X_2,P_1>P_2\),则称\((X_1,P_1)\)支配\((X_2,P_2)\),或\((X_2,P_2)\)被\((X_1,P_1)\)所支配。
求0/1背包的最优解
通过回溯方式,可以求得0/1背包的最优解\((x_0,x_1,\cdots,x_{n-1})\),

浙公网安备 33010602011771号