天天看點

《劍指Offer》60:n個骰子的點數

題目

把n個骰子扔在地上,所有骰子朝上一面的點數之和為S。輸入n,列印出S的所有可能的值出現的機率。

分析

直接法

假設骰子有face面,有n個骰子,那麼總排列數就有faceⁿ個。(例如,有3個6面骰子,那麼其總排列數為6³=216個)。所有骰子的和最小值為n(假設骰子最小值為1),而和最大值為n * face(例如,有3個6面骰子,那麼和最大值為18), 那麼就有 (n * face - n + 1)個可能和值,那麼建立長度為(n * face - n + 1)的一維數組進行統計不同S出現的次數。

然後骰子分别依次一個一個地投,并将其可能的值累加,最後将相應數組元素自增。

最後,周遊數組,除以總排列數,得出結果。

該算法時間複雜度為O(faceⁿ),當n越大,運算時間越長。(n從12開始增大,等待時間就開始難以接受)空間複雜度為O(n * face)。

動态規劃

确定問題解的表達式。可将f(n, s)表示n個骰子點數的和為s的排列情況總數

确定狀态轉移方程。n個骰子點數和為s的種類數隻與n-1個骰子的和有關。因為一個普通骰子有六個點數,那麼第n個骰子可能出現1到6的點數。是以第n個骰子點數為1的話,f(n,s)=f(n-1,s-1),當第n個骰子點數為2的話,f(n,s)=f(n-1,s-2),…,依次類推。在n-1個骰子的基礎上,再增加一個骰子出現點數和為s的結果隻有這6種情況!那麼有:

f(n,s) = f(n-1,s-1) + f(n-1,s-2) + f(n-1,s-3) + f(n-1,s-4) + f(n-1,s-5) + f(n-1,s-6)
           

上面就是狀态轉移方程,已知初始階段的解為:當n=1時, f(1,1)=f(1,2)=f(1,3)=f(1,4)=f(1,5)=f(1,6)=1。

該算法時間複雜度為O(n²),空間複雜度為O(n²)。

以3個6面骰子為例,所用到dp[i][j]數組如下圖所示。

i\j 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
1 1 1 1 1 1 1
2 1 2 3 4 5 6 5 4 3 2 1
3 1 3 6 10 15 21 25 27 27 25 21 15 10 6 3 1

放碼

一、直接法

import java.util.Arrays;

public class SumOfNDices {

	public static int DICE_MAX_VALUE = 6;
	
	public double[] getProbability(int numOfDice) {
		if(numOfDice < 1) {
			throw new IllegalArgumentException();
		}
		
		int maxSum = DICE_MAX_VALUE * numOfDice;
		int[] sums = new int[maxSum - numOfDice + 1];
		Arrays.fill(sums, 0);
		
		setSums(numOfDice, sums);
		
		int total = (int)Math.pow(DICE_MAX_VALUE, numOfDice);
		
		return Arrays.stream(sums).mapToDouble((a)->(a * 1.0 / total)).toArray();
		
	}
	
	public void setSums(int numOfDice, int[] sums) {
		for(int i = 1; i <= DICE_MAX_VALUE; i++) {
			setSums(numOfDice, numOfDice - 1, i, sums);
		}
	}
	
	public void setSums(int numOfDice, int leftNumOfDice, int sum, int[] sums) {
		if(leftNumOfDice == 0) {
			sums[sum - numOfDice]++;
		}else {
			for(int i = 1; i <= DICE_MAX_VALUE; i++) {
				setSums(numOfDice, leftNumOfDice - 1, i + sum, sums);
			}
		}
	}
	
}
           

二、動态規劃(二維數組)

public double[] getProbability2(int numOfDice) {
	if(numOfDice < 1) {
		throw new IllegalArgumentException();
	}
	int[][] dp = new int[numOfDice + 1][numOfDice * DICE_MAX_VALUE + 1];
	double[] result = new double[numOfDice * DICE_MAX_VALUE - numOfDice + 1];
	double total = Math.pow(DICE_MAX_VALUE, numOfDice);

	Arrays.fill(dp[1], 1, DICE_MAX_VALUE + 1, 1);

	for(int i = 1; i <= numOfDice; i++) {//如1, 2, 3, 4, 5, 6 
		for(int j = i; j <= DICE_MAX_VALUE * numOfDice; j++) {//n個6面骰子的和的可能值 :6, 7, 8, 9, ...  
			for(int k = 1; k <= DICE_MAX_VALUE; k++) {//f(n, s) = f(n - 1, s - 1) + f(n - 1, s - 2) + f(n - 1, s - 3) + ...  
				dp[i][j] += (j >= k ? dp[i - 1][j - k] : 0); // j >= k 預防數組越界 

				if(i == numOfDice) {
					result[j - i] = dp[i][j] / total;
				}
			}
		}
	}

	return result;
}
           

由于每個dp[i][j]隻于i-1時刻的狀态有關,是以可以删去一個次元,簡化算法。

三、動态規劃(一維數組)

  • 在上述解法的基礎上,删去一個次元
  • 第二個循環從後往前周遊,避免覆寫
public double[] getProbability3(int numOfDice) {
	if(numOfDice < 1) {
		throw new IllegalArgumentException();
	}
	int[] dp = new int[numOfDice * DICE_MAX_VALUE + 1];
	double[] result = new double[numOfDice * DICE_MAX_VALUE - numOfDice + 1];
	double total = Math.pow(DICE_MAX_VALUE, numOfDice);
	
	for(int i = 1; i <= DICE_MAX_VALUE; i++) {
		dp[i] = 1;
		result[i - 1] = 1.0 / DICE_MAX_VALUE;
	}
	
	for(int i = 2; i <= numOfDice; i++) {
		for(int j = DICE_MAX_VALUE * numOfDice; j >= 1; j--) {
			
			int temp = 0;
			for(int k = 1; k <= DICE_MAX_VALUE; k++) {
				temp += (j >= k) ? dp[j - k] : 0;
			}
			dp[j] = temp;
			
			if(i == numOfDice && j >= numOfDice) {
				result[j - i] = dp[j] / total;
			}
			
		}
	}
	
	return result;
}
           

測試

import java.util.Arrays;

import org.junit.Assert;
import org.junit.Test;

public class SumOfNDicesTest {

	@Test
	public void test() {
		SumOfNDices sd = new SumOfNDices();
		double[] result = sd.getProbability(1);
		System.out.println(result.length);
		System.out.println(Arrays.toString(result));
	}

	@Test
	public void test2() {
		SumOfNDices sd = new SumOfNDices();
		double[] result = sd.getProbability(10);
		double[] result2 = sd.getProbability2(10);
		
		Assert.assertArrayEquals(result, result2, 1E-10);
	}
	
	@Test
	public void test3() {
		SumOfNDices sd = new SumOfNDices();
		double[] result = sd.getProbability(10);
		double[] result3 = sd.getProbability3(10);

		Assert.assertArrayEquals(result, result3, 1E-10);
	}
}