POJ 3176 Cow Bowling(动态规划DP 经典)

时间:2021-07-10 18:45:20

原题

Cow Bowling

Time Limit: 1000MS Memory Limit: 65536K


Description

The cows don't use actual bowling balls when they go bowling. They each take a number (in the range 0..99), though, and line up in a standard bowling-pin-like triangle like this:

     7



    3 8



   8 1 0



  2 7 4 4



4 5 2 6 5

Then the other cows traverse the triangle starting from its tip and moving "down" to one of the two diagonally adjacent cows until the "bottom" row is reached. The cow's score is the sum of the numbers of the cows visited along the way. The cow with the highest score wins that frame.

Given a triangle with N (1 <= N <= 350) rows, determine the highest possible sum achievable.

Input

Line 1: A single integer, N

Lines 2..N+1: Line i+1 contains i space-separated integers that represent row i of the triangle.

Output

Line 1: The largest sum achievable using the traversal rules

Sample Input


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

Sample Output


30

Hint

Explanation of the sample:

     7

    *

    3 8

   *

   8 1 0

    *

  2 7 4 4

    *

4 5 2 6 5

The highest score is achievable by traversing the cows as shown above.


题意


数字三角形问题。有一个由非负整数组成的三角形,第一行只有一个数,除了最下行之外每个数的左下方和右下方各有一个数,从第一行的数开始,每次可以往左下或右下走一格,直到走到最下行,把沿途经过的数全部加起来。如何走才能使得这个和尽量大?


涉及知识及算法


动态规划dp。

动态规划是一种用途很广的问题求解方法,它本身并不是一个特定的算法,而是一种思想,一种手段。动态规划的核心是状态和状态转移方程。

为了得到高效的算法,需要用抽象的方法思考问题:把当前的位置 ( i, j ) 看成一个状态 ,然后定义状态 ( i, j ) 的指标函数 d( i, j ) 为从格子 ( i, j ) 出发时能得到的最大和 (包括格子 ( i, j ) 本身的值)。在这个状态定义下,原问题的解是 d (1, 1) 。 下面看看不同状态之间是如何转移的。从格子 ( i, j ) 出发有两种决策。如果往左走,则走 到 ( i + 1, j ) 后需要求 “ 从 ( i + 1, j ) 出发后能得到的最大和 ” 这一问题,即 d ( i + 1, j ) 。类似地,往 右走之后需要求解 d ( i + 1 , j + 1) 。由于可以在这两个决策中*选择,所以应选择 d ( i + 1, j ) 和 d (i + 1, j + 1) 中较大的一个。换句话说,得到了所谓的状态转移方程:

d(i,j)=a(i,j)+max(d(i+1,j),d(i+1,j+1))

如果往左走,那么最好情况等于 ( i, j ) 格子里的值 a ( i, j ) 与 “ 从 ( i + 1, j ) 出发的最大总和 ” 之 和,此时需注意这里的 “ 最大 ” 二字。如果连 “ 从 ( i + 1, j ) 出发走到底部 ” 这部分的和都不是最大 的,加上 a ( i, j ) 之后肯定也不是最大的。这个性质称为最优子结构( optimal substructure ), 也可以描述成 “ 全局最优解包含局部最优解 ” 。不管怎样,状态和状态转移方程一起完整地描 述了具体的算法。

代码

方法1:递推计算

#include <iostream>
#include <algorithm>
#include <cstring>
#include <cstdio>
using namespace std;
const int MAX_N =355;
int d[MAX_N][MAX_N],a[MAX_N][MAX_N];
int n;
int solve()
{
for(int i=1;i<=n;i++)
{
d[n][i]=a[n][i];
}
for(int i=n-1;i>=1;i--)
{
for(int j=1;j<=i;j++)
{
d[i][j]=a[i][j]+max(d[i+1][j],d[i+1][j+1]);
}
}
return d[1][1];
}

int main()
{
scanf("%d",&n);
memset(d,0,sizeof(d));
memset(a,0,sizeof(a));
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
if(i>=j)
{
scanf("%d",&a[i][j]);
}
}
}
printf("%d\n",solve());
return 0;
}
程序的时间复杂度显然是 O ( n ^2 ) ,但为什么可以这样计算呢?原因在于: i 是 逆序枚举的,因此在计算 d[i][j] 前,它所需要的 d[i + 1][j] 和 d[i + 1][j + 1] 一定已经计算出来了。

方法2:记忆化搜素

程序分成两部分。首先用 “memset(d, - 1,sizeof(d));” 把 d 全部初始化 为- 1 ,然后编写递归函数

#include <iostream>
#include <algorithm>
#include <cstring>
#include <cstdio>
using namespace std;
const int MAX_N =355;
int d[MAX_N][MAX_N],a[MAX_N][MAX_N];
int n;
int solve(int i, int j)
{
if(d[i][j] >= 0)
{
return d[i][j];
}
return d[i][j] = a[i][j]+(i == n ? 0 : max(solve(i+1,j),solve(i+1,j+1)));
}
int main()
{
scanf("%d",&n);
memset(d,-1,sizeof(d));
memset(a,0,sizeof(a));
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
if(i>=j)
{
scanf("%d",&a[i][j]);
}
}
}
printf("%d\n",solve(1,1));
return 0;
}
上述程序依然是递归的,但同时也把计算结果保存在数组 d 中。题目中说各个数都是非 负的,因此如果已经计算过某个 d[i][j] ,则它应是非负的。这样,只需把所有 d 初始化为- 1 ,即可通过判断是否 d[i][j]≥0 得知它是否已经被计算过。上述程序的方法称为记忆化( memoization ),它虽然不像递推法那样显式地指明了计算 顺序,但仍然可以保证每个结点只访问一次。 由于 i 和 j 都在 1 ~ n 之间,所有不相同的结点一共只有 O ( n^ 2 ) 个。无论以怎样的顺序访问, 时间复杂度均为 O ( n^2 ) 。从 2 n ~ n 2 是一个巨大的优化,这正是利用了数字三角形具有大量重叠 子问题的特点。 可以用记忆化搜索的方法计算状 态转移方程。当采用记忆化搜索时,不必事先确 定各状态的计算顺序,但需要记录每个状态 “ 是 否已经计算过 ”。

注:本篇文章大部分内容引用自 刘汝佳《算法竞赛入门经典》(第二版)