DP
文章目录
思想方法:
A.dp是把原问题视作若干重叠子问题进行逐层递进,同时具有无后效性,即已求解的子问题不受后续阶段的影响。
B.最优子结构
C. 状态转移方程
动态规划的两个重要要素是:
1、最优子结构。2、重叠子问题。
1)所谓最优化子结构是说若问题的一个最优解中包含了子问题的最优解,则该问题具有最优子结构。
2)重叠子问题,是指在递归解决方案中,若产生了大量的相同子问题,那么相同的子问题就会被重复计算很多次,这样算法的效率损耗就很大。
模板
\\矩阵连乘
void MatrixChain(int *p,int n,int **m,int **s)
{
for(int i=1;i<=n;i++)
m[i][i]=0;
for(int r=2;r<=n;r++)
{
for(int i=1;i<=n-r+1;i++)
{
int j=i+r-1;
m[i][j]=m[i+1][j]+p[i-1]*p[i]*p[j];
s[i][j]=i;
for(int k=i+1;k<j;k++)
{
int t=m[i][k]+m[k+1][j]+p[i-1]*p[i]*p[j];
if(t<m[i][j])
{
m[i][j]=t;
s[i][j]=k;
}
}
}
}
}
一. 线性动态规划
01背包问题
问题:
有N件物品和一个容量是V的背包。第i件物品的体积是vi,价值是wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包的容量,且价值最大。
输入格式
第一行有两个整数,N,V用空格隔开,分别表示物品数量和背包容积。
接下来有N行,每行两个整数vi,wi,用空格隔开,分别表示第i件物品的体积和价值。
输出格式
输出一个整数,表示最大价值
数据范围
0<N.V<=1000
0<vi,wi<=1000
输入样例
4 5
1 2
2 4
3 4
4 5
输出样例
8
#include<bits/stdc++.h>
using namespace std;
const int maxn =1000;
int main()
{
int N,V;
int dp[maxn],v[maxn],w[maxn];
cin>>N>>V;
for(int i=1;i<=N;i++)
cin>>v[i]>>w[i];
for(int i=1;i<=N;i++)
for(int j=V;j>=v[i];j--)
dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
//初始化时将所有dp[i]都变为0;
cout<<dp[V]<<endl;
return 0;
}
一个序列的最长子序列问题(LIS)
判断性继承思想
(一). n^2做法(易超时)
维护dp数组
求dp数组:
我们可以对于每一个ii,枚举在ii之前的每一个元素jj,然后对于每一个dp[j]dp[j],如果元素ii大于元素jj,那么就可以考虑继承,而最优解的得出则是依靠对于每一个继承而来的dp值,取max.
for(int i=1;i<=n;i++)
{
dp[i]=1;//初始化
for(int j=1;j<i;j++)//枚举i之前的每一个j
if(data[j]<data[i] && dp[i]<dp[j]+1)
//用if判断是否可以拼凑成上升子序列,
//并且判断当前状态是否优于之前枚举
//过的所有状态,如果是,则↓
dp[i]=dp[j]+1;//更新最优状态
}
最后输出dp[n];
(二). nlogn做法
降低时间复杂度- - -二分查找 nlogn
int n;
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i];
f[i]=0x7fffffff;
//初始值要设为INF
/*原因很简单,每遇到一个新的元素时,就跟已经记录的f数组当前所记录的最长
上升子序列的末尾元素相比较:如果小于此元素,那么就不断向前找,直到找到
一个刚好比它大的元素,替换;反之如果大于,么填到末尾元素的下一个q,INF
就是为了方便向后替换啊!*/
}
f[1]=a[1];
int len=1;//通过记录f数组的有效位数,求得个数
/*因为上文中所提到我们有可能要不断向前寻找,
所以可以采用二分查找的策略,这便是将时间复杂
度降成nlogn级别的关键因素。*/
for(int i=2;i<=n;i++)
{
int l=0,r=len,mid;
if(a[i]>f[len])f[++len]=a[i];
//如果刚好大于末尾,暂时向后顺次填充
else
{
while(l<r)
{
mid=(l+r)/2;
if(f[mid]>a[i])r=mid;
//如果仍然小于之前所记录的最小末尾,那么不断
//向前寻找(因为是最长上升子序列,所以f数组必
//然满足单调)
else l=mid+1;
}
f[l]=min(a[i],f[l]);//更新最小末尾
}
}
cout<<len;
ps:来自洛谷题解最长子序列(@皎月半洒花)
(三).n^2的完整做法
#include <iostream>
using namespace std;
const int MAXN = 1000 + 10;
int n, data[MAXN];
int dp[MAXN];
int from[MAXN];
void output(int x)
{
if(!x)return;
output(from[x]);
cout<<data[x]<<" ";
//迭代输出
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++)cin>>data[i];
// DP
for(int i=1;i<=n;i++)
{
dp[i]=1;
from[i]=0;
for(int j=1;j<i;j++)
if(data[j]<data[i] && dp[i]<dp[j]+1)
{
dp[i]=dp[j]+1;
from[i]=j;//逐个记录前驱
}
}
int ans=dp[1], pos=1;
for(int i=1;i<=n;i++)
if(ans<dp[i])
{
ans=dp[i];
pos=i;//由于需要递归输出
//所以要记录最长上升子序列的最后一
//个元素,来不断回溯出路径来
}
cout<<ans<<endl;
output(pos);
return 0;
}
两个序列的最长公共子序列(LCS)
dp[i][j],表示第一个串的前i位,第二个串的前j位的LCS长度。
如果当前的A1[i]A1[i]和A2[j]A2[j]相同(即是有新的公共元素) 那么
dp[ i ] [ j ] = max(dp[ i ] [ j ], dp[ i-1 ] [ j-1 ] + 1);
如果不相同,即无法更新公共元素,考虑继承:
dp[ i ] [ j ] = max(dp[ i-1 ][ j ] , dp[ i ][ j-1 ]);
1.n^2 的朴素算法,不过会被 10^5卡死。
#include<iostream>
using namespace std;
int dp[1001][1001],a1[2001],a2[2001],n,m;
int main()
{
//dp[i][j]表示两个串从头开始,直到第一个串的第i位
//和第二个串的第j位最多有多少个公共子元素
cin>>n>>m;
for(int i=1;i<=n;i++)scanf("%d",&a1[i]);
for(int i=1;i<=m;i++)scanf("%d",&a2[i]);
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
if(a1[i]==a2[j])
dp[i][j]=max(dp[i][j],dp[i-1][j-1]+1);
//因为更新,所以++;
}
cout<<dp[n][m];
}
\\完整做法
void LCSLength(int m,int n,char x[],char y[],int c[][],int b[][])//c[][]用来存储最长子序列的长度
{ //O(mn)
int i,j;
for (i = 1; i <= m; i++)
{
c[i][0] = 0;//当j=0时,c[i][j]=0
}
for (i = 1; i <= n; i++)
{
c[0][i] = 0;//当i=0时,c[i][j]=0
}
for (i = 1; i <= m; i++)
{
for (j = 1; j <= n; j++)
{
if (x[i]==y[j])
{
c[i][j]=c[i-1][j-1]+1;
b[i][j]=1;
}
else if (c[i-1][j]>=c[i][j-1])//Xi-1Yj的最长公共子序列为XiYj的最长公共子序列
{
c[i][j]=c[i-1][j];
b[i][j]=2;
}
else//XiYj-1的最长公共子序列为XiYj的最长公共子序列
{
c[i][j]=c[i][j-1];
b[i][j]=3;
}
}
}
return c[m][n];//最长子序列的长度
}
void LCS(int i,int j,char x[],int b[][])//O(m+n)
{
if (i ==0 || j==0)
{
return;
}
if (b[i][j]== 1)//x[i]==y[j]
{
LCS(i-1,j-1,x[],b[][]);
printf(“%c”,x[i]);
}
else if(b[i][j]== 2 )
{
LCS(i-1,j,x[],b[][]);
}
else
{
LCS(i,j-1,x[],b[][]);
}
}
- nlogn做法,map数组记录A序列的数字在B序列的位置,转化LIS问题。
#include<iostream>
#include<cstdio>
using namespace std;
int a[100001],b[100001],map[100001],f[100001];
int main()
{
int n;
cin>>n;
for(int i=1;i<=n;i++){scanf("%d",&a[i]);map[a[i]]=i;}
for(int i=1;i<=n;i++){scanf("%d",&b[i]);f[i]=0x7fffffff;}
int len=0;
f[0]=0;
for(int i=1;i<=n;i++)
{
int l=0,r=len,mid;
if(map[b[i]]>f[len])f[++len]=map[b[i]];
else
{
while(l<r)
{
mid=(l+r)/2;
if(f[mid]>map[b[i]])r=mid;
else l=mid+1;
}
f[l]=min(map[b[i]],f[l]);
}
}
cout<<len;
return 0;
}
最大子段和
二. 区间 与 环形 动态规划
解决的问题:通过小区间更新大区间,最后找出大区间的最优解。
方法:1.选取区间划分点 k,
例:在Mars星球上,每个Mars人都随身佩带着一串能量项链。在项链上有N颗能量珠。能量珠是一颗有头标记与尾标记的珠子,这些标记对应着某个正整数。并且,对于相邻的两颗珠子,前一颗珠子的尾标记一定等于后一颗珠子的头标记。因为只有这样,通过吸盘(吸盘是Mars人吸收能量的一种器官)的作用,这两颗珠子才能聚合成一颗珠子,同时释放出可以被吸盘吸收的能量。如果前一颗能量珠的头标记为m,尾标记为r,后一颗能量珠的头标记为r,尾标记为n,则聚合后释放的能量为m×r×n(Mars单位),新产生的珠子的头标记为m,尾标记为n。
需要时,Mars人就用吸盘夹住相邻的两颗珠子,通过聚合得到能量,直到项链上只剩下一颗珠子为止。显然,不同的聚合顺序得到的总能量是不同的,请你设计一个聚合顺序,使一串项链释放出的总能量最大。
例如:设N=4,4颗珠子的头标记与尾标记依次为(2,3)(3,5)(5,10)(10,2)。我们用记号⊕表示两颗珠子的聚合操作,(j⊕k)表示第j,k两颗珠子聚合后所释放的能量。则第4、1两颗珠子聚合后释放的能量为:
(4⊕11)=10×2×3=60。
这一串项链可以得到最优值的一个聚合顺序所释放的总能量为:
((44⊕11)⊕22)⊕33)=10×2×3+10×3×5+10×5×10=710。
输入:4
2 3 5 10
输出: 710
//区间动规
//重点就是将整体划分为区间,小区间之间合并获得大区间
//状态转移方程的推导如下
//一、将珠子划分为两个珠子一个区间时,这个区间的能量=左边珠子*右边珠子*右边珠子的下一个珠子
//二、区间包含3个珠子,可以是左边单个珠子的区间+右边两珠子的区间,或者左边两珠子的区间右边+单个珠子的区间
//即,先合并两个珠子的区间,释放能量,加上单个珠子区间的能量(单个珠子没有能量。。)
//Energy=max(两个珠子的区间的能量+单个珠子区间的能量,单个珠子的区间的能量+两个珠子的区间的能量 )
//三、继续推4个珠子的区间,5个珠子的区间。
//于是可以得到方程:Energy=max(不操作的能量,左区间合并后的能量+右区间合并后的能量+两区间合并产生能量)
//两区间合并后产生的能量=左区间第一个珠子*右区间第一个珠子*总区间后面的一个珠子
#include<bits/stdc++.h>
using namespace std;
int n,e[300],s[300][300],maxn=-1;
int main(){
cin>>n;
for(int i=1;i<=n;i++){cin>>e[i];e[i+n]=e[i];}
//珠子由环拆分为链,重复存储一遍
for(int i=2;i<2*n;i++){
for(int j=i-1;i-j<n&&j>=1;j--){//从i开始向前推
for(int k=j;k<i;k++)//k是项链的左右区间的划分点
s[j][i]=max(s[j][i],s[j][k]+s[k+1][i]+e[j]*e[k+1]*e[i+1]);
//状态转移方程:max(原来能量,左区间能量+右区间能量+合并后生成能量)
if(s[j][i]>maxn)maxn=s[j][i];//求最大值
}
}
cout<<maxn;
return 0;
}
//洛谷题解
2.石子合并
3.相邻元素的合并
4.区间首末元素的有关操作
5.流水作业调度(大佬博客)
三. 树与图上的动态规划
树形dp 思想方法:
1.实现形式:dfs,主要实现形式是dp[i][j][0/1],i表示以i为根的子树,j表示在以i为根的子树中选择j个子节点,0/1表示是否选择这个节点,且有时可压掉。
子节点与父节点之间建立状态转移方程:
f[u][
2.基本方程:
选择节点类: dp[i][0]=dp[j][1]
dp[i][1]=max/min(dp[j][0],dp[j][1])
树形背包类:dp[i][k]=dp[j][k]+val
dp[j][k]=max(dp[j][k],dp[i][k-1])
没有上司的舞会.
(入门)
f[x][0]表示以x为根的子树,且x不参加舞会的最大快乐值
f[x][1]表示以x为根的子树,且x参加了舞会的最大快乐值
则f[x][0]=sigma{max(f[y][0],f[y][1])} (y是x的儿子)
f[x][1]=sigma{f[y][0]}+h[x] (y是x的儿子)
先找到唯一的树根root
则ans=max(f[root][0],f[root][1])
#include<bits/stdc++.h>
using namespace std;
const int maxn=60005;
vector<int> son[maxn];
int h[maxn],f[maxn][2],v[maxn];
void dp(int x)
{
f[x][0]=0;
f[x][1]=h[x];
for(int i=0;i<son[x].size();i++)
{
int y=son[x][i];
dp(y);
f[x][0]+=max(f[y][0],f[y][1]);
f[x][1]+=f[y][0];
}
}
int main()
{
int n;
cin>>n;
for(int i=1;i<=n;i++)
cin>>h[i];
for(int i=1;i<=n-1;i++)
{
int x,y;
cin>>x>>y;
son[y].push_back(x);
v[x]=1;
}
int root;
for(int i=1;i<=n;i++)
if(!v[i])
{
root=i;
break;
}
dp(root);
cout<<max(f[root][0],f[root][1])<<endl;
return 0;
}
二叉苹果树
树形动规dfs
以父节点为中转点进行向下dfs
树形背包(一棵树中选多少个结点进行扩展的问题)
法一:
#include <iostream>
#include <cstdio>
#include <vector>
using namespace std;
vector <int> son[109];
int n,q;
int f[109][109],val[109][109],used[109];
void dfs(int x){
used[x]=1;//防止死循环做的标记
for(int i=0;i<son[x].size();i++){
int ny=son[x][i];//x结点的第i个子节点
if(used[ny]==1) continue;//如果标记过则代表这是它的父节点,直接跳过
used[ny]=1;
dfs(ny);//以ny为第二父节点进行遍历
for(int j=q;j>=1;j--){
for(int k=j-1;k>=0;k--){
f[x][j]=max(f[x][j],val[x][ny]+f[ny][k]+f[x][j-k-1]);
//f[ny][k]表示以ny为子节点可以选k条边
//f[x][j-k-1]表示剩余给x结点j-k-1条边,减1指去除父子结点之间的边
//val[x][ny]表示x结点和ny结点之间的苹果数
}
}
}
}
int main(){
scanf("%d%d",&n,&q);
for(int i=1;i<n;i++){
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
val[a][b]=c;//因为不知道关系存的两次价值(但只会用到一个)
val[b][a]=c;
son[a].push_back(b);
son[b].push_back(a);
}
dfs(1);
printf("%d",f[1][q]);
return 0;
}
法二:
#include <iostream>
#include <cstdio>
using namespace std;
struct node
{
int t; //这条边通向的节点
int apple; //第i条树枝的苹果数
int next; //第i条边的下一条边
};
node e[2*101];
int dp[101][101];
int head[101],n,q,tot=0;
void add(int x,int y,int z) //邻接表存数
{
e[++tot].t=y;
e[tot].apple=z;
e[tot].next=head[x];
head[x]=tot;
}
void dfs(int f,int fa,int apple) //递归遍历这棵树
{
int son[101]={0},cnt=0; //son[1]表示f的左儿子在第几条边,son[2]表示f的右儿子在第几条边
bool flag=false;
for(int xun=head[f];xun;xun=e[xun].next)
{
if(e[xun].t!=fa)
{
flag=true;
son[++cnt]=xun;
dfs(e[xun].t,f,e[xun].apple);
}
}
if(!flag) //如果没有儿子,说明它是叶子结点,直接回溯
{
return;
}
for(int i=1;i<=q;i++) //DP部分
{
for(int j=0;j<=i;j++)
{
int t1=0;
if(j-1>=0) t1+=e[son[1]].apple; //j-1>=0表示分配给了左儿子与i节点的一条相连的树枝
if(i-j-1>=0) t1+=e[son[2]].apple;//i-j-1>=0表示分配给了右儿子与i节点的一条相连的树枝
if(j!=0)
dp[f][i]=max(dp[f][i],dp[e[son[1]].t][j-1]+t1+dp[e[son[2]].t][i-j-1]); //j!=0,表示两个儿子都分配了
else //j==0,表示只分配给了右儿子树枝
dp[f][i]=max(dp[f][i],dp[e[son[2]].t][i-j-1]+t1);
}
}
}
int main()
{
scanf("%d %d",&n,&q);
for(int i=1;i<=n-1;i++)
{
int x,y,z;
scanf("%d %d %d",&x,&y,&z);
add(x,y,z);
add(y,x,z);
}
dfs(1,0,0);
printf("%d",dp[1][q]); //因为最终我们要求的苹果数是以1为根节点的子树中保留q根树枝的最大苹果数,所以最终的结果等于dp[1][q]
return 0;
}
图上的动态规划
跑路
分析:
设f[i][j][k] 表示 i到j是否能以2的k次方的距离相互到达。
转移的时候得运用倍增的思想:若两个点能以两端2的k-1次方的距离相互到达,那么两个点就能以2的k次方的距离相互到。
接下来用Floyd
#include<bits/stdc++.h>
using namespace std;
int dis[60][60],n,m;
bool G[60][60][65];
/*以上是变量说明部分,dis[i][j]表示i到j的路径/边的长度
G[i][j][k]表示,i到j是否存在一条长度为2^k的路径
如果有,为true,没有就是false*/
void init()
{
memset(G,false,sizeof(G));
memset(dis,10,sizeof(dis));
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
int x,y;
scanf("%d%d",&x,&y);
dis[x][y]=1;
G[x][y][0]=true;
/*初始化,x到y的路径(边)最短是1,也就是x到y存在
一条长度为2^0的路径(边)*/
}
}
void work()//此函数对G和dis做预处理
{
for(int k=1;k<=64;k++)
//对于本题的数据,2^64已经足够。
for(int i=1;i<=n;i++)
for(int t=1;t<=n;t++)
for(int j=1;j<=n;j++)
//枚举三个点
if(G[i][t][k-1]&&G[t][j][k-1])
/*如果i到t存在一条2^k-1长度的路径
并且t到j存在一条2^k-1长度的路径
就说明i到t,t到j都可以一秒到达,
路程*2刚好是2的幂,也可以一秒到达*/
{
G[i][j][k]=true;
//标记从i到j存在一条长度为2^k的路径
dis[i][j]=1;
//i到j距离可以一秒到达
}
}
void floyd()
{
for(int k=1;k<=n;k++)
//这里的注意点:枚举中间点的循环放在最前面
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);
//松弛操作。
}//Floyd图论求最短路。
int main()
{
init();
work();
floyd();
printf("%d",dis[1][n]);
return 0;
}
最优二叉搜索树
最优二叉搜索树(optimal binary search tree)问题。其形式化定义如下:给定一个n个不同关键字已排序的序列K=<k1,k2,…,kn>(因此k1<k2<…<kn),我们希望用这些关键字构造一棵二叉搜索树。对每个关键字ki,都有一个概率pi表示其搜索频率。有些要搜索的值可能不在K中,因此我们还与n+1个“伪关键字”d0,d1,d2…dn表示不在K中的值。d0表示所有小于k1的值,dn表示所有大于kn的值,对i=1,2,…,n-1,伪关键字di表示所有在ki和ki+1之间的值。对每个伪关键字di,也都有一个概率pi表示对应的搜索频率。
算法复杂度: 时间复杂度 O(n^3)
空间复杂度 O(n^2)
1)分析:尽可能使根节点最大,如果T(1,n)是最优二叉搜索树,则左子树T(1,k-1)和右子树T(k+1,n)也是最优二叉搜索树。
2)建立最优递归式
建立新树,使得左右子树的所有结点深度增加了1,实结点搜索成本:(搜索深度+1)*搜索概率p,虚结点:(深度)*搜索概率q
若 j=i-1 ,e[i][j]=q[i-1];
若 j>=i, e[i][j]=min{e[i][k-1]+e[k+1][j]+w(i,j)}
e[i,j]的值给出了最优二叉搜索树的期望搜索代价。为了记录最优二叉搜索树的结构,对于包含关键字k(i),…,k(j)(1<=i<=j<=n)的最优二叉搜索树,我们定义root[i,j]保存根结点k®的下标r。
对于计算w(i,j),为提高搜索效率,使用一个表w[1,……n]来记录w(i,j)
对基本情况,令w[i,i-1]=q(i-1)(1<=i<=n+1)。对j>=i的情况,可如下计算:
w[i,j]=w[i,j-1]+p(j)+q(j)
注意到所求的最优二叉搜索树和矩阵连乘相似,都是连续的下标域进行划分。我们用一个表e[1…n+1,0…n]来保存e[i,j]的值,第一维下标上界为n+1而不是n,原因在于对于只包含伪关键字d(n)的子树,我们需要计算并保存e[n+1,n]。第二维下标下界为0,是因为对于只包含伪关键字d0的子树,我们需要计算并保存e[1,0]。
#include<iostream>
using namespace std;
#define DBL_MAX 1111111;
const int n=5;
double OPTIMAL_BST(double *p,double *q,int n,double **e,double **w,int **root)
{
for(int i=1;i<=n+1;i++)
{
e[i][i-1]=q[i-1];
w[i][i-1]=q[i-1];
}
for(int l=1;l<=n;l++) /* 类似于矩阵链乘法 */
{
for(int i=1;i<=n-l+1;i++)
{
int j=i+l-1;
e[i][j]=DBL_MAX ;
w[i][j]=w[i][j-1]+p[j]+q[j];
root[i][j]=i;
for(int r=i;r<=j;r++)
{
double t=e[i][r-1]+e[r+1][j]+w[i][j];
if(t<e[i][j])
{
e[i][j]=t;
root[i][j]=r;
//cout<<root[i][j]<<"\t"<<i<<" ==============="<<j<<endl;
}
}
}
}
return e[1][n];
}
void CONSTRUCT_OPTIAML_BST(int **root,int i,int j)
{
if(i==1 && j==n)printf("K%d为根\n",root[i][j]);
if(i < j)
{
printf("K%d是K%d的左孩子\n",root[i][root[i][j]-1],root[i][j]);
CONSTRUCT_OPTIAML_BST(root,i,root[i][j]-1);
if(root[i][j]+1 < j) /* 注意这里的细节,不然会出现root[5][4]和root[6][5] */
printf("K%d是K%d的右孩子\n",root[root[i][j]+1][j],root[i][j]);
/* else cout<<root[i][j]+1<<"\t"<<j<<endl; */
CONSTRUCT_OPTIAML_BST(root,root[i][j]+1,j);
}
if(i == j)
{
printf("d%d是K%d的左孩子\n",i-1,i);
printf("d%d是K%d的右孩子\n",i,i);
}
if(i > j) printf("d%d是K%d的右孩子\n",j,j);
}
int main()
{
double p[6]={0,0.15,0.10,0.05,0.10,0.20};
double q[6]={0.05 ,0.10 ,0.05 ,0.05 ,0.05 ,0.10};
/* 动态申请 */
/* 下标范围:e[1..n+1,0..n] w[1..n+1,0..n] root[1..n,1..n] */
double **e=new double *[n+2];
for(int i=0;i<=n+1;i++) e[i]=new double[n+1];
double **w=new double *[n+2];
for(int i=0;i<=n+1;i++) w[i]=new double[n+1];
int **root=new int *[n+1];
for(int i=0;i<=n;i++) root[i]=new int[n+1];
/* 计算表e和root */
cout<<"最优期望搜索代价:"<<OPTIMAL_BST(p,q,n,e,w,root)<<endl;
/* 打印root */
CONSTRUCT_OPTIAML_BST(root,1,n);
/* 回收内存 */
for (int i = 0; i < n+2; i++)
{
delete e[i],delete w[i];
e[i] = NULL,w[i] = NULL;
if(i!=n+1)
{
delete root[i];
root[i] = NULL;
}
}
delete []e,delete[]w,delete []root;
e = NULL,w=NULL,root=NULL;
return 0;
}