每日N刷——动态规划(2017网易内推题,合唱团,C++实现)

12号网易内推笔试,趁现在赶紧刷刷网易内推的笔试题攒攒人品。

刷题地址https://www.nowcoder.com/contestRoom
进去找网易2017内推笔试题(一)就行了。

题目:有 n 个学生站成一排,每个学生有一个能力值,牛牛想从这 n个学生中按照顺序选取 k名学生,要求相邻两个学生的位置编号的差不超过 d,使得这 k 个学生的能力值的乘积最大,你能返回最大的乘积吗?

输入描述

每个输入包含1个测试用例。每个测试数据的第一行包含一个整数 n (1<=n<=50),表示学生的个数,接下来的一行,包含n个整数,按顺序表示每个学生的能力值 ai(-50<=ai<= 50)。接下来的一行包含两个整数k 和 d (1 <= k <= 10, 1 <= d <= 50)。

输出描述

输出一行表示最大的乘积。

输入例子

3
7 4 7
2 50

输出例子
49

分析:

看到这类题,果断要想到动态规划。
我也是最近才看了动态规划方面的东西,现在在这里简单总结一下,以我个人现在的理解。

动态规划的的本质是什么?——拆分问题。解决拆分问题逐渐递推到原本问题,拆分的问题也可以理解为原问题的一个子问题。
拆分问题的本质是什么?——状态和状态转移方程的定义。
那么,何为状态与状态转移方程呢?

状态:与子问题相关的各个变量的一组取值,称之为一个“状态”。“状态”下的值对应“状态”的解。

状态转移方程:确定了状态与解,然后找到递推或者递归的办法,将另一个状态的值推导出来,最终用递推公式推导出原问题的解,这就是状态转移方程的作用。

动态规划最大的好处就是他的递推性,后一个状态的解只与前一个状态有关。

那么,回到具体的笔试题中来,我们应该如何拆分这个问题呢?
思路:
  • 首先我们要明白,拆分问题永远是最重要的。想要拆分问题,先要考虑原问题问了什么。原问题需要知道的是,当我们按顺序选取了k名学生,每个人又都有能力值时,如何选择让k名学生能力值得乘积最大?
  • 拆分问题的第一思路就来了:原问题既然是k名学生,那我们可不可以先考虑一名学生乘积最大,然后逐渐变为两名,三名……直到k名呢?
    当然可以。从一名的解开始,这不就是我们前面所说的动态规划子问题的“状态”么?而演化到两名,三名甚至k名的解的递推过程,不就是我们想要的“状态转移方程”的过程吗?好,有了思路以后,我们就开始动手做吧。
  • 首先我们应设置一个学生能力值数组stu[i],然后设置二维数组fm[k][i],k表示选取学生的人数,i表示选取到的最后一人。为什么这么设定呢?因为设置了k,我们才知道究竟有多少人。设置了i,我们才知道结尾的人在哪。试想一下,如果我们要递推式,当结尾人确定为第i位的情况下,那么当状态中其中一个变量为k-1时,结尾的人的序号也就变为了i-1,那么当我们求出fm[k-1][i-1]时,是不是只要乘以stu[i],就可以得到fm[k][i]了?
  • 上一段说的有道理,但不完全对。因为题目告诉我们,能力值范围是一个[-50,50]的数,因此如若有最大正整数,必然也存在最大负整数,即状态的解中最小的数。因此我们不仅要设置fm[k][i]来作为某个状态的解,还要设置fn[k][i],fn在最后一步当然是不需要,因为当最终原问题结果出来以后,只需要一个最大的正整数即可。但是在最后一步之前我们不知道结尾,也就是第i个人的能力值是正是负,如果是负数,那我们不还要判断fn[k-1][i-1]的情况了么~
    即fm[k][i]=max(fm[k-1][i-1] 点乘 stu[i] ,fn[k-1][i-1] 点乘 stu[i])。
  • 此外还有几个小细节。首先fm[1][i]由于只有1个人,所以这个人必然是第i个人的能力值,也就是stu[i]。
  • 另外,顺序寻找k个人,结尾虽然我们说是第i个,但这第i个人的位置不固定,我们需要循环遍历去找。
  • 另外还有一个不好理解的地方,就是题目。。。题目讲从n个学生中按照顺序选取k名学生,编号差不超过d。这里的意思应该是可以顺序选取,但是中间可以跳开找最大的。。不然何来编号差一说,相邻的都是1了。

用C++实现后的代码如下:

/*有 n 个学生站成一排,每个学生有一个能力值.牛牛想从这 n 个学生中按照顺序选取 k 名学生.
要求相邻两个学生的位置编号的差不超过 d,使得这 k 个学生的能力值的乘积最大,你能返回最大的乘积吗?*/

/*有 n 个学生站成一排,每个学生有一个能力值.牛牛想从这 n 个学生中按照顺序选取 k 名学生.
要求相邻两个学生的位置编号的差不超过 d,使得这 k 个学生的能力值的乘积最大,你能返回最大的乘积吗?*/

#include <iostream>
inline long long max(long long a,long long b){return (a>b?a:b);}
inline long long min(long long a,long long b){return (a>b?b:a);}
using namespace std;
int main()
{
    int N,K,D,i,j,k;
    long long stu[51],fm[11][51],fn[11][51];
    long long ans = 0;
    //fm[k][i]表示当选中了k个学生,并且以第i个学生为结尾,所产生的最大乘积.
    while(cin >> N)
    {
        for(i=1;i<=N;++i)
            cin >> stu[i];       
        cin >> K;
        cin >> D;
        for(i=0;i<=K;++i)
            for(j=0;j<=N;fm[i][j]=fn[i][j]=0,++j);
        for(i=1;i<=N;++i)
        {
            fm[1][i]=fn[1][i]=stu[i];//状态开头的初始情况,如果只选了一个学生,那么这个人就是第i个学生。
            for(k=2;k<=K;++k)//从k=2开始循环,进入状态解释方程。先解决子问题,也就是人少的时候的fm,fn,然后递推。
            { 
                for(j=i-1;j>0 && i-j<=D;--j)
                /*j初始是i左边一位,也即末尾左边一位的数。
                但它或许与再之前乘积最大数的乘积不是最大,因此需要j--找再之前最大的数。
                全部循环完毕以后,找到乘积最大的末尾位置,作为fm[k-1][j]提供给fm[k][i]。
                即每个i都有一个fm[K][i],但当我们计算fm[K+1][i+1]时,需要找到fm[K][i]最大的那个i。
                相邻的人之间差距不能超过D,然后反向推导。含0的数组都空出来了所以j>0。*/
                {
                    fm[k][i]=max(fm[k][i],max(fm[k-1][j]*stu[i],fn[k-1][j]*stu[i]));
                    fn[k][i]=min(fn[k][i],min(fn[k-1][j]*stu[i],fm[k-1][j]*stu[i]));
                    //fm[k][i]=max(fm[k-1][i-1]*stu[i],fn[k-1][i-1]*stu[i]),因为上一个为最大负值也可能乘以一个负数变为正数。
                    //同时由于k个人变成了k+1个人,因此原来结尾是i,现在结尾变成了i+1。
                    //先由k=2开始实现循环,找出fm[2][i],fn[2][i],由此再通过递推式找出fm[3][i],fn[3][i]直至fm[K][i]。
                    //注意i也是在循环,因为结尾不可能永远是一个值,因此i也要不断变化。
                }
            }
            ans=max(ans,fm[K][i]);//i的不同导致即使K一样,但是不同的i值依旧会出现不同的fm[K][i]。
        }
        cout << ans;
    }
    return 0;
}

(部分标注了注释,方便理解)

最后还是要补充一句:
看懂不算懂,自己在VS里敲一遍才算真的懂!

    原文作者:曲谐_
    原文地址: https://www.jianshu.com/p/9847cc371858
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞