[SMOJ1725]松果

97 篇文章 0 订阅
9 篇文章 0 订阅

题目描述

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

数据范围

Ai10000 , D10000
对于40%的数据, M<N100 , Bi10000
对于100%的数据, M<N2000 , Bi10000


初看,这题 dp 的模型是非常明显的。如果用 f[i][j] 表示前 i 棵树跳 j 次的最多松果数,很容易就可以得到

f[i][j]=max(f[k][j1])+A[i](B[i]B[k]<D)

这种做法的正确性也是显然的,但是复杂度太高了,达到 O(n3) 的级别,只能拿 40 分。

分析一下数据范围,其实 O(n2) 的复杂度还是可以承受的,所以优化算法的关键就在于如何快速地找出符合条件的 f[k][j1] 的最大值。我们知道,朴素地找肯定是行不通的,那么只能用一个数据结构进行维护。

传统的维护最值数据结构在此题中均表现不佳,如线段树所需空间太大,树状数组无法满足位置范围限制的需求。但还有一种轻巧易用的数据结构,那就是单调队列

不妨维护这样一个队列:总是保证队列中的元素值单调递减,且队中元素的最小下标与最大下标差总是在 d 以内。但是要维护的值是基于跳的次数的不同而受到影响的,因此要维护的是一个二维的单调队列。也就是说,当计算一个新的值 f[i][j] 的时候,就取 j1 的单调队列队首元素值;计算完毕后更新一遍队列,就更新 j 的单调队列。

参考代码:

#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
这里还存在一个细节问题需要注意:如果计算完立即维护单调队列,且 j 按照一般的从小到大的计算顺序,就会出现问题。

不难理解:在计算 f[i][j] 的过程中取 f[k][j1] 的时候,显然合法的 k 应该满足 k<i。但是计算完马上更新队列就有可能导致接下来取了 f[i][j1] 的值,也就是“从当前跳到当前”的情况。

有两种解决方法可以避免出现这样的错误:一是参考 01 背包的一维空间做法, j 从大到小求解;二是对于同一个 i,统一求解完所有的 f[i][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;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值