题解:P12845 [蓝桥杯 2025 国 A] 连锁反应【数据正确性待检验】
TJUHuangTao · · 题解
前言
正解感觉可以用贪心加区间和并之类的维护,但是赛场上想到了一种大力出奇迹的乱搞不需要动脑做法,特此记录。代码赛时是过了随机的对拍,本贴里的代码是赛后按照回忆重新打的,不保证正确,仅供参考。目前是只有 85 分,不知道哪里写挂了,希望大家一起帮忙看看。
更新:感谢@ran_qwq 的补充,之前错误的原因是只考虑了有意义的点的直接出点,而忽略了可能有意义点的直接出点是无意义,而无意义的出点又是有意义,这种间接到达的情况,hack数据如下:
input:
4
1 0 3
2 0 0
3 0 0
4 0 0
output:
1
现已能够通过所有数据。
题意
有
O(n^2) 做法
首先考虑一下
那么考虑有环的情况,自然想到了 tarjan 缩点,并且上面的结论对缩完点的图依然成立,只考虑一个环,都有入度,但是还需要手动引爆一次整个环都炸了。所以答案就是对图缩点后统计入度为 0 的顶点数,
O(nlogn) 优化
那么很明显,上面做法的复杂度是卡在了建图的地方,需要
需要注意的是,发现我们只会关注叶子节点能够到达的那部分线段树节点,而对再往上的部分不关心,直接用原始的线段树优化建图,也就是每个结点都往儿子节点建边,后续不方便统计出度之类的。因此,正确的统计方法(@ran_qwq指正)应该是,首先对于每个叶子,将它连向的线段树节点的整个子树标记(实现上可以先标记单个节点再遍历一次线段树下推这个标记),然后将未标记的节点删去,有标记的那些节点进行缩点。之后对这个有标记的缩完点后的图直接统计入度为 0 的点数量即可。
这里拿样例为例,画一个图方便大家理解,首先对样例的点排序后重新编号,可以发现下面的覆盖关系:
- 1 -> [1 , 2]
- 2 -> [2 , 2]
- 3 -> [3 , 3]
- 4 -> [3 , 5]
- 5 -> [4 , 5]
用绿色的边表示线段树点向区间连边,红色的圈表示有标记的节点且缩点后的 SCC。对于红圈这个缩点后的标记子图,显然有2个入度为0的 SCC 即蓝星标记的那两个SCC,故答案为 2。得到下图所示: 可以这样具体实现:首先调用线段树的 get_leaf() 函数遍历一次,得到每个叶子节点的编号。之后叶子向线段树连边时,对应节点的 tag 数组置为1,之后再次遍历线段树调用 build() 函数,每个节点将 tag 标记下放到儿子,并且自身如果有标记的话才会向儿子连边。最后判断一下对于有标记的点再进行 tarjan 缩点,此时得到的 SCC 子图都是有标记的了,对这个子图统计入度为 0 的点数量即可。
代码
代码如下
#include <bits/stdc++.h>
#define int long long
#define inf 0x3f3f3f3f
#define ll long long
#define pii pair<int, int>
#define db double
#define all(a) a.begin(), a.end()
using namespace std;
const int maxn = 8e5 + 10;
const int mod = 998244353;
struct tii {
int a, b, c;
} arr[maxn];
bool cmp(tii x, tii y) {return x.a < y.a;}
int p[maxn], l[maxn], r[maxn];
vector<int> G[maxn];
int dfn[maxn], low[maxn], Cnt, instk[maxn], scc[maxn], cscc;
stack<int> stk;
void tarjan(int u) { //缩点板子
low[u] = dfn[u] = ++Cnt;
instk[u] = 1;
stk.push(u);
for (auto v : G[u]) {
if (!dfn[v]) tarjan(v), low[u] = min(low[u], low[v]);
else if (instk[v]) low[u] = min(low[u], dfn[v]);
}
if (low[u] == dfn[u]) {
int top;
cscc++;
do {
top = stk.top();
stk.pop();
instk[top] = 0;
scc[top] = cscc;
} while(top != u);
}
}
int leaf[maxn], up, tag[maxn], ans, in[maxn];
struct SEG {
#define ls rt << 1
#define rs rt << 1 | 1
#define mid ((l + r) >> 1)
int t[maxn];
void get_leaf(int rt, int l, int r) {
if (l == r) {
leaf[l] = rt;
up = max(up, rt);
return;
}
get_leaf(ls, l, mid), get_leaf(rs, mid + 1, r);
}
void build(int rt, int l, int r) {
if (l == r) {
return;
}
if (tag[rt])
G[rt].push_back(ls), G[rt].push_back(rs);
tag[ls] |= tag[rt], tag[rs] |= tag[rt];
build(ls, l, mid), build(rs, mid + 1, r);
}
void Union(int rt, int l, int r, int p, int q, int id) {
if (p > r || q < l) return;
if (p <= l && r <= q) {
G[leaf[id]].push_back(rt); //连边
tag[rt] = 1;
return;
}
Union(ls, l, mid, p, q, id), Union(rs, mid + 1, r, p, q, id);
}
} seg;
void solve() {
int n; cin >> n;
seg.get_leaf(1, 1, n);
for (int i = 1; i <= n; i++)
cin >> arr[i].a >> arr[i].b >> arr[i].c;
sort(arr + 1, arr + 1 + n, cmp);
for (int i = 1; i <= n; i++)
p[i] = arr[i].a, l[i] = arr[i].b, r[i] = arr[i].c;
for (int i = 1; i <= n; i++) {
int lo = p[i] - l[i], hi = p[i] + r[i];//寻找爆炸影响的 区间
int lopos = lower_bound(p + 1, p + 1 + n, lo) - p;
int hipos = upper_bound(p + 1, p + 1 + n, hi) - p - 1;
if (lopos <= hipos)
seg.Union(1, 1, n, lopos, hipos, i); //建边
}
seg.build(1, 1, n);
for (int i = 1; i <= up; i++)
if (tag[i] && !dfn[i]) tarjan(i); //缩点
for (int i = 1; i <= up; i++)
for (auto v : G[i])
if (!tag[i] || !tag[v]) continue; //跳过没有意义的点
else if (scc[i] != scc[v]) {
in[scc[v]]++; //统计入度
}
for (int i = 1; i <= cscc; i++)
ans += in[i] == 0; //此时的所有 SCC 都已经是有意义的点浓缩后的,直接统计入度为 0 的即可
cout << ans << "\n";
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int t = 1;
// cin >> t;
while (t--) solve();
return 0;
}
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int t = 1;
// cin >> t;
while (t--) solve();
return 0;
}