P1220 关路灯小结
万恶之源
前言
本来只是打算记下笔记的,但是发现越写越多,心想要不就顺便改成题解吧,于是有了此文,本文也参考了一些题解,因此本文仅作为其它题解的一些补充,一些做题可能遇到的问题。
正题
1. 这是一道区间动态规划(最重要的还是要有)
区间dp:区间dp就是在区间上进行动态规划,求解一段区间上的最优解。主要是通过合并小区间的 最优解进而得出整个大区间上最优解的dp算法。
怎么看出来的呢,首先关灯是没有代价的,那么假如我们在最左边,那么只需要一路往右走就一定是最优解,但是如果在中间的话,那么可能一会往左一会往右,假如我们已经知道了某个区间的最优解,那么这个区间向左或向右再拓展一个单位也仍然会是最优的,因此满足区间dp的特点。
每次的选择也很简单,在一个区间里,要么往右走,要么往左走,并且我们可以很容易发现关完一个区间要么在最左边,要么在右边,否则会带来额外的浪费,所以我们的选择就可以表达成,在一个区间的最左边时,要么继续往左走,要么折返向右走,在最右边时也同理。由此我们可以得到状态转移方程为
dp[i][j][0] = min(dp[i+1][j][0]+power(),dp[i+1][j][1]+power());
dp[i][j][1] = min(dp[i][j-1][0]+power(),dp[i][j-1][1]+power());
//其中dp[a][b][0/1]表示关闭区间[a,b]的灯后在左边或在右边
//power()表示未关的灯的耗能
2.只有状态转移方程还不够
虽然状态转移方程有了,但是这题并没有那么容易就可以水过去,这里i和j的枚举方式也是一个麻烦的点。
首先很显然dp[c][c][0/1]是为0的,那么我们可以考虑从中间向两边拓展,但是这里也有两种方式
一种是外层枚举i,内层枚举j,即先枚举起点再拓展终点
一种是外层枚举j,内层枚举i。即先枚举终点再拓展起点
详见代码↓↓↓
for(int i=c-1;i>0;i--)
for(int j=c+1;j<=n;j++)
{
dp[i][j][0]=min(dp[i+1][j][0]+power(i,i+1,i,j+1),dp[i+1][j][1]+power(i,j,i,j+1));
dp[i][j][1]=min(dp[i][j-1][0]+power(i,j,i-1,j),dp[i][j-1][1]+power(j-1,j,i-1,j));
}//c为初始地点
for(int j=c;j<=n;j++){
for(int i=j-1;i>0;i--){
dp[i][j][0] = min(dp[i+1][j][0]+power(i,i+1,i,j+1),dp[i+1][j][1]+power(i,j,i,j+1));
dp[i][j][1] = min(dp[i][j-1][0]+power(i,j,i-1,j),dp[i][j-1][1]+power(j-1,j,i-1,j));
}
}
他们并不一定都是正解,为此我们可以尝试模拟一下,第一种是先枚举起点再终点,拓展方式是这样的,觉得图奇丑无比难以接受的话可以直接看下面的结论。
高能预警
外层循环即圈圈由深蓝到绿再到浅蓝拓展,内层循环即圈圈向右拓展,由状态转移方程可以看出在这种枚举方式下,有一些区间并不能顺利的从子结构递推至父结构(例如深蓝的圈圈都没有不能转移到右子区间),所以这种方法是不行的,(其实我猜想这种枚举方式可以用于刷表法,但是不知道行不行)再看第二种,先枚举终点,再枚举起点。
枚举方式是蓝,粉,橙,紫,圈圈向左拓展。显然,这种枚举方式在求dp[i][j][0/1]时,其左右的子区间的答案都是已经确定下来了的,所以我们可以轻松得到最优解。
总之,对于\(dp[i][j][0]\),我们要确保\(dp[i+1][j][0]/[1]\)和\(dp[i][j-1][0]/[1]\)已经更新,于是,我们正序枚举j,正序枚举i。
3. power()函数(计算剩余能耗)
另外一个很麻烦的点时如何计算剩余灯的能耗,直观来讲就是全部灯的能耗减去区间灯的能耗乘以时间即可,我们很容易想到可以用前缀和(sumv[x]表示从1号灯到x号灯的能耗总和)来计算,但是这里隐含着一个边界问题,首先先上代码
int power(int i,int j,int l,int r)
//l,r是左开右开区间(取不到)
//表示从第i号灯走到第j号灯时,除l到r之外的灯(未关的灯)的耗能
{
return (L[j]-L[i])*(sumv[n]-(sumv[r-1]-sumv[l]));
}
//(L[j]-L[i])是路程,数值上等于时间
//sumv[x]是x的前缀和,(sumv[r-1]-sumv[l])是所求区间的能耗和
这里为什么是r-1呢,其实改成r也没问题,不过就需要你在其它地方也做出相应的更改,这里使用r-1是保证计算时用的区间时开区间,也就是不取它,达到两边的统一,有一种对称美,看起来舒服一点
最重要的是可以避免一些谜之bug。至于怎么知道它是左开右开的区间可以写个测试前缀和的程序来试一下,分块调试自己的程序可以省去很多干扰。