清零警报器
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\)。
至此转化为数据结构问题。列出需要维护的操作:
- 给定 \(m\) 个报警器 \((a_i,b_i)\),当 \(a_i \to b_i\) 路径上未定向边数 \(\textcolor{red}{=1}\) 时报警。
- 在树上维护以下操作:
- 给定 \(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]);
}
::::::

浙公网安备 33010602011771号