[ZJOI2010]基站选址

题目大意:

\(n\) 个点,每个点 \(i\) 可以花费 \(c_i\) 建造基站,一共可以建不超过 \(m\) 个基站。若距离一个点 \(i\) 不超过 \(S_i\) 的距离内有基站,则视为这个点被覆盖了。若一个点 \(i\) 没有被覆盖,则需要付出 \(w_i\) 的代价。

求最小费用?

\(m \leq n, m \leq 100, n \leq 2e4, D_i \leq 1e9, c_i \leq 1e4, S_i \leq 1e9, w_i \leq 1e4\)

思路:

首先有一个想法,就是我若在 \(i\) 建基站,且有 \(j(j < i)\) 没有被覆盖,若后面再在后面建基站,\(j\) 也不会被覆盖,简单的来讲就是后面的建基站不会对前面的状态有任何的改变,即没有后效性。

所以可以想到一个dp,\(dp[i][j]\) 代表只考虑前 \(i\) 个,一共建了 \(j\) 个基站,且 \(i\) 一定建(方便转移)。

转移:

\[dp[i][j] = min(dp[k][j-1] + c[i] + cost[k][i]) \]

这里的 \(cost[i][j]\) 代表在 \(i\)\(j\) 建造基站(此时 \(i\)\(j\) 是最后两个基站),\(i\)\(j\) 之间没有覆盖点的 \(\sum w_k\)

枚举 \(i\) , \(j\), \(k\), \(O(n^2*k)\) ,计算 \(cost[i][j]\) 枚举 \(i\)\(j\) , \(O(n)\),

一共 \(O(n^3*k)\)

但是计算 \((k,i)\) 时,可以将 \((k + 1, i)\) 直接取过来, 其实自己也不懂 。 反正就是可以 \(O(n^2*k)\) 然而并无卵用。。。

现在的复杂度都堆在枚举 \(k\) 与计算 \(cost[i][j]\) 上,我们要想办法优化。

首先,\(dp[k][j - 1]\) 是一个定值,会改变的只有 \(cost[k][i]\) , 我们来深入一下这个 \(cost[k][i]\) 的计算:显然,\(cost[k][i]\)\(i\) 的增大单调不递增,如果某个点的可更新右端点算了好累啊 叫他们 \(L_i, R_i\) 好了 , 如果一个点的 \(R_j\) 正好等于 \(i\) , 那么之后所有点都不会再包含它,只有 \(k\) 才可以包含它, 但如果 \(k\) 都救不了它,那么它就必须去世了 。此时 \(k\) < \(L_j\) , 所以就区间更新一下 \((1, L_j - 1)\) 给它们全部加上 \(w_j\) 即可。

又因为我们还要计算 dp 的最小值,所以在更新的同时用线段树维护一下前缀最小值,并且把上述一开始枚举的 \(1 \dots m\) 套在外面就好了。

代码:

#include <bits/stdc++.h>
 
using namespace std;
 
#define REP(i, a, n) for (register int i = a, _n = n; i <= _n; ++i)
#define DREP(i, a, n) for (register int i = a, _n = n; i >= _n; --i)
#define FOR(i, a, n) for (register int i = a, _n = n; i < _n; ++i)
#define EREP(i, a) for (register int i = first[a]; i; i = edge[i].nxt)
#define lson (p << 1)
#define rson (p << 1 | 1)
#define debug(x) cout << #x << " = " << x << endl
 
char IO;
inline int rd () {
    int res = 0;
    while ((IO = getchar()) && (IO < '0' || IO > '9'));
    while (IO >= '0' && IO <= '9') res = (res << 1) + (res << 3) + (IO ^ 48), IO = getchar();
    return res;
}
 
const int SIZE = 20005;
 
int n, m;
 
int d[SIZE], c[SIZE], s[SIZE], w[SIZE];
int L[SIZE], R[SIZE];
int ans = 2e9 + 500;

int first[SIZE], ecnt;
struct Edge {
    int v, nxt;
} edge[SIZE];
void Add_Edge (int u, int v) {
    ++ecnt;
    edge[ecnt].v = v;
    edge[ecnt].nxt = first[u];
    first[u] = ecnt;
}
 
int dp[SIZE];
 
int x;
int Mi[SIZE << 2], flag[SIZE << 2];
 
inline void Down (int p) {
    if (flag[p] == 0) return;
    flag[lson] += flag[p];
    flag[rson] += flag[p];
    Mi[lson] += flag[p];
    Mi[rson] += flag[p];
    flag[p] = 0;
}
inline void Up (int p) {
    Mi[p] = min(Mi[lson], Mi[rson]);
}
void Build (int L, int R, int p) {
    flag[p] = 0;
    if (L == R) {
        Mi[p] = dp[L];
        return;
    }
    int mid = L + R >> 1;
    Build(L, mid, lson);
    Build(mid + 1, R, rson);
    Up(p);
}
void update (int L, int R, int p, int a, int l, int r) {
    if (L == l && R == r) {
        flag[p] += a;
        Mi[p] += a;
        return;
    }
    Down(p);
    int mid = L + R >> 1;
    if (r <= mid) update(L, mid, lson, a, l, r);
    else if (l > mid) update(mid + 1, R, rson, a, l, r);
    else update(L, mid, lson, a, l, mid), update(mid + 1, R, rson, a, mid + 1, r);
    Up(p);
}
int Query (int L, int R, int p, int a) {
    if (R == a) return Mi[p];
    Down(p);
    int mid = L + R >> 1;
    if (a <= mid) return Query (L, mid, lson, a);
    else return min(Mi[lson], Query(mid + 1, R, rson, a));
}
 
int main () {
    n = rd(), m = rd();
 
    int ans = 0;
    REP (i, 2, n) d[i] = rd();
    REP (i, 1, n) c[i] = rd();
    REP (i, 1, n) s[i] = rd();
    REP (i, 1, n) w[i] = rd(), ans += w[i];
 
    REP (i, 1, n) {
        L[i] = lower_bound(d + 1, d + n + 1, d[i] - s[i]) - d;
        R[i] = upper_bound(d + 1, d + n + 1, d[i] + s[i]) - d - 1;
        Add_Edge (R[i], i);
    }
 
    memset(dp, 0x3f, sizeof dp); // 我们假设了i一定建站,所以j = 0时只能从dp[0]转移过来
    dp[0] = 0;
 
    REP (j, 1, m + 1) {
        Build(j - 1, n, 1);
        REP (i, j, n + 1) {
            if (i == n + 1) {
                ans = min(ans, Query(j - 1, n, 1, n));  // 统计答案,此时线段树中存的是建j-1个站的答案
                continue;
            }
            dp[i] = Query(j - 1, n, 1, i - 1) + c[i];
            EREP (k, i) {
                Edge& e = edge[k];
                if (L[e.v] > j - 1) update(j - 1, n, 1, w[e.v], j - 1, L[e.v] - 1);
            }
        }
    }
 
    printf ("%d\n", ans);
 
    return 0;
}

还有一些细节,就是新加一个节点 \(n + 1\) 用它来记录答案,并且由于线段树中保存的是 \(m - 1\) 所以要枚举到 \(m + 1\)

讲的有点糊,抱歉。

posted @ 2019-07-11 21:12  ZPAYAUR  阅读(219)  评论(0编辑  收藏  举报