真的吗?|

LgxTpre

园龄:2年6个月粉丝:24关注:6

浅谈一类优化思想:费用提前计算

写在前面

这个知识点虽然微乎其微,但却十分有用。网上貌似没有什么人愿意整理这项内容,于是便随意记录了一下,如有不足还请指出。

不得不说我太弱了,感觉现在网上很多题解报告都很抽象模糊。所以这篇文章也主打的就是一个感性理解,可能显得讲的略有琐碎。有问题欢迎提出。

配套使用题单,除了例题外还有一些能用到这个知识点的题,但仍数量不多,欢迎私信扩容。

当前决策对未来行动的费用影响只与当前决策有关

不如从一道经典的例题入手:

任务安排

n 个任务按顺序分批执行,每批任务开始需要一个固定的启动时间 S。第 i 个任务花费的时间是 ti,每个任务的花费是它完成的时刻乘上它自身的费用系数 fi。需要找到一个最佳的分批顺序使得总费用最小。

这里不考虑高端的斜率优化,看看暴力的 DP 是怎么优化的。首先有一个显然的 O(n3) 的式子。记 Ti=j=1itjFi=j=1ifj,设 gi,j 表示将前 i 个任务分成 j 个组的最小花费,有转移方程:

gi,j=min0k<i{gk,j1+(S×j+Ti)×(FiFk)}

注意到这个 DP 的状态设计之所以记录 j 这一维,是因为需要知道前面有多少次启动了机器,即分成了多少批任务。但是事实上并不关心启动了几次机器,只关心到底因为 S 造成了多少费用。一批任务启动的时间 S 会累加到后面每一个任务上,所以可以将对后面任务造成的影响,累加到当前的费用中。设 gi 表示把前 i 个任务分成若干个组的最小花费,有转移方程:

gi=min0j<i{gj+Ti×(FiFj)+S×(FnFj)}

这样成功将问题优化到了 O(n2)

可以发现在这个问题中,选择一个决策会带来一个对未来的代价,但又不能通过在状态中再增加一维记录代价满足需求。这个代价是必然的,无论后面如何选择,都不会改变这个决策所带来的影响,这就是当前决策对未来行动的费用影响只与当前决策有关,我们可以把这个代价看作决策本身的费用,将未来的代价提前计算出来,在决策的时候就计算上它将会带来的代价,通过状态向后传递。

这类思想很经典的应用是以关路灯为母题的一类区间 DP,不妨以其为例题来探索一下。

在一条线段上有 n 个路灯分别在 pi 的位置上,每个单位时间造成 ci 的代价。一个人起初在 S,每个单位时间可以移动一个单位距离,走到路灯的位置可以关掉路灯,使其停止造成代价。求关掉所有路灯造成的最小代价。

如果不知道有费用提前计算这么个东西,那么设计的状态可能是 fi,j,tim,0/1 表示关掉了 [i,j] 这个区间的灯,已经进行了 tim 的时间,现在在左/右端点的最小花费。转移则为关掉上一个路灯的花费,加上总共用过的时间乘当前选的路灯的功率,这样设计复杂度必然爆炸。

但是我们现在会了新东西。 类似上一道题,我们发现之所以要记录 tim 这一维度,是因为要知道现在关掉的灯运行了多久,也就是曾经的决策影响现在的代价。事实上一个路灯只要在一个时刻没有被关掉,它就会不断产生代价,因此可以在决策选择哪一个路灯时,将所有开着的路灯造成的代价作为关掉它的费用一块计算出来,提前累加在这个状态中。这样便成功压缩掉了一维,记 fi,j,0/1 表示已经关掉了 [i,j] 区间的路灯,现在在左/右端点的最小花费。得到了转移方程:

fi,j,0=min{fi+1,j,0+disi+1i×k=1|k[i+1,j]nfi+1,j,1+disji×k=1|k[i+1,j]nfi,j,1=min{fi,j1,0+disij×k=1|k[i,j1]nfi,j1,1+disj1j×k=1|k[i,j1]n

距离与功率和可以使用前缀和什么的优化,这里不多赘述。

这一类问题还包括但不限于[SDOI2008] Sue 的小球修缮长城 Fixing the Great Wall[BalticOI 2009 Day1] 甲虫,思想和做法都是相同类似的。

做完了经典的模型,再来些不一样的题看看吧。

[CF441E] Valera and Number

给出一个数 x,对其进行 m 次操作,分别为 p% 的概率对它 ×2(100p)% 的概率对它 +1。求该数最终二进制下末尾 0 的个数的期望。

这两个操作分别对应着二进制下在末尾插入一个 0,将末尾一串 1 变成 0 再将最后一个 0 变成 1。这个 +1 操作造成的进位十分棘手,怎么样才能尽量减小它的影响呢?考虑时空倒流,从后往前操作。注意到一旦出现了一个 ×2,那么在时间轴上往前的一个 +1 操作将变成 +2,不会再对最低位造成影响。

示例

上图的 261430 的最低位的 0 一直没有受到影响。那么设 fi,j,k 表示进行了末尾的 i 次操作,最后连着进行了 k×2 的操作(即通过 ×2 在末尾获得了 k0),在这 k0 之前造成了 +j 的影响。可以发现进行了 ×2 操作,如果当前的 j 为偶数,则可以看做进行了 j/2+1,末尾多进行了一个 ×2(即多获得了一个 0);如果当前的 j 为奇数,则相当于 k0 前面的第一个数是 1,无论怎么操作后面 0 的数量都将不再变化,可以直接计算贡献。所有操作进行完之后,可以将初始值看做先进行了 x+1,再把这部分贡献算上。

这样就得到了一个 O(m3) 的做法。能不能继续优化呢?回顾前面讲的两道题的答案计算式子,对于任务安排,答案是 Cost(花费)=Time(时间)×f(费用系数);对于关路灯,答案是 W()=P(功率)×T(时间)。没错,正是因为他们的计算为一次函数,具有线性性,所以可以提前计算,直接累计(不是一次函数的状况读者可以思考一下为什么不行,后文还会阐述)。不要忘记了,期望本身也具有线性性!所以在 ×2j 为偶数的时候,我们把通过 ×2 多获得的那一个 0 单独计算出来,权值为 1,直接乘上概率累加到答案中即可。这样便可以省掉 k 这一维,记 fi,j 为进行了末尾 i 次操作,使得若干个 0 之前有 +j 的影响。记一个数 i 末尾 0 的个数为 sumi,答案就是 i=0k1j=0|j%2=0ifi,j×p+i=0kfk,i×sumx+i

参考代码:

cin>>x>>k>>p,p/=100,f[0][0]=1.0;
for(int i=0;i<k;++i) for(int j=0;j<=i;++j) if(f[i][j])
{
	if(!(j&1)) f[i+1][j/2]+=f[i][j]*p,ans+=f[i][j]*p*1ll; //这里为了方便理解,因为权值为1
	f[i+1][j+1]+=f[i][j]*(1-p);
}
for(int i=0;i<=k;++i) ans+=__build_ctz(x+i)*f[k][i];
cout<<fixed<<setprecision(9)<<ans<<'\n';

[ARC126D] Pure Straight

给定一个长度为 n 序列 A,有 Ai[1,k]。每次操作可以交换两个相邻的元素,求最少操作多少次可以使得 A 中存在一个区间为 p,Ap=1,Ap+1=2,,Ap+k1=k

注意到这个 k 特别小,可以考虑一下状压 DP。设 fi,S 表示当前考虑到了序列的前 i 个数,最终答案中已经排好了 S 二进制位上为 1 的数并连在了一起。当决策一个新的数的时候,可以选择放入最终答案,也可以选择不放入最终答案。不妨先假设新插入的这个数已经紧贴在先前排好的序列右边了,要插入的话直接按照规则插入,不需要计算从别的地方移动过来的费用。如果选择放进最终答案,费用就是把他移动到相应的位置,即以其为结尾的序列的逆序对的个数;如果不选择放进最终答案,那么代价就是让最后答案中所有比它小的数或所有比它大的数“跨过它”,但是每个数具体被跨过了几次,我们并不好知道。考虑费用提前计算,把“被跨过了几次”转换成“有多少个数跨过了它”,将费用均摊在每个剩下选中的每个点上。那么就是所有比他小的数都要跨过它一次,或者所有比它大的数都要跨过它一次,两者贪心取 min 即可。这时注意到一个 fi 的决策只与 fi1 有关,于是又节省掉一维,时间复杂度 O(n×2k)

参考代码:

#define all ((1<<k)-1)
cin>>n>>k,memset(f,0x3f,sizeof f),f[0]=0;
for(int i=1;i<=n;++i)
{
	cin>>x,--x;
	for(int j=all;~j;--j) if(f[j]!=INF)
	{
		if(!(j>>x&1)) f[j|(1<<x)]=min(f[j|(1<<x)],f[j]+__builtin_popcount(j&(~((1<<x)-1))));
		f[j]+=min(__buildtin_popcount(j),__builtin_popcount(j^all));
	}
}
cout<<f[all]<<'\n';

现在再来系统总结一下使用这类优化方法的情景:

  1. 无论以后发生什么,当前对未来的代价都不会被改变,可以将对未来的代价当做当前决策本身的费用提前计算。
  2. 对状态增加一维来记录决策对未来的影响造成的复杂度代价过高,不能接受,考虑直接在当前代价中提前计算。
  3. 时间观即从过去考虑当前。
  4. 对未来的代价是线性的关系,根据线性性可以直接累加。

对于第四点,我们将在后文讨论不是线性的另一种状况。

当前决策对未来的贡献与未来有关

依然通过一道例题来引入:

方块消除

给一个长度为 n 的方块序列,每个方块有一个颜色,每次消除一段颜色相同长度为 x 的方块,并获得 x2 的分数,消除后剩下的方块会合并起来。寻找一种最优的消除方式使得最终得分尽可能大,求最大的得分。

依然是序列上的操作,考虑使用使用区间 DP 来解决。首先把颜色相同段缩成一个点,记录颜色和长度。套路的,设 fi,j 表示消除了 [i,j] 获得的最大得分,那么对于除去合并的消除转移方程也是十分的好写 fi,j=max(fi+1,j+leni2,fi,j1+lenj2)。那么怎么跨区间合并呢?如果保存状态记录当前决策每一段方块是否被消除,依旧是奇大无比的神秘复杂度,无法承受。类比关路灯的“费用提前计算”,我们预先计算得分,考虑当前消掉了一个长度为 L 的方块,之前剩下了一个长度为 T 的方块,但是他们并不能简单的合并,因为现在的得分函数是二次函数而非线性关系,L2+T2(L+T)2,也就是过去的贡献和现在消去的长度是相关的,将时态向后整体推一个,便引出了我们的主题:当前决策对未来的贡献与未来有关

既然不能从过去考虑现在,那能不能从现在考虑未来呢?如果在当前决策把未来可能出现的情况提前计算好,通过状态传递到了未来,那到了未来是不是能直接把这个决策拿来用,是不是就能够进行转移了呢?不妨设 fi,j,k 表示消掉了 [i,j] 这个区间,且后面有 k 个位置和 j 位置合并在一起消掉了。对于这个题,考虑为什么只需要记录 j 右边有多少个和它一起消掉了呢?这里引用一下论文里的证明:

假设现在有四个位置 i<j<k1<k2 且处理好了 [i,j],记“关系”的含义为两个位置未来会连在一起被消掉。假如说有关系 ik1jk2,那也就是说 [j+1,k21] 间的块要在 j 被消掉前消掉,所以 j 之前的所有点不能往这个区域内连关系。因此得到了 [i,j1] 内的块只能往 [i,j] 上面连关系,而只有 j 自己能往 j 往后的区域连。

有了这个关系,方程便十分的好写了。如果 j 和后面 k 个块一起消掉,就有 fi,j,k=fi,j1,0+(k+1)2;如果在 [i,j1] 这个区间中还有和 j 颜色相同的块 x,那么可以先消除 [x+1,j1],然后再合并 xj 一起消除,于是有 fi,j,k=fi,x,k+1+fx+1,j1,0。最后的答案即为 f1,n,0。需要枚举端点 i,j,以及右边合并个数 k,还有在考虑区间 [i,j] 的时候对于第二个决策枚举左边的断点 x,因此时间复杂度 O(n4)

回顾这个问题,我们起初想使用费用提前计算,把每次对未来的贡献摊在当前自己身上。可是发现未来的决策并不只于当前决策有关,还与未来本身状态相关。于是又开了一维状态,将目光放长远,预测未来可能出现的状况并计算,记录在状态中传递到未来,并在未来直接使用,这依然是一类费用提前计算的问题。

如同上一门类一样,这类问题很经典的应用是以[IOI2005]Riv 河流为母题的一类树形 DP,不妨以其为例题来探索一下。

给定一棵 n 个点的树,点边均带权。你可以选取 k 个关键点(根节点本身为关键点且不计算在 k 个之内),使得每个节点到离他最近的是关键点的祖先(记为 Fi)的权值和最小,权值和的定义为 indisiFi×vali

不失一般性的,可以设 fi,j,0/1 表示现在在 i 号节点,在 ii 的子树中选取了 j 个关键点,有没有选取 i 的最小权值和。但是这样有一个巨大的问题——我们并不容易知道具体有多少个节点选择了 i 为祖先关键点,无法统计答案。既然不知道有多少个节点选择了 i 为祖先关键点,那我们不妨反着考虑:能不能很方便的知道当前节点选择了谁作为祖先关键点呢?如果知道了这个,那只需要把 i 上的答案再合并到 Fi 上就好了。

考虑 我们刚刚学会 的“当前决策对未来的贡献与未来有关”,这个句子和我们想搞的“当前节点对祖先的贡献与祖先有关”简直就是排比句啊!我们可以预测选择的节点是谁,并把它记录在状态中,于是就得到了设 fi,j,k,0/1 表示现在在 i 号节点,在 ii 的子树中选取了 j 个关键点,其中 i 的祖先关键点为 k,有没有选取 i 的最小权值和。首先遍历整棵树,在每个节点上先枚举它的祖先关键点是谁,再枚举在它和它的子树中选了几个关键点,对于每个不同数量的关键点的决策做一个类似于树上背包的东西,即对于每一个子节点枚举在它和其子树中共选择了 1k 个关键点,综上我们得到了一个时间复杂度为 O(n2k2)优秀 算法。

这种树形 DP 将本应在当前节点计算的费用延后到它的子孙上,预测并记录子孙的状态,类似的题目也是层出不穷,例如[NOI2006] 网络收费[NOI2008] 奥运物流

因为笔者见识较少,所以对于第二种问题遇到的并没有第一种那么多,因此也没有准备更多的例题。

我们不如趁此再来总结一下使用这类优化方法的情景:

  1. 未来的费用并不只于当前代价相关,还与未来本身的状态相关。
  2. 通过增加一维状态来记录对未来的预测,从而在未来能够直接使用。
  3. 时间观从当前考虑未来。
  4. 对未来的代价并非线性的关系,不能简单的累加。

一点小小的扩展

在上文中,我们通过改变时间观,用“费用提前计算”这种特殊的方法有效的优化了许多 DP 式子。事实上有关费用提前计算的一些技巧不止适用于 DP 的优化,我们以一个例子大概了解一下:

[SCOI2007] 修车

n 个车主来修车,总共有 m 个维修技术人员,不同技术人员对不同的车维修时间不同,现在需要安排维修顺序使得顾客平均等待时间最少。求最小平均等待时间。

平均等待时间最少,就是总等待时间最少。类似于提前计算中的第二个题,一个人到底等了多长时间并不好计算,但是一个人到底被等了多长时间是好知道的,所以通过费用提前计算,把一个人等的时间摊到每个对其来说需要被等的人身上。我们将维修人员拆点,连边表示第 i 个车主的车由第 j 个维修人员修,且是这个维修人员修的倒数第 k 辆车,那么费用即为 k×timi,j,因为倒数第 k 个维修,算上其自己会共有 k 个人等待。那么剩下的就是费用流板子了。

套路是死的,但是人的脑子是活的;问题是做不完的,但是思想是在总结经验和大胆猜想中不断提升的。只要我们敢于探索,勇于尝试,总有一天能够实现身为 OIer 的梦想。

参考资料与致谢

  • 徐源盛 《对一类动态规划问题的研究》
  • 部分题解与题目来自 do_while_true 的汇总和指导
  • 感谢 Larry76My_Youth 的阅读意见反馈

本文作者:LgxTpre

本文链接:https://www.cnblogs.com/LittleTwoawa/p/17220583.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   LgxTpre  阅读(490)  评论(5编辑  收藏  举报
评论
收藏
关注
推荐
深色
回顶
收起
点击右上角即可分享
微信分享提示