P9871 [NOIP2023] 天天爱打卡 (离散化+线段树优化dp)
upd 2024.11.13
经典dp+线段树优化+离散化
可以看到求的是最大值,常用工具有 dp 和贪心。如果 dp 做的多的话应该可以看出这题就是个线性 dp(先忽略超大的数据范围)。
\(n\) 小的时候,可以直接以当前天数设状态,转移就是一段后缀天数跑步,贡献可以在线预处理。
\(O(n^2)\) 的复杂度啊,怎么优化?状态已经很简洁,可以想想拆式子,然后通过数据结构优化这方面。
发现可以用线段树优化了!然后复杂度是 \(O(n\log n)\) 了,但还是不够啊。
这时候你应该要注意到许多无效的转移位置(比如一段很长的跑步中间的部分),这些位置并不会改变贡献,怎么避免它?离散化。我们只关心端点附近的信息。于是复杂度就是 \(O(m\log m)\)。
前两个步骤略讲,主要是离散化。
首先考虑dp,朴素的,容易写出状态 \(f_i\) 表示考虑到第 \(i\) 个位置,且强制第 \(i\) 天跑步的最大能量值。
考虑枚举最后一段跑步的时间,有
\(f_i=\max(\max\limits_{k<j}f_k-(i-j)\times d+\sum\limits_{j<l_p\le r_p\le i}v_p)\)
不妨设 \(g_i=\max\limits_{j<i}f_i\),再自然的参变分离,有
\(f_i=\max(g_j+j\times d+\sum\limits_{j<l_p\le r_p\le i}v_p)-i\times d\)
容易看出只需要维护前面的最大值即可。考虑线段树优化。对于 \(g_j+j\times d\) 是定值,直接在线段树末尾插入即可。后面的部分可以在枚举的过程中不断把 \(r_p=i\) 的线段加入,也就是线段树上区间加。具体的说,对于每一个 \([l_i,r_i]\),给所有 \(j<l_i\) 加入 \(v_i\),表示若走满 \([j+1,i]\) 可以取到该线段。
这就是前面的部分,复杂度为 \(O(n\log n)\)。
考虑优化。首先数据范围显然想让我们把复杂度降到 \(O(m\log m)\)。于是我们就观察到 \(f\) 序列会分成很多段。
这些线段的左右端点把整个序列分为多段,观察每一段的决策,对于 \([l,r]\) 段,一定只有都跑和都不跑两种情况。考虑选每段的右端点作为该段的代表元。于是对于每个线段,我们只保留 \(l_i-1\) 与 \(r_i\) 两个端点即可,离散化完之后转移同样。
复杂度 \(O(m\log m)\)。
#include <bits/stdc++.h>
#define pii std::pair<int, int>
#define fi first
#define se second
#define pb push_back
typedef long long i64;
const int N = 1e5 + 10;
i64 n, m, k, d, tot;
struct node {
i64 l, r, v;
} a[N];
i64 b[N << 1], f[N << 1];
bool cmp(node a, node b) {
return a.r < b.r;
}
struct seg {
i64 v, lzy;
} t[(N << 1) << 2];
void pushup(int u) {t[u].v = std::max(t[u << 1].v, t[u << 1 | 1].v);}
void build(int u, int l, int r) {
t[u].lzy = 0;
if(l == r) {
t[u].v = (l ? -1e18 : 0);
return;
}
int mid = (l + r) >> 1;
build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
pushup(u);
}
void pd(int u) {
if(!t[u].lzy) return;
i64 tu = t[u].lzy;
t[u << 1].v += tu, t[u << 1].lzy += tu;
t[u << 1 | 1].v += tu, t[u << 1 | 1].lzy += tu;
t[u].lzy = 0;
}
i64 query(int u, int l, int r, int L, int R) {
if(L <= l && r <= R) {
return t[u].v;
}
int mid = (l + r) >> 1;
pd(u);
i64 ans = -1e18;
if(L <= mid) ans = std::max(ans, query(u << 1, l, mid, L, R));
if(R > mid) ans = std::max(ans, query(u << 1 | 1, mid + 1, r, L, R));
return ans;
}
void ins(int u, int l, int r, int L, i64 x) {
if(l == r) {
t[u].v = x;
return;
}
int mid = (l + r) >> 1;
pd(u);
if(L <= mid) ins(u << 1, l, mid, L, x);
else ins(u << 1 | 1, mid + 1, r, L, x);
pushup(u);
}
void update(int u, int l, int r, int L, int R, i64 x) {
if(L <= l && r <= R) {
t[u].v += x,t[u].lzy += x;
return;
}
int mid = (l + r) >> 1;
pd(u);
if(L <= mid) update(u << 1, l, mid, L, R, x);
if(R > mid) update(u << 1 | 1, mid + 1, r, L, R, x);
pushup(u);
}
void Solve() {
std::cin >> n >> m >> k >> d;
tot = 0;
for(int i = 1; i <= m; i++) {
std::cin >> a[i].r >> a[i].l >> a[i].v;
a[i].l = a[i].r - a[i].l;
b[++tot] = a[i].l, b[++tot] = a[i].r;
}
std::sort(b + 1, b + tot + 1);
std::sort(a + 1, a + m + 1, cmp);
tot = std::unique(b + 1, b + tot + 1) - b - 1;
for(int i = 1; i <= m; i++) {
a[i].l = std::lower_bound(b + 1, b + tot + 1, a[i].l) - b;
a[i].r = std::lower_bound(b + 1, b + tot + 1, a[i].r) - b;
}
build(1, 0, tot);
i64 g = 0;
for(int i = 1, lst = 0, nw = 1; i <= tot; i++) {
ins(1, 0, tot, i, g + d * b[i]);
for(; nw <= m && a[nw].r == i; nw++) {
update(1, 0, tot, 0, a[nw].l, a[nw].v);
}
for(; lst < i && b[lst] < b[i] - k; lst++);
if(lst < i) f[i] = query(1, 0, tot, lst, i - 1) - d * b[i];
else f[i] = -1e18;
// std::cout << f[i] << " \n"[i == tot];
g = std::max(g, f[i]);
}
std::cout << g << "\n";
}
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int c, t;
std::cin >> c >> t;
while(t--) Solve();
return 0;
}