动态规划问题《掷骰子等于目标和的方法数》

82 阅读4分钟

首先看题目

这里有 n 个一样的骰子,每个骰子上都有 k 个面,分别标号为 1 到 k 。

给定三个整数 n ,  k 和 target ,返回可能的方式(从总共 **kn **种方式中)滚动骰子的数量,使正面朝上的数字之和等于 **target 。

答案可能很大,你需要对 109 + 7 取模 。

示例 1:

输入: n = 1, k = 6, target = 3
输出: 1
解释: 你扔一个有 6 个面的骰子。
得到 3 的和只有一种方法。

示例 2:

输入: n = 2, k = 6, target = 7
输出: 6
解释: 你扔两个骰子,每个骰子有 6 个面。
得到 7 的和有 6 种方法:1+6 2+5 3+4 4+3 5+2 6+1。

示例 3:

输入: n = 30, k = 30, target = 500
输出: 222616187
解释: 返回的结果必须是对 109 + 7 取模。

在答题开始之前,为了寻找到解题思路,我特意制作了一张表寻找数字之间的规律

image.png

我们现在假定骰子有6个面(即k=6),一共有4个骰子(即n=4),得到总数为14(target=14)有多少种可能

根据图表我们可以很清楚的得到答案为146,可是这个数字与其他数字有什么关联呢? 我们做如下假定: 如果最后一颗骰子的点数为1,那么前三颗骰子必须掷出13点(即n=3,target=13) 如果最后一颗骰子的点数为2,那么前三颗骰子必须掷出12点(即n=3,target=12) 如果最后一颗骰子的点数为3,那么前三颗骰子必须掷出11点(即n=3,target=11) 如果最后一颗骰子的点数为4,那么前三颗骰子必须掷出10点(即n=3,target=10) 如果最后一颗骰子的点数为5,那么前三颗骰子必须掷出9点(即n=3,target=9) 如果最后一颗骰子的点数为6,那么前三颗骰子必须掷出8点(即n=3,target=8)

那么,我把这六种可能加起来,不就可以得到n=4,target=14的解吗, 接下来我们顺着这个思路继续找规律,为了方便后续解读,我把这个表的结果定义为f[n][t] 根据上面的思路我们可以得到:

f[n][t]=f[n-1][t-1]+f[n-1][t-2]+……+f[n-1][t-k]

这时候,我们发现如果k的值特别大的时候,这个函数的计算量也特别的打,而且不可避免的有重复计算,比如:

f[4][14]=f[3][13]+f[3][12]+f[3][11]f[3][10]f[3][9]f[3][8]

f[4][13]=f[3][12]+f[3][11]f[3][10]f[3][9]f[3][8]+f[3][7]

在上面的两个例子中f[3][12]+f[3][11]f[3][10]f[3][9]f[3][8]被重复的计算了,为了减少函数的计算量,我们做出以下优化

f[4][14]=f[4][13]+f[3][13]-f[3][7]

即f[n][t]=f[n][t-1]+f[n-1][t-1]-f[t-1][t-k-1]

同时我们通过观察得出:

当n=4时,target=4的结果与target=24的结果相等

target=5的结果与target=23的结果相等

target=6的结果与target=22的结果相等

……

也就是说:数组的第一项和最后一项相等,第二项与倒数第二项相等,一直到数组的最中间

接下来

我们用代码稍微实现一下

/**
 * @param {number} n
 * @param {number} k
 * @param {number} target
 * @return {number}
 */
var numRollsToTarget = function(n,k,target) {
    const max = n*k;
    if (target < n || target > max) {
        return 0; // 无法组成 target
    }
    const MOD = 1_000_000_007;
    const f = new Array();
    f[0]=new Array(k).fill(1)
    
    for (let i = 1; i < n; i++) {
        f[i]=[1]
        let maxIndex = (i+1)*(k-1)
        f[i][maxIndex]=1
        for (let j = 1; j <= maxIndex/2+1; j++) {
            if(f[i][j]) continue;
            let number = j-k>=0
                ?(f[i][j-1]+f[i-1][j]-f[i-1][j-k]) % MOD
                :(f[i][j-1]+f[i-1][j]) % MOD
            f[i][j] = number
            f[i][maxIndex-j] = number
        }
    }
    return (f[n-1][target-n]+2*MOD)  % MOD;//避免出现负数的情况
};

至此,题目解答已完成

当我们遇到动态规划问题的时候,通常的思路是从最后一步开始,一步一步的往前推导,最终把复杂问题化为简单问题,才能最终解决问题。