动态规划例题

作者: Aniwer | 来源:发表于2020-12-08 15:29 被阅读0次

    最优二分搜索树

    二分搜索树

    1. 是一棵空树
    2. 具有下列性质的二叉树
      • 若左子树不空,则左子树上所有结点的值均小于它的根结点的值
      • 若右子树不空,则右子树上所有结点的值均大于它的根结点的值
      • 左、右子树分别为二叉搜索树

    查询操作

    MEMBER(x,S):若x在S中则返回”yes”,否则返回”no”
    设有n个实数a_1<a_2<…<a_n构成集合S
    指令MEMBER(x,S)中的x可能是某个a_i,也可能不在S中。把不在S中的数按区间分为n+1类,以b_0b_1,…,b_n作为每一类数的代表(虚节点)

    定义

    p_i为MEMBER (a_i,S)出现的频率(i=1,2,…,n)
    q_j为MEMBER (b_j,S)出现的频率(j=0,1,2,…,n)
    \sum_{i=1}^np_i+\sum_{j=0}^nq_j=1
    定义一棵二分搜索树的总耗费:\sum_{i=1}^np_i(depth(a_i)+1)+\sum_{j=0}^nq_j(depth(b_j))
    最优二分搜索树即耗费最小的二分搜索树

    分析

    • T_0是最优二分搜索树,它的根是a_k(第k小的数)
      则ak的左子树中必然包含了{a_1a_{k-1}},ak的右子树中必然包含了{a_{k+1}a_n}。
    • 考察一棵树接到另一个结点之下构成一棵新树时耗费的增加
      设有一棵由结点b_ia_{i+1}b_{i+1},…,a_jb_j构成的树
      按定义该树的耗费为\sum_{l=i+1}^jp_l(depth(a_l)+1)+\sum_{l=i}^jq_l(depth(b_l))
      把这棵树接到到另一个结点之下构成一棵新树时,这棵子树中的每个结点的深度在新树中均
      增加了1,则该子树在新树中的耗费增加了\sum_{l=i+1}^jp_l+\sum_{l=i}^jq_l=q_i+(p_{i+1}+q_{i+1})+...+(p_j+q_j)=w_{ij}
    • 任何一颗子树中结点的编号都是连续的,而且,最优树中的任何一棵子树,也必然
      是关于子树中结点的最优树,因此最优二分搜索树具有最优子结构性质
    • 若规模为m≤n-1的最优子树均已知,就可以通过逐一计算以a_1a_2,…,a_n为根的树的耗费来确定(使耗费达到最小的)根a_k并找出最优二分搜索树,在上述计算中,规模较小( m≤n-1 )的最优子树在计算中要多次被用到,因此,该问题具有高度重复性
    • T_{ij}:有一棵由结点b_ia_{i+1}b_{i+1},…,a_jb_j构成的耗费最小的树
      树以a_k作为根(i+1≤k≤j)
      b_ia_{i+1}b_{i+1},…,a_{k-1}b_{k-1}在左子树
      b_ka_{k+1}b_{k+1},…,a_jb_j在右子树
      这样的树中耗费最小的必然是以T_{i,k-1}为其左子树,以T_{kj}为其右子树
      c_{ij}是最优子树T_{ij}的耗费
    • T_{ij}的总耗费由三个部分组成
      左子树的耗费:c_{i,k-1}+w_{i,k-1}
      右子树的耗费:c_{kj}+w_{kj}
      根的耗费:p_k
      总耗费=c_{i,k-1}+w_{i,k-1}+c_{k,j}+w_{k,j}$$c_{k,j}+w_{k,j}=c_{i,k-1}+c_{kj}+w_{ij}
    • 已知{p_i}(i=1,2,…,n),{q_i}(j=0,1,2,…,n)
      若已知w_{i,j-1},则根据w_{ij}=w_{i,j-1}+{p_j}+{q_j}的定义计算出w_{ij}
      所以,当c_{i,k-1}c_{kj}已知,以a_k为根的树的最小总耗费能在O(1)时间内计算出来
    • 综上,分别计算以a_{i+1},a_{i+2},...,a_j为根,含有结点b_ia_{i+1}b_{i+1},…,a_jb_j的树的总耗费,从中选出耗费最小的树,即为最优子树
      c_{ij}=\min_{i<k≤j}{c_{i,k-1}+c_{kj}+w_{ij}}
    • 三层循环,Θ(n^3)

    优化

    • 可以证明:若最小耗费树T_{i,j-1}T_{i+1,j}的根分别为a_pa_q,则必有⑴p≤q;⑵最小耗费树T_{ij}的根a_k满足p≤k≤q
    • 因此,求\min_{i<k≤j}{c_{i,k-1}+c_{kj}+w_{ij}}时,无需在a_{i+1}~a_j间一一尝试,只需要从a_p和~a_q找一个根即可

    流水作业调度

    问题

    n个作业,m项任务,m台机器
    设有n个任务,每一个作业i均被分解为m项任务:T_{i1},T_{i2},T_{im}(1≤i≤n,故共有n×m个任务),要把这些任务安排到m台机器上进行加工
    需要满足条件:

    1. 每个作业i的第j项任务T_{ij}(1≤i≤n, 1≤j≤m) 只能安排在机器P_j上进行加工
    2. 作业i的第j项任务T_{ij}(1≤i≤n, 2≤j≤m)的开始加工时间均安排在第j-1项任务T_{i,j-1}加工完毕之后
    3. 任何一台机器在任何一个时刻最多只能承担一项任务
      设任务T_{ij}在机器P_j上进行加工需要的时间t_{ij},如果所有t_{ij}(1≤i≤n, 1≤j≤m)均已给出,要找出一种安排任务的方法,使得完成这n个作业的加工时间为最少,这个安排称之为最优流水作业调度
      流水作业调度一般均指的是非优先调度,即任何任务一旦开始加工,就不允许被中断,直到该任务被完成

    分析

    • 当机器数m≥3时,流水作业调度问题是一个NP-hard问题。当m=2时,该问题可有多项式时间的算法。
    • t_{i1}a_i(作业i在P_1上加工所需时间),t_{i2}b_i(作业i在P_2上加工所需时间)
    • 当机器P_1空闲时,则任何一个作业的第一个任务都可以立即在P_1上执行
    • 必有一个最优调度使得在P_1上的加工是无间断的
    • 必有一个最优调度使得在P_2上的加工空闲时间(从0时刻起算)为最小,同时还满足在P_1上的加工是无间断的
    • 若在P_2上的加工次序与在P_1上的加工次序不同,则只可能增加加工时间。所以仅需要考虑在P_1P_2上加工次序完全相同的调度
    • 最优调度具有如下性质:
      在所确定的最优调度的排列中去掉第一个执行作业后,剩下的作业排列仍然还是一个最优调度,即该问题具有最优子结构的性质
      在计算规模为n的作业集合的最优调度时,该作业集合的子集合的最优调度会被多次用到,即该问题亦具有高度重复性

    求解

    • N={1,2,┅,n}是全部作业的集合,作业集S是N的子集合即有S⊆N
    • 对机器P_2需等待t个时间单位以后才可以用于S中的作业加工(t也可以为0即无须等
      待)
    • 记g(S,t)为在此情况下完成S中全部作业的最短时间
      g(S,t)=min_{i∈S}{a_i+ g(S-{i},b_i+max{t-a_i,0})}
    • 当S=N即全部作业开始加工时,t=0
    • g(N,0)=min_{1≤i≤n}{a_i+ g(N-{i},b_i)}
    • 该算法的时间复杂度为指数量级,因为算法中对N的每一个非空子集都要进行一次计算,而N的非空子集共有2^n-1个,因此不能直接使用动态规划方法来求解该问题

    进一步求解

    • Johnson不等式:min{a_j,b_i} ≥ min{a_i,b_j}
    • 当min{a_i,a_j,b_i,b_j}为a_i或者b_j时,Johnson不等式成立,此时把i排在前j排在后的调度用时较少;反之,若min{a_i,a_j,b_i,b_j}为a_j或者b_i时, 则j排在前i排在后的调度用时较少
    • 推广:当min{a_1,a_2,┅,a_n,b_1,b_2,┅,b_n}=a_k时,则对任何i≠k,都有min{a_i,b_k} ≥ min{a_k, b_i}成立,故此时应将作业k安排在最前面,作为最优调度的第一个执行的作业;当min{a_1,a_2,┅,a_n,b_1,b_2,┅,b_n}=b_k时,则对任何i≠k,都有min{a_k,b_i} ≥ min{a_i, b_k}成立,故此时应将作业k安排在最后面,作为最优调度的最后一个执行的作业
    • n个作业中首先开工(或最后开工)的作业确定之后,对剩下的n-1个作业采用相同方法可再确定其中的一个作业,应作为n-1个作业中最先或最后执行的作业;反复使用这个方法直到最后只剩一个作业为止,即可确定最优调度
    • 为O(nlgn)

    总结

    • 满足1)高度重复性 2)最优子结构性质时,一般采用动态规划法,但偶尔也可能得不到高效的算法
    • 若问题本身不是NP-hard问题,进一步分析后就有可能获得效率较高的算法
    • 若问题本身就是NP-hard问题,与其它的精确算法相比,动态规划法性能一般不算太坏, 但有时需要对动态规划法作进一步的加工

    备忘录LCS

    备忘录方法

    • 当某个问题可以用动态规划法求解,但二维数组中有相当一部分元素在整个计算中都不会被用到
    • 因此,不需要以递推方式逐个计算二维数组中元素,而采用备忘录方法:数组中的元素只是在需要计算时才去计算,计算采用递归方式,值计算出来之后将其保存起来以备它用
    • 若有大量的子问题无需求解时,用备忘录方法较省时

    求解

    • x_i=y_j时,求C[i,j]只需知道C[i-1,j-1],而无需用到C[i,0]~C[i,j-1]及C[i-1,j]~C[i-1,n],所以只需求出一个LCS时,可能有一些C[p,q]在整个求解过程中都不会用到
    • 将C[i,0]与C[0,j] 初始化为0,其余m×n个C[i,j]全部初始化为-1
    • 若x[i]=y[j],则去检查C[i-1,j-1]:若C[i-1,j-1]> -1(已经计算出来),就直接把C[i-1,j-1]+1赋 给C[i,j],返回;若C[i-1,j-1]=-1(尚未计算出来),就递归调用LCS(X,Y,i-1,j-1,C) 计算出C[i-1,j-1],然后再把C[i-1,j-1]+1赋给C[i,j],返回
    • 若x[i]不等于y[j],则检查C[i-1,j]和C[i,j-1]:若两者均 > -1(已经计算出来),则把max{ C[i-1,j], C[i,j-1]} 赋给C[i,j],返回;若C[i-1,j], C[i,j-1] 两者中有一个等于-1(尚未计算出来), 或两者均等于-1,就递归调用LCS将其计算出来,然后 再把max{ C[i-1,j], C[i,j-1]} 赋给C[i,j]

    最长递增子序列问题(LIS)

    问题

    假设A =< a_1, 𝑎_2, … , 𝑎_𝑛 >为由n个不同的实数组成的序列
    递增子序列𝐿 =< a_{𝑘_1}, a_{𝑘_2}, … , a_{𝑘_m}>
    其中𝑘_1< 𝑘_2<…< 𝑘_𝑚并且a_{𝑘_1} < a_{𝑘_2} < … < a_{𝑘_m}
    求最大的𝑚值

    最大子段和问题

    问题

    n个整数序列a_1a_n,求该序列形如\sum_{k=i}^ja_k的子段和的最大值
    当所有整数均为负整数时定义其最大子段和为0

    分治解法

    如果将所给的序列a[1..n]分为长度相等的两段a[1:n/2]和a[n/2+1:n],分别求出这两段最大子段和,则a[1..n]的最大子段和有三种情形:

    1. a[1:n]的最大子段和与a[1:n/2]的最大子段和相同;
    2. a[1:n]的最大子段和与a[n/2+1:n]的最大子段和相同;
    3. a[1:n]的最大子段和为\sum_{k=i}^ja_k,1≤ i ≤ n/2,n/2+1 ≤ j ≤ n
      对于1、2可以递归求解
      对于3:
      a[n/2]、a[n/2+1]在最优子序列中
      可以在a[1:n/2]中计算出s_1=max_{1≤ i ≤ n/2}\sum_{k=i}^ja_k
      可以在a[n/2+1:n]中计算出s_2=max_{n/2+1 ≤ i ≤ n}\sum_{k=n/2+1}^ia_k
      s_1+s_2即为最优

    代码

    void MaxSubSum(int *a,int left,int right)
    {
        int sum=0;
        if (left==right) sum=a[left]>0?a[left]:0;
        else{ 
            int center=(left+right)/2;
            int leftsum=MaxSubSum(a,left,center);
            int rightsum=MaxSubSum(a,center+1,right);
            int s1=0; int lefts=0;
            for(int i=center;i>=left;i--){ 
                lefts+=a[i]; 
                if (lefts>s1) s1=lefts;
            }
            int s2=0; int rights=0;
            for (int i=center+1; i<=right; i++){
                rights+=a[i]; 
                if (rights>s2) s2=rights;
            }
            sum=s1+s2;
            if (sum<leftsum) sum=leftsum;
            if (sum<rightsum) sum=rightsum;
        }
        return sum;
    }
    

    分析

    算法所需的计算时间T(n)满足典型的分治算法递归式:

    T(n)=\left\{ \begin{aligned} O(1) && n≤c \\ 2T(n/2)+O(n) && n>c \\ \end{aligned} \right.

    基于主方法和主定理,T(n)=O(nlogn)

    动态规划解法

    记b[j]=max_{1≤i<j}\sum_{k=i}^ja_k,1≤j≤n,则
    max_{1≤i≤j≤n}\sum_{k=i}^ja[k]=max_{1≤j≤n}max_{1≤i≤j}\sum_{k=i}^ja[k]=max_{1≤j≤n}b[j]
    因此,当b[j-1]>0,b[j]=b[j-1]+a[j];否则,b[j]=a[j]。得出
    b[j]=max_{1≤j≤n}{b[j-1]+a[j],a[j]}

    代码

    int MaxSum(int n,int *a)
    {
        int sum=0, b=0;//初始化最大子段和为0,b[0]=0
        for (int i = 1; i <= n; i++){
            if (b>0) b+=a[i];
            else b=a[i];
            if (b>sum) sum=b;//更新当前找到的最大子段和
        }
        return sum;
    }
    

    分析

    O(n)

    相关文章

      网友评论

        本文标题:动态规划例题

        本文链接:https://www.haomeiwen.com/subject/nptqgktx.html