No history yet

动态规划中的对数优化

Transcript

Beau

所以,Jo,我们之前聊了这么多对数在算法复杂度,比如二分搜索和平衡树里的应用,感觉都是在分析层面。它真的能直接用来优化代码本身的逻辑吗?尤其是在动态规划里面?

Jo

问到点子上了。当然可以,而且是在一些非常棘手的情况下。我们管这种技巧叫“对数域动态规划”,或者说 Log-space DP。

Beau

Log-space... 听起来很高深。所以是什么情况非用它不可?

Jo

最经典的一个场景,就是当你的 DP 转移方程里包含了大量的乘法,尤其是概率的连乘。想象一下,你有一个状态,它的值是一系列概率相乘得到的。

Beau

哦!我好像遇到过。比如计算一个事件序列发生的总概率。每个概率都是零点几,乘着乘着,很快就变成一个特别特别小的数,然后……就变成零了。浮点数下溢。

Jo

完全正确。计算机的浮点数精度是有限的。当你把一堆小于1的数乘在一起,结果会指数级地趋近于0,超出了 `double` 能表示的范围,就直接被当成0了。这在很多算法里是致命的。

Beau

那对数怎么解决这个问题?我猜……跟我们之前聊的,把乘法变加法有关?

Jo

正是。核心思想就是:我们不直接存储概率 P,而是存储它的对数,log(P)。

Beau

哦……等一下,我捋一捋。如果原来的转移方程是 `dp[i] = dp[i-1] * p_i`,那现在 `dp` 数组里存的是对数值,所以新的方程就变成了 `log(dp[i]) = log(dp[i-1] * p_i)`…… 根据对数定律,它就等于 `log(dp[i-1]) + log(p_i)`。

Jo

完全正确。你看,原来的乘法转移,现在变成了一个简单的加法转移。一个非常小的概率,比如 10 的负 20 次方,它的对数(比如以10为底)就是 -20。这是一个很正常的浮点数,完全不会溢出。你就一直在对数域里做加法,非常稳定。

Beau

这太巧妙了。所以整个 DP 过程都在对数域里计算,直到最后需要答案的时候,再用指数函数把它还原回去,比如求一个 `exp(log_p)`。

Jo

是的,很多时候甚至不需要还原。比如你只是想比较两个概率 P1 和 P2 的大小,那你直接比较 log(P1) 和 log(P2) 的大小就行了,因为对数函数是单调递增的。这就避免了最后一步还原可能带来的精度损失。

Beau

我明白了。这在一些概率 DP,或者像隐马尔可夫模型这种需要计算很长序列概率的算法里,简直是救命稻草。

Jo

是的。除了防止下溢,对数还能处理一些状态空间巨大的问题。有时候,DP 的状态值本身增长得特别快,比如斐波那契数列那样指数级增长,很快就会超出 `long long` 的范围。但它的对数值的增长就温和多了。

Beau

所以,本质上是用对数去“压缩”那个数值的动态范围,让它能被计算机舒服地处理。

Jo

说得很好,就是“压缩”这个词。其实这个思想在数论里也有体现,虽然不完全是 DP。比如离散对数问题,它就是研究在模运算的乘法群里,一个数是另一个数的多少次幂。这里面,指数和对数的关系也是核心,只不过是在一个有限的、循环的数学结构里。

Beau

那个……好像是密码学的基础?我记得看到过。听起来对数真的是个万金油工具,能把乘法世界的问题映射到加法世界来解决。

Jo

可以这么理解。它提供了一种变换视角的方法。当你在一个领域里举步维艰,比如连乘导致数值不稳定,或者状态值增长过快,试着取个对数,问题可能就转化成了一个更简单的、我们更擅长处理的加法问题。

Beau

所以,下次我再碰到那种DP状态里有连乘的,第一反应就应该是,能不能把它丢到对数域里去。用 `log` 来给我的 DP 方程降降维。

Jo

对,不是降维,更准确地说是“降级”。把乘法运算降级为加法运算。这恰恰是我们从最开始学习对数 `log(MN) = log(M) + log(N)` 时,就埋下的伏笔。现在,它在高级算法里开花结果了。

Beau

这个伏笔埋得可真够深的。从一个简单的数学公式,到解决复杂的工程计算问题。有意思。