掘金更文挑战
持续创作,加速成长!这是我参与「掘金日新计划 · 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-6
到x-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轮的值)的函数,我们可以在遍历到前置状态的某个值时,将变化累积到目标上。
刷表和记表的区别可以用一张图来概括:
以下给出题目列表,一般来说,每道题都可以用两种方法解决,但每道题都偏向于使用某种解法。
题目列表
题目连接 | 偏向解法 |
---|---|
剑指 Offer 60. n个骰子的点数 | 刷表法 |
题单更新中...