动态规划

来源:互联网 发布:像素设计软件 编辑:程序博客网 时间:2024/06/15 10:15

例题一、数字三角形

在上面的数字三角形中寻找一条从顶部到底部的路径,使得路径上所经过的数字之和最大。路径上的每一步都只能往左下或者右下走,只需要求出最大的和,不必给出具体路径

结题思路:

用二维数组存放数字三角形

D(r,j):第r行第j个数字

MaxSum(r,j):从D(r,j)到底边的个路径中最佳路径的数字之和

问题:求MaxSum(1,1)

典型的递归问题

D(r,j)出发。下一步只能走D(r+1,j)或者D(r+1,j+1).故对于N行的三角形

if(r==N)

MaxSum(r,j)=D(r,j)

else

MaxSum(r,j)=Max{ MaxSum(r+1,j),MaxSum(r+1,j+1)}+D(r,j);

#include<stdio.h>#include<algorithm>#define MAX 101using namespace std;int D[MAX][MAX];int n;int MaxSum(int i,int j){    if(i==n)        return D[i][j];    int x=MaxSum(i+1,j);    int y=MaxSum(i+1,j+1);    return max(x,y)+D[i][j];}int main(){    int i,j;    cin>>n;    for(i=1;i<=n;i++)        for(j=1;j<=i;j++)            cin>>D[i][j];    cout<<MaxSum(1,1)<<endl;}

不过这个会超时,因为很多步都重复计算了

改进:记忆型

没算出一个MaxSum(i,j)就保存起来,下次用到的时候就直接取

#include<stdio.h>#include<algorithm>using namespace std;#define MAX 101int D[MAX][MAX];int n;int maxSum[MAX][MAX];int MaxSum(int i,int j){    if(maxSum[i][j]!=-1)        return maxSum[i][j];    if(i==n)        maxSum[i][j]=D[i][j];    else    {        int x=MaxSum(i+1,j);        int y=MaxSum(i+1,j+1);        maxSum[i][j]=max(x,y)+D[i][j];    }    return maxSum[i][j];}int main(){    int i,j;    cin>>n;    for(int i=1;i<=n;i++)        for(int j=1;j<=i;j++)    {        cin>>D[i][j];        maxSum[i][j]=-1;    }    cout<<MaxSum(1,1)<<endl;}

递归转化成递推

//人人为我型递推型动归#include<iostream>#include<algorithm>using namespace std;#define MAX 101int D[MAX][MAX];int n;int maxSum[MAX][MAX];int main(){    int i,j;    cin>>n;    for(i=1;i<=n;i++)        for(j=1;j<=i;j++)            cin>>D[i][j];    for(int i=1;i<=n;i++)        maxSum[n][i]=D[n][i];    for(int i=n-1;i>=1;i--)        for(int j=1;j<=i;j++)            maxSum[i][j]=max(maxSum[i+1][j],maxSum[i+1][j+1])+D[i][j];    cout<<maxSum[1][1]<<endl;}
再次基础上还可以进行空间的优化,只需一维数组maxSum【100】,就可以存储每一行的数据

//人人为我型递推型动归#include<iostream>#include<algorithm>using namespace std;#define MAX 101int D[MAX][MAX];int n;int* maxSum;int main(){    int i,j;    cin>>n;    for(i=1;i<=n;i++)        for(j=1;j<=i;j++)            cin>>D[i][j];    maxSum=D[n];//maxSum指向第n行    for(int i=n-1;i>=1;i--)        for(int j=1;j<=i;j++)            maxSum[j]=max(maxSum[j],maxSum[j+1])+D[i][j];    cout<<maxSum[1]<<endl;    return 0;}
递归到动归的一般转化方法

*递归函数有n个参数,就定义一个n维的数组,数组的下标是递归函数参数的取值范围,数组元素的值是递归函数的返回值,这样就可以从边界值开始,逐步填充数组,相当于计算递归函数值的逆过程

动规结题的一般思路

1.将原问题分解为子问题

*把原问题分解为若干个子问题,子问题和原问题形式相同或类似,只不过规模变小了,子问题都解决,原问题即解决

*子问题的解一旦求出就会被保存,所以每个子问题只需要求解一次

2.确定状态

*在用动态规划结题是,我们往往将和子问题相关的各个变量的一组取值,称之为一个“状态”,一个“状态下的“值”,就是这个状态对应的子问题的解

3.确定一些初始状态(边界状态)的值

以“数字三角形”为例,初始状态就是底边数字,值就是底边数字值

4.确定状态转移方程

定义出什么是“状态”,以及在该“状态”下的“值”后,就要找出不同的状态之前如何迁移

例题二:最长上升子序列

问题描述

一个数的序列ai,当a 1 < a 2 < ... < a S 的时候,我们称这个序列是上升的。对于给定的一个序列(a 1 , a 2 , ..., a N ),我们可以得到一些上升的子序列(a i1 , a i2 , ..., a iK ),这里1 <= i1 <i2 < ... < iK <= N。比如,对于序列(1, 7, 3, 5, 9, 4, 8),有它的一些上升子序列,如(1, 7), (3, 4, 8)等等。这些子序列中最长的长度是4,比如子序列(1, 3, 5, 8).
你的任务,就是对于给定的序列,求出最长上升子序列的长度。

//"人人为我"递推型动归程序#include<iostream>#include<cstring>#include<algorithm>using namespace std;const int MAXN=1010;int a[MAXN];int maxLen[MAXN];int main(){    int N;    cin>>N;    for(int i=1;i<=N;i++)    {        cin>>a[i];        maxLen[i]=1;    }    for(int i=2;i<=N;i++)    {        for(int j=1;j<i;j++)        {            if(a[i]>a[j])                maxLen[i]=max(maxLen[i],maxLen[j]+1);        }    }    cout<<*max_element(maxLen+1,maxLen+N+1);    return 0;}
动归的三种形式

1)记忆递归型

优点:只经过有用的状态,没有浪费

缺点:可能会因递归层数太深而爆栈,比递推慢

2)“我为人人”递推型

比较符合思考的习惯

3)“人人为我”递推型

在选取最优备选状态的值Fm,Fn,,时,有可能有好的算法或数据结构可以用来显著降低时间复杂度


例三、最长公共子序列

给出两个字符串,求出这样的一个最长的公共子序列的长度:子序列中的每个字符都能在两个原串中找到,而且每个字符的先后顺序和原串中的先后顺序一致。

//"人人为我"递推型动归程序#include<iostream>#include<cstring>#include<algorithm>using namespace std;char sz1[1000];char sz2[1000];int maxLen[1000][1000];int main(){    while(cin>>sz1>>sz2)    {        int length1=strlen(sz1);        int length2=strlen(sz2);        int nTmp;        int i,j;        for(i=0;i<=length1;i++)            maxLen[i][0]=0;        for(j=0;j<=length2;j++)            maxLen[0][j]=0;        for(i=1;i<=length1;i++)        {            for(j=1;j<=length2;j++)            {                if(sz1[i-1]==sz2[j-1])                    maxLen[i][j]=maxLen[i-1][j-1]+1;                else                    maxLen[i][j]=max(maxLen[i][j-1],maxLen[i-1][j]);            }        }        cout<<maxLen[length1][length2]<<endl;    }    return 0;}

例四、最佳加法表达式

有一个由1...9组成的数字串,问如果将m个加号插入到这个数字串中,在各种可能形成的表达式中,值最小的那个表达式的值是多少

结题思路:

假定数字串长度是n,添玩加号后,表达式的最后一个加号添在第i个数字后面,那么整个表达式的最小值,就等于在前i个数字中插入m-1个加号所能形成的最小值,加上第i+1到n个数字所组成的数的值

设V(m,n)表示在n个数字中插入m个加号所能形成的最小值,那么:

if m=0

V(m,n)=n个数字构成的整数

else if  n<m+1

V(m,n)=~

else

V(m,n)=Min{ V(m-1,i) +Num(i+1,n) 

//递归#include<stdio.h>#include<string.h>#include<stdlib.h>#include<algorithm>using namespace std;int V[10][10];int Num(int i,int n){    int l=n-i+1;    int sum=0,d=1;    for(int i=1;i<=l;i++)    {        sum+=d*n;        d=d*10;        n--;    }    return sum;}int ans(int m,int n){    if(V[m][n]!=-1)        return V[m][n];    int s=1<<30;    for(int i=m;i<n;i++)        s=min(s,ans(m-1,i)+Num(i+1,n));    V[m][n]=s;    return s;}int main(){    int m;    while(~scanf("%d",&m))    {        memset(V,-1,sizeof V);        for(int i=1;i<=9;i++)            V[0][i]=Num(1,i);        for(int i=1;i<=m;i++)            for(int j=0;j<i+1;j++)            V[i][j]=1<<30;        printf("%d\n",ans(m,9));    }}

//递推#include<iostream>#include<cstdio>#include<cstring>#include<algorithm>using namespace std;const int INF=0x3f3f3f3f;const int N=1005;int a[N],num[N][N],dp[N][N];int main(){    int n,m;    while(~scanf("%d %d",&n,&m))    {        for(int i=1;i<=n;i++)            scanf("%d",&a[i]);                for(int i=1;i<=n;i++)        {            num[i][i]=a[i];            for(int j=i+1;j<=n;j++)            {                num[i][j]=num[i][j-1]*10+a[j];            }        }        memset(dp,0,sizeof dp);        for(int i=1;i<=n;i++)            dp[0][i]=num[1][i];          for(int i=1;i<=m;i++)            for(int j=i;j<=n;j++)                for(int k=i;k<=j;k++)                    dp[i][j]=min(dp[i][j],dp[i-1][k]+num[k+1][j]);        cout<<dp[m][n]<<endl;    }    return 0;}

例五、神奇的口袋

 有一个神奇的口袋,总的容积是40,用这个口袋可以变出一些物品,这些物品的总体积必须是40。

John现在有n(1≤n ≤ 20)个想要得到的物品,每个物品的体积分别是a1,a2……an。John可以从这些物品中选择一些,如果选出的物体的总体积是40,那么利用这个神奇的口袋,John就可以得到这些物品。现在的问题是,John有多少种不同的选择物品的方式。

//动归解法#include<iostream>#include<string.h>using namespace std;int a[30];int N;int Ways[40][30]; //Ways[i][j]表示从前j种物品里凑出体积i的方法数int main(){    cin>>N;    memset(Ways,0,sizeof Ways);    for(int i=1;i<=N;i++)    {        cin>>a[i];        Ways[0][i]=1;    }    Ways[0][0]=1;    for(int w=1;w<=40;w++)    {        for(int k=1;k<=N;k++)        {            Ways[w][k]=Ways[w][k-1];            if(w-a[k]>=0)                Ways[w][k]+=Ways[w-a[k]][k-1];        }    }    cout<<Ways[40][N];    return 0;}//“我为人人”型递推解法//定义一维数组 int sum【41】;依次放入物品,计算每次放入物品可达的容积,并在相应空间设置记录,最后判断sum[40]是否可达,到达了几次#define MAX 41int main(){    int n,i,j,input;    int sum[MAX];    for(i=0;i<MAX;i++)        sum[i]=0;    cin>>n;    for(i=0;i<n;i++)    {        cin>>input;        for(j=40;j>=1;j--)            if(sum[j]>0&&j+input<=40)                sum[j+input]+=sum[j];        sum[input]++;    }    cout<<sum[40]<<endl;    return 0;}

例六、0-1背包问题

有N件物品和一个容积为M的背包。第i件物品的体积w[i],价值是d[i]。求解将哪些物品装入背包可使价值总和最大。每种物品只有一件,可以选择放或者不放(N<=3500,M <= 13000)。

用F[i][j]表示取前i种物品,使它们总体积不超过j的最优取法取得的价值总和。要求F[N][M]

边界:

if(w[1]<=j)

F[1][j]=d[1];

else

F[1][j]=0;

用F[i][j]表示取前i种物品,使得它们总体积不超过j的最优解取法取得的价值总和

递推: F[i][j] = max(F[i-1][j],F[i-1][j-w[i]]+d[i])  取或者不取第i种物品

本题如用记忆型递归,需要一个很大的二维数组,会超内存。注意到这个二维数组的下一行的值,只用到了上一行的正上方及左边的值,因此可用滚动数组的思想,只要一行即可。即可以用一维数组,用“人人为我”递推型动归实现。

//"人人为我"递推型动归程序#include<iostream>#include<cstring>#include<algorithm>#include<stdio.h>using namespace std;int main(){    int f[10000];   //f[i]表示重量为j时最大的价值    int n,C;    scanf("%d %d",&n,&C);    memset(f,0,sizeof f);    int V,W;    for(int i=1;i<=n;i++)    {        scanf("%d %d",&V,&W);        for(int j=C;j>=0;j--)        {            if(j>=V)                f[j]=max(f[j],f[j-V]+W);        }    }    printf("%d\n",f[C]);}






0 0
原创粉丝点击