清晰解题: 网易笔试合唱团

闲言: 一切讲解不清晰的算法博文== 磨炼读者自学能力

本文参考合唱团——2016网易内推编程题

题目: 合唱团(网易编程题)

有 n 个学生站成一排,每个学生有一个能力值,牛牛想从这 n 个学生中按照顺序选取 k 名学生,要求相邻两个学生的位置编号的差不超过 d,使得这 k 个学生的能力值的乘积最大,你能返回最大的乘积吗?

输入描述:

  • 每个输入包含 1 个测试用例。每个测试数据的第一行包含一个整数 n ( 1 ≤ n ≤ 50 ) n (1 \leq n \leq 50) n(1n50),表示学生的个数,接下来的一行,包含 n 个整数,按顺序表示每个学生的能力值 a i a_i ai − 50 ≤ a i ≤ 50 -50 \leq a_i \leq 50 50ai50)。接下来的一行包含两个整数,k 和 d ( 1 ≤ k ≤ 10 , 1 ≤ d ≤ 50 ) d (1 \leq k \leq 10, 1 \leq d \leq 50) d(1k10,1d50)

输出描述

  • 输出一行表示最大的乘积

输入例子:

3
7 4 7
2 50

输出例子:

49

先修知识:

  • 动态规划: 动态规划表面上很难,其实存在很简单的套路:当求解的问题满足以下两个条件时, 就应该使用动态规划:
  1. 主问题的答案 包含了 可分解的子问题答案 (也就是说,问题可以被递归的思想求解)
  2. 递归求解时, 很多子问题的答案会被多次重复利用
  • 动态规划的本质思想就是递归, 但如果直接应用递归方法, 子问题的答案会被重复计算产生浪费, 同时递归更加耗费栈内存(具体为什么更加消耗栈内存, 需要额外了解函数调用过程中, 进程栈内存的管理方式), 所以通常用一个二维矩阵(表格)来保存不同子问题的答案, 避免重复计算。

题目难点:

  • 元素有正有负
  • 如何满足相邻元素的距离不超过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<=d

  • 沿着上述思路考虑, 当 a[p] 作为最后一个元素时, 能获得的长度为 k − 1 k-1 k1 的最大乘积序列的值是多少. 由于 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;
	
	}

}

  • 16
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 14
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值