牛客网2017校招真题在线编程之合唱团问题——动态规划问题首秀

先贴题目

题目描述

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

输入描述:

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

输出描述:

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

示例1

输入

3
7 4 7
2 50

输出

49

动态规划之我见(适合小白,因为我就是小白,这是我做的第一个动态规划问题,查了很多资料,理解了很久,非小白可跳过)

(1)首先,动态规划问题要求具有最优子结构,也就是说规模为n的问题的最优解,可以由规模为n-1(和/或更小规模)的子问题递推得到,其中子问题也是具有这种最优子结构的。因此动态规划问题得到的是全局最优解而不是局部最优解。
(2)其次,从上面可以看到,动态规划最重要的是最优子结构,也就是这个递推表达式,找到这个递推表达式,后面就是编程实现的事情了。这个递推表达式有个学名叫做状态转移方程
插一条:既然叫做状态转移方程,总得有“状态”吧,不然哪里的转移呢?我理解的“状态”,也就是“问题”!对,就是要求解的问题,只不过是一种表达而已,比如说合唱团问题中,n个人中找出k个人,使得能量值乘积最大,这就是我们所谓的状态,我们暂且把这个状态记作dp[n,k],
这里dp是dynamic planning的缩写啦,“圈内人”都这么叫的哈哈哈。“状态”,在我们解决问题的时候往往是一维数组或者二维数组(这完全是自己的理解啦,没在哪本看到谁这么说过),究竟是一维数组还是二维数组,要根据具体问题具体分析,比如合唱团问题里的状态就是一个
二维数组。还有一点就是,找状态的时候,先不要管约束条件,约束条件在最后寻找状态转移方程的时候满足其要求即可。
(3)终于可以说第三点啦,有了状态,接下里就是找状态转移方程啦。也就是规模为n的问题怎样由规模为n-1(和/或更小规模)的子问题递推得到,找到这个递推关系式,找这个递推关系式方法也得具体问题具体分析,这一点可以参考上一篇博客(https://www.cnblogs.com/VanJing/p/9325158.html),
里面介绍了几个动态规划的例子,仔细研读必有收获。

总之,找准“状态”,找对“状态转移方程”是解决动态规划类问题的重点。


合唱团题目相关
1.题目分析 Ps.参考牛客网讨论
题目要求n个学生中选择k个,使这k个学生的能力值乘积最大。这是一个最优化的问题。另外,在优化过程中,提出了相邻两个学生的位置编号差不超过d的约束。
如果不用递归或者动态规划,问题很难入手,并且,限制条件d也需要对每一个进行约束,编程十分复杂。

2.贴程序(Python3.5.2实现)
别看程序就这么几行,但是不好理解。。。。
 1 #/user/bin/python
 2 #coding = utf-8
 3 n = int(input())
 4 arr = list(map(int, input().split()))
 5 K, D = map(int, input().split())
 6 
 7 fm = [[0] * n for _ in range(K)]
 8 fn = [[0] * n for _ in range(K)]
 9 res = 0
10 
11 for i in range(n):
12     fm[0][i] = arr[i]
13     fn[0][i] = arr[i]
14 
15 for i in range(n):
16     for k in range(1,K):
17         for j in range(i-1, max(0, i-D)-1, - 1):
18             fm[k][i] = max(fm[k][i],max(fm[k-1][j] * arr[i], fn[k-1][j] * arr[i]))
19             fn[k][i] = min(fn[k][i],min(fm[k-1][j] * arr[i], fn[k-1][j] * arr[i]))
20     res = max(res, fm[K-1][i])
21     
22 print(res)
23             
24             

 

2. 问题分析:
从n个学生中,选择k个,根据“动态规划之我见”部分,这里状态定义为f[n][k],为了和后面分析以及程序一致,将状态写成f[k][n]

因为能量值arr有正有负,最后要找能量值乘积的最大值,我们知道负负得正,所以要维护两个dp数组,一个存储最大,一个存储最小。
定义fm[k][i]表示当选中了k个学生,并且以第i个学生为结尾,所产生的最大乘积;
fn[k][i]表示当选中了k个学生,并且以第i个学生为结尾,所产生的最小乘积;
所以fm[k=1..K][i=1..n],fn[k=1..K][i=1..n]为K*n的数组,程序6~7行就是初始化这两个数组。
再分析一下fm和fn。其意义不是很好理解,具体可以在纸上画出一个K行n列的表格出来,第一行,表示从n个人(假设为1...n,其能量值为arr[1]...arr[n])中1个人,那么最大的能量值乘积自然是选中的这个人的能量值,因此无论是fm还是fn,第一行的值就是arr[i],
这也就是程序11~13行干的事情。

fm[k][i]表示第k个人选中arr[i]时能量乘积最大值,fn同理。(理解这一点非常非常重要)
再看状态转移方程。状态转移方程,也就是子问题的求解。从n个学生中,选择k个,可以看成是:先从n个学生里选择第k个(假设arr[i]),然后在剩下的里(arr[i]的左边部分)选择k-1个,并且让第k个和前k-1个满足约束条件。根据上一行,第k个人选中arr[i]表示为fm[k][i],
剩下k-1个人选中arr[j]表示为fm[k-1][j],(这里j需要满足约束条件,稍后再讨论)。如果arr[i]为正,则最大的能量值乘积为arr[i]*fm[k-1][j];如果arr[i]为负。则最大的能量值乘积为arr[i]*fn[k-1][j],无论arr[i]为正数还是负数,都是取arr[i]*fm[k-1][j]
和arr[i]*fn[k-1][j]中最大的那个,因此可以统一表示成max(fm[k-1][j] * arr[i], fn[k-1][j] * arr[i])。fn同理。这一部分实现如15~19行(除掉17行)所示。
其中,第18行为什么要在max(fm[k-1][j] * arr[i], fn[k-1][j] * arr[i])外面套了一个与fm[k][i]比较取最大,第19行为什么要在min(fn[k-1][j] * arr[i], fn[k-1][j] * arr[i])外面套了一个与fm[k][i]比较取最小,这一点尚未思考清楚。也不想在这一点
上太纠结,欢迎讨论。

再看约束条件,约束条件要求相邻两个学生的位置编号的差不超过d,这一部分代码实现主要体现在第17行。arr[i]的编号为i(Python中索引为i-1),因此剩下k-1个人里,只能从编号为i-1(Python里应该为i-2)向左数d个位置,在i-1-D到i的位置(不包含i)里找第k-1个人
但是如果D太大,总不能在i<1的部分找人吧,因此需要取max(0,i-D),Ps.程序里是max(0,i-D)-1,因为Python里range取左开右闭,为了能取到边界条件0或者i-D,因此加了个-1.
再PS.动态规划里的边界条件处理也是非常重要的,需要尤其注意,不然非常容易出错。
说到边界条件,回看第16行!range(1,K),为什么不是range(K)呢?因为range(K)时i会从0开始迭代,会掩盖掉选择选中一个人的情况(也就是掩盖掉11~13行结果),因此这些细节一定要注意啊,不然很难做到程序百分百正确。


还有一点,打印结果!打印部分不仅仅是第22行,还有第20行,max(res,fm[K-1][i]),这是在第一层循环里的哦,但是不用在第二层循环里,不想细说了,自己理解去吧,不是很难。


ok,除了其中的一个小bug,基本理解完了。真的花了我n多时间啊啊!!!总体而言还是看了很多大神的代码和分析,才把这个完完全全理解掉的。对于我这个小白,最终能看到下面这个图,真的蛮开心!

 

 

写在最最最后:第一次自己敲字写博客,排版什么的自己真的不是很擅长,也没那个闲工夫去调整,各位看官凑合着看吧,谢谢!

 
     
 
    
posted on 2018-07-18 21:11 VanJing 阅读( ...) 评论( ...) 编辑 收藏

转载于:https://www.cnblogs.com/VanJing/p/9332630.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值