斜率优化建图学习笔记 & JZOJ 地壳运动题解
本章学习斜率优化建图
请放心食用
引言
- 最小生成树() ()
- 从裸题到一丁点技巧,再到丧心病狂的神仙题
- 原始时间复杂度 与
- 永远的 ······
正话
本文第一句就是正话
像多次查询而你非得(无奈)每次重新建图,跑一边 的题
很多时候带有技巧性,优先考虑建图的优化
如果熟悉斜率优化 的话,像一些边有多种信息会产生贡献,如二维最小生成树(边有两条信息会在不同时候产生不一样的贡献)
可以考虑斜率那些事儿
配上例题,更好食用
地壳运动(mst)
JZ是一个坐落在地壳运动活跃的山区的城市,常受地质灾害的袭击。
城市中建立了N个应急避难所以躲避灾害,这些避难所从1~N编号。此外有M条道路连接这些避难所,所有避难所间均可通过这M条道路直接或间接到达。由于是在规划良好的市区,道路可以由若干个平行于x或y坐标轴的线段组成,所以避难所xi和yi之间的道路可以用(ui,vi)来表示,道路的长度为ui+vi。由于地壳运动会导致地面拉伸或收缩,可用两个实数k1,k2描述城市的伸缩程度,此时某条道路的实际长度变为 。有若干个独立的询问,每次询问给出k1和k2,政府都希望在此地壳运动前提下,以最小的花费维护其中一些道路,使得只用这些被维护的道路仍能使所有避难所间相互连通。因为花费与道路的实际总长成正比,所以你需要对每一次询问求出被维护道路的最短实际总长。
第一行三个整数N,M,Q,分别表示避难所数量、道路数量、询问数量。
接下来M行每行四个整数xi,yi,ui,vi。xi,yi表示道路连接的两个避难所编号,ui,vi意义如上文所述。
最后Q行每行两个实数,表示每次询问的的k1和k2。
输出Q行,每行一个实数,表示实际总长,保留三位小数。
4 8 3
2 1 3 6
3 2 0 7
4 1 7 0
1 4 4 6
2 1 2 7
1 2 2 10
2 2 5 5
4 4 8 9
0.626436771146 0.472537839745
0.977631137354 0.190235819672
0.418883351791 0.221987861358
12.253
9.671
6.878
解法
- 分,依题暴力,每次重新建图,重跑
- 分,用脑袋想想,去掉那些无用的边,捡一些可能的较好边,然后愉快拿到全场第二高分( , 有初一A掉的)
- 分,多样做法,下面介绍一种
依题有两块地区的花费
这让我们无法挑取好边,导致每次都要重新建图
倘若我们把它当成一次函数
化简它
你问我为何要化成这样,我只好回答你
1.参考其他人的解法
2.经验(虽然我没经验)
3.依题
前两点无可反驳,第三点怎个依题,愿闻其详
好,我们要找决策集合,所以一次函数的 与 要和 相关,而两地的实际长度由 决定
也就是说靠 来确定一条直线
就很自然想到拿这两个东西来当斜率或与截距相关
而本题要在两点间取最优边,基本套路就是用若干直线切决策点,最小化截距
因为 所以最小化截距就是最小化
达到目的,看具体实现
既然要最小化截距,那么我们就要维护下凸壳,上凸的绝对没有下凸的优(自己画一下)
所以我们就有了一点思绪
对于 这两地,我们需寻找其中的最优决策
而我们将在它的决策集合中挑选
基本会想到先将看成在平面直角坐标系中的一个点
然后配合化简的式子分析:
先前提到最小化截距
那么我们将决策集合的点先按 从小到大排序
然后单调队列维护下凸壳,也就是维护斜率单调递增
但尽管如此,我们的决策点仍旧是太多了,还是会超时
然后我们想想,如果斜率单调递增,那么对于斜率小于直线斜率的点对 和
因为排过序了,所以有
这两点你画画图,用直线去切,不难发现
和 ,后者更优(此时的直线斜率更小)
这样的话,我们凭此弹掉队首的劣点,就能确定最优的点是谁了,直接 取队首
但前提是斜率单调递增
把 排个序就好了,升序
具体来说,就是
- 维护每两个地区下凸壳
- 排序
- 每两个地区重新选边,弹去队首劣点,取新的队首作为最优决策
- 跑 ,算答案
代码(仍需开O)
#pragma GCC optimize(2) #pragma GCC optimize(3) #include<cstdio> #include<algorithm> #include<vector> using namespace std; const int N = 35 , Q = 200000; const double INF = 1e18; int n , m , q , p[N + 5][N + 5]; double ans[Q + 5] , dis[N + 5] , vis[N + 5] , g[N + 5][N + 5]; struct que{ int id; double k1 , k2 , k; }ask[Q + 5]; struct point{ int x , y; }; vector<point> vec[N + 5][N + 5] , e[N + 5][N + 5]; inline bool cmp(point x , point y) {return x.x < y.x;} inline bool cmp1(que x , que y) {return x.k < y.k;} inline void choose(que l) { for(register int i = 1; i <= n; i++) for(register int j = 1; j <= n; j++) g[i][j] = INF; for(register int i = 1; i <= n; i++) for(register int j = i + 1; j <= n; j++) if (e[i][j].size()) { if (e[i][j].size() - p[i][j] == 1) g[i][j] = 1.0 * e[i][j][p[i][j]].x * l.k1 + 1.0 * e[i][j][p[i][j]].y * l.k2; else { while (p[i][j] < e[i][j].size() - 1 && 1.0 * (e[i][j][p[i][j]].y - e[i][j][p[i][j] + 1].y) / (e[i][j][p[i][j]].x - e[i][j][p[i][j] + 1].x) < l.k) p[i][j]++; g[i][j] = 1.0 * e[i][j][p[i][j]].x * l.k1 + 1.0 * e[i][j][p[i][j]].y * l.k2;; } g[j][i] = g[i][j]; } } inline double prim() { for(register int i = 0; i <= n; i++) dis[i] = INF , vis[i] = 0; int k; double res = 0; dis[1] = 0; for(register int i = 1; i <= n; i++) { k = 0; for(register int j = 1; j <= n; j++) if (!vis[j] && dis[j] < dis[k]) k = j; vis[k] = 1 , res += dis[k]; for(register int j = 1; j <= n; j++) if (!vis[j] && dis[j] > g[k][j]) dis[j] = g[k][j]; } return res; } int main() { // freopen("mst.in" , "r" , stdin); // freopen("mst.out" , "w" , stdout); scanf("%d%d%d" , &n , &m , &q); int x , y , u , v , s; for(register int i = 1; i <= m; i++) { scanf("%d%d%d%d" , &x , &y , &u , &v); if (x == y) continue; if (x > y) swap(x , y); vec[x][y].push_back((point){u , v}); } for(register int i = 1; i <= n; i++) for(register int j = i + 1; j <= n; j++) if (vec[i][j].size()) { sort(vec[i][j].begin() , vec[i][j].end() , cmp); s = 0 , e[i][j].push_back(vec[i][j][0]); for(register int k = 1; k < vec[i][j].size(); k++) { if (vec[i][j][k].x == e[i][j][s].x) { if (vec[i][j][k].y >= e[i][j][s].y) continue; else e[i][j].pop_back() , s--; } if (s == 0) s++ , e[i][j].push_back(vec[i][j][k]); else { while (s > 0 && 1.0 * (e[i][j][s].y - e[i][j][s - 1].y) / (e[i][j][s].x - e[i][j][s - 1].x) > 1.0 * (vec[i][j][k].y - e[i][j][s].y) / (vec[i][j][k].x - e[i][j][s].x)) e[i][j].pop_back() , s--; s++ , e[i][j].push_back(vec[i][j][k]); } } } double k1 , k2; for(register int i = 1; i <= q; i++) { scanf("%lf%lf" , &k1 , &k2); ask[i].id = i , ask[i].k1 = k1 , ask[i].k2 = k2 , ask[i].k = -1.0 * k1 / k2; } sort(ask + 1 , ask + q + 1 , cmp1); for(register int i = 1; i <= q; i++) choose(ask[i]) , ans[ask[i].id] = prim(); for(register int i = 1; i <= q; i++) printf("%.3lf\n" , ans[i]); }
函数是很美妙的,关于斜率高深莫测的那些事儿,还得慢慢摸索
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具