动态规划(Ⅲ)

发布时间 2023-07-10 15:09:42作者: Bloodstalk

前言

这部分主要讲一讲 DP 优化的一些方法,显然我的实力不太够,所以只能写一些比较简单的东西。

动态规划(Ⅰ)中提到的,动态规划的优化一般就可以从两个方面入手:一个是状态表示、另一个是决策集合,也就是状态转移的角度来优化。下文主要就是分别从这两个角度,阐述不同的 DP 优化方法。

倍增优化 DP

倍增优化 DP,是从状态表示的方面来进行 DP 优化。简单来说,因为动态规划经常采用按阶段的递推形式实现,所以也可以按照类似的方法,使用倍增把阶段的线性增长优化为成倍增长,也就是把一段 \(\mathcal O(n)\) 的部分优化成 \(\mathcal O(\log n)\),显然,优化效果显著。

下面给一道例题来讲一下倍增优化 DP 的方式。

例题

P1081 [NOIP2012 提高组] 开车旅行

题面太长了就不放了。

这道题 \(70\) 分代码是比较好写的,建议先写一下 \(70\) 分的 \(\mathcal O(n^2)\) 做法:

维护四个数组 \(city_1[i],city_2[i],dis_1[i],dis_2[i]\),分别表示距离城市 \(i\) 最近的城市,次近的城市,以及它们分别到城市 \(i\) 的距离。我们朴素地暴力 \(\mathcal O(n^2)\) 便能解决这个问题。

然后对于两个问题,我们便运用这四个数组,\(O(nm)\) 的枚举即可,没啥难度。
code

然后我们考虑怎么优化,其实这个题我感觉不像是一个真正的 DP,因为你一旦确定了起点,其实你的路径就是固定的了,而正是因为这个性质,使得我们可以采用倍增的方法来优化我们的复杂度。


倍增优化 DP

想要倍增优化,我们就考虑我们想知道什么:行驶到了哪座城市、小 A 行驶的路程、小 B 行驶的路程。根据这个,我们设出倍增 DP 的数组:

下文中,\(k=0\) 代表小 A 开车,\(k=1\) 代表小 B 开车。

\(f_{i,j,k}\) 表示从城市 \(j\) 出发,行驶 \(2^i\) 天,\(k\) 先开车,最终会到达的城市;

\(da_{i,j,k}\) 表示从城市 \(j\) 出发,行驶 \(2^i\) 天,\(k\) 先开车,小 A 行驶的路程;

\(db_{i,j,k}\) 表示从城市 \(j\) 出发,行驶 \(2^i\) 天,\(k\) 先开车,小 B 行驶的路程。

假设我们已经知道了暴力解法中的四个数组的值,我们考虑怎么求倍增数组。

初值的设定:

f[0][i][0] = city2[i], f[0][i][1] = city1[i];
da[0][i][0] = dis2[i], da[0][i][1] = 0;
db[0][i][0] = 0 ,db[0][i][1] = dis1[i];

结合数组意义应该不难看懂,接下来,我们按照预处理倍增数组的一般思路,递增第一维。

需要注意的是,当 \(i=1\) 时,我们从 \(i=0\) 扩展而来,这时候司机是变了一个人的;而当 \(i>1\) 时,我们从 \(i-1\) 转移而来,这时第 \(2^i\) 天和 \(2^{i-1}\) 天都是偶数,是由同一个人转移而来,所以要分类讨论一下。

\(i=1\) 时:

f[1][j][k] = f[0][f[0][j][k]][k ^ 1];
da[1][j][k] = da[0][j][k] + da[0][f[0][j][k]][k ^ 1];
db[1][j][k] = db[0][j][k] + db[0][f[0][j][k]][k ^ 1];

\(i>1\) 时:

f[i][j][k] = f[i - 1][f[i - 1][j][k]][k];
da[i][j][k] = da[i - 1][j][k] + da[i - 1][f[i - 1][j][k]][k];
db[i][j][k] = db[i - 1][j][k] + db[i - 1][f[i - 1][j][k]][k];

求解问题

朴素的求解问题是一步一步模拟走过的城市。有了倍增数组后,我们自然就可以进行倍增跳步了。具体实现如下:

  • 我们给定 \(pos=0,disA = 0 ,disB= 0\),分别表示现在在哪个城市,小 A 的路程,小 B 的路程。

  • \(i\)\(\log n\) 枚举到 \(0\),判断 \(f_{i,pos,0}\) 是否越界,并且 \(disA + disB + da_{i,pos,0} + db_{i,pos,0} \leq x\) 是否成立。如果不越界且成立,那么就直接令 \(pos=f_{i,pos,0},disA += da_{i,pos,0},disB += db_{i,pos,0}\),这也就实现了我们的跳步操作。

第二步结束后,\(disA\)\(disB\) 就可以求出来了,有了这个,问题 \(1,2\) 就都很好解决了,我们就可以在 \(\mathcal O(n\log n)\) 的时间下解决问题。

dis = disA = disB = 0 , pos = i;
for(re int j=18;j>=0;j--)
{
	if(f[j][pos][0] != n+1 && disA + disB + da[j][pos][0] + db[j][pos][0] <= x)
	{
		disA += da[j][pos][0];
		disB += db[j][pos][0];
		pos = f[j][pos][0];
	}
}

解决根源的初始化

其实我们发现,我们还漏了一个地方,也就是那四个数组的初始化其实还是 \(\mathcal O(n^2)\) 的,但因为这个地方的优化跟倍增没有关系,所以放到最后来说。

具体来说,我们维护一个 multiset,存储节点编号和节点高度,初始时我们插入 \(4\) 个边界,两个 \(-\inf\),两个 \(\inf\),然后我们从右向左将每个城市插入进去,然后利用 multiset \(\mathcal O(\log n)\) 的查询它的前驱,前驱的前驱,后继,后继的后继,然后取一个最小值和次小值即可。(multiset 里面按高度从小到大排序)。

这样一来,初始化也是 \(\mathcal O(n\log n)\) 的了,综上,我们运用倍增优化 DP,就在 \(\mathcal O((n+m)\log n)\) 解决了这个问题。

code

此外的例题还有 AcWing294.计算重复,这里有两种倍增的手法,但其中一种更为优秀,具体可以看第一篇题解。

总结

倍增优化 DP 其实感觉不怎么像是 DP,因为能使用倍增 DP 的前提是,每个路线是已经确定的了,是静态的。当然这其中也有 DP 的思想,因为我们设的倍增数组其实就是 DP 的体现。

数据结构优化 DP