题目描述
有 N 棵松果树从左往右排一行,桃桃是一只松鼠,它现在在第一棵松果树上。它想吃尽量多的松果,但它不想在地上走,而只想从一棵树跳到另一棵树上。松鼠的体力有个上限,每次不能跳的太远,也不能跳太多次。每当它跳到一棵树上,就会把那棵树上的松果全部都吃了。它最多能吃到多少个松果?
输入格式 1725.in
第一行,三个整数:
N 、 D 、M 。 N 表示松果树的数量,D 表示松鼠每次跳跃的最大距离, M 表示松鼠最多能跳跃M次。
接下来有N 行,每行两个整数: Ai 和 Bi 。其中 Ai 表示第 i 棵树上的松果的数量,Bi 表示第 i 棵树与第1棵树的距离,其中B1 保证是0。
数据保证这 N 棵树从左往右的次序给出,即Bi 是递增的,不存在多棵树在同一地点。
输出格式 1725.out
一个整数。
输入样例 1725.in
5 5 2
6 0
8 3
4 5
6 7
9 10
输出样例 1725.out
20
数据范围
Ai≤10000 , D≤10000
对于40%的数据, M<N≤100 , Bi≤10000
对于100%的数据, M<N≤2000 , Bi≤10000
初看,这题 dp 的模型是非常明显的。如果用
f[i][j]
表示前
i
棵树跳
这种做法的正确性也是显然的,但是复杂度太高了,达到 O(n3) 的级别,只能拿 40 分。
分析一下数据范围,其实 O(n2) 的复杂度还是可以承受的,所以优化算法的关键就在于如何快速地找出符合条件的 f[k][j−1] 的最大值。我们知道,朴素地找肯定是行不通的,那么只能用一个数据结构进行维护。
传统的维护最值数据结构在此题中均表现不佳,如线段树所需空间太大,树状数组无法满足位置范围限制的需求。但还有一种轻巧易用的数据结构,那就是单调队列。
不妨维护这样一个队列:总是保证队列中的元素值单调递减,且队中元素的最小下标与最大下标差总是在
d
以内。但是要维护的值是基于跳的次数的不同而受到影响的,因此要维护的是一个二维的单调队列。也就是说,当计算一个新的值
参考代码:
#include <algorithm> #include <cstdio> #include <cstdlib> #include <cstring> #include <iostream> #include <queue> using namespace std; const int maxn = 2e3 + 10; const int maxm = 2e3 + 10; struct Tnode { int pos, val; Tnode () : pos(0), val(0) {} Tnode (int x, int y) : pos(x), val(y) {} }; int n, d, m; int a[maxn], b[maxn]; int dp[maxn][maxm]; int head[maxm], tail[maxm]; Tnode q[maxn][maxm]; void debug_output() { for (int i = 0; i <= m; i++) { for (int j = head[i]; j < tail[i]; j++) printf("%d %d|", q[i][j].pos, q[i][j].val); putchar('\n'); } puts("============="); } int main(void) { freopen("1725.in", "r", stdin); freopen("1725.out", "w", stdout); scanf("%d%d%d", &n, &d, &m); for (int i = 0; i < n; i++) scanf("%d%d", &a[i], &b[i]); dp[0][0] = a[0]; q[0][tail[0]++] = Tnode(0, a[0]); for (int i = 1; i < n; i++) { for (int j = 1; j <= min(i, m); j++) { while (head[j - 1] < tail[j - 1] && b[i] - q[j - 1][head[j - 1]].pos > d) head[j - 1]++; //将与当前位置距离超过 d 的元素出队 dp[i][j] = q[j - 1][head[j - 1]].val + a[i]; //队首值即为跳 j - 1 次且与当前位置距离在 d 以内的最大值 } for (int j = 1; j <= min(i, m); j++) { //计算完后统一维护队列 while (head[j] < tail[j] && q[j][tail[j] - 1].val < dp[i][j]) tail[j]--; q[j][tail[j]++] = Tnode(b[i], dp[i][j]); //printf("i=%d j=%d\n", i, j); debug_output(); } } int ans = 0; for (int i = 0; i < n; i++) ans = max(ans, dp[i][m]); printf("%d\n", ans); return 0; }
关于利用单调性优化 dp 的问题,JSOI 2009 有一篇相关的论文讲得很棒,如果有兴趣可以自行查阅。
2017/8/5 update
这里还存在一个细节问题需要注意:如果计算完立即维护单调队列,且
不难理解:在计算
f[i][j]
的过程中取
f[k][j−1]
的时候,显然合法的
k
应该满足
有两种解决方法可以避免出现这样的错误:一是参考 01 背包的一维空间做法,
j
从大到小求解;二是对于同一个
另:附线段树版本代码
注意下面的代码中用了一个小优化,即先统一二分预处理上一步的最远可能位置,并记为
lasti
。
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
using namespace std;
const int MAXN = 2e3 + 10;
struct Tnode {
int val;
int left_child, right_child;
} tree[MAXN * MAXN << 1];
int freepoint;
struct SegmentTree {
int root;
SegmentTree () : root(0) {}
int get_point(int l, int r) {
tree[freepoint] = (Tnode){0, -1, -1};
return freepoint++;
}
int create(int l, int r) {
int p = get_point(l, r);
if (l < r) {
int mid = l + r >> 1;
tree[p].left_child = create(l, mid);
tree[p].right_child = create(mid + 1, r);
}
return p;
}
void push_up(int cur) {
tree[cur].val = max(tree[tree[cur].left_child].val, tree[tree[cur].right_child].val);
}
void update_one(int cur, int l, int r, int pos, int v) {
Tnode& nod = tree[cur];
if (l == pos && r == pos) {
nod.val = v;
return;
}
int mid = l + r >> 1;
if (pos <= mid) update_one(nod.left_child, l, mid, pos, v);
else update_one(nod.right_child, mid + 1, r, pos, v);
push_up(cur);
}
int query(int cur, int ll, int rr, int l, int r) {
Tnode& nod = tree[cur];
if (cur == -1 || r < ll || rr < l) return 0;
// printf("%d %d %d %d %d\n", cur, ll, rr, l, r);
if (l <= ll && rr <= r) return nod.val;
int mid = ll + rr >> 1;
return max(query(nod.left_child, ll, mid, l, r), query(nod.right_child, mid + 1, rr, l, r));
}
} SegT[MAXN];
int N, D, M;
int A[MAXN], B[MAXN], last[MAXN], f[MAXN][MAXN];
int main(void) {
freopen("1725.in", "r", stdin);
freopen("1725.out", "w", stdout);
scanf("%d%d%d", &N, &D, &M);
for (int i = 1; i <= N; i++) scanf("%d%d", &A[i], &B[i]);
for (int i = 2; i <= N; i++) last[i] = lower_bound(B + 1, B + i, B[i] - D) - B;
for (int i = 0; i <= M; i++) SegT[i].root = SegT[i].create(1, N);
int ans;
ans = f[1][0] = A[1]; SegT[0].update_one(SegT[0].root, 1, N, 1, A[1]);
for (int i = 2; i <= N; i++)
for (int j = min(i - 1, M); j; j--) {
f[i][j] = SegT[j - 1].query(SegT[j - 1].root, 1, N, last[i], i - 1) + A[i];
ans = max(ans, f[i][j]);
SegT[j].update_one(SegT[j].root, 1, N, i, f[i][j]);
}
printf("%d\n", ans);
return 0;
}