算法:动态规划(一)

一、数字三角形

在上面的数字三角形中寻找一条从顶部到底边的路径,使得路径上所经过的数字之和最大。路径上的每一步都只能往左下或右下走。只需要求出这个最大和即可,不必给出具体路径。(三角形的行数大于1小于等于100,数字为0-99)

输入格式:

5  //三角形行数。下面是三角形

7

38

810

2744

45265

要求输出最大和

解题思路:

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

D(r,j):第r行第j个数字(r,j从1开始算)

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

问题:求MaxSum(1,1)

典型的递归问题。

D(r,j)出发,下一步只能走D(r+1,j)或者D(r,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 <iostream>//程序超时,复杂度太大
#include <algorithm>
#define MAX 101
using 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,)<<endl;
}

上面程序的时间复杂度为2n,对于n=100肯定超时。(2的100次方)

改进:如果每算出一个MaxSum(r,j)就保存起来,下次用到其值的时候就直接取用,则可以免去重复计算。那么可以用O(n2)时间计算。因为三角形的数字总数是n*(n+1)/2。

#include <iostream>
#include <algorithm>
using namespace std;
#define MAX 101
int 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(i=1;i<=n;i++)
    for(j=1;j<=i;j++)
    {
      cin>>D[i][j];
      maxSum[i][j]=-1;
    }
  cout<<MaxSum(1,1)<<endl;
}

用一个二维数组存放计算过的数,先将二维数组初始化-1,然后开始存放计算过的数,如果算过那么就不是-1,则不用再次计算。

递归转成递推

从最底层的最大和开始一层层的往上推

#include <iostream>
#include <algorithm>
using namespace std;
#define MAX 101
int D[MAX][MAX];
int n;
int maxSum[MAX][MAX];
int main()
{
  int i,j;
  cin>>n;
  for(i=i;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][j];
  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数组存储每一个MaxSum(r,j),只要从底层一行行向上递推,那么只要一维数组maxSum[100]即可,即只要存储一行的MaxSum值就可以。

计算是只用算下方和右下方即可,当算出一个数后,下一个数字的左下就没有用,就可以存放上次算出来的最大值数字。

进一步考虑,maxSum数字也不要,直接用D的第n行代替maxSum即可,节省空间,时间复杂度不变。

#include<ioatream>
#include<algorithm>
using namespace std;
#define MAX 101
int 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]<<ednl;
}

二、动态规划解体一般思路

递归到动规的一般转化方法

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

动规解题的一般思路

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

把原问题分解为若干个子问题,子问题和原问题形式相同或类似,只不过规模变小了。子问题都解决,原问题即解决。子问题的解一旦求出就会被保存,所以每个子问题只需求解一次。

2、确定状态

在用动态规划解题时,我们往往将和子问题相关的各个变量的一组取值,称之为一个“状态”。一个“状态”对应于一个或多个子问题,所谓某个“状态”下的“值”,就是这个“状态”所对应的子问题的解。

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

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

4、确定状态转移方程

定义出什么是“状态”,以及在该“状态”下的“值”后,就要找出不同的状态之间如何迁移——即如何从一个或多个“值”已知的“状态”,求出另一个“状态”的“值”。状态的迁移可以用递推公式表示,此递推公式也可被称作“状态转移方程”。

能用动态规划解决的问题的特点

1、问题具有最优子结构性质。如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质。

2、无后效性。当前的若干个状态值一旦确定,则此后过程的演变就只和这若干个状态的值有关,和之前是采取哪种手段或经过哪条路径演变到当前的这若干个状态无关。

三、最长上升子序列

问题描述

输入数据:

输入的第一行是序列的长度N(1<=N<=1000)。第二行给出序列中的N个整数,这些整数的取值范围都在0到1000.

输出要求:

最长上升子序列的长度

输入样例:

7

1 7 3 5 9 4 8 

输出样例:

4

#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、递推型

效率高,有可能使用滚动数组节省空间。

例题:最长公共子序列

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

输入两个串:S1,S2

设MaxLen(i.j)表示:S1的左边i个字符形成的子串,与S2左边的j个字符形成的子串的最长公共子序列的长度(i,j从0开始算)  MaxLen(i,J)就是本题的“状态” 。假定len1=strlen(S1),len2=strlen(S2),那么题目要求就是 MaxLen(len1,len2)。边界条件:MaxLen(n,0)=0;MaxLen(0,n)=0;

递推公式:

if(S1[i-1]==S2[j-1])
  MaxLen(i,j)=MaxLen(i-1,j-1)+1;
else
  MaxLen(i,j)=max(MaxLen(i,j-1),MaxLen(i-1,j));
//时间复杂度O(m*n)m n是两个字符串长度

S1[i=1]!=S2[j-1]时,MaxLen(S1,S2)不会比MaxLen(S1,S2j-1)和MaxLen(S1i-1,S2)两者之中任何一个小,也不会比两者都大。

#include<iostream>
#include<cstring>
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
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值