论区间dp:常用模型(附极角排序教程)
论区间dp:常用模型
- 前言
- 例1.关路灯
- 例2.青蛙的烦恼
- 例3.行政划分
- 总结
前言
区间dp很简单,重要的是判断在什么样的题里用区间dpdpdp
本文适合对区间dpdpdp有初步了解的人阅读
本文有三道例题
1.P1220 关路灯
2.青蛙的烦恼
3.行政划分
例1.关路灯
题面如下
一看题面,似乎找不到什么规律,难道要状压?肯定不行,会TLE
仔细想想,关掉的路灯必然是连续的,如下图
红色的路线一定优于蓝色路线,蓝色在由333走到555时,沿途关掉444将会取得更优解,途中红色就是这种做法
也就是说,不允许有“回首掏”操作的出现
这个很重要,基本的区间dp有明显的区间,而更普遍的情况就如这题,区间其实是隐藏起来的,不允许回首掏,那么必然连续,则可以形成区间,使用区间dp
接下来的步骤就简单多了,设状态dp[i][j][k]dp[i][j][k]dp[i][j][k]为关掉区间[i,j][i,j][i,j]的所有路灯的最小花费,如最后关掉的路灯位于区间左端点,kkk为000,位于右端点则为111
dp[i][j][0]dp[i][j][0]dp[i][j][0]可由dp[i+1][j][0]dp[i+1][j][0]dp[i+1][j][0]和dp[i+1][j][0]dp[i+1][j][0]dp[i+1][j][0]转移得到,dp[i][j][1]dp[i][j][1]dp[i][j][1]同理
状态转移方程如下
dp[i][j][0]=min(dp[i+1][j][0]+dist∗w,dp[i+1][j][1]+dist∗w)dp[i][j][0] = min(dp[i+1][j][0]+dist*w,dp[i+1][j][1]+dist*w)dp[i][j][0]=min(dp[i+1][j][0]+dist∗w,dp[i+1][j][1]+dist∗w)
dp[i][j][1]=min(dp[i][j−1][1]+dist∗w,dp[i][j−1][0]+dist∗w)dp[i][j][1] = min(dp[i][j-1][1]+dist*w,dp[i][j-1][0]+dist*w)dp[i][j][1]=min(dp[i][j−1][1]+dist∗w,dp[i][j−1][0]+dist∗w)
注意,distdistdist为所要走的路径长,互相之间不相等,详见代码
www为剩余亮着的路灯每秒耗电量,用前缀和可以O(1)O(1)O(1)求
代码在此
#include<bits/stdc++.h>
using namespace std;
int n,c;
int a[100010],p[100010];
int dp[300][300][3];
int sump[100010];
int sp = 0;
int dist[300][300];
int main(){cin>>n>>c;for(int i = 1;i<=250;i++){for(int j = 1;j<=250;j++){dp[i][j][0] = dp[i][j][1] = 2000000000;}}dp[c][c][1] = dp[c][c][0] = 0;for(int i = 1;i<=n;i++){cin>>a[i]>>p[i];sp+=p[i];sump[i] = sump[i-1]+p[i];}for(int i = 1;i<=n;i++){for(int j = i;j<=n;j++){dist[i][j] = a[j]-a[i];}}for(int len = 2;len<=n;len++){for(int i = 1,j = i+len-1;j<=n;i++,j++){dp[i][j][0] = min(dp[i+1][j][0]+(a[i+1]-a[i])*(sp-sump[j]+sump[i]),dp[i+1][j][1]+(dist[i][j])*(sp-sump[j]+sump[i]));dp[i][j][1] = min(dp[i][j-1][1]+(a[j]-a[j-1])*(sp-sump[j-1]+sump[i-1]),dp[i][j-1][0]+(dist[i][j])*(sp-sump[j-1]+sump[i-1]));}}cout<<min(dp[1][n][0],dp[1][n][1]);return 0;
}
例2.青蛙的烦恼
依旧先给题面
这题更抽象了,像极了最短Hamilton路径(状压dp,请看我的博客)
唯一的区别就在凸多边形,貌似没用,但是我们可以想一想,如果是最简单的四边形,情况是什么样呢?
容易想得到图中红色和蓝色的路径,那么哪个更优呢,必定是红色的
首先,两条路径均经过边232323,不考虑
然后,根据三角形两边之和大于第三边,得出了12+3412+3412+34小于13+2413+2413+24(此处数字表示边)
你一定也发现了规律,边不能交叉,这就导致了这题中回首掏操作一定不为最优解
那么又能得出什么呢?青蛙到过的点是连续的!区间有了,区间dpdpdp启动!
我们先破环为链,再倍长,这是解决环形问题的基本步骤
然后就和上一题没什么区别了
代码如下
#include<bits/stdc++.h>
using namespace std;
int n;
double ans = 100000000.00;
struct node{double x,y;
}a[114514];
double dp[1500][1500][2];
double dist(node a,node b){return sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y));
}
int main(){cin>>n;for(int i = 1;i<=n;i++){cin>>a[i].x>>a[i].y;a[i+n].x = a[i].x;a[i+n].y = a[i].y;}memset(dp,0x7f,sizeof(dp));dp[1][1][0] = dp[1][1][1] = dp[n+1][n+1][0] = dp[n+1][n+1][1] = 0;for(int len = 2;len<=n;len++){for(int i = 1,j = i+len-1;j<=n+n;i++,j++){dp[i][j][0] = min(dp[i][j][0],dp[i+1][j][0]+dist(a[i],a[i+1]));dp[i][j][0] = min(dp[i][j][0],dp[i+1][j][1]+dist(a[i],a[j]));dp[i][j][1] = min(dp[i][j][1],dp[i][j-1][0]+dist(a[j],a[i]));dp[i][j][1] = min(dp[i][j][1],dp[i][j-1][1]+dist(a[j],a[j-1]));}}for(int i = 1;i<=n;i++){ans = min(ans,dp[i][i+n-1][0]);ans = min(ans,dp[i][i+n-1][1]);}printf("%.3lf",ans);return 0;
}
例3.行政划分
题面
看到这个题,无论是谁都不会先想dpdpdp的,当务之急是解决那一串式子
我们可以先用完全平方公式,变形过程如下
SSS平均和SSS和NNN都定了,我们求最小ΣSi2\Sigma S_i^2ΣSi2就行了
这一题的线性dpdpdp更加明显,既然是分割,边就不能交叉,这样就不能回首掏,得到区间,区间dpdpdp再次启动
求三角形面积可以用海伦公式
设置状态dp[i][j]dp[i][j]dp[i][j]为分割完点从iii到jjj的最小ΣSi2\Sigma S_i^2ΣSi2
初始设置每个dp[i][i+1]dp[i][i+1]dp[i][i+1]为000,状态的转移可以枚举区间断点
如区间[i,j][i,j][i,j],边界i,ji,ji,j和断点kkk可以构成三角形,并上dp[i][k]dp[i][k]dp[i][k]和dp[k][j]dp[k][j]dp[k][j]即可得到整个区间
可以看下图
可以得出状态转移方程
dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j]+h∗h);dp[i][j] = min(dp[i][j],dp[i][k]+dp[k][j]+h*h);dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j]+h∗h);
注:hhh为三角形ijkijkijk的面积
然而到这还没完,题目中给定的点并不是按照一定顺序给的,我们要把它们排成顺时针或逆时针
这就要用到极角排序了
(以下内容和区间dpdpdp关系不大,如不想解决本题可以跳过)
极角排序就是选定一个原点,利用反正切函数来按照不同点和原点之间的极角大小来排序(反正切函数可以看作一定范围内,正切函数的反函数)
我们将原点选在这个凸包yyy坐标的最低点,如有多个最低点则选择靠左的一个,然后使用c++的atan2(y,x)atan2(y,x)atan2(y,x)函数,输入y,xy,xy,x坐标即可计算角度,与正常的反正切函数不同,本函数算出的角度属于000到180180180度,是的,909090度也能算,简直就是为极角排序专门设计的,orz%%%
代码这样写(极角排序)
bool cmp(node p,node q){if(atan2(p.y-miny,p.x-minx)!=atan2(q.y-miny,q.x-minx))return atan2(p.y-miny,p.x-minx)<atan2(q.y-miny,q.x-minx);else return p.x<q.x;
}
void jijiao_sort(){cin>>n;for(int i = 1;i<=n;i++){cin>>a[i].x>>a[i].y;if(a[i].y<miny||(a[i].y==miny&&a[i].x<minx)){miny = a[i].y;minx = a[i].x;}}sort(a+1,a+n+1,cmp);return;
}
注意了,为了防止原点不排在最前面,cmpcmpcmp函数里必须要判断arctanarctanarctan的值是否相等,如相等将左侧的点排在前面,必须,必须,必须,否则遇到yyy坐标相等就会排乱,将原点放在第二位,你就WA了,作者因为这个调了一下午,警示后人!!!
有了极角排序,我们终于可以完成这道题了
代码
#include<bits/stdc++.h>
using namespace std;
int n;
double minx = 1145141919.810,miny = 11145141919.810;
double dp[60][60];
double s,sb;
struct node{double x,y;
}a[114514];
double get_s(node a,node b,node c){double aa = sqrt((c.x-b.x)*(c.x-b.x)+(c.y-b.y)*(c.y-b.y));double bb = sqrt((c.x-a.x)*(c.x-a.x)+(c.y-a.y)*(c.y-a.y));double cc = sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y));double p = (aa+bb+cc)/2;return sqrt(p*(p-aa)*(p-bb)*(p-cc));
}
bool cmp(node p,node q){if(atan2(p.y-miny,p.x-minx)!=atan2(q.y-miny,q.x-minx))return atan2(p.y-miny,p.x-minx)<atan2(q.y-miny,q.x-minx);else return p.x<q.x;
}
void jijiao_sort(){cin>>n;for(int i = 1;i<=n;i++){cin>>a[i].x>>a[i].y;if(a[i].y<miny||(a[i].y==miny&&a[i].x<minx)){miny = a[i].y;minx = a[i].x;}}sort(a+1,a+n+1,cmp);return;
}
int main(){jijiao_sort();for(int i = 3;i<=n;i++){s+=get_s(a[1],a[i],a[i-1]);}sb = s/(n-2);memset(dp,0x7f,sizeof(dp));for(int i = 1;i<=n-1;i++){dp[i][i+1] = 0;}for(int len = 3;len<=n;len++){for(int i = 1,j = i+len-1;j<=n;i++,j++){for(int k = i+1;k<j;k++){double h = get_s(a[i],a[j],a[k]);dp[i][j] = min(dp[i][j],dp[i][k]+dp[k][j]+h*h);}}}double ans = (dp[1][n]-s*sb)/(n-2);if(ans<1e-7){ans = 0;}printf("%.2lf\n",sqrt(ans));return 0;
}
总结
经过了三道例题的训练,相信你也对区间dpdpdp的套路有一定理解了
区间dpdpdp的典型特征有区间合并(本文没讲这类,比较基础),不能回首掏,平面几何线段不交叉,看到这些,就多考虑考虑区间dpdpdp吧
本文作者是蒟蒻,如有错误请各位神犇指点
森林古猿出品,必属精品,请认准CSDN森林古猿1!