解题思路
数据结构优化
前置知识:线段树区间修改 / 区间求最小值 +
看到本题,我们首先要想最暴力的状态设置以及转移方程式。
令 表示在第
个村庄建设第
个基站同时 只考虑 前
个村庄的最小费用。
ps.这里的"只考虑"指的是只计算前 个村庄的补偿款或者建设基站的费用。
不难想到状态转移方程:
表示的是第
个村庄到第
个村庄都不修建基站的补偿费用。
这样子的空间复杂度是:O的,时间复杂度是 o(
) 的,显然通过不了本题。
我们可以优化这个 过程。
首先最简单的就是优化空间(其实优化不优化都能通过本题。不过作为一名Oier,这你能忍?
考虑到我们用的只有 以及
,用滚动数组优化即可。
优化后的状态转移方程:
不难发现时间复杂度瓶颈在于计算 ,这个我们每次都需要遍历一次才能知道
有没有办法动态更新?有的。
不妨设:
表示最左边能够覆盖村庄
的基站。
表示最右边能够覆盖村庄
的基站。
这两个都可以在 的时间内用二分的方法预处理出来。
观察到我们的 在进行状态转移的时候只会不停的右移,不断变成
,
.......
这个 右移影响到了什么呢?可以发现:
- 假设有一个村庄
,
,我们不在
村庄修建基站的话,那么为了不给村庄
补偿款,则必须在区间
修建基站,那么假设上一个修建基站的位置是在
之间的话,就必须提供补偿款。
也就是说,假如我们现在在进行 的状态转移,倘若
没有修基站,有一些(注意不是一个),姑且称之为村庄
, 村庄
的
是在
,相当于要给
现在的
加上
,因为上一个建立基站的位置
,并且下一个建设基站的位置大于
,所以说就得付补偿款
,考虑到要动态修改一段区间,所以采用 线段树。
转移的话,线段树里面区间取 即可 (线段树里面存的值是
)。
复杂度: O( *
)
最后的一个小 就是开一个超级点,令其
那么这个超级点在最后就一定会被选择修建基站但是不影响答案,这个点就设置为第 个点就行了,然后
,
,可以帮助我们更好的统计答案。
Code
#include <bits/stdc++.h>
using namespace std;
#define mid ((L[x] + R[x]) >> 1)
#define int long long
inline int read() {
int x = 0 , flag = 1;
char ch = getchar();
for( ; ch > '9' || ch < '0' ; ch = getchar()) if(ch == '-') flag = -1;
for( ; ch >= '0' && ch <= '9' ; ch = getchar()) x = (x << 3) + (x << 1) + ch - '0';
return x * flag;
}
const int MAXN = 2e4 + 50;
int n, K, tot = 0;
int D[MAXN], C[MAXN], S[MAXN], W[MAXN], st[MAXN], ed[MAXN], start[MAXN];
int dp[MAXN];
struct Edge {
int next,to;
} edge[MAXN << 1];
struct SegmentTree {
int Min[MAXN << 2], laz[MAXN << 2], L[MAXN << 2], R[MAXN << 2];
void build(int x, int l, int r) {
L[x] = l, R[x] = r; laz[x] = 0;
if(l == r) {Min[x] = dp[l]; return ;}
build(x << 1, l, mid); build(x << 1 | 1, mid + 1, r);
Min[x] = min(Min[x << 1], Min[x << 1 | 1]);
return ;
}
void ad(int x,int k) {Min[x] += k, laz[x] += k; return ;}
void pushdown(int x) {
if(laz[x] == 0) return ;
ad(x << 1 , laz[x]); ad(x << 1 | 1 , laz[x]);
laz[x] = 0;
return ;
}
void change(int x,int l,int r,int k) {
if(l > r) return ;
if(L[x] >= l && R[x] <= r) {ad(x, k); return ;}
pushdown(x);
if(l <= mid) change(x << 1 , l , r , k);
if(r > mid) change(x << 1 | 1 , l , r , k);
Min[x] = min(Min[x << 1], Min[x << 1 | 1]);
return ;
}
int GetMin(int x, int l, int r) {
int M = 1e18;
if(l > r) return 1e18;
if(L[x] >= l && R[x] <= r) return Min[x];
pushdown(x);
if(l <= mid) M = min(GetMin(x << 1, l, r), M);
if(r > mid) M = min(GetMin(x << 1 | 1, l, r), M);
return M;
}
} T;
void add(int from, int to) {
edge[++ tot].to = to;
edge[tot].next = start[from];
start[from] = tot;
return ;
}
void Prepare() {
for(int i = 1; i <= n ; i ++) {
int L = i, R = i;
for(int j = log2(i) ; j >= 0 ; j --) {
int k = L - (1 << j);
if(k >= 1 && D[i] - D[k] <= S[i]) L = k;
}
for(int j = log2(n - i + 1) ; j >= 0 ; j --) {
int k = R + (1 << j);
if(k <= n && D[k] - D[i] <= S[i]) R = k;
}
st[i] = L, ed[i] = R; add(ed[i], i);
}
return ;
}
signed main() {
n = read(), K = read();
for(int i = 2 ; i <= n ; i ++) D[i] = read();
for(int i = 1 ; i <= n ; i ++) C[i] = read();
for(int i = 1 ; i <= n ; i ++) S[i] = read();
for(int i = 1 ; i <= n ; i ++) W[i] = read();
n ++; K ++;
D[n] = 1e18, W[n] = 1e18, S[n] = 0, C[n] = 0;
Prepare();
int now = 0;
for(int i = 1 ; i <= n ; i ++) {
dp[i] = now + C[i];
for(int j = start[i] ; j ; j = edge[j].next) {
int to = edge[j].to;
now += W[to];//初始化,因为一开始只能建一个基站,所以会有很多村庄无法覆盖,now要不断的取前缀和。
}
}
int Ans = dp[n];
for(int j = 2 ; j <= K ; j ++) {
T.build(1, 1, n);
for(int i = 1 ; i <= n ; i ++) {
dp[i] = T.GetMin(1, 1, i - 1) + C[i];
for(int k = start[i] ; k ; k = edge[k].next) {
int to = edge[k].to;
T.change(1, 1, st[to] - 1, W[to]);
}
}
Ans = min(Ans, dp[n]);
}
cout << Ans;
return 0;
} 总结
做了几道数据结构优化 的题目,感觉上来说,都是一个做法:动态更新产生的费用,每次转移后都是要动态的更新新产生的费用。
基站选址这道题对应的是更新 ,也就是产生的新的无法被覆盖的村庄。
大概感受就是如此。
关于数据结构优化
的好题
推荐练习题:
CF597C Subsequences (偏入门)
CF1389F Bicolored Segments (有点难?)
[USACO05DEC]Cleaning Shifts S (不难)

京公网安备 11010502036488号