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

摘要:
简而言之,找到正确的“状态”和“状态转移方程”是解决动态规划问题的关键。此外,在优化过程中,提出了两个相邻学生之间的位置数差不超过d的约束。如果不使用递归或动态编程,则很难开始问题,并且约束d还需要约束每个约束,因此编程非常复杂。

先贴题目

题目描述

有 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多时间啊啊!!!总体而言还是看了很多大神的代码和分析,才把这个完完全全理解掉的。对于我这个小白,最终能看到下面这个图,真的蛮开心!

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

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

免责声明:文章转载自《牛客网2017校招真题在线编程之合唱团问题——动态规划问题首秀》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇接口上传base64编码图片WinForm 子线程修改主线程(UI线程)下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

tfgan折腾笔记(二):核心函数详述——gan_model族

定义model的函数有: 1.gan_model 函数原型: defgan_model( #Lambdas defining models. generator_fn, discriminator_fn, #Real data and conditioning. real_data, generator_inputs,...

使用C/C++ 实现ShellCode编写与提取

简单来说,shell code 的核心就是把代码写成 “与地址无关” 的风格,让它不论是在什么环境下都可以被执行。 具体注意: 使用 API 时应该动态调用(GetProAddress) 不能使用全局变量,或者用 static 修饰的变量 在 shellcode 工程中要自定义入口函数 确保调用 API 之前都已经加载了与之对应的 DLL 所有的字符串都...

石子合并(动态规划DP)

时限: 1000ms 内存限制:10000K 总时限:3000ms 描述: 在一个圆形操场的四周摆放着n堆石子(n<= 100),现要将石子有次序地合并成一堆。规定每次只能选取相邻的两堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。 编一程序,读入石子堆数n及每堆的石子数(<=20)。选择一种合并石子的方案,使得做n-1次合并,得分...

mac os hot key

了解常见 Mac OS X 快捷键。快捷键是通过按下键盘上的组合键来调用 Mac OS X 功能的一种方式。 要使用快捷键或组合键,您可以同时按修饰键和字符键。例如,同时按下 Command 键(标有符号的按键)和“c”键会将当前选中的任何内容(文本、图形等等)拷贝至夹纸板。这也称作 Command-C 组合键(或快捷键)。 许多组合键中都包含修饰键...

Uboot--Linux参数传递--ATAG【转】

转自:https://blog.csdn.net/gx19862005/article/details/28596539 Linux内核源码分析--内核启动命令行的传递过程(Linux-3.0 ARMv7) Linux内核在启动的时候需要一些参数,以获得当前硬件的信息或者启动所需资源在内存中的位置等等。这些信息可以通过bootloader传递给内核,比较常...

数据结构 练习 19-活动选择问题的实现(动态规划 和 贪心)

问题叙述:如下图表示活动的开始和结束时间,s[i],开始时间;f[j]结束时间。现在要进行一些列如下活动,注意每个时间段只能进行一场活动,也就是活动不能同时进行,要求举行的活动次数最多。求调度方法。 老规矩,动态规划,要找出两个问题: 1,子问题的最优解; 2,子问题是什么。 abviously,本问题的最优解为:活动数的次数最多,子问题是:看递推公式...