合唱团(动态规划问题)

问题描述

题目描述
有 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);
    }
}

猜你喜欢

转载自blog.csdn.net/u010953266/article/details/79856047
今日推荐