算法设计课程总结3(DP动态规划)

来源:互联网 发布:centos设置无线网络 编辑:程序博客网 时间:2024/05/17 04:30

[3]动态规划

动态规划思想和分治类似,也是将问题分解成多个子问题,通过求解子问题来获取原问题的解。
用动态规划求解问题应满足的性质:①最优子结构性质、②重叠子问题性质。

①重叠子问题性质:每次产生的子问题并不总是新问题,有些子问题会重复出现多次,可以把它们保存下来。
②最优子结构性质:如果一个解是最优解,那么这个解所包含的子问题也一定是最优的。

动态规划一般可以用这两种方式实现:

①自底向上:所有子问题只算一次,打备忘录,没有递归代价
②自顶向下:递归,可能重复计算,但也可以改进

矩阵连乘问题

一串合法的矩阵序列的标准乘积A1A2…An,因为加括号的方式不同,所需要的乘法的次数是不一样的。
一个mxn的矩阵和一个nxk的矩阵的标准乘积,所需的乘法次数是mnk,给定一串序列,现在就要找出使乘法次数最少的加括号的方式。
用打表的方式,假设A[i:j](从Ai到Aj)所需的最少乘法次数是m[i][j]则要求的就是m[1][n]。可以看到如果从i到j的最优解中,包含的所有子问题一定也是最优的,否则用更优的代替,则会产生原最优解不是最优的矛盾。
所以可以满足这样的递推条件:
这里写图片描述

for(int i=0;i<n;i++){    m[i][i]=0;//对角线上的元素表示子问题长度为1的情况}for(int r=2;r<=n;r++)//子问题的长度r从2到n    for(int i=0;i<n-r+1;i++)//i是该长度下每个子问题的起始处    {        int j=i+r-1;//j是该长度下对应起始处i的子问题终止处        m[i][j]=m[i+1][j]+p[i]*p[i+1]*p[j+1];//初始化为i分离出来,后面从i+1到j,这样的子问题        s[i][j]=i;//对应的断点是i        for(int k=i+1;k<j;k++)//对于后面每个可能的断点k        {            int t=m[i][k]+m[k+1][j]+p[i]*p[k+1]*p[j+1];//t是以k这里为断点时的乘法次数            if(t<m[i][j])//如果这个值比当前找到的m[i][j]要小            {                m[i][j]=t;//那么就更新一下这个值                s[i][j]=k;//同时还要更新断点为k处            }        }    }

这里写图片描述

这个问题还可以用自顶向下的递归算法,但是因为重叠子问题性质,为了避免重复计算,可以用备忘录的方法进行改进。不妨先对整个备忘录矩阵有效的部分初始化为0,当计算中发现要求的值大于0时,说明已经计算过,直接取用而不需要重复计算了。

int LookupChain(int i,int j){    if(m[i][j]>0)//当发现大于初始化时的0时        return m[i][j];//直接取用,否则要计算    if(i==j)//自己到自己的乘法次数是0        return 0;    //初始化为i分离出来,后面从i+1到j,这样的子问题    int u=LookupChain(i,i)+LookupChain(i+1,j)+p[i-1]*p[i]*p[j];    s[i][j]=i;//对应的断点是i    for(int k=i+1;k<j;k++)//对于后面每个可能的断点k    {        int t=LoopupChain(i,k)+LookupChain(k+1,j)+p[i-1]*p[k]*p[j];//计算对应的解的值        if(t<u)//如果比当前的更小        {            u=t;//更新解的值            s[i][j]=k;//更新对应的断点        }    }    m[i][j]=u;//找出以后,把这个值存进去,即写入备忘录    return u;}

最长公共子序列问题

最长公共子序列也是一个动态规划问题,最长公共子序列是具有最优子结构性质的。因为对于两个序列X(m)和Y(n),如果最后一个元素Xm=Yn是最长公共子序列的最后一个元素Zk,那么Z(k-1)显然就是X(m-1)和Y(n-1)的最长公共子序列:
这里写图片描述
如果最后一个元素不相等,并且Zk不等于其中的某一个元素,那么可以将那个元素所在的串去掉它,而Z(k)还是这两个序列的最长公共子序列:
这里写图片描述
因此可以设立一个矩阵c,用打表的方法,c[i][j]表示序列X(i)和Y(j)的最长公共子序列的长度,显然当i是0或者j是0时,这个值都是0,这可以作为打表的基准。

然后对矩阵进行遍历,如果x[i]和y[j]相等,那么也就是说对应的子序列就只要由X(i-1)和Y(j-1)的子序列补上这个相等的元素,也就是c[i-1][j-1]+1即是这个更大的问题的解:
这里写图片描述
如果不相等时候,c[i][j]就只会是c[i-1][j]和c[i][j-1]两者之中较大的那个:
这里写图片描述
这里写图片描述
因为它们都是X(i)和Y(j)对应的序列的最接近的子问题,而不需要像之前分析的那样去判断究竟是和x[i]不等还是和y[j]不等了。因此该问题的递归关系:
这里写图片描述

#include<iostream>#include<cstdio>#include<vector>using namespace std;int n,a,b,count=0;char *x,*y;int main(){    scanf("%d",&n);    while(n--)    {        scanf("%d%d",&a,&b);        x=new char[2*a];        y=new char[2*b];        //c[i][j]表示x串前i+1个和y串前j+1个构成的子问题的最长公共子序列        vector<vector<int> > c(a+1,vector<int>(b+1));        getchar();//吃掉\n        for(int i=0;i<2*a;i++)            scanf("%c",&x[i]);        for(int j=0;j<2*b;j++)            scanf("%c",&y[j]);        for(int i=0;i<a;i++)        {            x[i+1]=x[2*i];            //cout<<x[i];        }        //cout<<endl;        for(int j=0;j<b;j++)        {            y[j+1]=y[2*j];            //cout<<y[j];        }        //break;        //动态规划部分        for(int i=0;i<=a;i++)            c[i][0]=0;//构造的条件,因为至少有一方是没有字符的        for(int i=0;i<=b;i++)            c[0][i]=0;//构造的条件,因为至少有一方是没有字符的        for(int i=1;i<=a;i++)            for(int j=1;j<=b;j++)            {                //从i-1,j-1开始,延长两个试试                if(x[i]==y[j])//字符一样,说明最长公共子序列要被加长                    c[i][j]=c[i-1][j-1]+1;                //不行的话,每次只延长其中一个串,看哪个更长                else if(c[i-1][j]>=c[i][j-1])                    c[i][j]=c[i-1][j];//长度就是更长的那个子问题                else                    c[i][j]=c[i][j-1];            }        cout<<"Case "<<++count<<endl;        cout<<c[a][b]<<endl;//原来那个问题的规模是a长度和b长度(串原长)        delete[] x;        delete[] y;    }return 0;}

最大子段和问题

该问题是在给定的有正有负的整数序列中,找出连续的一段(这就是子段和子序列的不同),使其和最大。在这个问题中,可以记:
这里写图片描述
即以j位置结尾的子段和中最大的值,则所求的最大子段和是b[j]中j从1到n任取时最大的一个。加法加到正数会变大,加到负数会变小,所以每个b[j]都满足这样的动态规划递归式:
这里写图片描述
至于要找出最大的一个,只需要做个记录,每次找到b[j]后和记录比较更新。

int MaxSum(int n,int *a){    int sum=0,b=0;//因为只要找最大的,不需要记录每个b[j],只用一个b    for(int i=1;i<=n;i++)//对于从1~n的每个b[j]    {        if(b>0)//这个b还是上一层的b[j-1],如果是正的            b=a[i]+b;//那么这一层的b[j]就加上它        else//如果不是正的            b=a[i];//这一层的b就只是a[i]自己        if(b>sum)//如果这一层的b[j]比之前找到的更大            sum=b;//那么更新之    }return sum;}

最大子矩阵和问题

最大矩阵和问题就是二维的最大子段和问题,是在一个矩阵中找到一个子矩阵,使其中的元素和最大:
这里写图片描述
如图中所示,可以每次固定i和j这两条线,而数组b[k]是从a[i][k]加到a[j][k]这一竖条,这样横向地去看,就是对数组b求最大子段和的问题了,可以直接调用最大子段和问题的函数。而纵向地,每次i变动时把所有b[k]清零(因为j和i马上重合),而j变动时(即j向下走了一行),把每个b[k]加上新的这一行a[j][k]即可,可以避免重复计算。
也就是说在外面套了两层循环i和j,这两个问题本质是一样的问题。

最大m子段和问题

最大子矩阵和问题是最大子段和问题在维度上的推广,而最大m子段和问题是最大子段和问题在子段个数上的推广。给定n个整数组成的序列,求其中的m个不相交子段,使得m个子段的总和达到最大。
b(i,j)表示数组a前j项中i个子段和的最大值,且第i个子段包含a[j](i从1到m,j从i到n,j如果小于i那么前j项中不可能找出i个子段),满足以下的递推式:
这里写图片描述
在max中,前一半的意思是第i个子段含有a[j-1]的情况:
这里写图片描述
这时候干脆把a[j]拿出来,在前j-1项中仍然是去找这i个子段和的最大值(针对第i个子段的最优子结构性质)。

后一半的意思是第i个子段仅仅含有a[j]的情况(不含有a[j-1]也就只能含有a[j],因为子段必须连起来):
这里写图片描述
这时候仍然可以把a[j]拿出来,但是因为在这种情况下缺少了a[j-1]的支撑,第i个子段已经没有了,子问题是搜寻i-1个子段和的最大值,而搜寻的合法范围显然是从i-1位置到j-1位置,即图中的右边界t在这个范围内找最大。

凸多边形最优三角剖分问题

给定凸多边形P={v0,v1,…,vn}是一个n+1边形,要让其三角剖分中的弦上权之和最小:
这里写图片描述
这个问题满足最优子结构性质,如图中已知最优三角剖分包含这个红色的三角形,则分出的两个子多边形的剖分也一定是最优的,否则会出现原多边形的剖分不是最优的矛盾:
这里写图片描述
定义t[i][j]是凸子多边形{vi-1,vi,…,vj}的最优三角剖分值,如下图中蓝色部分为t[3][6]所对应的凸子多边形:
这里写图片描述
显然所求的凸8边形的最优三角剖分值是t[1][7],所求凸n+1边形的最优三角剖分值就是t[1][n]。定义退化的两顶点多边形t[i][i]=0,则对于t[i][j]当j>i时候,从顶点i-1到顶点j的凸子多边形的最优三角剖分值,和矩阵连乘问题一样,就是去找一个断点k,使得t[i][k]+t[k-1][j]+三角形vi-1vkvj的权值整个最小:
这里写图片描述
因此递推式为(和矩阵连乘的很像):
这里写图片描述

流水作业调度的Johnson算法

流水作业调度是给定n个作业,每个作业都要现在M1上加工完成再到M2上加工,M1和M2加工作业i所需的时间分别是ai和bi,目标是使得从第一个作业在机器M1上开始加工到最后一个作业在M2上加工完成所需时间最少的作业投放次序。

①先找两个集合N1={i|ai<bi即在M1上时间短的作业},N2={i|ai>=bi即在M2上时间短的作业}
②对N1关于M1非减序排序,对N2关于M2非增序排序
③有序集N1和有序集N2合并

如下面这个例子:
这里写图片描述
最终调度的顺序是J4,J1,J3,J2,J5,J6。

0-1背包问题

0-1背包问题是一个带约束的整数规划问题,给定n种物品,每个物品i质量为wi,价值为vi,背包容量为c,要找出选择放入的序列,使得背包中总价值最大。也就是说,目标是:
这里写图片描述
约束是:
这里写图片描述
这个问题具有最优子结构性质:如果一堆物品是最优解,将这堆物品中去掉一个物品,背包容量减去这个物品质量,这样剩下的物品也是所有物品中去掉这个物品后对于这个更小的背包的最优解,否则如果有一个更优的解,则用这个更优的解加上那个去掉的物品,得到的解比原来那一堆物品的解更优,产生了矛盾。
这里写图片描述
假设m(i,j)是剩余背包重量为j,可选择物品从i到n时的0-1背包问题的最优解值,可以知道0-1背包问题的递归式如下:
这里写图片描述
自底向上,第一次处理的是在背包中只放第n个物品:
这里写图片描述
这时候装得下就是vn,装不下就是0。

#include<iostream>#include<cstdio>using namespace std;int n,c;int* va;//价值int* we;//重量int** m;//用于打表int* x;//用于回溯int count=0;//求最小int min(int a,int b){    return a<b?a:b;}//求最大int max(int a,int b){    return a>b?a:b;}//动态规划求解过程void Knapsack(){    /*初始化(只有最后一个物品的子问题)*/    int jMax=min(we[n]-1,c);//小于等于这个值容量的背包肯定放不下第n个物体    for(int j=0;j<=jMax;j++)        m[n][j]=0;//因此它装入的价值是0    for(int j=we[n];j<=c;j++)//对于比那个容量更大的背包         m[n][j]=va[n];//肯定装得下第n个物体了,在这个子问题中把它装进来就是最优    /*动态规划(从后往前考察)*/    for(int i=n-1;i>1;i--)//对于之前的每一个子问题(先不考虑原问题)    {        jMax=min(we[i]-1,c);//小于等于这个值容量的背包肯定放不下第i个物体        for(int j=0;j<=jMax;j++)            m[i][j]=m[i+1][j];//因此它装入的价值就是不放i(从i+1开始)的子问题的值        for(int j=we[i];j<=c;j++)            m[i][j]=max(m[i+1][j],m[i+1][j-we[i]]+va[i]);//装得下时取装和不装的最大    }    /*对原问题单独做,是为了不做那些没必要的m[1][不足c]计算,这已经是最前面的了*/    if(c>=we[1])//如果能装下第一个物品        m[1][c]=max(m[2][c],m[2][c-we[1]]+va[1]);//那么还是取装和不装时候的最大    else//否则        m[1][c]=m[2][c];//就是不装第1个物品的那个子问题的值}//回溯过程void Traceback(){    //对于前n-1个    for(int i=1;i<n;i++)//对于每个起始的背包下标i        if(m[i][c]==m[i+1][c])//如果在c大的背包下该问题和从i+1起始的问题值一样            x[i]=0;//说明这个物体没装        else//否则        {            x[i]=1;//这个物体肯定装了(所以才变了)            c-=we[i];//那么从背包里减掉它        }    //最后第n个物体    x[n]=(0!=m[n][c])?1:0;//如果第n个物体在剩余的c中装得下,那就装了}int main(){    while(cin>>n>>c)    {        //打表空间        m=new int*[n+1];        for(int i=0;i<n+1;i++)            m[i]=new int[c+1];        //value空间        va=new int[n+1];        //weight空间        we=new int[n+1];        //回溯空间        x=new int[n+1];        //输入        for(int i=1;i<n+1;i++)            cin>>va[i];        for(int i=1;i<n+1;i++)            cin>>we[i];        cout<<"Case "<<++count<<endl;        //特判        if(1==n)        {            if(we[1]<=c)                cout<<va[1]<<" "<<1<<endl;            else                cout<<0<<" "<<0<<endl;            goto BYE;        }        //动态规划打表        Knapsack();        //输出        cout<<m[1][c]<<" ";        //回溯(注意回溯完c就改变了)        Traceback();        //输出        for(int i=1;i<n+1;i++)            cout<<x[i];        cout<<endl;        BYE:        //释放空间        for(int i=0;i<n+1;i++)            delete[] m[i];        delete[] m;        delete[] x;        delete[] va;        delete[] we;    }return 0;}

这里写图片描述

原创粉丝点击