算法学习: 动态规划1

动态规划

引入例题: 数字三角形

问题背景:
在数字三角形钟寻找一条从顶部到底部的路径,使得路径上经过的数字之和最大。

常用作法: 递归型动归
#include<iostream>
#include<algorithm>
#define MAX 101
using namespace std;
int D[MAX][MAX];
int maxSum[MAX][MAX];
int n;
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;
    }

此时存在的主要问题: 函数的调用需要大量的资源开销

改进: 递归转化为递推计算:

uCfsv6.png

上图中右侧的矩阵为MaxSum 数组,记录了三角形中相应的数字到底边的最大,其中最后一行即为原始数据。此时的循环计算顺序为从上到下。

#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 =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,只需要一个一维数组。

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

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

动规解题的一般思路

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

    • 把原问题分解为若干个子问题,子问题和原问题形式相同或类似,只不过规模变小了。子问题都解决,原问题即解决(数字三角形例)。
    • 子问题的解一旦求出就会被保存,所以每个子问题只需求解一次。
  2. 确定状态
    所有“状态”的集合,构成问题的“状态空间”。“状态空间”的大小,与用动态规划解决问题的时间复杂度直接相关。在数字三角形的例子里,一共有 N × ( N + 1 ) / 2 N×(N+1)/2 N×(N+1)/2个数字,所以这个问题的状态空间里一共就有 N × ( N + 1 ) / 2 N×(N+1)/2 N×(N+1)/2个状态。整个问题的时间复杂度是状态数目乘以计算每个状态所需时间。在数字三角形里每个“状态”只需要经过一次,且在每个状态上作计算所花的时间都是和N无关的常数。

  3. 确定一些初始状态(边界状态)的值
    以“数字三角形”为例,初始状态就是底边数字,值就是底边数字值。

  4. 确定状态转移方程

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

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

  1. 问题具有最优子结构性质。如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质。
  2. 无后效性。当前的若干个状态值一旦确定,则此后过程的演变就只和这若干个状态的值有关,和之前是采取哪种手段或经过哪条路径演变到当前的这若干个状态,没有关系。

一些例题

最长公共子序列

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

Sample Input:
abcfbc abfcab
programming contest

Sample Output:
4
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));

[外链图片转存失败(img-pQtkpbJA-1569314524146)(/home/siyuan/programming-and-algorithm/4.算法基础/week4/pic/MaxLen.png)]

由上图可以推断, 当S1[i-1] != S2[j-1]时,MaxLen(S1, S2) 不会比MaxLen(S1, S2_j-1)和MaxLen(S1_i-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);
        cout<<length2<<endl;
        int nTemp;
        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;
}

例题二: Help Jimmy

uF2zX6.png

场景中包括多个长度和高度各不相同的平台。地面是最低的平台,高度为零,长度无限。
Jimmy老鼠在时刻0从高于所有平台的某处开始下落,它的下落速度始终为1米/秒。当Jimmy落到某个平台上
时,游戏者选择让它向左还是向右跑,它跑动的速度也是1米/秒。当Jimmy跑到平台的边缘时,开始继续下落。Jimmy每次下落的高度不能超过MAX米,不然就会摔死,游戏也会结束。
设计一个程序,计算Jimmy到地面时可能的最早时间。

输入数据
第一行是测试数据的组数t(0 <= t <= 20)。每组测试数据的第一行是四个整数N,X,Y,MAX,用空格分隔。N是平台的数目(不包括地面),X和Y是Jimmy开始下落的位置的横竖坐标,MAX是一次下落的最大高度。接下来的N行每行描述一个平台,包括三个整数,X1[i],X2[i]和H[i]。H[i]表示平台的高度,X1[i]和X2[i]表示平台左右端点的横坐标。1 <= N <= 1000,-20000<= X, X1[i], X2[i] <= 20000,0 < H[i] < Y <= 20000(i = 1…N)。所有坐标的单位都是米。
Jimmy的大小和平台的厚度均忽略不计。如果Jimmy恰好落在某个平台的边缘,被视为落在平台上。所有的平台均不重叠或相连。测试数据保Jimmy一定能安全到达地面。

输出要求
对输入的每组测试数据,输出一个整数,
Jimmy到地面时可能的最早时间。
输入样例
1
3 8 17 20
0 10 8
0 10 13
4 14 3
输出样例
23

解题思路

Jimmy跳到一块板上后,可以有两种选择,向左走,或向右走。走到左端和走到右端所需的时间,是很容易算的。
如果我们能知道,以左端为起点到达地面的最短时间,和以右端为起点到达地面的最短时间,那么向左走还是向右走,就很容选择了。因此,整个问题就被分解成两个子问题,即Jimmy所在位置下方第一块板左端为起点到地面的最短时间,和右端为起点到地面的最短时间。这两个子问题在形式上和原问题是完全一致的。将板子从上到下从1开始进行无重复的编号(越高的板子编号越小,高度相同的几块板子,哪块编号在前无所谓),那么,和上面两个子问题相关的变量就只有板子的编号。

不妨认为Jimmy开始的位置是一个编号为0,长度为0的板子,假设LeftMinTime(k)表示从k号板子左端到地面的最短时间,RightMinTime(k)表示从k号板子右端到地面的最短时间,那么,求板子k左端点到地面的最短时间的方法如下:

if ( 板子k左端正下方没有别的板子) {
	if( 板子k的高度 h(k) 大于Max)
		LeftMinTime(k) =;
	else
		LeftMinTime(k) = h(k);
}
else if( 板子k左端正下方的板子编号是m )
		LeftMinTime(k) = h(k)-h(m) +
 			Min( LeftMinTime(m) + Lx(k)-Lx(m),
 			RightMinTime(m) + Rx(m)-Lx(k));
}

上面,h(i)就代表i号板子的高度,Lx(i)就代表i号板子左端点的横坐标,Rx(i)就代表i号板子右端点的横坐标。那么 h(k)-h(m) 当然就是从k号板子跳到m号板子所需要的时间,Lx(k)-Lx(m) 就是从m号板子的落脚点走到m号板子左端点的时间,Rx(m)-Lx(k)就是从m号板子的落脚点走到右端点所需的时间。 求RightMinTime(k)的过程类似。不妨认为Jimmy开始的位置是一个编号为0,长度为0的板子,那么整个问题就是要求LeftMinTime(0)。
输入数据中,板子并没有按高度排序,所以程序中一定要首先将板子排序。

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
#define MAX_N 100
#define INFINITE 100000000

int t, n, x, y, maxHeight;
struct Platform
{
    int Lx, Rx, h;
    bool operator<(const Platform &p2) const{
        return h>p2.h;
    }
};
Platform platForms[MAX_N+10];
int leftMInTime[MAX_N+10]; //各板子从左走的最短时间
int rightMinTime[MAX_N+10]; //各板子从右走的最短时间

int main()
{
    cin>>t;
    while (t--)
    {
        cin>>n>>x>>y>>maxHeight;
        platForms[0].Lx=x;
        platForms[0].Rx=x;
        platForms[0].h=y;
        for(int j=1; j<=n; j++)
        {
            cin>>platForms[j].Lx>>platForms[j].Rx>>platForms[j].h;
        }
        sort(platForms, platForms+n+1);
        for(int i=n; i>=0; --i) 
        {
            int  j;
            for(j=i+1; j<=n; ++j)
            {   //找到i的左端的下面的那块板子
                if (platForms[j].Lx<=platForms[j].Rx && platForms[j].Lx >= platForms[j].Lx)
                    break;
            }
            if(j>n) 
            {   //找不到
                if(platForms[i].h > maxHeight)
                    leftMInTime[i] = INFINITE;
                else
                    leftMInTime[i] = y+min(leftMInTime[j]+platForms[i].Lx-platForms[j].Lx, 
                    rightMinTime[j]+platForms[j].Rx-platForms[i].Lx );
            }
            for(j=i+1;j<=n; ++j){
                //找到i的右端的下面的呢块板子
                if( platForms[i].Rx <= platForms[j].Rx && platForms[i].Rx >= platForms[j].Lx)
                    break;
            }
            if( j > n ) {
                if( platForms[i].h > maxHeight )
                    rightMinTime[i] = INFINITE;
                else 
                    rightMinTime[i] = platForms[i].h;
                }
            else {
                int y = platForms[i].h - platForms[j].h;
                if( y > maxHeight) rightMinTime[i] = INFINITE;
                else
                    rightMinTime[i] = y + min(leftMInTime[j]+platForms[i].Rx-platForms[j].Lx,
                        rightMinTime[j]+platForms[j].Rx-platForms[i].Rx);
                }
        }
        printf("%d\n", min(leftMInTime[0],rightMinTime[0]));
    }
    return 0;
}

编程练习: 滑雪

描述

Michael喜欢滑雪百这并不奇怪, 因为滑雪的确很刺激。可是为了获得速度,滑的区域必须向下倾斜,而且当你滑到坡底,你不得不再次走上坡或者等待升降机来载你。Michael想知道载一个区域中最长的滑坡。区域由一个二维数组给出。数组的每个数字代表点的高度。下面是一个例子

1 2 3 4 5
16 17 18 19 6
15 24 25 20 7
14 23 22 21 8
13 12 11 10 9
一个人可以从某个点滑向上下左右相邻四个点之一,当且仅当高度减小。在上面的例子中,一条可滑行的滑坡为24-17-16-1。当然25-24-23-…-3-2-1更长。事实上,这是最长的一条。

输入

输入的第一行表示区域的行数R和列数C(1 <= R,C <= 100)。下面是R行,每行有C个整数,代表高度h,0<=h<=10000。

输出

输出最长区域的长度。

样例输入

5 5
1 2 3 4 5
16 17 18 19 6
15 24 25 20 7
14 23 22 21 8
13 12 11 10 9
样例输出

25

#include<stdio.h>
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;

#define Max 100
int map[Max][Max], length[Max][Max];
int n=0, m=0;
int max_length=0;
int search(int a, int b);

int main() 
{
    while(cin>>n>>m)
    {
        max_length = 0;
        for(int i=0;i<n;i++)
            for(int j=0; j<m; j++)
                cin>>map[i][j];
        memset(length, 0, sizeof(length));
        for(int i=0; i<n; i++)
            for(int j=0; j<m; j++)
                search(i,j);
        cout<<max_length<<endl;
    }
    return 0;
}

int search(int a, int b)
{
    if(length[a][b]!=0)
        return length[a][b];
    int max=0;
    if(a-1>=0 && map[a][b]> map[a-1][b])
    {
        if(max<search(a-1,b))
            max=search(a-1,b);
    }
    if(b-1>=0 && map[a][b-1] < map[a][b])
        if(max < search(a,b-1))
            max = search(a, b-1);
    if(b+1<m && map[a][b+1]< map[a][b])
        if(max < search(a, b+1))
            max = search(a, b+1);
    if(a+1<n && map[a][b] > map[a+1][b])
        if(max < search(a+1,b)) 
            max = search(a+1,b);
    length[a][b] = max + 1;
    if(max_length<length[a][b])
        max_length = length[a][b];
    return length[a][b];
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值