【番外】动态规划的刷表法和记表法

360 阅读3分钟

掘金更文挑战

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第5天,点击查看活动详情


我大抵是病了,面前的两道题都写不出来,一道是动规题,另一道还是动规题。

百题水平的选手,识别动规并不是什么难事,但是能看出来和能写出来还是差了很远的距离。这篇文章将带大家填平这中间的部分沟壑。

本次要介绍的是动态规划的一种解题技巧,刷表法和记表法。

刷表法和记表法所解决主要问题:状态转移方程中新状态是若干个老状态的映射,加和可,取最值亦可,但应满足结合律。可以帮助我们在知道转移方程后,快速写出没有bug代码。

剑指 Offer 60. n个骰子的点数 为例,题目要求n个骰子组成点数和的概率情况:

假设 n - 1个骰子点数和为x的解为 f(n-1, x),则点数符合的转移公式为:

f(n, x) = \sum_{i=1}^{6}{f(n-1, x-i)} \times \frac{1}{6}\

翻译成中文,就是n个骰子点数和为x的概率,是n-1个骰子点数和为x-6x-1 的概率和乘 1/6

每一个新的结果,一次计算便由上一轮的若干结果求得,是通过记录的前置值算新值,俗称记表法。

根据这种思想,可能会写出下面的代码:

public double[] dicesProbability(int n) {
        double p = 1/6.0;
        double[] ans = new double[]{p, p, p, p, p, p};
​
        for(int i=2; i<=n; i++){
            int len = ans.length + 5;
            double[] temp = new double[len];
            for(int j=0; j<len; j++){
                // 骰子数为n,点数和为x的概率是骰子数位n-1的时候,与x相差1~6的情况概率的和。
                for(int l = Math.max(0, j-5); l<=j && l < ans.length; l++){
                        temp[j] += ans[l];
                    }
                temp[j]/=6;
            }
            ans = temp;
        }
​
        return ans;
    }

但说实话,这种情况虽然直观,但是却不太好写。

for(int l = Math.max(0, j-5); l<=j && l < ans.length; l++)

上面循环中的l既要满足l指向的点数与x相差1~6,又要保证合法,不溢出。思维难度就比较大了,比较容易出错。

而这道题如果使用刷表法,

public double[] dicesProbability(int n) {
        double p = 1/6.0;
        double[] arr = new double[]{p, p, p, p, p, p};
​
        for(int i=1; i<n; i++){
            double[] temp = new double[arr.length + 5];
            for(int j=0; j<arr.length; j++){
                // 解前置状态即n-1个骰子点数和为x处,分别更新n个骰子x+1到x+6的状态
                for(int k=0; k<6; k++){
                    temp[j+k] += arr[j] * p;
                }
            }
            arr = temp;
        }
​
        return arr;
    }

由于f(n, x) (第n轮)通常是若干个f(n-1)(第n轮的值)的函数,我们可以在遍历到前置状态的某个值时,将变化累积到目标上。

刷表和记表的区别可以用一张图来概括:

刷表法与记表法.jpg

以下给出题目列表,一般来说,每道题都可以用两种方法解决,但每道题都偏向于使用某种解法。

题目列表

题目连接偏向解法
剑指 Offer 60. n个骰子的点数刷表法

题单更新中...