动态规划[SDUT]全题解超详细注释哦!

动态规划【SDUT】

定义:动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。
特征

  1. 最优子结构:当问题的最优解包含了其子问题的最优解时,称该问题具有最优子结构性质。
  2. 重叠子问题:在用递归算法自顶向下解问题时,每次产生的子问题并不总是新问题,有些子问题被反复计算多次。动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只解一次,而后将其解保存在一个表格中,在以后尽可能多地利用这些子问题的解。

比如数字三角形这道题,是我们做dp的一道入门题。

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 510, INF = 1e9;

int n, a[N][N], f[N][N];

int main()
{
    scanf("%d", &n);
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= i; j ++ )
            scanf("%d", &a[i][j]);

    for (int i = 0; i <= n; i ++ )
        for (int j = 0; j <= i + 1; j ++ )
            f[i][j] = -INF;//用无穷小覆盖掉数组外边的数据 

    f[1][1] = a[1][1];
    for (int i = 2; i <= n; i ++ )
        for (int j = 1; j <= i; j ++ )
            f[i][j] = max(f[i - 1][j - 1] + a[i][j], f[i - 1][j] + a[i][j]);

    int res = -INF;
    for (int i = 1; i <= n; i ++ ) 
    	res = max(res, f[n][i]);

    printf("%d\n", res);
    
    return 0;
}

小鑫去爬山这道题也是一样的,初识动态转移方程。

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 510, INF = 1e9;

int n, a[N][N], f[N][N];

int main()
{
    scanf("%d", &n);
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= i; j ++ )
            scanf("%d", &a[i][j]);
    for (int i = 0; i <= n; i ++ )
        for (int j = 0; j <= i; j ++ )
            f[i][j] = INF;//用无穷大覆盖掉数组外边的数据 
    for(int i=1;i<=n;i++)
    {
    	f[n][i]=a[n][i];
	}        
    for (int i = n-1; i >= 1; i -- )
        for (int j = 1; j <= i; j ++ )
            f[i][j] = min(f[i + 1][j ] + a[i][j], f[i + 1][j+1] + a[i][j]);

    printf("%d\n",f[1][1]);
    
    return 0;
}

对于这种算法,我们认为当一个问题被拆解为n个子问题时,这些子问题并非相互独立的。这不同于分治算法。
分治法算的思想是:将一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破,分而治之。

分治策略是:对于一个规模为n的问题,若该问题可以容易地解决(比如说规模n较小)则直接解决,否则将其分解为k个规模较小的子问题,这些子问题互相独立且与原问题形式相同,递归地解这些子问题,然后将各子问题的解合并得到原问题的解。
基于这一点可以参考两个题。
马兰过河卒
取数字问题
这是dp的两道例题。
马兰过河卒是一道经典的动态规划,思路也比较简单。我们认为每个点的可行路径是与前边点的可行路径有关联,且符合动态规划特征。可以写出动态转移方程,然后判断限制条件即可。

#include<iostream>
using namespace std;
long long int f[20][20];
int n,i,j,x,q,y,mx,my,m;
int Mx[9]={-2,-1,1,2,2,1,-1,-2,0};
int My[9]={-1,-2,-2,-1,1,2,2,1,0};
bool judge(int x,int y)
{
   for(q=0;q<=8;q++)
   {
   	  if(x==Mx[q]+mx&&y==My[q]+my)
   	     return true;//确认被马拦下 
   	     
   }	
  return false; 
}
int main()
{   
	scanf("%d %d %d %d",&n,&m,&mx,&my);
	f[0][0]=1;
	for(i=0;i<=n;i++)
	{
		for(j=0;j<=m;j++)
		{   if(i==0&j==0)continue;
		    else if(judge(i,j))
			   f[i][j]=0;//最先判断是否可以走!!! 
		    else if(i==0)f[i][j]=f[i][j-1];
		    else if(j==0)f[i][j]=f[i-1][j];
			else 
			 f[i][j]=f[i-1][j]+f[i][j-1];//状态转移 
		}
	 } 
	 printf("%lld\n",f[n][m]);
}

取数字问题

Description
给定M×N的矩阵,其中的每个元素都是-10到10之间的整数。你的任务是从左上角(1,1)走到右下角(M,N),每一步只能够向右或者向下,并且不能够走出矩阵的范围。你所经过的方格里面的数字都必须被选取,请找出一条最合适的道路,使得在路上被选取的数字之和是尽可能小的正整数。
Input
输入第1行是两个整数M和N,(2<=M<=10,2<=N<=10),分别表示矩阵的行和列的数目。接下来M行,每行包括N个整数,就是矩阵中的每一行的N个元素。
Output
输出只有一行,就是一个整数,表示所选道路上数字之和所能达到的最小正整数。如果不能达到任何正整数,输出-1。
Sample
Input

2 2
0 2
1 0

Output

1

题解:这道题我与其比较有缘。第一次见是在新生赛的时候,那时候我不知道贪心,不知道dp,更不知道dfs。
那个时候我坚信的是新生赛第一道题大概是那么一个签到题吧,所以我就埋头苦做了一番。怎么说,我一开始的做法是右边与下边取最小值,小的加到和里面。就是贪心法(那个时候还不知道hiahia),然后样例也过了,一直WA了。我怕题目会有苛判条件,我改了这个贪心算法一个小时。一个小时的时候我突然发现当前最优并不代表全局最优,我应该找一种算法可以去递归的取最小值。然后我就误打误撞的写了动态规划的转移方程出来,就是每一步的答案值f[i][j]=min(f[i-1][j],f[i][j-1])+a[i][j];然后题目还是WA,这个时候一个半小时过去了,我一个题都没A,这还是第一个题,心态直接崩了。我那时候才知道自己多么的菜。
这个题的标准解法是dfs,而不是动态规划,虽然它被放到了SDUT动态规划里面,我觉得应该就是让我们去注意这一点。
因为题目说的是“使得在路上被选取的数字之和是尽可能小的正整数。”如果我用动态规划,可以解得最小数,而不是最小正数。
那我们遇到负数就要抛弃吗?那肯定不能抛弃,因为后边遇到正的抵消之后可能它正好是那么一个最小正整数。
在这里插入图片描述
就像是人生最开始的那一个选择,它可能是目前最好的选择,但它不一定是人生最好的选择。如果不把人生的每一种可能性都走一遍,永远不能确定哪一条路才是最精彩的。
所以可能小白现在很菜,将来也会变得很强呢!
正是因为一开始说的动态规划的子问题有重叠性,你在取舍的过程中就注定了你的子问题并不是一个独立的问题,它总是与前一个点相互关联。如果不是一个独立的问题,我们就无法加上“只保存正数的和”这个限制条件,最后dp的结果我们就无法确定它的正负。
这个时候我们不得不放弃动态规划这种思想,用另一种思想去考虑这道题。
所以我们就用DFS去遍历所有的可能性。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int m[20][20];
int min = 0x3f3f3f3f;
int a, b;
void search(int i,int j,int sum)
{
    sum += m[i][j];//走到一个格子,则加上格子中的数字;
    if (i<a)//如果,还没到最下边,则向下走
        search(i+1,j,sum);
    if (j<b)//如果,还没到最右边,则向右走
        search(i,j+1,sum);
    if (i==a&&j==b&&sum<min&&sum>0)//如果到达(a, b)并且sum的值已经变为正整数,则更新;
        min=sum;
}
int main()
{
    int i, j;
    scanf("%d %d", &a, &b);
    for(i=1;i<=a;i++)
    {
      for(j=1;j<=b;j++)
      scanf("%d", &m[i][j]);
    }
    search(1, 1, 0);//调用函数,从(1,1)开始
    if(min==0x3f3f3f3f)//如果max的值保持不变,即不能达到任何正整数
      min = -1;
    printf("%d\n", min);
    return 0;
}

然后是跟路径有关的题递归的函数走迷宫

递归的函数

Description
给定一个函数 f(a, b, c):
如果 a ≤ 0 或 b ≤ 0 或 c ≤ 0 返回值为 1;
如果 a > 20 或 b > 20 或 c > 20 返回值为 f(20, 20, 20);
如果 a < b 并且 b < c 返回 f(a, b, c−1) + f(a, b−1, c−1) − f(a, b−1, c);
其它情况返回 f(a−1, b, c) + f(a−1, b−1, c) + f(a−1, b, c−1) − f(a-1, b-1, c-1)。
看起来简单的一个函数?你能做对吗?
Input
输入包含多组测试数据,对于每组测试数据:
输入只有一行为 3 个整数a, b, c(a, b, c < 30)。
Output
对于每组测试数据,输出函数的计算结果。
Sample
Input

1 1 1
2 2 2

Output

2
4

题解:这道题的话考察记忆化搜索
记忆化搜索实际上是递归来实现的,但是递归的过程中有许多的结果是被反复计算的,这样会大大降低算法的执行效率。而记忆化搜索是在递归的过程中,将已经计算出来的结果保存起来,当之后的计算用到的时候直接取出结果,避免重复运算,因此极大的提高了算法的效率。
举个例子:这是斐波那契数列的递归代码,定义简单不多赘述。

 long long f(int n)
 {
     if(n==1||n==2) return 1;
     return f(n-1)+f(n-2);
 }

枚举一些斐波那契数列的计算次数:
在这里插入图片描述
可以看出计算f[5]的时候计算了三次,计算f[6]还是要计算f[5]的那三次,总共计算了五次。如果不把f[5]记录下来,那这相当于白白浪费掉这些时间。
先来看递归的函数这道题的正确代码:(本文很多代码是sdut acm学长写的,要说起来就是上课过程中转载的hiahia,别怪我没声明

#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>

using namespace std;
const int N = 25, INF = 0x3f3f3f3f;
int dp[N][N][N];
int f(int a, int b, int c) {
  if (a <= 0 || b <= 0 || c <= 0) return 1;
  if (a > 20 || b > 20 || c > 20) return f(20, 20, 20);
  if (dp[a][b][c] != INF) return dp[a][b][c];
  int res;
  if (a < b && b < c)
      res = f(a, b, c - 1) + f(a, b - 1, c - 1) - f(a, b - 1, c);
  else
      res = f(a - 1, b, c) + f(a - 1, b - 1, c) + f(a - 1, b, c - 1) -f(a - 1, b - 1, c - 1);
  return dp[a][b][c] = res;
}

int main() {
  int a, b, c;
  memset(dp, 0x3f3f3f3f, sizeof dp);
  while (~scanf("%d %d %d", &a, &b, &c)) {
      printf("%d\n", f(a, b, c));
  }
  return 0;
}

如果我不用dp这个数组去保存数据:(错误示例)

int f(int a, int b, int c) {
  if (a <= 0 || b <= 0 || c <= 0) return 1;
  if (a > 20 || b > 20 || c > 20) return f(20, 20, 20);
  int res;
  if (a < b && b < c)
      res = f(a, b, c - 1) + f(a, b - 1, c - 1) - f(a, b - 1, c);
  else
      res = f(a - 1, b, c) + f(a - 1, b - 1, c) + f(a - 1, b, c - 1) -
            f(a - 1, b - 1, c - 1);
  return res;
}

结果会如何?
如果试过的同学都知道是TLE(hiahia)。这就是记忆化搜索的优势了,节省运行时间。
关于记忆化搜索推荐看一下落谷的一道题----滑雪

走迷宫

Description
有一个m×n格的迷宫(表示有m行、n列),其中有可走的也有不可走的,如果用1表示可以走,0表示不可以走,输入这m*n个数据和起始点、结束点(起始点和结束点都是用两个数据来描述的,分别表示这个点的行号和列号)。现在要你编程找出所有可行的道路,要求所走的路中没有重复的点,走时只能是上下左右四个方向。如果一条路都不可行,则输出相应信息(用-1表示无路)。
Input
第一行是两个数m,n(1< m, n< 15),接下来是m行n列由1和0组成的数据,最后两行是起始点和结束点。
Output
所有可行的路径,输出时按照左上右下的顺序。描述一个点时用(x,y)的形式,除开始点外,其他的都要用“->”表示。如果没有一条可行的路则输出-1。
Sample
Input

5 4
1 1 0 0
1 1 1 1
0 1 1 0
1 1 0 1
1 1 1 1
1 1
5 4

Output

(1,1)->(1,2)->(2,2)->(2,3)->(3,3)->(3,2)->(4,2)->(4,1)->(5,1)->(5,2)->(5,3)->(5,4)
(1,1)->(1,2)->(2,2)->(2,3)->(3,3)->(3,2)->(4,2)->(5,2)->(5,3)->(5,4)
(1,1)->(1,2)->(2,2)->(3,2)->(4,2)->(4,1)->(5,1)->(5,2)->(5,3)->(5,4)
(1,1)->(1,2)->(2,2)->(3,2)->(4,2)->(5,2)->(5,3)->(5,4)
(1,1)->(2,1)->(2,2)->(2,3)->(3,3)->(3,2)->(4,2)->(4,1)->(5,1)->(5,2)->(5,3)->(5,4)
(1,1)->(2,1)->(2,2)->(2,3)->(3,3)->(3,2)->(4,2)->(5,2)->(5,3)->(5,4)
(1,1)->(2,1)->(2,2)->(3,2)->(4,2)->(4,1)->(5,1)->(5,2)->(5,3)->(5,4)
(1,1)->(2,1)->(2,2)->(3,2)->(4,2)->(5,2)->(5,3)->(5,4)

思路的话就是从起点(x,y)开始判断,如果是0就代表遇上墙了,返回。如果是1就保存这个点的坐标。因为题目说不可重复走一个点,所以保存完就赋值为0即可,往下遍历其上下左右的坐标即可,实现递归。

# include<stdio.h>
# include<string.h>
int a[50][50];  //存数据  
int dir[4][2]={{0,-1},{-1,0},{0,1},{1,0}};  //4个方向
int b[101];     //用于线性储存路径,两个为一组
int c,d,flag;
 
void find(int x,int y,int ii);
 //x y为起始坐标 ,ii=0线性记录路径 
int main( ) 
{
    int n,m,x,y,i,j,ii;
    while(scanf("%d %d",&n,&m) == 2)
    {
        ii = flag = 0;
        memset(b,0,sizeof(b));
        memset(a,0,sizeof(a));//初始化0 使得迷宫周围形成墙壁 遇到0 就return  
        for(i=1;i<=n;i++)
            for(j=1;j<=m;j++)
                scanf("%d",&a[i][j]);//读入数据 
        scanf("%d %d %d %d",&x,&y,&c,&d); //x,y起点。c,d终点 
        find(x,y,0);
        if(!flag) 
		  printf("-1\n");//可走到终点flag会被赋值 若未走到终点则输出-1; 
    }
    return 0;
}
 
void find(int x,int y,int ii)
{
    int i;
    if(x == c && y == d )    //来到终点则输出
    {
        flag = 1;//判断有没有路可以来到终点
        for(i=0;i<ii;i+=2)
        {
            printf("(%d,%d)",b[i],b[i+1]);
            printf("->");
        }
        printf("(%d,%d)\n",c,d);
        return;
    }
    if(!a[x][y])
	  return ;    //到达边界或已走或墙壁返回
    for(i=0;i<4;i++)    //依次走4个方向
    {
        a[x][y] = 0;     //如果该方向可走,则标记已走,为0
        b[ii] = x;         //记下横坐标
        b[ii+1] = y;       //记下纵坐标 
        find(x+dir[i][0],y+dir[i][1],ii+2);   //横坐标和纵坐标遍历上下左右; 
        a[x][y] = 1;      //恢复原位置的可走标签
    }
}

还有一道变形式的走迷宫,过两天补上。
其他dfs思路的题比如导弹防御n皇后等等。
然后再写最长公共子序列这道题。

最长公共子序列问题

Description
给定两个序列 X={x1,x2,…,xm} 和 Y={y1,y2,…,yn},找出X和Y的最长公共子序列。

Input
输入数据有多组,每组有两行 ,每行为一个长度不超过500的字符串(输入全是大写英文字母(A,Z)),表示序列X和Y。

Output
每组输出一行,表示所求得的最长公共子序列的长度,若不存在公共子序列,则输出0。

Sample
Input

ABCBDAB
BDCABA

Output

4

题解思路:先把坐标轴上的点都默认赋值为0,相当于数组边界;假如先把ABCD按顺序与A字符比较,依次向下。此时只有A字符与A字符相等,给【A】【A】这一点对应坐标加1,而其他字符仍然是0(字符“A”与串“ABCD”最长公共子序列为1)。那我们看对应的【B】【A】这一点的数值应该是几???
在这里插入图片描述
应该也是1。因为B不与A相等,所以它并不是直接加1的。而它赋值的方式是看列“ABCD”中前一个字符"A"的最长子序列1,B只需要"继承"这个数量即可。
在这里插入图片描述
而【B】【B】这个点因为字符“AB”中B与“ABCD”中的那个“B”相等,所以就在之前【A】【A】的数值上继承并加1。
在这里插入图片描述
而对于【B】【C】这一点我们都知道“AB”与“ABCD”的最长公共子序列是2,所以遍历到【B】【C】或者【B】【D】答案都应该是2。那么【B】【C】这一点是如何去“继承”的呢?
我们可以很轻松的总结出规律,如果是字符不相等的点,继承相邻的最大值字符相等的点,对应之前相等点的数值加1
在这里插入图片描述

这样就可以用Dp的思想去递推表达式。

#include<bits/stdc++.h>
using namespace std;
char x[505], y[505];
int q[505][505];
int main()
{
    int i, j, length_x, length_y;
    while(~scanf("%s %s", x, y))
    {
        length_x = strlen(x);
        length_y = strlen(y);
        for(i = 0; i <= length_x; i++)
        {/// 初始化状态数组q的第一列
            q[i][0] = 0;
        }
        for(j = 0; j <= length_y; j++)
        {/// 初始化状态数组q的第一行
            q[0][j] = 0;
        }
        for(i = 1; i <= length_x; i++)
        {///此处 i 最后应该等于 length_x 此时为x[length_x - 1]对应于q[length_x]
            for(j = 1; j <= length_y; j++)
            {///此处 j 最后应该等于 length_j 此时为x[length_j - 1]对应于q[length_j]
                if(x[i - 1] == y[j - 1])
                    q[i][j] = q[i - 1][j - 1] + 1;
                else 
				{ q[i][j]=max(q[i - 1][j], q[i][j - 1]);
               	} 
            }///取上方和左方的最大值, max(q[i - 1][j], q[i][j - 1])
        }
        printf("%d\n", q[length_x][length_y]);///输出对应最后一个字符的长度
    }
    return 0;
}

我看网上有的这个题还有别的形式。比如:这个多个字符串求最长公共子序列的形式。
在这里插入图片描述
这个题放到以后的博客里写,可以参考这位博主。这个求最长公共子序列的个数确实不是难事,但如果让你求出所有的最长公共子序列呢?如果LCS不唯一呢?那就需要用到回溯算法,去求你的路径。这里把函数的那一部分简单的写一下。

void all(int i,int j,string str) //回溯之前的路线
{
	while(i>0&&j>0)
	{
		if(X[i-1]==Y[j-1])
		{
		    //用数组把这个符号记录下来 X[i-1] ,可以开辟动态空间
		}
		i--;
		j--; //走到前一个相等的字符面前
		else
		{
			if(a[i-1][j]>a[i][j-1])//从大的方向来,回大的方向去; 
			  i--;
			else if(a[i-1][j]<a[i][j-1])
			  j--;
			else
			{
			   all(i-1,j,str);
			   all(i,j-1;str);
			   return;	
			}    
		 } 
	}
	reverse()//最后把记录下来的数组翻转一下就可以输出了; 
}

然后是最长上升子序列这道题。

最长上升子序列

Description
一个数的序列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)。

你的任务,就是对于给定的序列,求出最长上升子序列的长度。
Input
输入的第一行是序列的长度N (1 <= N <= 1000)。第二行给出序列中的N个整数,这些整数的取值范围都在0到10000。
Output
最长上升子序列的长度。
Sample
Input

7
1 7 3 5 9 4 8

Output

4

这道题在今天结训赛开始前准备写,然后结训赛直接就被出在题目列表里了,说来也巧。我直接当新题做了,晚上补一波题解。这道题感觉不难,思路挺简单的,就是建立一个dp模型。题解放在代码中了,代码如下:

#include<stdio.h>
#include<string.h>
int main()
{
	int m[1008];//上升子序列个数
	int a[1008];//具体数值
	int n;
	scanf("%d", &n);
	for (int i = 0; i < n; i++)
	{
		scanf("%d", &a[i]);
		m[i] = 1;//初始化1,每个序列数从1开始递增 
	}
	for (int i = 1; i < n; i++)
	{
		int max = 0;
		for (int c = 0; c < i; c++)//找f(n-1)的过程!!! 
		{
			if (a[i]>a[c] && m[c]>max)//遍历前边的数,使得前一个的值较小 
				max = m[c];//而且前一个值的上升子序列个数是最多的 
		}
		m[i] = max + 1;//这个值的上升子序列数就是前边最多的上升子序列数加1; 
	}
	int max = 0;
	for (int i = 0; i < n;i++)
	if (m[i]>max)
		max = m[i];//找到在这一串数字里最多的那个子序列数m[x] :
	printf("%d\n", max);
}

还有另一个变形叫上升子序列。它比较简单,跟刚刚那道题思路完全一样,就把记录最长子序列的长度改成记录递增子序列的最大和就行了。题目描述不写了,基本同上题一致。

#include <iostream>
int n,a[10000],sum[10000],max;//sum记录递增子序列的最大和。
int main()
{  
	while(~scanf("%d",&n))
{   for(int i=0;i<n;i++)
	{
		scanf("%d",&a[i]);
	}
	sum[0]=a[0];
	for(int i=1;i<n;i++)
	{  int max=0;
	   for(int j=0;j<i;j++)
	   {
	   	 if(a[i]>a[j]&&sum[j]>max)
			 {max=sum[j];	  
			 }
			 sum[i]=max+a[i];
	   }
	   
	}
	max=0;
	for(int i=0;i<n;i++)
	{ if(sum[i]>max)
	     max=sum[i];
	}
	printf("%d\n",max);	
	}	
}

最后写一个反向建立dp的题目,免费馅饼

免费馅饼

Description
都说天上不会掉馅饼,但有一天gameboy正走在回家的小径上,忽然天上掉下大把大把的馅饼。说来gameboy的人品实在是太好了,这馅饼别处都不掉,就掉落在他身旁的10米范围内。馅饼如果掉在了地上当然就不能吃了,所以gameboy马上卸下身上的背包去接。但由于小径两侧都不能站人,所以他只能在小径上接。由于gameboy平时老呆在房间里玩游戏,虽然在游戏中是个身手敏捷的高手,但在现实中运动神经特别迟钝,每秒种只有在移动不超过一米的范围内接住坠落的馅饼。现在给这条小径如图标上坐标:
在这里插入图片描述
为了使问题简化,假设在接下来的一段时间里,馅饼都掉落在0-10这11个位置。开始时gameboy站在5这个位置,因此在第一秒,他只能接到4,5,6这三个位置中期中一个位置上的馅饼。问gameboy最多可能接到多少个馅饼?(假设他的背包可以容纳无穷多个馅饼)

Input
输入数据有多组。每组数据的第一行为以正整数n(0 < n < 100000),表示有n个馅饼掉在这条小径上。在结下来的n行中,每行有两个整数x,T(0 <= T < 100000),表示在第T秒有一个馅饼掉在x点上。同一秒钟在同一点上可能掉下多个馅饼。n=0时输入结束。

Output
每一组输入数据对应一行输出。输出一个整数m,表示gameboy最多可能接到m个馅饼。

提示:本题的输入数据量比较大,建议用scanf读入,用cin可能会超时。

Sample
Input

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

Output

4

题解:我们知道dp方程是一种递归思想,在赋值的过程要满足无后效性。所以我们推出的结果,只跟前一个结果有关,而从不关心它怎么来的。所以我们可以看这样一组dp方程:

=+   后         假设所有自身的数值为1,已知前max=1,
max    max     自身        则根据后自身=1,计算出后max的结果为2
同理              前  =+   前               的结果也是2。
                 max    max     自身

所以逆向建立Dp和正向建立是一样的,只有最后归结的位置不同。
这个题首先我想到的就是建立一个dp方程,下一个时刻的饼最大和总是上一个时刻的最大和+1,然后加个相邻位置的判断条件。我求到最后,我只知道一定是时间越长能接的饼越多,所以我遍历最后一秒去求max,结果不对。是因为我最后求出的max不知道是从哪一个起点走哪一条路径过来的,而题目限定必须从5这个位置出发,基于这一点我们不得不逆向建立Dp。因为每个点都是求相邻最大值赋值过来的,所以最后输出5这个位置的数值就可以了。

#include<bits/stdc++.h>
using namespace std;
int dp[100005][12];
int main()
{   int n,x,t,maxt,i,k;
    while(scanf("%d",&n)&&n)
    {
        maxt=0;
        memset(dp,0,sizeof(dp));
        while(n--)
        {
            scanf("%d %d",&x,&t);
            dp[t][x]++;//t代表着时刻  x表示掉落饼的位置; 
            if(maxt<t) maxt=t; //maxt为最后的一秒 
        }

        for(i=maxt-1;i>=0;i--)
        {
            for(k=0;k<=10;k++)//边界是0,取max自动舍去 
                dp[i][k]+= max(max(dp[i+1][k-1],dp[i+1][k]),dp[i+1][k+1]);
            //前一秒的max总是后一秒max加前一秒本身
        }   //对下一秒的 自身 相邻位置 取max 
            //逆向思维 因为只能确定d[0][5]是开始;
		  

        cout << dp[0][5] << endl;//逆向回溯到【0】【5】这个点一定是最大收益点; 


    }

}

新手上道,很多见解可能有误,多多包涵!

发布于2.4日,小年当天,祝大家新年快乐,万事如意!!!

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值