清零警报器

📅 2026/8/1 17:41:01 👁️ 阅读次数 📝 编程学习
清零警报器

P4215 踩气球

维护一个长为 \(n\) 的序列 \(a_i\),支持 \(q\) 次操作,每次操作给定 \(x\),令 \(a_x \gets a_x-1\)。保证任意时刻 \(\forall i, a_i \ge 0\)

给定 \(m\) 个警报器,第 \(i\) 个警报器会在 \(\sum \limits_{k \in [l_i,r_i]}a_k=0\) 时报警。每次操作结束后,求出报警的次数。强制在线。

\(n,m,q \le 10^5\),1 秒,125 MB。

:::::success[题解]

对序列 \(a_i\) 建立线段树,维护区间和。

对于一个警报器 \([l_i,r_i]\),我们将其挂到线段树上的 \(O(\log n)\) 段节点。修改是对线段树单点修改。

对于被修改的节点,可以发现只有区间和清零时,我们才需要更新节点上挂载的警报器。这里,我们对每个警报器维护区间和,容易 \(O(1)\) 更新 & 判定是否报警。

由于区间和单调递减,每个警报器只会被其所在的节点暴力更新 \(1\) 次,拆成 \(\log\) 个节点。视 \(n,m,q\) 同阶总复杂度 \(O(n \log n)\)

:::::

:::::info[代码]

const int N = 1e5 + 5;
int n, m, q, ans, a[N]; 
i64 c[N], s[N];
void push(int x, i64 v) {mdebug(x, v);c[x] -= v;if (c[x] == 0) ans++;
}
struct segtree {#define ls (rt << 1)#define rs (rt << 1 | 1)i64 sum[N << 2];vector<int> to[N << 2];void pushup(int rt) {sum[rt] = sum[ls] + sum[rs];}void build(int l = 1, int r = n, int rt = 1) {if (l == r) return sum[rt] = a[l], void();int mid = (l + r) >> 1;build(l, mid, ls), build(mid + 1, r, rs), pushup(rt);}void modify(int tl, int tr, int c, int l = 1, int r = n, int rt = 1) {if (tl <= l && r <= tr) return to[rt].emplace_back(c), void();int mid = (l + r) >> 1;if (tl <= mid) modify(tl, tr, c, l, mid, ls);if (tr > mid) modify(tl, tr, c, mid + 1, r, rs);}void update(int x, int l = 1, int r = n, int rt = 1) {if (l == r) {if (--sum[rt] == 0) for (int x : to[rt]) push(x, a[l]);return;}int mid = (l + r) >> 1;if (x <= mid) update(x, l, mid, ls);else update(x, mid + 1, r, rs);pushup(rt);if (sum[rt] == 0) for (int x : to[rt]) push(x, s[r] - s[l-1]);}
} T;void _main() {cin >> n >> m;for (int i = 1; i <= n; i++) cin >> a[i], s[i] = s[i - 1] + a[i];T.build();for (int i = 1, l, r; i <= m; i++) {cin >> l >> r; c[i] = s[r] - s[l - 1];T.modify(l, r, i);}cin >> q;for (int x; q--; ) {cin >> x; x = (x + ans - 1) % n + 1;T.update(x);cout << ans << '\n';}
}

:::::

P14241 [CCPC 2024 Shandong I] 传感器

P4215 的双倍经验。

P10787 [NOI2024] 树的定向

给定一棵 \(n\) 个节点的树,边有标号,再给定 \(m\) 条限制 \((a_i,b_i)\)

你需要构造一组定向方案,满足 \(\forall i \in [1,m], a_i \to b_i\) 不可达。再此基础上,最小化定向方案的字典序,并输出构造。数据保证有解。

\(n,m \le 5 \times 10^5\),3 秒,2048 MB。

::::::success[题解]

从 B 性质入手思考,容易得到 2-SAT 字典序最小解的转化,进而得到一般情形 k-SAT 的转化,从而问题不可做。

这启示我们从 A 性质入手思考。经过推演,可以发现有且仅有以下两种定向方案合法:

  • 奇数层向下,偶数层向上。
  • 奇数层向上,偶数层向上。

从而 A 性质的解法为,从两种方案中取出字典序较小的方案。

对于一般情形,相邻点对的限制相当于钦定了某些边的定向。显然,我们需要先对这些边定向。一部分边定向后,又会产生新的必须定向的边,这样得到一个迭代过程。同时,对于路径上已经存在反向边的限制,我们可以将其忽略。

现在的问题是,不存在必须定向的边时,如何进行定向。根据 A 性质的构造,我们得到本题的关键结论:

性质 若对剩余所有限制 \((a_i,b_i)\)\(a_i \to b_i\) 至少存在 \(2\) 条未定向边,则必然存在合法方案。

这是有解的充分不必要条件。这告诉我们,不存在必须定向的边时,可以直接钦定标号最小边的方向,重复这个流程。

下面复述一遍算法流程:

  • 维护队列 \(Q\),存放仅剩 \(1\) 条未定向边的所有限制。
  • 重复如下操作直到所有边被定向:
    • 取出队首限制。若其有效,则将路径上唯一未定向边定向。直到 \(Q\) 为空。
    • 将标号最小边定向,并将新的限制加入 \(Q\)

至此转化为数据结构问题。列出需要维护的操作:

  1. 给定 \(m\) 个报警器 \((a_i,b_i)\),当 \(a_i \to b_i\) 路径上未定向边数 \(\textcolor{red}{=1}\) 时报警。
  2. 在树上维护以下操作:
    • 给定 \(a_i,b_i\),判定 \(a_i \to b_i\) 是否存在反向边。
    • 给定 \(a_i,b_i\),找出 \(a_i \to b_i\) 未定向边的标号。
    • 给定 \(i\),将第 \(i\) 条边定向。

这两部分都存在 1log 做法,下面介绍思维难度较小的 2log 做法。

树剖拍到序列上。对于第一部分,可以使用清零警报器维护。具体地,我们对序列建立线段树,将警报器挂在 \(\log^2 n\) 个节点上。每次修改时,对于需要更新的线段树节点,当且仅当区间和 \(\le 1\) 时扫描挂载的警报器,判定其是否报警。由于每个警报器只会被 \(O(\log^2 n)\) 个节点检查 \(O(1)\) 次,总复杂度 \(O(m \log^2 n)\)

对于第二部分,仍转化到序列上。用一棵线段树维护 01 序列,区间查询存在性是简单的。求标号可以在线段树上二分,复杂度也是 2log。

\(n,m\) 同阶复杂度 \(O(n \log^2 n)\),实现优秀的话跑的飞快。具体实现细节见代码。

::::::

::::::info[代码]

const int N = 5e5 + 5;
int n, m, s[N], t[N], low[N], a[N], b[N], ans[N];
vector<pair<int, int>> e[N];int dn, sz[N], fa[N], dep[N], son[N], dfn[N], top[N], seq[N], eid[N];
void dfs1(int u) {sz[u] = 1;for (auto [v, id] : e[u]) {if (v == fa[u]) continue;eid[v] = id, low[id] = v;fa[v] = u, dep[v] = dep[u] + 1, dfs1(v), sz[u] += sz[v];if (sz[v] > sz[son[u]]) son[u] = v;}
}
void dfs2(int u, int t) {dfn[u] = ++dn, top[u] = t, seq[dn] = u;if (son[u]) dfs2(son[u], t);for (auto [v, id] : e[u]) if (v != fa[u] && v != son[u]) dfs2(v, v);
}
int lca(int u, int v) {while (top[u] != top[v]) {if (dep[top[u]] < dep[top[v]]) swap(u, v);u = fa[top[u]];} return dep[u] < dep[v] ? u : v;
}void check(int x, int c);
// -1: undirected 0: up 1: down 
struct segtree {#define ls (rt << 1)#define rs (rt << 1 | 1)int cnt[N << 2];u8 has[N << 2];vector<int> to[N << 2];void pushup(int rt) {cnt[rt] = cnt[ls] + cnt[rs];has[rt] = has[ls] | has[rs];}void build(int l = 1, int r = n, int rt = 1) {if (l == r) return cnt[rt] = 1, void();int mid = (l + r) >> 1;build(l, mid, ls), build(mid + 1, r, rs), pushup(rt);}    bool exist(int tl, int tr, int c, int l = 1, int r = n, int rt = 1) {if (tl <= l && r <= tr) return has[rt] >> c & 1;int mid = (l + r) >> 1;if (tl <= mid && exist(tl, tr, c, l, mid, ls)) return true;if (tr > mid && exist(tl, tr, c, mid + 1, r, rs)) return true;return false;}int search(int tl, int tr, int l = 1, int r = n, int rt = 1) {if (!cnt[rt]) return -1;if (r < tl || l > tr) return -1;if (l == r) return l;int mid = (l + r) >> 1, tmp = -1;tmp = search(tl, tr, l, mid, ls);if (tmp != -1) return tmp;return search(tl, tr, mid + 1, r, rs);}void update(int x, int c, int l = 1, int r = n, int rt = 1) {if (l == r) {cnt[rt]--;for (int x : to[rt]) check(x, 1);return has[rt] |= 1 << c, void();}int mid = (l + r) >> 1;if (x <= mid) update(x, c, l, mid, ls);else update(x, c, mid + 1, r, rs);pushup(rt);if (cnt[rt] == 1)for (int x : to[rt]) check(x, r - l);if (cnt[rt] == 0)for (int x : to[rt]) check(x, 1);}void modify(int tl, int tr, int c, int l = 1, int r = n, int rt = 1) {if (tl <= l && r <= tr) return to[rt].emplace_back(c), void();int mid = (l + r) >> 1;if (tl <= mid) modify(tl, tr, c, l, mid, ls);if (tr > mid) modify(tl, tr, c, mid + 1, r, rs);}
} T;int hd=1, tl=0, q[40*N], cnt[N];
bool del[N];void push(int u, int v, int to) {cnt[to] = dep[u] + dep[v] - 2 * dep[lca(u, v)];while (top[u] != top[v]) {if (dep[top[u]] < dep[top[v]]) swap(u, v);T.modify(dfn[top[u]], dfn[u], to);u = fa[top[u]];}if (dfn[u] > dfn[v]) swap(u, v);if (dfn[u] < dfn[v]) T.modify(dfn[u]+1, dfn[v], to);
}bool removed(int id) {if (del[id]) return true;int u = a[id], v = b[id], x = lca(u, v);while (top[u] != top[x]) {if (T.exist(dfn[top[u]], dfn[u], 1)) return del[id] = true;u = fa[top[u]];}if (u != x && T.exist(dfn[x]+1, dfn[u], 1)) return del[id] = true;while (top[v] != top[x]) {if (T.exist(dfn[top[v]], dfn[v], 0)) return del[id] = true;v = fa[top[v]];}if (v != x && T.exist(dfn[x]+1, dfn[v], 0)) return del[id] = true;return false;
}void check(int x, int c) {cnt[x] -= c;if (cnt[x] == 1 && !removed(x)) q[++tl] = x;
} 
void direct(int i, int st) {ans[i] = low[i] == s[i] ? st : (st ^ 1);T.update(dfn[low[i]], st);
}pair<int, int> search(int id) {int u = a[id], v = b[id], x = lca(u, v);while (top[u] != top[x]) {int r = T.search(dfn[top[u]], dfn[u]);if (r != -1) return {eid[seq[r]], 1};u = fa[top[u]];}if (u != x) {int r = T.search(dfn[x]+1, dfn[u]);if (r != -1) return {eid[seq[r]], 1};}while (top[v] != top[x]) {int r = T.search(dfn[top[v]], dfn[v]);if (r != -1) return {eid[seq[r]], 0};v = fa[top[v]];}if (v != x) {int r = T.search(dfn[x]+1, dfn[v]);if (r != -1) return {eid[seq[r]], 0};}return {-1, -1};
}void _main() {read(n, n, m);for (int i = 1; i < n; i++) {read(s[i], t[i]);e[s[i]].emplace_back(t[i], i);e[t[i]].emplace_back(s[i], i);}dfs1(1), dfs2(1, 1), T.build();for (int i = 1; i <= m; i++) read(a[i], b[i]), push(a[i], b[i], i);for (int i = 1; i <= m; i++) check(i, 0);fill(ans + 1, ans + n, -1);for (int i = 1; i < n; ) {while (hd <= tl) {int x = q[hd++];if (removed(x)) continue;auto [i, st] = search(x);del[x] = true, direct(i, st);}while (i < n && ans[i] != -1) i++;if (i >= n) break;direct(i, low[i] == t[i]);}for (int i = 1; i < n; i++) write(ans[i]);   
} 

::::::