[动态规划]数字三角形、最长上升子序列

来源:互联网 发布:linux的socket 编辑:程序博客网 时间:2024/06/12 22:10

POJ 1163 数字三角形

7
3 8
8 1 0
2 7 4 4
4 5 2 6 5

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

解法:这道题用递归做是解法很显然的:

递归  设f(i,j) 为三角形上从点(i,j)出发向下走的最长路经, 则f(i,j) = max(f(i+1,j), f(i+1,j+1))+d(i,j)  要输出的就是f(0,0)即从最上面一点出发的最长路径。

但是用递归做会超时,原因是每次向下求解路径时,都会重复走以前走过的路经,也就是存在重复计算。深度遍历每条路径,存在大量重复计算,则时间复杂度为2n,对于 n = 100,肯定超时。

这时候就要介绍我们的动态规划算法了。


动态规划简介

动态规划是求解包含重叠子问题的最优化方法

  • 基本思想:将原问题分解为相似的子问题,在求解 的过程中通过保存子问题的解求出原问题的解(注意:不是简单分而治之)。
  • 只能应用于有最优子结构的问题(即局部最优解能 决定全局最优解,或问题能分解成子问题来求解)。 1+1=2?
  • 经典组合优化问题的基础,如最短路径、背包问题、 项目管理、网络流优化等。

对于上述这道题,一种可能的改进思想是:从下往上计算,对于每 一点,只需要保留从下面来的路径中和最大的 路径的和即可。

#define MAX_NUM 100int N;int D[MAX_NUM + 10][MAX_NUM + 10];int Sum[MAX_NUM + 10][MAX_NUM + 10];// 1void solution1() {  for (int i = 1; i <= N; i++) {    Sum[N][i] = D[N][i];  }  for (int i = N; i > 1; i--) {    for (int j = 1; j < i; j++) {      if (Sum[i][j] > Sum[i][j + 1])        Sum[i - 1][j] = Sum[i][j] + D[i - 1][j];      else        Sum[i - 1][j] = Sum[i][j + 1] + D[i - 1][j];    }  }  cout << endl;  printf("%d\n", Sum[1][1]);  return;}int main() {  cin >> N;  for (int i = 1; i <= N; i++) {    for (int j = 1; j <= i; j++)      cin >> D[i][j];  }  solution1();  return 0;}

这样就避免了子问题的大量重复计算。另外还可以从空间上优化程序:
没必要用二维Sum数组存储每一个Sum(r,j),只要从底层一行行向上递推,那么只要一维数组Sum[100]即可,即只要存储一行的Sum值 就可以。


递归到动态规划的转化

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


动态规划解题步骤

  1. 将原问题分解为子问题:把原问题分解为若干个子问题,子问题和原问题形式相同 或类似,只不过规模变小了。子问题都解决,原问题即解决(数字三角形例)。子问题的解一旦求出就会被保存,所以每个子问题只需求 解一次。
  2. 确定状态:在用动态规划解题时,我们往往将和子问题相关的各个变量的一组取值,称之为一个“状态”。一个“状态”对应于一个或多个子问题, 所谓某个“状态”下的“值”,就是这个“状 态”所对应的子问题的解。
  3. 确定一些初始状态(边界状态)的值:以“数字三角形”为例,初始状态就是底边数字,值就是底边数字值。
  4. 确定状态转移方程:定义出什么是“状态”,以及在该 “状态”下的“值”后,就要 找出不同的状态之间如何迁移――即如何从一个或多个“值”已知的 “状态”,求出另一个“状态”的“值”(“人人为我”递推型)。状 态的迁移可以用递推公式表示,此递推公式也可被称作“状态转移方程”。数字三角形的状态转移方程:
    这里写图片描述

让我们再看第二个栗子:

例题:最长上升子序列:

一个数的序列bi,当b1 < b2 < … < bS的时候,我 们称这个序列是上升的。对于给定的一个序列(a1, a2, …, aN),我们可以得到一些上升的子序列(ai1, ai2, …, aiK),这里1 <= i1 < i2 < … < iK <= N
比如,对于序列(1, 7, 3, 5, 9, 4, 8),有它的一些上升 子序列,如(1, 7), (3, 4, 8)等等。这些子序列中最长的长度是4,比如子序列(1, 3, 5, 8)或(1, 3, 4, 8).

  1. 找子问题:以ak为终点的序列可以作为子问题。
  2. 状态:子问题只与数字—变量的位置有关,即可将位置k看作是状态,k位置对应的即是以ak为终点的序列的最短序列长度。
  3. 状态转移方程:maxLen (k)表示以ak做为“终点”的 最长上升子序列的长度那么:

初始状态:maxLen (1) = 1
maxLen (k) = max { maxLen (i):1<=i < k 且 ai < ak且 k≠1 } + 1
若找不到这样的i,则maxLen(k) = 1 maxLen(k)的值,就是在ak左边,“终点”数值小于ak ,且长度最大的那个上升子序列的长度再加1。因为ak左边任何“终点”小于 ak的子序列,加上ak后就能形成一个更长的上升子序列。

有了思路程序就水到渠成了:

#define MAX_NUM 1010int a[MAX_NUM];int MaxLen[MAX_NUM];int main(int argc, char const *argv[]) {  int i, j, N;  cin >> N;  for (i = 1; i <= N; i++) {    cin >> a[i];    MaxLen[i] = 1;  }  for (i = 2; i <= N; i++) //每次求以第i个数为终点的最长上升子序列长度    for (j = 1; j < i; j++) { //求以第j个数为终点的最长上升子序列长度      if (a[j] < a[i])        MaxLen[i] = max(MaxLen[i], MaxLen[j] + 1);    }  cout << *max_element(MaxLen + 1, MaxLen + N + 1);  return 0;}
0 0
原创粉丝点击