[CP / Codeforces] DP 的细节处理(一)

最近渐渐地会做一些基础的 DP 题了,但是因为细节上没处理好,常常花很多时间去 debug,导致效率不高,因此打算写这样一篇(一系列)文章分析下 DP 的细节处理。

以这道 1500* 的 E. Block Sequence 为例,当我们得出了:

(点击展开) dp[i]=dp[i+1]+1, i+1+v[i]>ndp[i]=min(dp[i+1+v[i]],dp[i+1]+1), i+1+v[i]n
这样的状态转移方程后,接下来要做的事情不是马上开一个 DP array 把递推式敲进去,因为前者几乎不需要任何思考,无论放在什么时候做,都可以很快完成。

在我看来,或者说,在我做了几十道 DP 题之后得出的惨痛教训告诉我,应当先思考两个问题:

  • DP 数组的初值是什么?
  • DP 数组的边界值是什么?

之所以把边界值放在初值后面考虑,是因为我一般先赋初值再去设置相对较少的边界值,如果倒过来的话,就需要加一些判断语句,否则边界值可能会被初值覆盖。

首先说初值。DP 数组的初值就是我们在执行递推代码前,DP 数组各个元素的初始值。一般来说,如果我们使用 vector 来创建 DP 数组,那么其初值就是 0(假设元素类型是整数)。在很多情况下,不需要特别关注此初值,保持默认的 0 即可,比如统计某某取法的个数啦、判断能获得的最大值啦……这样。然而,我必须说,这是一个坏习惯,因为你不知道什么时候会出问题。

就比如本题。本题求的是最小值,而 0 作为一个可能的答案(在本题中是最小的答案),如果作为初值,就会导致无论给什么输入,输出都是 0。正确的做法是设置一个很大的值,这个值需要满足两个条件:第一,不在答案的区间之内;第二,在给定的数据规模和运算次数下不会发生溢出。因此,我们可以使用 n 或者 0x3f3f3f3f 作为初值。

因此我建议把初值的设置作为每次做 DP 题时必要的步骤,显式的指明初值 以提醒自己。

注意,初值的具体取值视问题而定,比如背包问题,对于至少 / 恰好 / 至多等限定,需要不同的初值取值。

然后是边界值,可以理解为 dfs 的递归出口条件,根据题意和状态转移方程设置即可。对于线性 DP 来说,一维 DP 一般不会出错,比较容易出错的是高维,因为常常需要设置多个边界值,尤其是在引入降维优化空间复杂度时,我经常忘记设置边界值——不过今天写了这篇文章之后应该不会再犯类似的错误了。

对了,本题题解供参考:

void solve() {
int n;
std::cin >> n;
std::vector<int> v(n);
for (int i = 0; i < n; i++) {
std::cin >> v[i];
}
std::vector<int> dp(n + 1, n);
dp[n] = 0;
for (int i = n - 1; i >= 0; i--) {
dp[i] = dp[i + 1] + 1;
if (i + 1 + v[i] <= n)
dp[i] = std::min(dp[i], dp[i + 1 + v[i]]);
}
std::cout << dp[0] << '\n';
}
posted @   ZXPrism  阅读(24)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示