20250726
T1
过河
注意到石子和跳跃距离 max 很小,只考虑每个石子及其后 \(t - 1\) 个位置即可。按照合并重复段,然后考虑前一段最后 \(t\) 个位置中每个位置能跳到这一段的哪些位置,这只能是 \(\mathcal{O}(t)\) 个区间和一段后缀的并。因此容易从前一段转移到这一段,每段内暴力转移即可。可以把起点和终点都视为石子,好写一点。
代码
#include <iostream>
#include <algorithm>
#include <string.h>
#define int long long
using namespace std;
inline void Cmin(int &x, int y) { x = min(x, y); }
int n;
int L, R, m, lim;
int f[105][305];
int dp[40005], mark[40005];
int a[305];
signed main() {
freopen("river.in", "r", stdin);
freopen("river.out", "w", stdout);
cin >> n >> L >> R >> m;
for (int i = 1; i <= m; i++) cin >> a[i];
if (L == R) {
int ans = 0;
for (int i = 1; i <= m; i++) ans += (a[i] % L == 0);
cout << ans << "\n";
return 0;
}
lim = (R + R - L - 1) / (R - L);
sort(a + 1, a + m + 1);
a[m + 1] = n;
int t = 0;
for (int i = 0, j; i <= m + 1;) {
j = i + 1;
while (j <= m + 1 && a[j] <= a[j - 1] + R + 100) ++j;
memset(mark, 0, sizeof mark);
for (int k = i; k < j; k++) mark[a[k] - a[i]] = (k && k != m + 1);
int cl = a[i], cr = a[j - 1] + R - 1;
memset(dp, 63, sizeof dp);
if (t) {
for (int _ = 0; _ < R; _++) {
for (int k = 1, xl = a[i - 1] + _, xr = xl; k < lim; k++) {
xl += L, xr += R;
if (xl >= cl + R)
break;
for (int x = max(xl, cl); x <= min(xr, cl + R - 1); x++) Cmin(dp[x - cl], f[t][_]);
}
for (int k = max(cl, a[i - 1] + _ + lim * L); k < cl + R; k++) Cmin(dp[k - cl], f[t][_]);
}
} else
dp[0] = 0;
for (int x = 0; x <= cr - cl; x++) dp[x] += mark[x];
for (int x = 0; x <= cr - cl; x++) {
for (int y = max(0ll, x - R); y <= x - L; y++)
Cmin(dp[x], dp[y] + mark[x]);
}
++t;
for (int k = a[j - 1]; k <= cr; k++) f[t][k - a[j - 1]] = dp[k - a[i]];
i = j;
}
int ans = 2147483647;
for (int i = 0; i < R; i++) Cmin(ans, f[t][i]);
cout << ans << "\n";
return 0;
}
T2
挖油
朴素的区间 dp:\(f_{l, r}\) 表示已经确定答案在 \([l, r]\) 中之后,最少还需要多少时间确认答案。令 \(f_{l, l - 1} = 0\),转移为 \(f_{l, r} = \min\limits_{l \le k \le r} \{ t_k + \max\{ f_{l, k - 1}, f_{k + 1, r} \} \}\)。显然固定 \(l\) 或 \(r\) 后,dp 值关于另一维都是单调的,于是后面那个 \(\max\) 在一段前缀取后一项,在一段后缀取前一项。那么转移即可以视为两个区间最大值的 \(\min\)。设分界点为 \(p\),又会发现不管固定 \(l, r, r - l\),\(p\) 的移动都是单调的。这个时候考虑取后一项时的转移,会发现对应的区间左右端点均单增,于是可以单调队列维护。另一边,发现对于每个 \(r\),每次用到它的时候它对应的区间左右端点都是单调左移,于是对每个 \(r\) 都开一个单调队列即可维护。时间复杂度 \(\mathcal{O}(n ^ 2)\)。
代码
#include <iostream>
#include <string.h>
#include <queue>
using namespace std;
const int N = 6005;
inline void Cmin(int &x, int y) { x = min(x, y); }
int n;
int t[N];
int dp[N][N];
struct Mono_Queue {
deque<pair<int, int> > q;
void push(int v, int t) {
while (q.size() && q.back().first >= v) q.pop_back();
q.push_back({ v, t });
}
void pop(int t) { while (q.size() && q.front().second < t) q.pop_front(); }
int query() { return q.front().first; }
} qL[N], qR[N];
int main() {
freopen("oil.in", "r", stdin);
freopen("oil.out", "w", stdout);
cin >> n;
memset(dp, 63, sizeof dp);
for (int i = 1; i <= n; i++) {
cin >> t[i], dp[i][i] = t[i];
dp[i][i - 1] = dp[i][i + 1] = 0;
}
for (int d = 2; d <= n; d++) {
int p = 1;
for (int l = 1, r = d; r <= n; ++l, ++r) {
while (p < r && dp[l][p - 1] <= dp[p + 1][r]) ++p;
qL[r].push(t[l] + dp[l + 1][r], -l);
qL[r].pop(-(p - 1));
qR[l].push(t[r] + dp[l][r - 1], r);
qR[l].pop(p);
dp[l][r] = min(qL[r].query(), qR[l].query());
}
}
cout << dp[1][n] << "\n";
return 0;
}
T3
ZZH 的旅行
显然转移视为斜率优化的形式,只需要维护出每个点子树的凸包即可。使用 dsu on tree 加李超树,时间复杂度 \(\mathcal{O}(n\log^2n)\),数据范围 \(10^6\) 轻松跑过。
不过直接李超树合并似乎就是单 \(\log\) 的,虽然我不会写。
代码
#include <iostream>
#include <algorithm>
#include <vector>
#define int long long
using namespace std;
const int N = 1000000000, inf = 0x3f3f3f3f3f3f3f3f;
int n;
int rt;
struct Line {
int k, b;
Line(int x = 0, int y = 0) { k = x, b = y; }
int operator()(int x) { return k * x + b; }
} Ln[1000005];
struct node {
Line mx;
int l, r;
} T[4000005];
struct Segment_Tree {
int ncnt;
void Clear() { rt = ncnt = 0; }
void Update(int &o, int l, int r, Line x) {
if (!o)
T[o = ++ncnt] = (node) { { 0, 0 }, 0, 0 };
int mid = (l + r) >> 1;
if (T[o].mx.k == 0 && T[o].mx.b == 0) {
T[o].mx = x;
return;
}
if (x(mid) > T[o].mx(mid))
swap(T[o].mx, x);
int L = T[o].mx(l), R = T[o].mx(r), ll = x(l), rr = x(r);
if (L < ll)
Update(T[o].l, l, mid, x);
if (R < rr)
Update(T[o].r, mid + 1, r, x);
}
int Query(int o, int l, int r, int x) {
if (!o)
return 0;
if (l == r)
return T[o].mx(x);
int mid = (l + r) >> 1;
int t1;
if (x <= mid)
t1 = Query(T[o].l, l, mid, x);
else
t1 = Query(T[o].r, mid + 1, r, x);
return max(t1, T[o].mx(x));
}
} seg;
int a[1000005], b[1000005];
int head[1000005], nxt[2000005], to[2000005], ew[2000005], ecnt;
void add(int u, int v, int ww) { to[++ecnt] = v, nxt[ecnt] = head[u], head[u] = ecnt, ew[ecnt] = ww; }
int R[1000005], dfn[1000005], dep[1000005], ncnt;
int sz[1000005], son[1000005], _dfn[1000005];
void dfs(int x, int fa) {
_dfn[dfn[x] = ++ncnt] = x;
sz[x] = 1;
for (int i = head[x]; i; i = nxt[i]) {
int v = to[i];
if (v != fa) {
dep[v] = dep[x] + ew[i];
dfs(v, x);
sz[x] += sz[v];
if (sz[v] > sz[son[x]])
son[x] = v;
}
}
R[x] = ncnt;
}
int dp[1000005], p[1000005];
void dfs2(int x, int fa) {
for (int i = head[x]; i; i = nxt[i]) {
int v = to[i];
if (v != fa && v != son[x]) {
dfs2(v, x);
seg.Clear();
}
}
if (son[x])
dfs2(son[x], x);
for (int i = head[x]; i; i = nxt[i]) {
int v = to[i];
if (v != fa && v != son[x]) {
for (int j = dfn[v]; j <= R[v]; j++) {
int t = _dfn[j];
seg.Update(rt, -N, N, Line(b[t], -dep[t] * b[t] + dp[t]));
}
}
}
dp[x] = seg.Query(rt, -N, N, a[x] + dep[x]);
seg.Update(rt, -N, N, Line(b[x], -dep[x] * b[x] + dp[x]));
}
signed main() {
freopen("journey.in", "r", stdin);
freopen("journey.out", "w", stdout);
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i] >> b[i];
for (int i = 1, u, v, ww; i < n; i++) {
cin >> u >> v >> ww;
add(u, v, ww);
add(v, u, ww);
}
dfs(1, 0);
dfs2(1, 0);
for (int i = 1; i <= n; i++) cout << dp[i] << "\n";
return 0;
}
T4
数据结构
这谁想得到啊。/yun
考虑每个时刻,所有被 chkmin 过至少一次的位置构成的子序列,其内部必是单调的。设这些位置构成的集合为 \(S\),我们考虑把整个序列分成 \(S\) 和 \([1, n] \text{\\} S\) 两部分考虑。每个操作对两个集合分别的修改都是容易维护的,线段树即可,但是我们没法维护每个点在哪个时刻会加入集合 \(S\)。考虑在线段树之前求出这个东西。设第 \(i\) 个 chkmin 操作前边有 \(p_i\) 个全局加,这个 chkmin 的值是 \(b_i\),那么第 \(i\) 个点在第 \(j\) 次 chkmin 被干,如果它没在前面被干过,可以表示为 \(a_i + i \times p_j \ge b_j\)。显然可以视为斜率的形式,对于每个点可以二分,然后对前面的操作构造凸包查询。现在对 \(n\) 个点求,整体二分即可。
整体二分:
对于当前区间,先加入左半边,对所有答案在当前区间的东西查询,分为答案在左半边和答案在右半边,然后不撤销左半边,直接递归右半边,然后撤销左半边的加入,递归左半边,返回。这样需要数据结构支持撤销,因此似乎不支持均摊。(但是这个题其实不需要像这样,因为确定答案不在左半边之后右半边的 chk 就不再需要左半边的东西都在凸包中了,因此虽然这个凸包不支持撤销,但是可以直接在确定每个东西属于左半边还是右半边之后删掉左半边的加入然后直接递归。)
代码
#include <iostream>
#include <algorithm>
#include <vector>
#define int long long
using namespace std;
struct Line {
int k, b;
Line(int x = 0, int y = 0) { k = x, b = y; }
int operator()(int x) { return k * x + b; }
};
inline bool chk(Line a, Line b, Line c) { return ((__int128)b.b - a.b) * (b.k - c.k) < ((__int128)c.b - b.b) * (a.k - b.k); }
int n, q;
int A[200005], p[200005];
int op[200005];
int opx[200005], opy[200005];
int op1[200005], pre[200005], o1cnt;
int ct[200005];
vector<int> vec[200005];
struct Conv {
Line stk[200005];
int sz, cur;
void Clear() { sz = 0, cur = 1; }
void Insert(Line t) {
if (sz && stk[sz].k == t.k) {
if (stk[sz].b >= t.b)
--sz;
else
return;
}
while (sz > 1 && !chk(stk[sz - 1], stk[sz], t)) --sz;
stk[++sz] = t;
}
int Query(int x) {
while (cur < sz && stk[cur](x) >= stk[cur + 1](x)) ++cur;
return stk[cur](x);
}
} C;
void Solve(int l, int r, vector<int> cur) {
if (l == r) {
if (r <= o1cnt)
vec[op1[r]] = cur;
return;
}
int mid = (l + r) >> 1;
C.Clear();
for (int i = l; i <= mid; i++) C.Insert(Line(-pre[i], opx[op1[i]]));
vector<int> vl, vr;
for (auto v : cur) (C.Query(v) <= A[v]) ? vl.emplace_back(v) : vr.emplace_back(v);
Solve(l, mid, vl);
Solve(mid + 1, r, vr);
}
struct Node {
int sumS, sumSid, cntS, mx, mxp;
int sum_S, sum_Sid;
Node(int a = 0, int b = 0, int c = 0, int d = 0, int e = 0, int f = 0, int g = 0) {
sumS = a, sumSid = b, cntS = c, mx = d, mxp = e, sum_S = f, sum_Sid = g;
}
};
struct Tag {
int ctg, atg, atgS;
Tag(int x = 0, int y = 0, int z = 0) { ctg = x, atg = y, atgS = z; }
};
Node operator+(Node a, Node b) {
Node ret = (Node) {
a.sumS + b.sumS, a.sumSid + b.sumSid, a.cntS + b.cntS, max(a.mx, b.mx),
((b.mxp != -1) ? b.mxp : a.mxp), a.sum_S + b.sum_S, a.sum_Sid + b.sum_Sid };
return ret;
}
void operator+=(Node &x, Tag v) {
if (v.ctg != -1 && x.mxp != -1)
x.sumS = x.cntS * v.ctg, x.mx = v.ctg;
x.sumS += v.atgS * x.sumSid;
if (x.mxp != -1)
x.mx += v.atgS * x.mxp;
x.sum_S += v.atg * x.sum_Sid;
}
void operator+=(Tag &x, Tag y) {
if (y.ctg != -1) {
x.ctg = y.ctg;
x.atgS = 0;
}
x.atg += y.atg;
x.atgS += y.atgS;
}
struct Segment_Tree {
Node T[800005];
Tag tg[800005];
inline void tag(int o, Tag t) { T[o] += t, tg[o] += t; }
inline void pushdown(int o) {
if (tg[o].ctg == -1 && tg[o].atg == 0)
return;
tag(o << 1, tg[o]);
tag(o << 1 | 1, tg[o]);
tg[o] = Tag(-1, 0);
}
inline void Add() { tag(1, Tag(-1, 1, 1)); }
void Build(int o, int l, int r) {
tg[o] = Tag(-1, 0, 0);
if (l == r)
return T[o] = Node(0, 0, 0, 0, -1, A[l], r), void();
int mid = (l + r) >> 1;
Build(o << 1, l, mid);
Build(o << 1 | 1, mid + 1, r);
T[o] = T[o << 1] + T[o << 1 | 1];
}
void Change(int o, int l, int r, int x) {
if (l == r) {
T[o].sumS = T[o].mx = T[o].sum_S;
T[o].mxp = T[o].sumSid = l;
T[o].sum_S = T[o].sum_Sid = 0;
T[o].cntS = 1;
return;
}
pushdown(o);
int mid = (l + r) >> 1;
if (x <= mid)
Change(o << 1, l, mid, x);
else
Change(o << 1 | 1, mid + 1, r, x);
T[o] = T[o << 1] + T[o << 1 | 1];
}
void Cover(int o, int l, int r, int L, int R, int v) {
if (L <= l && r <= R)
return tag(o, Tag(v, 0, 0));
pushdown(o);
int mid = (l + r) >> 1;
if (L <= mid)
Cover(o << 1, l, mid, L, R, v);
if (R > mid)
Cover(o << 1 | 1, mid + 1, r, L, R, v);
T[o] = T[o << 1] + T[o << 1 | 1];
}
int Search(int o, int l, int r, int v) {
if (l == r)
return T[o].mx >= v ? l : n + 1;
pushdown(o);
int mid = (l + r) >> 1;
if (T[o << 1].mx >= v)
return Search(o << 1, l, mid, v);
else
return Search(o << 1 | 1, mid + 1, r, v);
}
int Query(int o, int l, int r, int L, int R) {
if (L <= l && r <= R)
return T[o].sumS + T[o].sum_S;
pushdown(o);
int mid = (l + r) >> 1;
if (R <= mid)
return Query(o << 1, l, mid, L, R);
if (L > mid)
return Query(o << 1 | 1, mid + 1, r, L, R);
return Query(o << 1, l, mid, L, R) + Query(o << 1 | 1, mid + 1, r, L, R);
}
} seg;
signed main() {
freopen("datastruct.in", "r", stdin);
freopen("datastruct.out", "w", stdout);
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin >> n >> q;
for (int i = 1; i <= n; i++) cin >> A[i];
for (int i = 1, tmp = 0; i <= q; i++) {
cin >> op[i];
if (op[i] == 1) {
cin >> opx[i];
op1[++o1cnt] = i;
pre[o1cnt] = tmp;
} else if (op[i] == 3)
cin >> opx[i] >> opy[i];
else
++tmp;
}
vector<int> tmp;
for (int i = 1; i <= n; i++) tmp.emplace_back(i);
Solve(1, o1cnt + 1, tmp);
seg.Build(1, 1, n);
for (int i = 1; i <= q; i++) {
if (op[i] == 1) {
int p = seg.Search(1, 1, n, opx[i]);
for (auto v : vec[i]) seg.Change(1, 1, n, v), p = min(p, v);
if (p <= n)
seg.Cover(1, 1, n, p, n, opx[i]);
} else if (op[i] == 2)
seg.Add();
else
cout << seg.Query(1, 1, n, opx[i], opy[i]) << "\n";
}
return 0;
}

浙公网安备 33010602011771号