题解:P12845 [蓝桥杯 2025 国 A] 连锁反应【数据强度待检验】
一些说明
本题解是纯思维解法。只需要用到排序算法。(也用到了枚举和贪心,但我认为这是一种思路而非算法。也用到了类似双指针的思路)但可能有些难理解,我还是尽量讲详细些。有些啰嗦,概要已加粗,可以先阅读。
遇到不理解的可以往后看,有些正确性和解释会放在结论后面。
为了方便,本题解中
暴力可以不看,但对正解的思路有帮助,还是建议阅读。如果没学过搜索就跳过那一段就好。
“直接引爆”指不考虑其它炸弹的再次爆炸,爆炸的炸弹一次即可炸到的区域或炸弹。“最终能引爆”指其它炸弹也可以爆炸的情况下能炸到的区域或炸弹。
O(n^2) 暴力
显然的做法是建图、缩点、拓扑排序,然后树形 dp。但是太难打了与本题正解无关,故不多言。
贪心+搜索做法
首先先按
那么就去找,引爆哪些炸弹可以最终引爆
这时可以搜索。如果
那有这么多炸弹,到底要选哪一个呢?这时候就要用到贪心。选择右端点最远的。 设这个炸弹为
正确性解释
为什么这样是对的呢?这个炸弹引爆了
这个正确性是建立在保证
(连通性的证明:两个有交的闭区间的并一定是一个闭区间,如
而向左延拓再多也没有意义,因为炸弹
然后再找到最左没爆的炸弹,重复上述操作。
算法不稳定,最坏会是
O(n+\max\{p_i\}) 正解
优化的方向
前面暴力的时间浪费在搜索时,每个炸弹无论是否可以引爆都要被枚举一遍。现在看能不能尽量只枚举可以引爆炸弹
当然还要用到前文讲到的连通性,很重要的结论(下文称为范围不跨越)是:若炸弹
做法
于是又是贪心,先找到所有能直接引爆
正确性:因为范围不跨越,如果右边的炸弹都不能直接引爆炸弹
具体实现
可以对于每个
不能每枚举一个炸弹都把
j=n-1;
for(i=n-1;i&&j>=0;i--){ //如果 i=0 就不用再循环了,因为数组初始值本来就是 0。
for(;j>=0&&_[j].a>=_[i].l;j--){ //如果 i 号炸弹直接炸到 j 号炸弹。
b[j]=i;
}
}
接下来就是引爆的过程。枚举每枚炸弹,找到
maxr=0;
for(i=0;i<n;sum++){ //每次循环时,炸弹 i 都是没被引燃的最左炸弹。sum++ 表示主动引燃了一枚炸弹。
for(j=i;b[j]>j;j=b[j]); //不断向右炸找最远能炸到 i 的。
maxr=max(maxr,_[j].r); //_[j].r 没有被枚举。故预判之。
for(;i<j;i++){ //这是之前引爆的炸弹。
maxr=max(maxr,_[i].r);
}
for(;i<n&&_[i].a<=maxr;i++){ //枚举能被引燃的炸弹,maxr 表示最右炸到的范围。
maxr=max(maxr,_[i].r); //新引爆的炸弹,也要更新最右能炸到的范围。
}
}
cout<<sum;
由于每枚炸弹只会被引爆一次,所以就算把所有炸弹枚举了总复杂度也是
时间复杂度:后面利用单调性,每枚炸弹最多被访问
还是放一下完整的代码吧。我用这个代码弄到了截至目前的最优解。(为了最优解还得手写桶排。。。)
#include<iostream>
#include<algorithm>
using namespace std;
void read(int &a){
a=0;
char c=getchar();
for(;c<48&&c;c=getchar()){}
for(;c>47&&c<58;c=getchar()){
a=(a<<3)+(a<<1)+c-48;
}
}
struct xyq{
int l,a,r;
}t[200005],_[200005];
struct rule{
bool operator()(const xyq &s1,const xyq &s2){
return s1.a<s2.a;
}
};
int zyl[200005],b[200005]; //zyl:最右直接向左炸到 i 号炸弹的。也就是题解中的 b[i]。
int main(){
int n,i,j,sum=0,maxr=0,maxa=0,ykb;
cin>>n;
for(i=0;i<n;i++){
read(t[i].a);
read(t[i].l);
read(t[i].r);
b[t[i].a+1]++;
maxa=max(maxa,t[i].a);
t[i].l=t[i].a-t[i].l;
t[i].r=t[i].a+t[i].r;
}
if(maxa>(n<<4)){ //这是排序过程,大概判断一下用 sort 还是桶排快。
for(i=0;i<n;i++){
_[i]=t[i];
}
sort(_,_+n,rule());
}else{
for(i=2;i<=maxa+1;i++){
b[i]+=b[i-1];
}
for(i=0;i<n;i++){
_[b[t[i].a]++]=t[i];
}
}
j=n-1;
for(i=n-1;i&&j>=0;i--){ //如果 i=0 就不用再循环了,因为初始值本来就是 0。
for(;j>=0&&_[j].a>=_[i].l;j--){ //如果 i 号炸弹直接炸到 j 号炸弹。
zyl[j]=i;
}
}
for(i=0;i<n;sum++){ //枚举第 sum 次引爆。此时 i 为最左边的炸弹。
for(j=i;zyl[j]>j;j=zyl[j]); //不断向右炸找最远能炸到 i 的。
maxr=max(maxr,_[j].r); //_[j].r 没有被枚举。故预判之。
for(;i<j;i++){
maxr=max(maxr,_[i].r);
}
for(;i<n&&_[i].a<=maxr;i++){ //枚举能被引燃的炸弹,maxr 表示最右炸到的范围。
maxr=max(maxr,_[i].r); //更新最右能炸到的范围。
}
}
cout<<sum;
return 0;
}
题外话
唉,AFO 已经快四年了。高考完了来回忆一下 OI 时光,真是感慨万千。题解都是线段树,我肯定不会了,没想到还另辟蹊径,想到了纯结论的线性解法。(所以难度是不是可以降蓝了。)
这是我做这题时的一些心路历程。也提醒大家一个错解。
一个错解
一开始我想到的思路是排序后对每个炸弹先分别求出向左和右能炸到的最远炸弹编号
对于这个最远距离,我是用递推的方法求解的:以向左为例,从左往右依次确定。每枚炸弹能向左炸到的最左炸弹
这个漏考虑了一个问题:可以先引爆右边的某个范围更大的炸弹。一开始我以为这样的方式肯定不优,所以可以不考虑。但是对于这组数据:
4
0 0 0
1 0 2
2 2 0
3 0 0
| 处理出来的数据: | |||
|---|---|---|---|
于是输出了错误答案
不知道这个问题可不可以被修复。但我目前没想到可以证明正确的方法。
其实我的思路和这篇题解有相似之处。这篇也错误输出了
逐渐想到的正解
几乎做了一个下午,居然是错的。我当然不甘心,看能不能弥补这个漏洞。于是就想到了能不能不求
于是我就想,哪些炸弹能最终引爆炸弹
但是正要写代码时,又发现了严重的问题。在引爆关系是一条链时,这样的复杂度会降到
怎么找
冥思无果,我都关了电脑,打算改日再想了。但就在我躺在飘窗上时不断想,之所以会