动态规划题目-合唱团
题目地址及我参考的解析
题目
[编程题]合唱团
- 热度指数:86848 时间限制:1秒 空间限制:32768K
有 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
我的思考过程
起初看题目与解析都非常费劲,@菜鸡华的解析很棒,但是我在看的时候还是有一些疑惑,通过走读他的源码与理解其他网友的解析与源码,我才弄明白这道题的解析过程。
总体解题思想梳理
我们首先定义one为“选中的同学中”的最后一名的位置(也就是第k名同学)。
我们用fmax[one][k]表示“先选上第one个同学,让其作为选中者的最后一名,加上这个同学总共需要选择k个同学”可获得的最大乘积,所以我们只需要求出每一名同学作为第one个可以获得的最大乘积之后,再比较“各人作为最后一名所获得的最大乘积”的最大值即可。
那么我们怎么求出所有同学的fmax[one][k]呢?
这里我们就需要对fmax[one][k]这个原问题进行分解了:
我们首先定义left为“被选中的同学中,one同学前面那个同学”的位置(也就是第k-1名同学)。
又因为不同同学的能力值有正有负,所以我们需要比较这两种情况下的最大值,如果第one名同学的能力值为正,那就需要乘最大的数才能得到最大值,如果能力值为负,那就需要乘最小的值才能获得最大值,比较得到的两个最大值,比较结果就是fmax[one][k]的值。
根据以上分析则fmax[one][k] = max{fmax[left][k-1]*arr[one],fmin[left][k-1]*arr[one]}
这里我们用fmin表示最小值,与fmax相对。
这里还要说一下left的取值范围,left需要 >= max{one-d,k-1} && <= one-1,必须大于等于k-1是因为“如果<k了,就没办法选满k个人了”。
最后得到了fmax之后,我们遍历比较“选中k个人时,不同同学作为最后一名被选中者所得到的最大乘积”,得到其中的最大值就是结果了。
代码及注释
private static void mine() {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
int arr[] = new int[n + 1];
for (int i = 1; i <= n; i++) {
arr[i] = scanner.nextInt();
}
int K = scanner.nextInt();
int D = scanner.nextInt();
//这个地方用k:代表着我需要求出来选择小于等于k个人的所有情况
//fmax[j][i]表示“先选上第j个同学,让其作为选中者的最后一名,总共需要选择i个”可获得的最大乘积
long[][] fmax = new long[n + 1][K + 1];
long[][] fmin = new long[n + 1][K + 1];
//初始化选择“总共需要选择1个的情况”
for (int i = 1; i <= n; i++) {
fmax[i][1] = arr[i];
fmin[i][1] = arr[i];
}
//从选择2个开始,一直到K个
for (int i = 2; i <= K; i++) {
/**
* 我们至少应该从第i位同学开始计算其/fmax[j][i]的值(要不然最后不可能选够i位学生),
* 这里我们令left为“被选中的同学们中,第j个同学前面那个同学的位置”,
* fmax[j][i] = 从left第一个可能的值(max{j-d,k-1}<=left<=j-1)开始
* 一直到j-1的位置,计算得到f[left][i-1]*arr[j]的值中最大的那个。
*/
for (int j = i; j <= n; j++) {
long tempmax = Long.MIN_VALUE;
long tempmin = Long.MAX_VALUE;
for (int left = Math.max(j - D, i - 1); left <= j - 1; left++) {
if (tempmax < Math.max(fmax[left][i - 1] * arr[j], fmin[left][i - 1] * arr[j])) {
tempmax = Math.max(fmax[left][i - 1] * arr[j], fmin[left][i - 1] * arr[j]);
}
if (tempmin > Math.min(fmax[left][i - 1] * arr[j], fmin[left][i - 1] * arr[j])) {
tempmin = Math.min(fmax[left][i - 1] * arr[j], fmin[left][i - 1] * arr[j]);
}
}
fmax[j][i] = tempmax;
fmin[j][i] = tempmin;
}
}
/**
* 遍历选择k位同学的一列,比较得出以谁为最后一名获得的乘积最大
*/
long result = Long.MIN_VALUE;
for (int i = K; i <= n; i++) {
if (result < fmax[i][K]) {
result = fmax[i][K];
}
}
System.out.println(result);
}
通过解析的思路总结可以借鉴的地方用来做动态规划题目
我们经常做从n中选k,单纯的从n中选k很简单,但是这个题难在加上了d的约束与能力值的正负。
但我们可以从中借鉴一下解析中思考此类问题的方式:
- 从n个人中顺序选择k个人,这k个人中一定会有个最后一名(ps:为什么不从第一名考虑呢?因为第一名不好递推),而且这个最后一名一定是在[n-k,n]中选择的,所以我们从这个角度入手,求出[n-k,n]范围的人他们作为最后一名出现,所得到的最大乘积,最后比较其中的最大值即可。
- 那么我们怎么求出某人作为最后一名出现可以获得的最大乘积呢?
- fmax[one][k] = max{fmax[left][k-1]*arr[one],fmin[left][k-1]*arr[one]}
- 这个公式的左边使我们要求的,右边是向前递推并结合题目的约束得到的。
最后
我觉得用眼睛光看是没有办法理解明白的,最好自己举个例子,走读一下代码比较好。