动态规划:合唱团问题解析
Posted ToBeDeveloper
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了动态规划:合唱团问题解析相关的知识,希望对你有一定的参考价值。
牛客网网易的校招编程题
题目:有 n 个学生站成一排,每个学生有一个能力值,牛牛想从这 n 个学生中按照顺序选取 k 名学生,要求相邻两个学生的位置编号的差不超过 d,使得这 k 个学生的能力值的乘积最大,你能返回最大的乘积吗?
输入:每个输入包含 1 个测试用例。每个测试数据的第一行包含一个整数 n (1 <= n <= 50),表示学生的个数,接下来的一行,包含 n 个整数,按顺序表示每个学生的能力值 ai(-50 <= ai <= 50)。接下来的一行包含两个整数,k 和 d (1 <= k <= 10, 1 <= d <= 50)。
输出:输出一行表示最大的乘积。
因为本人刚学动态规划,所以我先把问题简化后先用递归方式求解,再改进为记忆化搜索,然后用动态规划解决问题,最后求解原问题。
简化后的问题:从 n 个自然数中选取 k 个数,使得这 k 个数的乘积最大。
递归求解
先尝试用递归的方式自上而下的解决,定义状态函数为 F(start, k),start 为自然数数组索引的起点,k 为要取的数的数量,返回从 start 到数组结束位置中取得的 k 个数的乘积的最大值。假设自然数组为 (X0, X1 ,X2, …, Xn-1) 共 n 个数,我们最终要求的就是 F(0, k)。假设我们选取其中一个数为必选的数,可以得出如下的递归树去解释该问题。
递归的终止条件为当 start >= n 的时候,这时 start 索引已经越界,所以直接返回数字 1 乘以被选取的数字,就相当于返回数组最后一个数字 Xn-1; 同时,当最后 k <= 0 的时候,说明这时无可选取的数字,也就是返回数字 1。基于以上条件,当存在选取超过一定范围内的 k 个数时,会返回范围内所有数字的乘积。实现的代码如下所示:
1 long long recursive(int a[], int index, int n, int k) { 2 if (k <= 0 || index >= n) 3 return 1; 4 5 long long result = 0; 6 for (int i=index; i < n; i++) 7 result = max(result, a[i] * recursive(a, i+1, n, k-1)); 8 return result; 9 } 10 11 long long result(int a[], int n, int k){ 12 return recursive(a, 0, n, k); 13 }
记忆化搜索
因为递归在处理更大规模的数据时运行效率是很低的,存在大量的重复运算,所以我们可以用记忆化搜索的方式去优化递归的方法。因为每个状态依赖于两个变量的变化,所以需要一个二维的数组去存储已经计算过的值。实现的代码如下所示:
1 vector<vector<long long>> memo; 2 long long memoSearch(int a[], int index, int n, int k) { 3 if (k <= 0 || index >= n) 4 return 1; 5 if (memo[index][k] != -1) 6 return memo[index][k]; 7 8 long long result = 0; 9 for (int i = index; i < n; i++) 10 result = max(result, a[i] * memoSearch(a, i+1, n, k-1)); 11 memo[index][k] = result; 12 return result; 13 } 14 15 long long result(int a[], int n, int k){ 16 memo = vector<vector<long long>>(n, vector<long long>(k+1, -1)); 17 return memoSearch(a, 0, n, k); 18 }
动态规划
通过上面递归的分析,我们知道该问题是要求一个最优的解,当自顶向下的分析问题时,我们发现该问题是存在最优子问题的,同时这些子问题可能被重复的计算,所以我们可以用动态规划的方法去自底向上的解决问题,提高计算效率。
我们从最基本的一个子问题 F(start, 1) 开始分析,F(start, 1)=max(Xstart*F(1, 0), Xstart+1*F(2, 0), …, Xn-1*F(n, 0))。因为假设 k=0 时返回数字 1,所以可得 F(start, 1)=max(Xstart, Xstart+1, …, Xn-1)。所以当 k = 1 时,我们保留原来所有数组的值,而当 k = 2 时,从头遍历数组,在位置 (start, 2) 上存储 Xstart*F(start+1, 1)。所以在编程实现时需要三个 for 循环,第一重循环以 k 计数,第二重以自然数组下标 n 计数,第三重循环取该下标 n 后被存储的数,循环内计算该下标的自然数与存储的数的最大值的积。当计算完最后一列 k 时,最后一列 k 中的最大值就是我们要求的问题的最优解。实现的代码如下所示:
1 vector<vector<long long>> memo; 2 long long dpAlgorithm(int a[], int n, int k) { 3 memo = vector<vector<long long>>(n, vector<long long>(k+1, -1)); 4 long long result = 0; 5 for (int j = 1; j < k+1; j++) { 6 for (int i = 0; i < n; i++) { 7 if (j == 1) { 8 memo[i][j] = a[i]; 9 } 10 else { 11 long long temp = 0; 12 for (int index = i + 1; index < n; index++) { 13 temp = max(temp, memo[index][j - 1]); 14 memo[i][j] = temp * a[i]; 15 } 16 } 17 if (j == k) result = max(result, memo[i][j]); 18 } 19 } 20 return result; 21 }
动规算法优化
要实现前面所述的动态规划算法,我们需要一个 n * (k + 1) 的二维矩阵去存储已经计算出的最优值,当 n 和 k 的值很大的时候,就需要更多额外的空间去求解。而实际上我们可以进一步的优化这个空间复杂度。因为在这个问题中,当我们选取一个固定的值时,我们是从其之后存储的最大值与该固定的值相乘,所以之前被存储的值事实上是可以被覆盖的。所以我们只需要一个一维的长度为n的矩阵去实现该算法。实现的代码如下所示:
1 vector<vector<long long>> memo; 2 long long dpAlgorithm2(int a[], int n, int k) { 3 vector<long long> memo2 = vector<long long>(n, -1); 4 long long result = 0; 5 for (int j = 1; j < k + 1; j++) { 6 for (int i = 0; i < n; i++) { 7 if (j == 1) { 8 memo2[i] = a[i]; 9 } 10 else { 11 for (int index = i + 1; index < n; index++) { 12 memo2[i] = max(memo2[i], a[i] * memo2[index]); 13 } 14 } 15 if (j == k) result = max(result, memo2[i]); 16 } 17 } 18 return result; 19 }
以上是关于动态规划:合唱团问题解析的主要内容,如果未能解决你的问题,请参考以下文章