原题链接:https://www.luogu.com.cn/problem/P1442
在二维坐标系内有?n?个平台(定义平台是一条两端点纵坐标相同的开线段,开线段指线段两个端点不算做线段本身)和一个铁球,铁球如果下面没有物体,则每秒会下落一个单位长度。
球每次落到某个平台上后,游戏者可以选择水平向左或水平向右滚,球滚动速度是每秒?1?个单位长度。由于铁球的质量不太好,每次落下的高度不能超过 h。
设计一种策略,使得球尽快落到地面而不被摔碎。
假设地面高度为?0,且无限宽。球体积相对平台极小,可以看作一个质点。请注意,球滚动至平台的一个端点处即可下落,不需要滚动至下一个格子。例如下图,小球在?(9,9)?处已经开始下落。
第一行有两个整数,分别代表平台个数?n?和最大下落高度?h。
第二行有两个整数 x,y,表示铁球起始时在坐标为 (x,y)?的位置。
第?3到第 (n+2)?行,每行三个整数,第(i+2)?行的整数 hi?,li?,ri??分别代表第?i?个平台的端点纵坐标 hi??和左右端点的横坐标li?,ri?。
输出一行一个整数表示最小的坠落时间。
输入 #1
5 3 6 10 5 2 4 9 3 9 6 7 10 2 1 5 3 8 11
输出 #1
15
输入 #2
10 156 84 139 63 22 50 79 96 100 87 77 98 60 24 53 47 1 29 62 55 89 68 68 78 10 5 85 85 67 71 73 57 61
输出 #2
155
对于全部的测试点,保证:
首先当球在一个线段上时,存在俩种操作。
第一种操作是从线段左边往下滚掉到下一个线段。
第二种操作是从线段右边往下滚掉到下一个线段。
所以我们要么每次往左滚,要么就是往右滚,并且不管是往左边滚还是往右边滚都是只能掉到某一个确定的线段上的,也就是说可能的情况是有限的,这个观察题目描述中给出的图就可以发现了,这明显就是线性dp的常规操作,所以我们可以考虑进行dp,并且这个题目说了所有线段的l,r,h都是不同的,所以我们可以根据高度从小到达对所有线段进行排序,由于这个球从上面掉下来最终肯定会到达地面的某一个位置,我们可以把地面作为初始位置考虑从下往上进行dp。
状态定义:
定义f[i][0]表示从第i个线段开始,并且当前第i个线段往左边滚下去并且最终到达地面的最少花费时间,f[i][1]表示从第i个线段开始,并且当前第i个线段往右边滚下去并且最终到达地面的最少花费时间。
初始状态
我们是以地面为初始状态,所以f[0][0]=f[0][1]=0
状态转移
当然如果当前线段掉下去之后是到达地面要进行特殊处理
这个状态转移里面的时间花费就是从上往下掉落高度时间的花费,和在某个线段上移动的花费,总共只有四种情况,可以自己画四个图看一下四种情况就可以列出四个表达式了,这里我就不具体描述了,这个自己画个图一眼就看出来了。
(1)当前位置往左边掉下去
- f[i][0] = min(f[i][0], f[Left[i]][0] + a[i].stl - a[Left[i]].stl + a[i].h - a[Left[i]].h);
- f[i][0] = min(f[i][0], f[Left[i]][1] + a[Left[i]].str - a[i].stl + a[i].h - a[Left[i]].h);
(2)当前位置往右边掉下去
- f[i][1] = min(f[i][1], f[Right[i]][0] + a[i].str - a[Right[i]].stl + a[i].h - a[Right[i]].h);
- f[i][1] = min(f[i][1], f[Right[i]][1] + a[Right[i]].str - a[i].str + a[i].h - a[Right[i]].h);
(3)特殊处理当前位置掉下去是地面
- f[i][0] = a[i].h;
- f[i][1] = a[i].h;
上述dp过程描述完了,但是dp处理里面的Left[i]和Right[i]怎么计算呢,我来画一个图描述一下
在上述图片中设线段5再往下就是地面了,线段5所在位置高度为1 ,我们是从下往上进行dp,首先计算的是线段5,线段5往下只会掉到地面特殊处理,计算完线段5之后,线段5包含横坐标区间[2,4],也就是说上面的球掉下来的位置的横坐标如果是[2,4],那么会被线段5接住,不会直接掉到地面,也就是说我们要将线段5包含区间[2,4]修改为编号5,然后处理线段4,线段4往左掉下去位置为3,刚好包含在线段5,[2,4]之间,所以线段4往左掉下去到达的线段的编号就是5,然后区间[3,7]被线段4覆盖,区间[3,7]修改为4,之前的[3,4]由编号5变为编号4,然后对于上面的所有线段都进行同样的处理即可,但是这些处理怎么进行呢,我们可以发现这些处理实际上就是俩个操作,区间修改操作和单点查询操作,这不就是线段树的经典操作吗,所以可以使用线段树进行维护,具体描述见代码处。
总结:
这个题目思路还是比较简单的,但是代码确实比较难调,没办法涉及到线段树的题目调起来就是比较麻烦,我在写这个题目时遇到了几个问题导致调了很久。
(1)易错点:首先计算线段上的位移时间时应该用原本的线段左右端点进行计算,不能用离散化后的值进行计算,我就是因为一直用的离散化后的值进行计算导致调了很长时间才调出来。
cpp代码如下:
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10, M = N * 2;
int n, m;
struct Node
{
int stl, str; //stl,str表示的是线段初始的左右端点横坐标值
int l, r, h; //l,r表示的是线段离散化之后的左右端点的横坐标值
} a[N];
int startx, starty, cnt; //(startx,starty)是球的初始坐标,cnt记录离散化之后总的点的数量
int Left[N], Right[N]; //Left[i]表示点i往左边掉下去到达的线段的标号,Right[i]表示点i从右边掉下去到达的线段的编号
int points[M]; //points数组用于离散化
struct Tree //线段树的结构体数组
{
int l, r, id;
int add;
} tr[M * 4];
LL f[N][2]; //dp处理数组
void build(int u, int l, int r) //构建线段树
{
if (l == r)
tr[u] = {l, r, 0, 0};
else
{
tr[u] = {l, r, 0, 0};
int mid = l + r >> 1;
build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
}
}
void pushdown(int u) //线段树的下传懒标记
{
Tree &root = tr[u], &left = tr[u << 1], &right = tr[u << 1 | 1];
if (root.add)
{
left.add = root.add, left.id = root.add;
right.add = root.add, right.id = root.add;
root.add = 0;
}
}
void modify(int u, int l, int r, int id) //线段树区间修改函数
{
if (l <= tr[u].l && r >= tr[u].r)
{
tr[u].add = id;
tr[u].id = id;
}
else
{
pushdown(u);
int mid = tr[u].l + tr[u].r >> 1;
if (l <= mid)
modify(u << 1, l, r, id);
if (r > mid)
modify(u << 1 | 1, l, r, id);
}
}
int query(int u, int x) //线段树单点查询函数
{
if (x == tr[u].l && x == tr[u].r)
return tr[u].id;
else
{
pushdown(u);
int mid = tr[u].l + tr[u].r >> 1;
if (x <= mid)
return query(u << 1, x);
else if (x > mid)
return query(u << 1 | 1, x);
}
return -1;
}
int main()
{
cin >> n >> m;
cin >> startx >> starty;
n++;
a[n] = {startx, startx, startx, startx, starty};
//利用points将所有线段左右端点进行离散化
points[++cnt] = startx;
for (int i = 1; i < n; i++)
{
int l, r, h;
scanf("%d%d%d", &h, &l, &r);
a[i] = {l, r, l, r, h};
points[++cnt] = l, points[++cnt] = r;
}
//对points数组排序进行离散化
sort(points + 1, points + 1 + cnt);
//对所有线段按照高度从小到达排序
sort(a + 1, a + 1 + n, [&](Node A, Node B)
{ return A.h < B.h; });
//将离散化的数据进行填充
for (int i = 1; i <= n; i++)
{
a[i].l = lower_bound(points + 1, points + 1 + cnt, a[i].l) - points;
a[i].r = lower_bound(points + 1, points + 1 + cnt, a[i].r) - points;
}
build(1, 1, cnt); //初始化构建线段树
//对当前位置往左边掉下去或者往右边掉下去会到达的线段的编号进行处理
//线段树预处理
for (int i = 1; i <= n; i++)
{
Left[i] = query(1, a[i].l);
Right[i] = query(1, a[i].r);
modify(1, a[i].l, a[i].r, i);
}
//dp处理过程
memset(f, 0x3f, sizeof f);
f[0][0] = f[0][1] = 0;
for (int i = 1; i <= n; i++)
{
if (a[i].h - a[Left[i]].h <= m) //当前位置往左边掉下去
{
if (Left[i])
{ //花费的时间有俩部分,1.高度花费,2,在线段上移动的花费 这个花费可以看题目描述给出的图,很容易就可以看出来了这里就不仔细描述了
f[i][0] = min(f[i][0], f[Left[i]][0] + a[i].stl - a[Left[i]].stl + a[i].h - a[Left[i]].h);
f[i][0] = min(f[i][0], f[Left[i]][1] + a[Left[i]].str - a[i].stl + a[i].h - a[Left[i]].h);
}
else
f[i][0] = a[i].h; //到达的下一个线段是地面特殊处理
}
if (a[i].h - a[Right[i]].h <= m) //当前位置往右边掉下去
{
if (Right[i])
{ // 花费的时间有俩部分,1.高度花费,2,在线段上移动的花费 这个花费可以看题目描述给出的图,很容易就可以看出来了这里就不仔细描述了
f[i][1] = min(f[i][1], f[Right[i]][0] + a[i].str - a[Right[i]].stl + a[i].h - a[Right[i]].h);
f[i][1] = min(f[i][1], f[Right[i]][1] + a[Right[i]].str - a[i].str + a[i].h - a[Right[i]].h);
}
else
f[i][1] = a[i].h; //到达的下一个位置是地面特殊处理
}
}
//记录答案并输出
LL ans = 2e18;
for (int i = 1; i <= n; i++)
if (a[i].stl == startx && a[i].str == startx)
{
ans = min(f[i][0], f[i][1]);
break;
}
cout << ans << endl;
return 0;
}