NOIP 2017 普及组 跳房子

颓了一天整整六个小时颓出来的题,写篇颓废纪念一下

是,的,没,错,这道题花了 t m 我^{tm} 一整天。

好了不废话了,直接给出题目链接

如果想用更狠的官方数据来测,可以进这个OJ

老实说,蒟蒻感觉这题目有些绕……

一上来看到数据范围我吓呆了, n 500000 n \leq 500000 ……

考虑二分最少所花的金币 g g

左边界显然为1,右边界是 d d 与最后一个点的距离 - d d 中的较大值。

其实直接搞成那啥十万也可以。

我们显然要用一个 c h e c k check 函数检查 g g 枚金币够不够,也就意味着我们要检查从起点能跳的最大距离是否大于等于 k k

显然是个动态规划了。

在这里,设 a i a_i 表示格子 i i 的距离, s i s_i 表示格子 i i 的值, f i f_i 表示从格子 i i 出发能取到的最大值。

扫描二维码关注公众号,回复: 11605714 查看本文章

只要学过DP应该都会转移方程吧……

f i = m a x ( m a x f_i = max(max { f j f_j } , 0 ) + s i ,0)+s_i ( j n , a i + d g a j a i + d + g ) (j \leq n,a_i+d-g \leq a_j \leq a_i+d+g)

起点能取到的最大值可以理解为 f 0 f_0

我如何颓掉六个小时的呢?

注意看看数据范围……

这个算法时间复杂度是 O ( N 2 ) O(N^2) 的,还要乘个二分的常数(20左右),面对五十万的数据范围,我不知道说什么好……

然而!

题目数据太良心。常数其实只有十几,格子之间并没有隔得很近,于是 O ( N 2 ) O(N^2) 几乎变成了 O ( N ) O(N)

开始我没发现啊qwq,做完了看题解发现的

如果你就此止步,注意开long long,不然会出锅的。

于是初级的AC代码(十分钟就写完了qwq,被坑了六个小时):

#include <cstdio>
#include <queue>
#include <algorithm>
#define int long long

using namespace std;

int a[1000005], s[1000005], f[1000005], n, d, k;
priority_queue<int> q;

inline bool check(int g) {
    f[n] = s[n];
    for (int i = n - 1; i >= 0; i--) {
        f[i] = s[i];
        for (int j = i + 1; a[i] + d + g >= a[j] && j <= n; j++)
            if (a[i] + d - g <= a[j] && f[j] + s[i] > f[i]) {
                f[i] = f[j] + s[i];
            }
    }
    return f[0] >= k;
}

signed main() {
    scanf("%lld%lld%lld", &n, &d, &k);
    for (int i = 1; i <= n; i++) scanf("%lld%lld", a + i, s + i);
    int l = 1, r = 100005, mid, ans = r;
    while (l <= r) {
        mid = (l + r) / 2;
        if (check(mid))
            ans = mid, r = mid - 1;
        else
            l = mid + 1;
    }
    if (check(100005))
        printf("%lld", ans);
    else
        puts("-1");
}

这应该就是普及组的初衷了,但是谁知道等我初一(我是个xxs)的时候普及组会不会提高难度呢。

于是我的优化是通过OI界著名数据结构神器单调队列实现的。

进阶思路:

显然我们就是要找到“位置满足要求的元素”中分数最大的。而且这个“位置满足要求的元素”在不断变化。单调队列就专门干这事儿。

为了叙述方便,提前 deque<int>q作为单调队列,deque<int>dq作为辅助队列。

这个队列中存放的是“可能被使用的元素”的编号。

什么是“可能被使用的元素”?

我们可以这样想,对于当前的格子 i i ,如果队列中的元素 a a 比机器人下一个格子(i-1) 能跳的最近距离大,同时有一个元素 b b ,且 a < b a<b (即 a a < a b a_a < a_b ), f a f b f_a \geq f_b ,那么我们需要保留元素 b b 吗?既然元素 a a 对于下一个格子来说有着足够的距离,那么也就意味着 b b 在被“淘汰”(距离太远)前永远不可能成为满足要求的元素中的最大值,此时 b b 就不是“可能被使用的元素”,应该删除。

上面一段话一定要看懂,这是单调队列的基本思想。看不懂可以结合下面的代码,如果实在看不懂,那就暂时不要学习单调队列。

整个队列删除完后,元素编号应该是递增的,每个元素能取到的最大值也是递增的。所以就直接给原来的DP降维了。

将队列的删除(维护)操作封装到一个函数defend中就是:

inline void defend()
{
	int p = -1;//能取到的最大值至少大于-1,不然对于其它元素来说没有利用价值,还不如不取
	int t = q.size();//保存当前队列的元素个数
	while (t --)
	{
		if (f[q.front()] > p)//如果这个元素的值比编号在他之前的格子的值都大,那么他是有保留价值的
		q.push_back(q.front()), p = f[q.front()];//更新p,将新的有保留价值元素插到队尾。
		q.pop_front();//弹出队首元素
	}
}

这里有一个小问题,就是如果一个元素距离太近,怎么办?显然这个元素不能说是没有保留价值的,那么我们的dq队列就是来存这些元素的。当遇到一个新的格子时,只要从队尾开始依次检查是否能使用队中元素就可以了。然后在将dq队列中当前有用的元素都抖出来后,维护一遍队列就可以了。当然dq队列的元素编号是从左往右递增的。

代码如下:

inline bool check(int g)
{
	q.clear(); dq.clear();
	for (int i = n; i >= 0; i --)
	{
		bool flag = 0;
		while (q.size() && a[q.back()] > a[i] + d + g) q.pop_back();//距离太远的元素不能使用
		while (dq.size() && a[dq.back()] >= a[i] + d - g)//如果dq的队尾元素可以使用了
		{
			if (a[dq.back()] <= a[i] + d + g && f[dq.back()] >= 0)//并且距离在限制范围内,值大于0
			q.push_front(dq.back());//就把他插入到队列中
			dq.pop_back();//不管有没有用都要删除dq队尾元素,因为他已经不是距离太近的元素了
			flag = 1;
		}
		if (flag) defend();//维护一遍队列
		f[i] = s[i];
		if (q.size()) f[i] += f[q.back()];//加上队尾元素能取到的最大值
		if (a[i - 1] + d - g <= a[i]) q.push_front(i);//如果这个元素距离合法
		else dq.push_front(i);//如果这个元素距离太近,插到dq中
		defend();//维护一遍队列
	}
	return f[0] >= k;
}

就这个单调队列,我写了五个多小时……

哎,自己还是太菜鸡了。毕竟只在一周前,在滑动窗口中接触过单调队列。

附上完整代码:

#include <cstdio>
#include <deque>
#define int long long

using namespace std;

int a[1000005], s[1000005], f[1000005], n, d, k;
deque<int> q;
deque<int> dq;

inline void defend()
{
	int p = -1;
	int t = q.size();
	while (t --)
	{
		if (f[q.front()] > p)
		q.push_back(q.front()), p = f[q.front()];
		q.pop_front();
	}
}

inline bool check(int g)
{
	q.clear(); dq.clear();
	for (int i = n; i >= 0; i --)
	{
		bool flag = 0;
		while (q.size() && a[q.back()] > a[i] + d + g) q.pop_back();
		while (dq.size() && a[dq.back()] >= a[i] + d - g)
		{
			if (a[dq.back()] <= a[i] + d + g && f[dq.back()] >= 0)
			q.push_front(dq.back());
			dq.pop_back();
			flag = 1;
		}
		if (flag) defend();
		f[i] = s[i];
		if (q.size()) f[i] += f[q.back()];
		if (a[i - 1] + d - g <= a[i]) q.push_front(i);
		else dq.push_front(i);
		defend();
	}
	return f[0] >= k;
}

signed main()
{
	scanf("%lld%lld%lld", &n, &d, &k);
	for (int i = 1; i <= n; i ++)
	scanf("%lld%lld", a + i, s + i);
	int l = 1, r, rl;
	if (a[n] > d) r = rl = a[n];
	else r = rl = d;
	int mid, ans = r;
	while (l <= r)
	{
		mid = (l + r) / 2;
		if (check(mid)) ans = mid, r = mid - 1;
		else l = mid + 1;
	}
	if (check(rl)) printf("%lld", ans);
	else puts("-1");//如果是无解的qaq
}

另外由于官方数据太水(一开始就没有想考单调队列),所以单调队列耗时和朴素DP耗时差不多。。。

猜你喜欢

转载自blog.csdn.net/jvruo_shabi/article/details/108339069