问题描述
题目描述
有 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
问题来源
https://www.nowcoder.com/practice/661c49118ca241909add3a11c96408c8
问题分析
- 如果不考虑时间限制,可以直接通过暴力遍历的方式求解。
我们可以预先考虑好遍历的流程,
1° 依次选择前k个人
2° 计算k个人的能力值成绩,与全局最大乘积比较
3° 尝试修改最后一个人的选择方案,如果生成新的解决方案,则回到第二步,如果无法改变最后一个人的选择方案,那么就尝试修改倒数第二个人的选择方案,(其他情况类似考虑)
4° 在没有新的可执行方案下 输出全局最大乘积
这样的思路其实和递归一致,首先确认前n个人的选择方案,然后再遍历第n+1个人的可选方案,如果第n+2层,那么确定了n+1的方案之后可以下钻一层,如果当前的可选方案全部遍历完毕,那么就上钻一层,直至无新的可选方案。
这里需要一个类似栈的结构来保存当前已经选择的方案,和下一轮的待选位置。在我的个人实践中,我觉得用数组+游标会更加方便一些。当然采用遍历的方式的代码时间复杂度很高,最后无法满足题目的时间限制(在文章最后,我会附上这种思路的实现代码)
- 考虑动态规划的思路
我们想要在 n个人中以此选择k个人,最后要求k个人的能力值最大
那么我们如果最后方案中包含第n个人,那么我们就需要在前n-1个人中选择k-1个人,使得他们的能力值的乘积最大(这里暂时不分析 能力值会出现负数的情况,之后会给出解释)
基于这样的考虑,我们可以把问题缩小为,求解前 i 个人中 k-1 个人的能力乘积最大值,(i的取值范围为 k-1到n),然后利用这个结果,我们可以得到在前i+1个人中选择k个人(包含第i+1个人)的能力乘积最大值,通过比较的结果,我们可以拿到全局最大值。
我们可以将问题递归的缩减下去,直至将问题分解为 求解前 i 个人中 1 个人的能力最大值乘积(即能力值本身)
上面文字可能显得有些晦涩,我接下来用符号来描述,希望对读者理解方法本质有帮助。
首先定义数组 dpMax[ k ] [n ] : dpMax[i][j]表示从前j个人中选择i个人(需要包括第j个人),在这种情况下我们能得到的最大数字成绩。
dpMax[ k] [ j ] = abilityList[j] * Max ( dpMax[k-1] [ h] ) h<j && h>0 && j - h >D
另外我们基于题目可以得知dpMax[1][i] : dpMax[1][j] = abilityList[j]
将上面的过程理顺,我们可以通过求解 Max( dpMax[k ][h] ) h属于于[ k , n] 来获得全局最大值。
最后我们讨论一下能力值可能存在负数的问题,在一开始的问题分析的时候,如果考虑这一部分,那么会干扰我们的思路。如果我们需要得到在前i个人中选择j个人(包含第i个人)的情况下,得到乘积最大值。如果第i个人是负数,那么他就要求我们需要求得在前i-1个人中选择j-1个人的情况下,得到乘积最小值。如果第i个人的能力值是正数,我们仍然是需要求得在前i-1个人中选择j-1个人的情况下,得到乘积最大值。
基于这样的分析,我们需要维护2个dp数组,一个用来维护上一轮最小值,一个用来维护上一轮的最大值。
动态规划代码实现
public class DpMain {
public static void main(String[] args){
Scanner in = new Scanner(System.in);
int NumCount = in.nextInt();
int[] abilityList = new int[NumCount+1];
for(int i = 1 ; i <= NumCount ; i++){
abilityList[i] = in.nextInt();
}
int K = in.nextInt();
int D = in.nextInt();
//定义dp数组
//dp[i][j] 表示从前j个数中选择包含第j个数的数列的计算值
long[][] dpMaxList = new long[K+1][NumCount+1];
long[][] dpMinList = new long[K+1][NumCount+1];
//首先初始化dp[1][]
for(int i = 1; i <= NumCount ;i++){
dpMaxList[1][i] = dpMinList[1][i] = abilityList[i];
}
long cachedMaxValue = 0;
long cachedMinValue = 0;
for(int layer = 2; layer <= K ; layer++){
for(int j = layer; j <= NumCount; j++){
cachedMaxValue = Long.MIN_VALUE;
cachedMinValue = Long.MAX_VALUE;
for(int preIndex = j-1; preIndex >= layer-1 && j - preIndex <= D ; preIndex--){
long multiplyMax = dpMaxList[layer-1][preIndex] * abilityList[j];
long multiplyMin = dpMinList[layer-1][preIndex] * abilityList[j];
//促使cachedMaxValue cachedMinValue 记录全局最小值
if(multiplyMax > multiplyMin){
if(cachedMaxValue < multiplyMax){
cachedMaxValue = multiplyMax;
}
if(cachedMinValue > multiplyMin){
cachedMinValue = multiplyMin;
}
}else{
if(cachedMaxValue < multiplyMin){
cachedMaxValue = multiplyMin;
}
if(cachedMinValue > multiplyMax){
cachedMinValue = multiplyMax;
}
}
}
dpMaxList[layer][j] = cachedMaxValue;
dpMinList[layer][j] = cachedMinValue;
}
}
long GlobalKMax = Long.MIN_VALUE;
for(int i = K ; i <= NumCount ; i++){
if(dpMaxList[K][i] > GlobalKMax){
GlobalKMax = dpMaxList[K][i];
}
}
System.out.println(GlobalKMax);
}
}
这个实现肯定不是最优实现,代码基本展现了我之前对问题的分析。这里给出一个可行的优化方案,因为我们只需要维护前两2轮的最小值和最大值,所以dp数组可以没必要开这么大,当然这个也是dp问题常见的优化方法之一。
附录:暴力遍历方法的代码实现
public class Main {
public static void main(String[] args){
Scanner in = new Scanner(System.in);
int numCount = in.nextInt();
int[] abilityArray = new int[numCount];
for(int i = 0 ; i < numCount ; i++){
abilityArray[i] = in.nextInt();
}
int k = in.nextInt();
int d = in.nextInt();
long MaxValue = Long.MIN_VALUE;
int[] IndexList = new int[k];
//初始化数据
int ValueCache = 1;
IndexList[0] = -1;
int SelectIndex = 0;//指向现在正在挑选的位置
//开始遍历
while(true){
if(SelectIndex < 0 || SelectIndex >= k){
break;
}
IndexList[SelectIndex]++;
int CachedCurrentIndexValue = IndexList[SelectIndex];
if ((k - SelectIndex) > (numCount - CachedCurrentIndexValue)){
//(k - SelectIndex -1) > (numCount - CachedCurrentIndexValue -1)
//假如待选个数 大于 可选个数,那么就要终止后续的选择或者判断
SelectIndex --;
continue;
}
if(SelectIndex > 0 && (CachedCurrentIndexValue - IndexList[SelectIndex-1]) > d ){
SelectIndex --;
continue;
}
if(SelectIndex == (k-1)){
//选择好了 k个数
long multiplyValue = 1;
for(int i = 0 ;i < k ; i++){
multiplyValue *= abilityArray[IndexList[i]];
}
if (MaxValue < multiplyValue) {
MaxValue = multiplyValue;
}
}else {
//如果还没有选择完 那么就要开始选择下一位
SelectIndex++;
IndexList[SelectIndex] = CachedCurrentIndexValue;
}
}
System.out.println(MaxValue);
}
}