P5025 [SNOI2017]炸弹
原题链接 https://www.luogu.org/problem/P5025


闲话时刻:
第一道 AC 的黑题,虽然众人皆说水。。。
其实思路不是很难,代码也不是很难打,是一些我们已经学过的东西凝合在一起,只要基础扎实的话,做出这道题目来说也就很简单了(不包括我);
题目大意:
有 n 个点,每个点可影响到它左右各 R [ i ] 范围内的点,并且影响到的点会产生连锁反应,求每个点能影响到多少个点;
题解:
一个很简单的思路:
向每个炸弹爆炸范围内的其他炸弹连一条有向边 < u , v >,表示 u 能炸到 v,最后我们从每个点开始跑 dfs,看看能到达多少个点就好了;
但是。。。
这样连边的话,最劣情况下会连 n2 条边,看一眼 n 的范围:N ≤ 500000,嗯,显然不行 ~o(* ̄▽ ̄*)o;
考虑建边优化:
不难想到一个炸弹的爆炸范围是一个长度为 2 * R [ i ] 的区间,这个区间内的所有炸弹都会被引爆,因此被引爆的炸弹也是一个连续的区间;
区间操作?你想到了什么?
线段树优化建边
假如说一个炸弹 x 能炸到第 2~6 个炸弹,考虑怎么建边:
一般操作:

线段树优化建边:
建了 5 条边?看我的!

我们发现,这种建边方式只需要建 ⌈ log2 5 ⌉ = 3 条边;
看了上面的图,应该对线段树优化建边有了一个初步的认识了:
我们将线段树上原有的边也看作是我们建边的一部分,每次单点向区间建边时,向线段树区间查询那样,一直递归下去形成若干个小区间,向这些小区间建边;查询的话可以通过线段树原有的边来访问到叶子节点;
再具体一点,假如第 2 个炸弹能炸到第 4~8 个炸弹,那么建边就是这样的:

这样虽然 2 连向了 [ 5 , 8 ] 这个节点,但是我们能通过继续往下递归找到 5,6,7,8 点,从而达到优化建边的目的;
建完边了,但是这边还是有点多哦,看起来很乱,缩点试试?
缩点是可以的,假如一个环内的任意一个炸弹被引爆了,那么整个环内的所有炸弹都会被引爆,所以我们不妨将这一环内的炸弹看作是一个大炸弹;
然后我们就又用到了 tarjan 缩点;
这个就不多说了,大家应该都会;
然后对于每个强联通分量,我们都 dfs 一遍累加它能到达的所有强联通分量的大小,这个题就做完了;
woc?这么简单?
没错就是这么简单qwq!
细节提示
这道题思路挺简单的,但是里面的坑真不少,归结一下我掉进的坑,顺便提醒您们一下哦:
1. 每个强联通分量的大小不是里面的节点个数,而是里面的叶子节点个数(炸弹数),那些区间节点是不能被包含在内的;
2. dfs 的过程中,强联通分量之间可能会有重边,记得记录一下,防止重复计算;
在这里解释一下为什么有重边:
比如说这个缩点之前的奇奇怪怪的图:

嗯,是没有重边,缩完点之后呢?

对吧。
3. 注意开 long long!
代码如下:
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<vector>
using namespace std;
long long read()
{
char ch=getchar();
long long a=,x=;
while(ch<''||ch>'')
{
if(ch=='-') x=-x;
ch=getchar();
}
while(ch>=''&&ch<='')
{
a=(a<<)+(a<<)+(ch-'');
ch=getchar();
}
return a*x;
}
const int N=;
const long long mod=;
long long n,tim,top,maxn,scc_sum,edge_sum,Edge_sum;
long long zb[N],R[N],num[N],where[N],head[N],Head[N],dfn[N],low[N],vis[N],st[N],scc[N];
long long ans[N],sum,size[N];
vector<int> son[N<<];
struct node
{
int to,from,next;
}a[N<<],b[N<<];
void add(int from,int to) //链表建图,应用于线段树上
{
edge_sum++;
a[edge_sum].next=head[from];
a[edge_sum].to=to;
a[edge_sum].from=from;
head[from]=edge_sum;
}
void build(long long node,int l,int r)
{
if(l==r)
{
where[l]=node; //坐标为l的炸弹在线段树的第node个节点
maxn=max(maxn,node); //记录线段树上最大的节点编号是多少
num[node]=; //表示线段树上的第node个节点是叶子节点
return ;
}
int mid=(l+r)>>;
add(node,node<<);
add(node,node<<|); //向左右子树建边
build(node<<,l,mid); //递归
build(node<<|,mid+,r);
}
void Add(int node,int l,int r,int x,int y,int v)
{
if(x<=l&&r<=y)
{
add(v,node); //点向区间连边
return ;
}
int mid=(l+r)>>;
if(x<=mid) Add(node<<,l,mid,x,y,v);
if(y>mid) Add(node<<|,mid+,r,x,y,v);
}
void tarjan(int u) //tarjan缩点
{
dfn[u]=low[u]=++tim;
st[++top]=u;
vis[u]=;
for(int i=head[u];i;i=a[i].next)
{
int v=a[i].to;
if(!dfn[v])
{
tarjan(v);
low[u]=min(low[u],low[v]);
}
else if(vis[v]) low[u]=min(low[u],dfn[v]);
}
if(low[u]==dfn[u])
{
scc_sum++;
while(st[top]!=u)
{
vis[st[top]]=;
scc[st[top]]=scc_sum;
size[scc_sum]+=num[st[top]]; //坑点1:注意只记录叶子节点(炸弹)个数,而不是节点个数,num只有叶子节点才有值
top--;
}
//把u弹出去(表示不会do while)
vis[st[top]]=;
scc[st[top]]=scc_sum;
size[scc_sum]+=num[st[top]];
top--;
}
}
void rebuild()
{
for(int i=;i<=edge_sum;i++) //线段树上所有的边
{
int u=a[i].from;
int v=a[i].to;
if(scc[u]!=scc[v])
son[scc[u]].push_back(scc[v]); //vector存图,比较方便
}
}
void dfs(int u)
{
if(ans[u]) return ; //记搜
ans[u]=size[u]; //首先一个强联通分量里的点都能互相到达
for(int i=;i<son[u].size();i++)
{
int v=son[u][i];
dfs(v);
//这里有个vis数组判重边用的特别精髓,给大家解释下:
//vis[i]:表示本轮从哪个点到的i
//想一想:假如u->v有两条边,那么在走第一条边的时候,vis[v]就会被标记u,那么在遍历第二遍的时候就直接continue了,不会重复计算
//vis数组可以不用清空
if(vis[v]==u) continue;
vis[v]=u;
ans[u]=(ans[u]+ans[v])%mod;
}
}
int main()
{
n=read(); //n个炸弹
for(int i=;i<=n;i++)
{
zb[i]=read(); //炸弹的位置,题目保证是严格递增的了
R[i]=read(); //炸弹的爆炸半径
}
build(,,n); //线段树建树
for(int i=;i<=n;i++) //将每个炸弹与能炸到的炸弹区间连边
{
long long l=lower_bound(zb+,zb++n,zb[i]-R[i])-zb; //找到能炸到的最左边的炸弹
long long r=upper_bound(zb+,zb++n,zb[i]+R[i])-zb-;//找到能炸到的最右边的炸弹
Add(,,n,l,r,where[i]); //连边,where[i]表示炸弹i在线段树上是哪个节点,不懂的结合上面的图体会一下
}
for(int i=;i<=maxn;i++) //缩点操作,注意这里是对线段树上的所有点进行缩点,不单单是叶子节点
{
if(!dfn[i]) tarjan(i); //图不一定联通,多进行几次
}
rebuild(); //对缩完点后的DAG进行重新建图
for(int i=;i<=scc_sum;i++) dfs(i); //dfs求每个强联通分量能够到达多少个炸弹
for(int i=;i<=n;i++) //求答案
{
sum=(sum+i*ans[scc[where[i]]]%mod)%mod;
//where[i]:炸弹i在线段树上是第几个节点
//scc[i]:编号为i的点在哪个强联通分量里
//ans[i]:编号为i的强联通分量能够到达多少个炸弹
//合起来就是:炸弹i在线段树上所对应的节点所在的强联通分量能到达多少个炸弹,are you ok?
}
printf("%lld\n",sum%mod);
return ;
}
然后发现跑的有点慢能A不就行了嘛:

主要是在 dfs 的时候 vector 就显得很慢了,所以我们重建图的时候可以先用链表建图,等跑完 dfs 之后再用 vector 存儿子判重边就好了:
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<vector>
using namespace std;
long long read()
{
char ch=getchar();
long long a=,x=;
while(ch<''||ch>'')
{
if(ch=='-') x=-x;
ch=getchar();
}
while(ch>=''&&ch<='')
{
a=(a<<)+(a<<)+(ch-'');
ch=getchar();
}
return a*x;
}
const int N=;
const long long mod=;
long long n,tim,top,maxn,scc_sum,edge_sum,Edge_sum;
long long zb[N],R[N],num[N],where[N],head[N],Head[N],dfn[N],low[N],vis[N],st[N],scc[N];
long long ans[N],sum,size[N];
struct node
{
int to,from,next;
}a[N<<],b[N<<];
void add(int from,int to) //链表建图,应用于线段树上
{
edge_sum++;
a[edge_sum].next=head[from];
a[edge_sum].to=to;
a[edge_sum].from=from;
head[from]=edge_sum;
}
void readd(int from,int to)
{
Edge_sum++;
b[Edge_sum].from=from;
b[Edge_sum].to=to;
b[Edge_sum].next=Head[from];
Head[from]=Edge_sum;
}
void build(long long node,int l,int r)
{
if(l==r)
{
where[l]=node; //坐标为l的炸弹在线段树的第node个节点
maxn=max(maxn,node); //记录线段树上最大的节点编号是多少
num[node]=; //表示线段树上的第node个节点是叶子节点
return ;
}
int mid=(l+r)>>;
add(node,node<<);
add(node,node<<|); //向左右子树建边
build(node<<,l,mid); //递归
build(node<<|,mid+,r);
}
void Add(int node,int l,int r,int x,int y,int v)
{
if(x<=l&&r<=y)
{
add(v,node); //点向区间连边
return ;
}
int mid=(l+r)>>;
if(x<=mid) Add(node<<,l,mid,x,y,v);
if(y>mid) Add(node<<|,mid+,r,x,y,v);
}
void tarjan(int u) //tarjan缩点
{
dfn[u]=low[u]=++tim;
st[++top]=u;
vis[u]=;
for(int i=head[u];i;i=a[i].next)
{
int v=a[i].to;
if(!dfn[v])
{
tarjan(v);
low[u]=min(low[u],low[v]);
}
else if(vis[v]) low[u]=min(low[u],dfn[v]);
}
if(low[u]==dfn[u])
{
scc_sum++;
while(st[top]!=u)
{
vis[st[top]]=;
scc[st[top]]=scc_sum;
size[scc_sum]+=num[st[top]]; //坑点1:注意只记录叶子节点(炸弹)个数,而不是节点个数,num只有叶子节点才有值
top--;
}
//把u弹出去(表示不会do while)
vis[st[top]]=;
scc[st[top]]=scc_sum;
size[scc_sum]+=num[st[top]];
top--;
}
}
void rebuild()
{
for(int i=;i<=edge_sum;i++) //线段树上所有的边
{
int u=a[i].from;
int v=a[i].to;
if(scc[u]!=scc[v])
//son[scc[u]].push_back(scc[v]); //vector存图,比较方便
readd(scc[u],scc[v]);
}
}
void dfs(int u)
{
/*if(ans[u]) return ; //记搜
ans[u]=size[u]; //首先一个强联通分量里的点都能互相到达
for(int i=0;i<son[u].size();i++)
{
int v=son[u][i];
dfs(v);
//这里有个vis数组判重边用的特别精髓,给大家解释下:
//vis[i]:表示本轮从哪个点到的i
//想一想:假如u->v有两条边,那么在走第一条边的时候,vis[v]就会被标记u,那么在遍历第二遍的时候就直接continue了,不会重复计算
//vis数组可以不用清空
if(vis[v]==u) continue;
vis[v]=u;
ans[u]=(ans[u]+ans[v])%mod;
}*/
if(ans[u]) return ;
ans[u]=size[u];
vector<int> son;
for(int i=Head[u];i;i=b[i].next)
{
int v=b[i].to;
dfs(v);son.push_back(v); //把点u的所有儿子放进vector里
}
for(int i=;i<son.size();i++)
{
if(vis[son[i]]==u) continue;
vis[son[i]]=u;
ans[u]=(ans[u]+ans[son[i]])%mod;
}
}
int main()
{
n=read(); //n个炸弹
for(int i=;i<=n;i++)
{
zb[i]=read(); //炸弹的位置,题目保证是严格递增的了
R[i]=read(); //炸弹的爆炸半径
}
build(,,n); //线段树建树
for(int i=;i<=n;i++) //将每个炸弹与能炸到的炸弹区间连边
{
long long l=lower_bound(zb+,zb++n,zb[i]-R[i])-zb; //找到能炸到的最左边的炸弹
long long r=upper_bound(zb+,zb++n,zb[i]+R[i])-zb-;//找到能炸到的最右边的炸弹
Add(,,n,l,r,where[i]); //连边,where[i]表示炸弹i在线段树上是哪个节点,不懂的结合上面的图体会一下
}
for(int i=;i<=maxn;i++) //缩点操作,注意这里是对线段树上的所有点进行缩点,不单单是叶子节点
{
if(!dfn[i]) tarjan(i); //图不一定联通,多进行几次
}
rebuild(); //对缩完点后的DAG进行重新建图
for(int i=;i<=scc_sum;i++) dfs(i); //dfs求每个强联通分量能够到达多少个炸弹
for(int i=;i<=n;i++) //求答案
{
sum=(sum+i*ans[scc[where[i]]]%mod)%mod;
//where[i]:炸弹i在线段树上是第几个节点
//scc[i]:编号为i的点在哪个强联通分量里
//ans[i]:编号为i的强联通分量能够到达多少个炸弹
//合起来就是:炸弹i在线段树上所对应的节点所在的强联通分量能到达多少个炸弹,are you ok?
}
printf("%lld\n",sum%mod);
return ;
}

嗯,果然在时间和空间上都有很大的优化哦~
P5025 [SNOI2017]炸弹的更多相关文章
- P5025 [SNOI2017]炸弹 题解
蒟蒻的第一篇黑题题解(学了这么长时间了才第一道也是没谁了.) 题目链接: Solution: 朴素: 根据题目描述,我们可以处理每一个x节点左右爆炸半径范围内的点,然后模拟一次爆炸 (for),遍历每 ...
- [bzoj5017][Snoi2017]炸弹 tarjan缩点+线段树优化建图+拓扑
5017: [Snoi2017]炸弹 Time Limit: 30 Sec Memory Limit: 512 MBSubmit: 608 Solved: 190[Submit][Status][ ...
- [LOJ#2255][BZOJ5017][Snoi2017]炸弹
[LOJ#2255][BZOJ5017][Snoi2017]炸弹 试题描述 在一条直线上有 N 个炸弹,每个炸弹的坐标是 Xi,爆炸半径是 Ri,当一个炸弹爆炸时,如果另一个炸弹所在位置 Xj 满足: ...
- [SNOI2017]炸弹[线段树优化建图]
[SNOI2017]炸弹 线段树优化建图,然后跑一边tarjan把点全部缩起来,炸一次肯定是有连锁反应的所以整个连通块都一样-于是就可以发现有些是只有单向边的不能忘记更新,没了. #include & ...
- BZOJ5017题解SNOI2017炸弹--玄学递推
题目链接 https://www.lydsy.com/JudgeOnline/problem.php?id=5017 分析 老师讲课谈到了这道题,课上想出了个连边建图然后乱搞的操作,被老师钦定的递推方 ...
- [SNOI2017]炸弹
嘟嘟嘟 这题有一些别的瞎搞神奇做法,而且复杂度似乎更优,不过我为了练线段树,就乖乖的官方正解了. 做法就是线段树优化建图+强连通分量缩点+DAGdp. 如果一个炸弹\(i\)能引爆另一个炸弹\(j\) ...
- bzoj千题计划311:bzoj5017: [Snoi2017]炸弹(线段树优化tarjan构图)
https://www.lydsy.com/JudgeOnline/problem.php?id=5017 暴力: 对于每一个炸弹,枚举所有的炸弹,看它爆炸能不能引爆那个炸弹 如果能,由这个炸弹向引爆 ...
- BZOJ5017 [SNOI2017]炸弹 - 线段树优化建图+Tarjan
Solution 一个点向一个区间内的所有点连边, 可以用线段树优化建图来优化 : 前置技能传送门 然后就得到一个有向图, 一个联通块内的炸弹可以互相引爆, 所以进行缩点变成$DAG$ 然后拓扑排序. ...
- bzoj5017: [Snoi2017]炸弹
Description 在一条直线上有 N 个炸弹,每个炸弹的坐标是 Xi,爆炸半径是 Ri,当一个炸弹爆炸时,如果另一个炸弹所在位置 Xj 满足: Xi−Ri≤Xj≤Xi+Ri,那么,该炸弹也会被 ...
随机推荐
- Oracle——无法在查询中执行 DML 操作
今天在调用Oracle Function遇到一个异常
- 2.Vue 获取企业微信的Code并把Code发送的后台进行验证
1 . 在企业微信配置请求的页面写入下面代码 mounted() { //获取微信请求的的Code let code = this.$route.query.code; if (code) { thi ...
- BUAA OO 2019 第四单元作业总结
目录 第四单元总结 总 UML UML 类图 UML 时序图 UML 状态图 架构设计 第十三次作业 第十四次作业 课程总结 历次作业总结 架构设计 面向对象方法理解 测试方法理解与实践 改进建议 尽 ...
- php操作表格(写)
一,转载:http://www.thinkphp.cn/extend/832.html 二,转载:http://m.blog.csdn.net/article/details?id=7827038
- PHP 结合 Bootstrap 实现学生列表以及添加学生功能实现(继上篇登录及注册功能之后)
本人是一位学生,正在学习当中,可能BUG众多,请见谅并指正,谢谢!!! 学生列表实现 HTML: <!DOCTYPE html> <html> <head> < ...
- 74.js---移动端文章的瀑布流的实现。
移动端文章的瀑布流的实现. 1.首先在前端html页面已经通过PHP代码循环完全数据. 2.然后在js先全部隐藏,通过判断滑动到底部,每次加载一部分数据,直到数据全部显示完全. js代码: // ...
- 解决cxf+springmvc发布的webservice,缺少types,portType和message标签的问题
用cxf+spring发布了webservice,发现生成的wsdl的types,message和portType都以import的方式导入的.. 原因:命名空间问题 我想要生成的wsdl在同个文件中 ...
- org.springframework.dao.DuplicateKeyException: 问题
转自:https://blog.51cto.com/chengxuyuan/1786938 org.springframework.dao.DuplicateKeyException: a diffe ...
- Laravel5.6---搜索查询 自带paginate()分页 get传参
laravel的paginate()分页,如果用post传参,点击第二页时会默认使用get,就会返回原始数据了 需要把查询数据get方式也放到paginate()分页参数中 一.路由 Route::g ...
- I、Mac 下的Vue
Mac 下的Vue 1. 安装brew /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/i ...