关于最小代价子母树

第一次尝试写动态规划(Dynamic Planning) = =

问题如下:

-------------------------------------------------------------------------------------------------------------------------

最小代价子母树

设有一排数,共n个,例如:22 14 7 13 26 15 11.任意2个相邻的数可以进行归并,归并的代价为该两个数的和,经过不断的归并,最后归为一堆,而全部归并代价的和称为总代价,给出一种归并算法,使总代价为最小.
输入、输出数据格式与“石子合并”相同。
输入样例:
4
12 5 16 4

-------------------------------------------------------------------------------------------------------------------------

 

为了说明算法的过程,我们先分析一些简单的情况:

 

1·把序列的n个数,看成二元树的叶子(二叉树)的权。如果用一种括号把两个正整数括起来,则它们对应结点的父辈的权就是由这次加法产生的中间和。这样就把对序列任何一种加括号的方法与一棵带权的二元树对应。上面例子中加括号方法对应二元树如图


 

 


一棵带权二元树的代价就是树中所有根结点权之和。代价最小的带权二元树称为最优二元树。问题转化为求最优带权二元树。

那么,什么是最优带权二元树呢?

最优二叉树,又称哈夫曼树,是一类带权路径长度最短的树,有着广泛的应用。

我们首先给出路径和路径长度的概念。从树中一个结点到另一个结点之间的分支构成这两个结点之间的路径,路径上的分支数目称做路径长度。树的路径长度是从树根到每一结点的路径长度之和。这种路径长度最短的二叉树是。
若将上述概念推广到一般情况,考虑带权的结点。结点的带权路径长度为从该结点树根之间的路径长度与结点上权的乘积。树的带权路径长度为树中所有叶子结点的带路径长度之和,通常记作

WPL=∑W(k)L(k) k=1...n

假设有n个权值W(1),W(2),......,W(n),试构造一棵有n个叶子结点的二叉树,每个叶子结点带权为W(k),则其中带权路径长度WPL最小的二叉树称做最优二又树或哈夫显树。

 

例如,图中的两棵二叉树,都有4个叶子结点a、b、c、d,分别带权4,1,2,3,它们的带权路径长度分别为


(a)WPL=4×2十1×2十2×2十3×2=20

(b)WPL=4×1十1×3十2×3十3×2=19

如何构造哈夫曼树呢?俗称哈夫曼算法。现叙述如下:
(1)根据给定的n个权值W(1),W(2),......,W(n)构成n棵二叉树的集合F={T(1),T(2),......,T(n)}
其中每棵二叉树T(k)中只有一个带权为W(k)的根结点,其左右子树均空。
(2)在F中选取两棵根结点的权值最小的树作为左右子树构造一棵新的二叉树,且置新的二叉树的根结点的权值为其左、右子树上根结点的权值之和。
(3)在F中删除这两棵树,同时将新得到的二叉树加入F中。
(4)重复(2)和(3),直到F只含一棵树为止。这棵树便是哈夫曼树。


2·如果一棵带权的二元树是最优的,那末它的任何一棵子树对于这棵子树的树叶来说也是最优的。因此,带权最优二元树的问题符合最优化原则。可以用动态归划方法求解。
3·对于这个问题在决策时要考虑到树叶序列的划分方案。


[状态分析]
1·从序列4,4,8,5,4,3,5分析它的二元权的结构。图给出两棵二元树(a)、(b)。

tu29.JPG (21617 bytes)

这棵7片叶叶的二元树,图(a)中的树的左子树是三片叶子,右子树是4片叶子,对应于树叶的一种划分方案。记为(左3,右4)。对应添号形式是:(4+4)+8)+((5+4)+(3+5)))其代价和为91。
图(b)对应于另一种划分方案记为(左4,右3),其代价和是94。可见,由于树叶的划分方案不同,所得的二元树的代价也不同。对七片叶子的二元树,可以有
(左1,右6),(左2,右5),(左3,右4),(左4,右3),(左5,右2),(左6,右1)六种划分方法,对于每一种划分方法都对元一棵二元树,从中找出最优的二元树。


2·决策的前提是最优化原理。对于每一种划分方案,比如(左3,右4),应该知道前三数4,4,8作为树叶构成最优的子二元树是什么,也应知道5,4,3,5作为树叶构成最优二元树的最优子二元树是什么。根据最优化原理,由这棵最优子二元树合成的二元树一定是对于(左3,右4)划分方案最优二元树。从此可见,二元树的构成中每一步的决策不仅与前一步决定有关,而且与若干步的决策有关。二元树的代价计算是一个递推过程。我们选用序列4,4,8,5,4,3,5把它可能构成的各类带权二元树的代价与权的递推计算方法表示如图中。

 

计算方法说明如下,
[1]一片树叶权重为叶片的权,代价为0,得图的第一层。
[2]二片树叶。它的构成状态共有:(4,4),(4,8),(8,5),(5,4),(4,3),(3,5)共六种。括号内数字之和是中间和即为子树根的权。于是生成2片树叶权重。因为1片树叶权为0,故2片的代价与权重一样。
[3]三片树叶,它的构成状态有:(4,4,8),(4,8,5),(8,5,4),(5,4,3),(4,3,5)共计5种。于是生成二片树叶的权。代价计算时应考虑3片树叶的划分状态可能(左1,右2)或(左2,右1),3片树叶的代价应考虑它们代价中的最小者。若是对(4,4,8)进行划分时其状态有:
(1)以(左1,右2)划分时其状态有(4),(4,8),其中(4)的代价为0,(4,8)代价为12。代价和为12=0+12。
(2)以(左2,右1)划分时其状态有(4,4),(8),其中(4,4)的代价为8,8的代价为0。代价和为8。
因为8比12小,3片树叶4,4,8的代价是它的权加上划分状态中代价小的。于是得出第一个代价为16+8=24。余类推。
[4]四片树叶时,不难算出其权重为21,21,20,17。代价计算应考虑前面划分的可能情况。以树叶权重为4,4,8,5为例。它的划分方法有:
(左1,右3),(左2,右2),(左3,右1)三种。
把每一种划分的代价计算如下:
(1)以(左1,右3)划分状态是:(4),(4,4,5),其中(4)的代价为0,(4,4,5)代价为29,0+29=29(代价和)。
(2)以(左2,右2)划分状态是:(4,4),(8,5),其中(4,4)的代价为8,(8,5)代价为13,8+13二21(代价和)。
(3)以(左3,右1)划分状态是:(4,4,8),(5),其中(4,4,8)的代价24,(5)的代价为0,24十0二24(代价和)。
这三种划分状态代价和最小的是21。于是4片中4,4,8,5的权为21。代价为21+21=42。其他代价可仿此计算。
[5]五片树叶时,权重不难算出。代价计算应考虑划分:
(左1,右4),(左2,右3),(左3,右2),(左4,右1)的代价。
当权重为25,树叶是4,4,8,5,4。
划分(左1,右4)的状态是(4),(4,8,5,4),代价和为0+42=42。
划分(左2,右3)的状态是(4,4),(8,5,4),代价和为8+26=34。
因为8,5,4是三片叶子中第三个,从3片树叶的代价中查到26。
划分(左3,右2)的状态是(4,4,8),(5,4)。由图中查得代价和为24+9=33。
划分(左4,右1)的状态是(4,4,8,5),(4),由图中查得代价和为42+0=42。
于是关于权重为25,树叶为4,4,8,5,4的代价为25+33=58。
当权重为24,树叶为4,8,5,4,3。
划分(左1,右4)代价为0+39=39
划分(左2,右3)代价为12+19=31
划分(左3,右2)代价为29+7=36
划分(左4,右1)代价为42+0=42
31是最小代价,权重为24,于是得代价为31+24=55。
当权重为25,树叶为8,5,4,3,5按划分计算可得其代价为57。
其他的计算由读者去完成。
从以上分析,带权二元树的代价计算是一个比较复杂的递推过程。高层的代价计算,必须用本身的划分状态,利用底层的代价进行计算。虽然递推关系比较复杂,对大多数问题来说,动态规划算法的复杂性有可能是多项式级的。动态规划是一个很有实用价值的用途甚广的组合算法。

 

刚开始犯了很多错,

比如先开始是这么写的(一脸蒙蔽):

 

1 for (int i=1;i<=n;i++)
2         for (int j=i+1;j<=n;j++)
3              for (int k=i;k<=j-1;k++)
4                f[i][j]=(f[i][j]>f[i][k]+f[k+1][j]+g[i][j])?f[i][k]+f[k+1][j]+g[i][j]:f[i][j];//未考虑递推关系

 

手动挥手.jpg

 

正解如下:

 1 #include "iostream"
 2 #include "cstdio"
 3 #include "queue"
 4 #define qwq 21000000
 5 int n,m,q;
 6 using namespace std;
 7 int f[1000][1000];
 8 int g[1000][1000];
 9 int gra[1000],minn=-2100000;
10 int main()
11 {
12     int n;
13     cin>>n;
14     for (int i=1;i<=n;i++)
15         cin>>gra[i];
16         
17     for (int i=1;i<=n;i++)
18         g[i][i]=gra[i];            
19          
20     for (int i=1;i<=n;i++)
21         for (int j=i+1;j<=n;j++)
22             g[i][j]=g[i][j-1]+gra[j];
23             
24     for (int i=1;i<=n;i++)
25         for (int j=1;j<=n;j++)
26             f[i][j]=(i!=j)?qwq:0; //i -> j(i)的代价为 +∞(0) 
27             
28     for (int p=2;p<=n;p++)//穷举堆数//因为是递推(Recursion)所以从小堆数向大堆数
29      for (int i=1;i<=(n-p+1);i++)//一段的确定性(Certainty) 列举起点
30      {
31          int j=i+p-1;            //终点(Destination)
32          for (int k=i;k<=j-1;k++)//穷举隔开位置 //注意超界所致的溢出(Overflow)
33              f[i][j]=(f[i][j]>f[i][k]+f[k+1][j]+g[i][j])?f[i][k]+f[k+1][j]+g[i][j]:f[i][j];//三目运算符不清楚的可以百度,下面也有简介
34         }        //正确写出状态转移方程//无后效性 (Unfollow-up Effect)
35     cout<<f[1][n]<<endl;
36 }

动态规划真的是个神奇的东西,

它适合求解多阶段(状态转换)决策问题的最优解,也可用于含有线性或非线性递推关系的最优解问题。

但其实它并不全能,

 

1,最优化原理

2,无后效性

必须满足这两个条件才能用,

当然,

写动态规划最主要的是写出状态转移方程,

其次是边界条件。

附一:

三目运算符普遍的用处就是减少if语句的使用

例如:

 1 i=(括号条件成立与否)?(成立):(不成立); 

 

posted @ 2016-07-12 21:53  RiotAGI  阅读(1435)  评论(0编辑  收藏  举报