## 好冷好热好冷好热 #贪心 #模拟 首先可以想到很显然的解法,就是设计状态 $(i,j)$ 表示如果第 $i$ 秒温度为 $j$,前 $i$ 秒是否均满足条件。当然这个是会炸掉的。 不难发现后面的 $j$ 实际上可取的值是一个区间(所有人的温度要求的交和可能的温度的交)。所以我们只需要维护区间就行了。 但是枚举 $i$ 还是会炸。所以考虑对时间进行离散化,离散化之后就只需要枚举最多 $n$ 个 $i$。如果当前 $i$ 与上一个 $i$ 的差值为 $\Delta i$,那么区间就变成 $[l-\Delta i,r+\Delta i]$。再对当前 $i$ 对应的所有要求温度区间取交集即可。如果取完变空集就不满足条件。 ```cpp #include <fstream> #include <algorithm> #include <utility> #include <vector> using namespace std; using LL = long long; const LL kMaxN = 1e5 + 5; ifstream cin("temp.in"); ofstream cout("temp.out"); LL l[kMaxN], h[kMaxN], t[kMaxN], val[kMaxN], T, n, m, k; vector<pair<LL, LL>> v[kMaxN]; int main() { cin.tie(0)->sync_with_stdio(0); for (cin >> T; T; T--) { cin >> n >> m; fill(v + 1, v + k + 1, vector<pair<LL, LL>>()); k = 0; for (LL i = 1; i <= n; i++) { cin >> t[i] >> l[i] >> h[i]; val[++k] = t[i]; } sort(val + 1, val + k + 1); k = unique(val + 1, val + k + 1) - val - 1; for (LL i = 1; i <= n; i++) { t[i] = lower_bound(val + 1, val + k + 1, t[i]) - val; v[t[i]].emplace_back(l[i], h[i]); } []() { LL l = m, r = m; for (LL i = 1; i <= k; i++) { l -= val[i] - val[i - 1]; r += val[i] - val[i - 1]; for (const auto &j : v[i]) { if (r < j.first || l > j.second) { cout << "NO\n"; return; } l = max(l, j.first); r = min(r, j.second); } if (l > r) { cout << "NO\n"; } } cout << "YES\n"; }(); } return 0; } ``` ## 杀戮尖塔 #线段树 #二分 #倍增 #树链剖分 首先这道题目有多个不同的遗物,以区间或路径为基础很难处理这些不同的遗物。所以考虑分遗物处理区间。 对于 $x$ 到 $1$ 的路径,很显然我们可以写一个二分,二分出最下方一个点 $i$ 使得 $i\sim x$ 的路径上正好有一个遗物,开 $n$ 棵动态开点线段树用于判断。当然,预处理倍增父亲后直接倍增就行了。 这样子是不好做的,因为我们无法快速查询 $x$ 到 $x$ 上方的祖先之间是否有 $y$ 存在。考虑做树剖,先把 $x$ 往上跳若干条没有 $y$ 存在的链,然后在剩下的那条有 $y$ 的链上做二分或者倍增。时间复杂度 $\mathcal O(q\log^2 n)$。 这样子的在线做法的时间复杂度还是太高了。考虑离线,对于每个 $i$ 可能作为哪些 $x$ 的答案。不难发现每个 $i$ 可能给 $i$ 子树中的任意点做贡献,但是对于 $x$ 它要求深度最大的 $i$。所以按照深度枚举 $i$ 然后对指定线段树上 $i$ 进行子树覆盖即可。时间复杂度 $\mathcal O((q+n)\log_2 n)$。 当然我写的在线做法。 ```cpp #include <fstream> #include <vector> using namespace std; const int kMaxN = 1e5 + 5, kLog = 20; ifstream cin("slay.in"); ofstream cout("slay.out"); struct Node { int lc, rc, sum; } tr[kLog * kMaxN]; int rt[kMaxN], fa[kMaxN], dp[kMaxN][kLog], dfn[kMaxN], top[kMaxN], dep[kMaxN], siz[kMaxN], son[kMaxN], n, q, cnt, tim; vector<int> e[kMaxN]; void modify(int &p, int l, int r, int x) { if (!p) { p = ++cnt; } tr[p].sum = 1; if (l == r) { return; } int m = (l + r) / 2; if (x <= m) { modify(tr[p].lc, l, m, x); } else { modify(tr[p].rc, m + 1, r, x); } } int query(int p, int l, int r, int s, int t) { if (!p || s > t) { return 0; } if (s <= l && r <= t) { return tr[p].sum; } int m = (l + r) / 2, res = 0; if (s <= m) { res |= query(tr[p].lc, l, m, s, t); } if (m < t) { res |= query(tr[p].rc, m + 1, r, s, t); } return res; } void dfs1(int x, int f) { dep[x] = dep[f] + 1; fa[x] = f; siz[x] = 1; dp[x][0] = f; for (int i = 1; i < kLog; i++) { dp[x][i] = dp[dp[x][i - 1]][i - 1]; } for (int i : e[x]) { if (i == f) { continue; } dfs1(i, x); siz[x] += siz[i]; if (siz[i] > siz[son[x]]) { son[x] = i; } } } void dfs2(int x, int t) { top[x] = t; dfn[x] = ++tim; if (son[x]) { dfs2(son[x], t); } for (int i : e[x]) { if (i == fa[x] || i == son[x]) { continue; } dfs2(i, i); } } int jump(int x, int y) { for (int i = 0; i < kLog; i++) { if (y & (1 << i)) { x = dp[x][i]; } } return x; } int query(int x, int y) { for (; x && !query(rt[y], 1, n, dfn[top[x]], dfn[x]); x = fa[top[x]]) { } if (!x) { return -1; } int res = 0; for (int l = 0, r = dep[x] - 1; l <= r; ) { int m = (l + r) / 2, p = jump(x, m); if (query(rt[y], 1, n, dfn[p], dfn[x])) { res = p; r = m - 1; } else { l = m + 1; } } return res; } int main() { cin.tie(0)->sync_with_stdio(0); cin >> n; for (int i = 2, x; i <= n; i++) { cin >> x; e[x].push_back(i); } dfs1(1, 0); dfs2(1, 1); for (int i = 1, k; i <= n; i++) { cin >> k; for (int x; k; k--) { cin >> x; modify(rt[x], 1, n, dfn[i]); } } cin >> q; for (int x, y; q; q--) { cin >> x >> y; cout << query(x, y) << '\n'; } return 0; } ``` ## 故障机器人 #广度优先搜索 首先机器人在路径长度超过 $d$ 之后会炸掉,炸掉后对人没有影响,但是人只要被任意一个机器人的任意一种走法抓住就会死掉,所以我们直接规定所有机器人都不能炸掉就行了(保证每个机器人路径长度始终小于等于 $d$)。 我们需要判断如果在 $t$ 时刻到 $i$ 点的话是否又被机器人抓到的可能性,如果有就不能在这个时刻走到 $i$。具体来说,如果 $k$ 个机器人当中有任何一个可以正好在 $t$ 时刻到 $i$,$i$ 就走不了。 然而,机器人可以在两个点之间反复横跳(如果已走步数小于 $d$ 的话)。所以,我们只需要判断机器人是否有走到 $i$ 的路径且这个路径长与 $t$ 同奇偶,如果还路径长小于等于 $t$ 的话,就是可以走到 $i$ 的。 (如果 $t$ 超出了 $d$,就把 $t$ 变成小于等于 $d$ 且和 $t$ 同奇偶的数)这是奇偶最短路的模板。 现在我们的主要问题就是对于 $k$ 个怪物跑单源最短路太慢了。不过我们只需要判断是否有一个怪物可到达即可,这预示着我们可以对所有怪物一起处理。即多个源点的奇偶最短路。 处理完所有点的是能否走的情况后,我们开始构造方案。对于一个点 $i$,如果有多个同奇偶的 $t$ 可以经过这个 $i$,那么一定是 $t$ 最小的更优,因为在所有同奇偶的 $t$ 当中,$t$ 越大可以到达的点一定是更多的,这使得我们的行动愈发受限。 所以对 $1$ 的起点跑奇偶最短路,辅佐对能否被怪物干死的判断,即可得到一条 $1\sim n$ 的路径。在奇数长路径和偶数长路径中选更短的那条即可。 最后是输出答案。本题的 std 书写错误,它是反着求最小字典序的(也就是 $1\rightarrow 3\rightarrow 2\rightarrow 4$ 比 $1\rightarrow 2\rightarrow 3\rightarrow 4$ 更优;而我的代码正确地处理了正着的最小字典序。因为 std 的错误处理,导致我 WA 了 $10$ 分的测试点。现在展现的是我正确处理了字典序的代码(实际 $90$ 分): ```cpp #include <fstream> #include <algorithm> #include <vector> #include <queue> using namespace std; ifstream cin("defect.in"); ofstream cout("defect.out"); const int kMaxN = 1e5 + 5; int dis[kMaxN][2], len[kMaxN][2], a[kMaxN], f[kMaxN], n, m, d, k; vector<int> e[kMaxN], p[kMaxN][2], g[kMaxN]; queue<pair<int, int>> q; void bfs() { fill(dis[0], dis[0] + 2 * kMaxN, 1e9); for (int i = 1; i <= k; i++) { dis[a[i]][0] = 0; q.emplace(a[i], 0); } for (; q.size(); q.pop()) { auto t = q.front(); for (int i : e[t.first]) { if (dis[t.first][t.second] + 1 < dis[i][t.second ^ 1]) { dis[i][t.second ^ 1] = dis[t.first][t.second] + 1; q.emplace(i, t.second ^ 1); } } } } // 在 t 时刻,x 是否会被干掉 bool check(int x, int t) { if (t % 2 == d % 2) { t = min(t, d); } else { t = min(t, d - 1); } return dis[x][t % 2] <= t; } void solve() { if (check(1, 0)) { cout << "-1\n"; exit(0); } fill(len[0], len[0] + 2 * kMaxN, 1e9); for (q.emplace(1, 0), len[1][0] = 0; q.size(); q.pop()) { auto t = q.front(); for (int i : e[t.first]) { if (!check(i, len[t.first][t.second] + 1)) { if (len[t.first][t.second] + 1 < len[i][t.second ^ 1]) { p[i][t.second ^ 1].clear(); p[i][t.second ^ 1].push_back(t.first); len[i][t.second ^ 1] = len[t.first][t.second] + 1; q.emplace(i, t.second ^ 1); } else if (len[t.first][t.second] + 1 == len[i][t.second ^ 1]) { p[i][t.second ^ 1].push_back(t.first); } } } } } void paint(int x, int b) { if (f[x]) { return; } f[x] = 1; for (int i : p[x][b]) { g[i].push_back(x); paint(i, b ^ 1); } } void print() { for (int x = 1; x != n; ) { cout << x << ' '; x = *min_element(g[x].begin(), g[x].end()); } cout << n << '\n'; } int main() { cin.tie(0)->sync_with_stdio(0); cin >> n >> m >> d; for (int i = 1, u, v; i <= m; i++) { cin >> u >> v; e[u].push_back(v); e[v].push_back(u); } cin >> k; for (int i = 1; i <= k; i++) { cin >> a[i]; } bfs(); solve(); if (len[n][0] == 1e9 && len[n][1] == 1e9) { cout << "-1\n"; return 0; } if (len[n][0] < len[n][1]) { cout << len[n][0] << '\n'; paint(n, 0); } else { cout << len[n][1] << '\n'; paint(n, 1); } print(); return 0; } ``` ## 树上纯树 #李超线段树 #树形dp 考虑最简单的动态规划:设 $dp_i$ 表示 $i$ 到其子树中的任意叶子节点需要的最小费用。那么对于已经是叶子节点的 $i$,有 $dp_i=0$;否则枚举子树中的任意节点 $j$ $(j\ne i)$,求 $\min(a_ib_j+dp_j)$。 时间复杂度 $\mathcal O(n^2)$,可以写一个启发式合并。然后……就不会优化了。需要用李超线段树解决子树的问题,然后线段树合并。但是我不会。