1. 程式人生 > >清晰解題: 網易筆試合唱團

清晰解題: 網易筆試合唱團

閒言: 一切講解不清晰的演算法博文== 磨鍊讀者自學能力

題目: 合唱團(網易程式設計題)

有 n 個學生站成一排,每個學生有一個能力值,牛牛想從這 n 個學生中按照順序選取 k 名學生,要求相鄰兩個學生的位置編號的差不超過 d,使得這 k 個學生的能力值的乘積最大,你能返回最大的乘積嗎?

輸入描述:

  • 每個輸入包含 1 個測試用例。每個測試資料的第一行包含一個整數$ n (1 \leq n\leq 50)$,表示學生的個數,接下來的一行,包含 n 個整數,按順序表示每個學生的能力值 aia_i50ai50-50 \leq a_i \leq 50)。接下來的一行包含兩個整數,k 和 d
    (1k10,1d50)d (1 \leq k \leq 10, 1 \leq d \leq 50)

輸出描述

  • 輸出一行表示最大的乘積

輸入例子:

3
7 4 7
2 50

輸出例子:

49

先修知識:

  • 動態規劃: 動態規劃表面上很難,其實存在很簡單的套路:當求解的問題滿足以下兩個條件時, 就應該使用動態規劃:
  1. 主問題的答案 包含了 可分解的子問題答案 (也就是說,問題可以被遞迴的思想求解)
  2. 遞迴求解時, 很多子問題的答案會被多次重複利用
  • 動態規劃的本質思想就是遞迴, 但如果直接應用遞迴方法, 子問題的答案會被重複計算產生浪費, 同時遞迴更加耗費棧記憶體(具體為什麼更加消耗棧記憶體, 需要額外瞭解函式呼叫過程中, 程序棧記憶體的管理方式), 所以通常用一個二維矩陣(表格)來儲存不同子問題的答案, 避免重複計算。

題目難點:

  • 元素有正有負
  • 如何滿足相鄰元素的距離不超過d 的限制

巧妙地分解問題

  • 給定n個元素, 尋找k 個元素使乘積最大,可以從這k 個元素中最後一個元素所在的位置入手來思考。

  • 對於陣列 a=【7,4,7】, 假如 k()k(所需元素個數) =2, d()d(相鄰元素的最大編號差)=2. 如果假設 a[2] 為目標序列的最後一個元素時, 還需要在a[2] 之前的元素中,尋找到一個長度為 $k-1 $的乘積序列, 且該序列的最後一個元素a[p] 與a[2]的距離小於等於d, 即 2<

    =d2-p <=d

  • 沿著上述思路考慮, 當 a[p] 作為最後一個元素時, 能獲得的長度為 k1k-1 的最大乘積序列的值是多少. 由於k=2k =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 , k=0, d=0;a
		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(computeBestK(array, k , d));
	}

	public static long computeBestK(int[] array, int k, int d) {
		
		if(array.length == 0 || k == 0 || d ==0)
			return 0;
		if(array.length == 1 && k == 1 )
			return array[0];
		
		if(array.length >1 && k >=1 )
		{
			long max = Long.MIN_VALUE;
			
			for (int i = k-1; i < array.length; i++) {
				long maxEndByCurrent = computeMaxEndBy(array, k, d, i);
				if( max < maxEndByCurrent)
					max = maxEndByCurrent;
			}
			return max;
			
		}
		else
		{
			System.out.println("input case error");
			return -1;
		}
	}

	private static long computeMaxEndBy(int[] array, int k, int d, int end) {
		if(k == 1)
			return array[end];
		
		long max = Long.MIN_VALUE;
		
		for (int j = 1; j <= d && (end-j)>=0 &&  (end-j)>= (k-1)-1; j++) {
			//(end-j)>= (k-1)-1 是需要保證在向前尋找的時候,結尾元素之前至少還需要有k-1個元素,否則元素數目不夠
			long res1 = array[end] * computeMaxEndBy(array, k-1, d, end-j);   ;
			long res2 = array[end] * computeMinEndBy(array, k-1, d, end-j);
			
			long larger = res1 > res2 ? res1: res2;
			
			if(max < larger)
				max = larger;
		}
		
		return max;
	}

	private static long computeMinEndBy(int[] array, int k, int d, int end) {
		if(k == 1)
			return array[end];
		
		long min = Long.MAX_VALUE;
		for( int j =1 ; j <= d && (end-j)>=0 && (end-j)>= (k-1)-1; j++)
			//(end-j)>= (k-1)-1 是需要保證在向前尋找的時候,結尾元素之前至少還需要有k-1個元素,否則元素數目不夠
		{
			long res1 = array[end] * computeMaxEndBy(array, k-1, d, end-j);   ;
			long res2 = array[end] * computeMinEndBy(array, k-1, d, end-j);
			
			long smaller = res1 < res2 ? res1: res2;
			
			if(min > smaller)
				min = smaller;
		}
		if( min == Long.MAX_VALUE)
			System.out.println("k"+k+"d"+d+"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;
	
	}

}