闲言: 一切讲解不清晰的算法博文== 磨炼读者自学能力
本文参考合唱团——2016网易内推编程题
题目: 合唱团(网易编程题)
有 n 个学生站成一排,每个学生有一个能力值,牛牛想从这 n 个学生中按照顺序选取 k 名学生,要求相邻两个学生的位置编号的差不超过 d,使得这 k 个学生的能力值的乘积最大,你能返回最大的乘积吗?
输入描述:
- 每个输入包含 1 个测试用例。每个测试数据的第一行包含一个整数 n ( 1 ≤ n ≤ 50 ) n (1 \leq n \leq 50) n(1≤n≤50),表示学生的个数,接下来的一行,包含 n 个整数,按顺序表示每个学生的能力值 a i a_i ai( − 50 ≤ a i ≤ 50 -50 \leq a_i \leq 50 −50≤ai≤50)。接下来的一行包含两个整数,k 和 d ( 1 ≤ k ≤ 10 , 1 ≤ d ≤ 50 ) d (1 \leq k \leq 10, 1 \leq d \leq 50) d(1≤k≤10,1≤d≤50)。
输出描述
- 输出一行表示最大的乘积
输入例子:
3
7 4 7
2 50
输出例子:
49
先修知识:
- 动态规划: 动态规划表面上很难,其实存在很简单的套路:当求解的问题满足以下两个条件时, 就应该使用动态规划:
- 主问题的答案 包含了 可分解的子问题答案 (也就是说,问题可以被递归的思想求解)
- 递归求解时, 很多子问题的答案会被多次重复利用
- 动态规划的本质思想就是递归, 但如果直接应用递归方法, 子问题的答案会被重复计算产生浪费, 同时递归更加耗费栈内存(具体为什么更加消耗栈内存, 需要额外了解函数调用过程中, 进程栈内存的管理方式), 所以通常用一个二维矩阵(表格)来保存不同子问题的答案, 避免重复计算。
题目难点:
- 元素有正有负
- 如何满足相邻元素的距离不超过d 的限制
巧妙地分解问题
-
给定n个元素, 寻找k 个元素使乘积最大,可以从这k 个元素中最后一个元素所在的位置入手来思考。
-
对于数组 a=【7,4,7】, 假如 k ( 所 需 元 素 个 数 ) k(所需元素个数) k(所需元素个数) =2, d ( 相 邻 元 素 的 最 大 编 号 差 ) d(相邻元素的最大编号差) d(相邻元素的最大编号差)=2. 如果假设 a[2] 为目标序列的最后一个元素时, 还需要在a[2] 之前的元素中,寻找到一个长度为 $k-1 $的乘积序列, 且该序列的最后一个元素a[p] 与a[2]的距离小于等于d, 即 2 − p < = d 2-p <=d 2−p<=d
-
沿着上述思路考虑, 当 a[p] 作为最后一个元素时, 能获得的长度为 k − 1 k-1 k−1 的最大乘积序列的值是多少. 由于 k = 2 k =2 k=2 , 我们所需要的序列仅包含1个元素, 即 a[p] 本身, 此处 p 值只能取 0 或 1, 对应的乘积序列值分别为 7 和 4 。
-
得到子问题的解后,挑选其中最大的一个(这里是7) 与a[2](同样是7) 相乘, 求出以 a[2] 作为最后一个元素时, 能获得的最大乘积序列的值是49
-
以上分析中用到的例子都是正值, 当有负值时, 我们只需要额外计算, 当以a[i]为最后一个元素时,能获得的乘积序列的最小值是多少, 因为需要考虑负负得正。
通过上述分析可以发现该问题符合动态规划使用的两个条件
- 问题既可以被分解为若干子问题:
- 不断地尝试固定目标序列的最后一个元素, 在剩下的元素中, 寻找 $length -1 $ 的子序列。
- 有些子问题的答案又有可能被重复利用
- 在更换目标序列的最后一个元素后, 又需要再次搜寻一遍长度为 $length -1 $ 的子序列, 这个过程中包含了诸多重复计算。
建立表格 dpMax[i][j] , dpMin[p][q]
-
dpMax[i][j] 表示: 以数组中a[i] 为结尾元素时, 长度为 j+1 的最大乘积子序列的 乘积值
-
dpMin[i][j]表示: 以数组中a[i] 为结尾元素时, 长度为j+1的最小乘积子序列的 乘积值
-
显然当子序列长度 j=0也就是乘积序列长度为1 时, dpMax[i][j] == dpMin[i][j] = a[i]
-
这里用 j+1表示长度,而不用j表示的原因是避免数组空间有所浪费, 谁让数组的下标是从0开始的呢。。。没办法
-
以此为基础, 利用以下递推公式可以逐次求出任意位置的dpMax[i][j] 和 dpMin[i][j]
-
dpMax[i][j] =
biggest(
bigger(
dpMax[p][j-1] * a[i] ,
dpMin[p][j-1]* a[i])
)
) for p = i-1 ..... i-d
dpMin[i][j] =
smallest(
smaller(
dpMax[p][j-1] * a[i] ,
dpMin[p][j-1]* a[i])
)
) for p = i-1 ..... i-d
- 解释: biggest 函数 求的是多个值中的最大值, bigger函数求得是两个值中的较大值。
- 还需要注意 p > = 0
这里放上递归写法的JAVA代码,思路清晰,但是由于递归算法, 重复计算过多, 不能通过运行时间测试,最终需要改为DP:
import java.util.Scanner;
public class ComputeMaxProduct {
public static void main(String[] args) {
Scanner cin = new Scanner(System.in);
int n=0 , targetLength=0, maxDistance=0;
int[] array = null;
while(cin.hasNextInt())
{
n = cin.nextInt();
array = new int[n];
for (int i = 0; i < n; i++) {
array[i] = cin.nextInt();
}
targetLength = cin.nextInt();
maxDistance = cin.nextInt();
}
System.out.println(computeBestK(array, targetLength , maxDistance));
}
public static long computeBestK(int[] array, int targetLength, int maxDistance) {
if(array.length == 0 || targetLength == 0 || maxDistance ==0)
return 0;
if(array.length == 1 && targetLength == 1 )
return array[0];
if(array.length >1 && targetLength >=1 )
{
long max = Long.MIN_VALUE;
for (int i = targetLength-1; i < array.length; i++) {
long maxEndByCurrent = computeMaxEndBy(array, targetLength, maxDistance, i);
if( max < maxEndByCurrent)
max = maxEndByCurrent;
}
return max;
}
else
{
System.out.println("input case error");
return -1;
}
}
private static long computeMaxEndBy(int[] array, int targetLength, int maxDistance, int end) {
if(targetLength == 1)
return array[end];
long max = Long.MIN_VALUE;
for (int j = 1; j <= maxDistance && (end-j)>=0 && (end-j)>= (targetLength-1)-1; j++) {
//(end-j)>= (k-1)-1 是需要保证在向前寻找的时候,结尾元素之前至少还需要有k-1个元素,否则元素数目不够
long res1 = array[end] * computeMaxEndBy(array, targetLength-1, maxDistance, end-j); ;
long res2 = array[end] * computeMinEndBy(array, targetLength-1, maxDistance, end-j);
long larger = res1 > res2 ? res1: res2;
if(max < larger)
max = larger;
}
return max;
}
private static long computeMinEndBy(int[] array, int targetLength, int maxDistance, int end) {
if(targetLength == 1)
return array[end];
long min = Long.MAX_VALUE;
for( int j =1 ; j <= maxDistance && (end-j)>=0 && (end-j)>= (targetLength-1)-1; j++)
//(end-j)>= (k-1)-1 是需要保证在向前寻找的时候,结尾元素之前至少还需要有k-1个元素,否则元素数目不够
{
long res1 = array[end] * computeMaxEndBy(array, targetLength-1, maxDistance, end-j); ;
long res2 = array[end] * computeMinEndBy(array, targetLength-1, maxDistance, end-j);
long smaller = res1 < res2 ? res1: res2;
if(min > smaller)
min = smaller;
}
if( min == Long.MAX_VALUE)
System.out.println("k"+targetLength+"d"+maxDistance+"end"+end);
return min;
}
这里放上DP解法的Java代码
import java.util.Scanner;
public class ComputeMaxProductDP {
public static void main(String[] args) {
Scanner cin = new Scanner(System.in);
int n = 0, k = 0, d = 0;
int[] array = null;
while (cin.hasNextInt()) {
n = cin.nextInt();
array = new int[n];
for (int i = 0; i < n; i++) {
array[i] = cin.nextInt();
}
k = cin.nextInt();
d = cin.nextInt();
}
System.out.println(computeMaxProduct(array, k, d));
}
static long max(long a, long b) {
return a > b ? a : b;
};
static long min(long a, long b) {
return a < b ? a : b;
};
private static long computeMaxProduct(int[] array, int k, int d) {
long dpMax[][] = new long[array.length][k];
long dpMin[][] = new long[array.length][k];
// dpMax[i][j] 表示以数组元素A【i】作结尾时, 序列长度为j+1的最大乘积结果
for (int i = 0; i < array.length; i++) {
// 最大乘积序列长度为1 时, a[i] 作为结尾元素时, 乘积序列的结果就是它本身
dpMax[i][0] = array[i];
dpMin[i][0] = array[i];
}
// 状态转移方程是 dpMax[i][j] = max(dpMax[i-1][j-1]* A[i], dpMin[i-d][j-1] *
// A[i])
// Tip: 一定注意, dpMax[i][j] 的含义是乘积序列长度为 j+1 时 A【i】 为最后一个元素时, 能够找到的最大乘积结果。 使用 j+1 的原因是因为数组下标从 0 开始, 如果用 j 表示长度, 那么j=0 位置的元素是无意义的, 该位置的空间会被浪费
long maxSoFar = Long.MIN_VALUE;
for (int j = 1; j < k; j++) {// 开始计算乘积序列长度大于1 的情况
for (int i = j ; i < array.length; i++) {
// 长度为 j+1 时, 结尾元素 i 的位置至少要从 j 开始找起, 以保证从a[0] 到 a[j] 至少有 j+1 个元素。
dpMax[i][j] = Long.MIN_VALUE;
dpMin[i][j] = Long.MAX_VALUE;
for (int x = 1; x <= d && (i - x) >= j - 1; x++) {
// 倒数第二个元素的位置为 i-x , 下标i-x 至少需要大于等于j-1
long resMax = max(dpMax[i - x][j - 1] * array[i], dpMin[i - x][j - 1] * array[i]);
long resMin = min(dpMax[i - x][j - 1] * array[i], dpMin[i - x][j - 1] * array[i]);
if (resMax > dpMax[i][j])
dpMax[i][j] = resMax;
if (resMin < dpMin[i][j])
dpMin[i][j] = resMin;
}
}
}
// 最后一个元素的位置从 k-1 找起,遍历一下已经计算好的DP 表格, 获得最终解
for (int i = k-1; i < array.length; i++) {
if (dpMax[i][k-1] > maxSoFar) {
maxSoFar = dpMax[i][k-1];
}
}
return maxSoFar;
}
}