[Snoi2017]炸弹

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zhouyuheng2003/article/details/83278984

题目相关

题面:

在这里插入图片描述
BZOJ的题目链接:https://www.lydsy.com/JudgeOnline/problem.php?id=5017
LOJ的数据下载链接:https://loj.ac/problem/2255/testdata
一句话题意?题面那么短,自己看吧

解法

首先看到题目,容易发现一个性质,一个炸弹能炸到的范围是一个区间,这个性质看起来很简单,但是事实上非常有用,方便解题
考虑每个炸弹炸到的范围是一个区间,也就是说求出每个炸弹炸到的区间就可以计算答案了,很容易想到的一个想法是把最左端和最右端分别求出来,但是你会发现炸到右边的点可能会炸到更左边,然后再炸到更右边,所以无法分别计算区间左端点和区间右端点

算法1

考虑暴力算法,对于每个炸弹,向自己能炸到的炸弹连边,对于互相能炸到的炸弹,其能炸到的区间一定是相同的,所以用tarjan进行有向图缩点,对于剩下的这个DAG进行dp,统计一个点子孙的数量,f[i][j]表示第j个点是否是i的子孙,时间和空间复杂度均为O(n2)O(n^2),所以不是很优。考虑一个点的的子孙集合一定是一段连续的区间,所以在dp的时候记子节点的最大、最小编号即可,空间复杂度转成O(n)O(n),此时算法瓶颈在边数过多,考虑线段树优化连边,点数仍然为O(n)O(n),边数优化成O(nlogn)O(nlogn),就可以过了,这种算法是网上最常见的算法,但并不是最优的

算法2

这道题事实上能做到O(n)O(n),先讲一个并不是网上看到的O(n)O(n)算法,考虑连边的时候不使用线段树优化连边,而是对于每一个点找到其左侧和右侧分别离自己最近的能炸到自己的点,将这两个点向自己连边。由于连边需要的只要先让能直接炸到自己的点向自己连边,容易发现,在左边的点如果能炸到自己,那么也一定能炸到左边连边的那个点,右边同理,所以这个算法正确,由于每个点只连出2条边,复杂度O(n)O(n)
这个算法我自己写过验证的,所以贴出一波代码
总复杂度由于离散化所以是O(nlogn)O(nlogn)的,但是其实可以通过基数排序来使复杂度变为O(n)O(n)优于算法1

#include<cstdio>
#include<cctype>
#include<algorithm>
#include<cstring>
#define rg register
typedef long long LL;
template <typename T> inline T max(const T a,const T b){return a>b?a:b;}
template <typename T> inline T min(const T a,const T b){return a<b?a:b;}
template <typename T> inline void mind(T&a,const T b){a=a<b?a:b;}
template <typename T> inline void maxd(T&a,const T b){a=a>b?a:b;}
template <typename T> inline T abs(const T a){return a>0?a:-a;}
template <typename T> inline void swap(T&a,T&b){T c=a;a=b;b=c;}
template <typename T> inline T gcd(const T a,const T b){if(!b)return a;return gcd(b,a%b);}
template <typename T> inline T lcm(const T a,const T b){return a/gcd(a,b)*b;}
template <typename T> inline T square(const T x){return x*x;};
template <typename T> inline void read(T&x)
{
    char cu=getchar();x=0;bool fla=0;
    while(!isdigit(cu)){if(cu=='-')fla=1;cu=getchar();}
    while(isdigit(cu))x=x*10+cu-'0',cu=getchar();
    if(fla)x=-x;
}
template <typename T> void printe(const T x)
{
    if(x>=10)printe(x/10);
    putchar(x%10+'0');
}
template <typename T> inline void print(const T x)
{
	if(x<0)putchar('-'),printe(-x);
	else printe(x);
}
const int maxn=500003,maxm=1000003;
int n,Stack[maxn],Top;
LL x[maxn],l[maxn],r[maxn];
int head[maxn],nxt[maxm],tow[maxm],tmp;
int belo[maxn],n_d,MIN[maxn],MAX[maxn];
int stack[maxn],top;
inline void addb(const int u,const int v)
{
	tmp++;
	nxt[tmp]=head[u];
	head[u]=tmp;
	tow[tmp]=v;
}
int tot,DFN[maxn],LOW[maxn];
int beg;
inline void push(const int x){stack[++top]=x;}
inline int pop(){return stack[top--];}
void DFS(int u)
{
	DFN[u]=LOW[u]=++tot;
	push(u);
	for(rg int i=head[u];i;i=nxt[i])
	{
		const int v=tow[i];
		if(!DFN[v])DFS(v),mind(LOW[u],LOW[v]);
		else if(belo[v]==0)mind(LOW[u],DFN[v]);
	}
	if(LOW[u]==DFN[u])
	{
		n_d++;
		int cl=pop();
		MIN[n_d]=cl,MAX[n_d]=cl,belo[cl]=n_d;
		while(cl!=u)cl=pop(),mind(MIN[n_d],cl),maxd(MAX[n_d],cl),belo[cl]=n_d;
	}
}
int head_d[maxn],nxt_d[maxm],tow_d[maxm],tmp_d;
inline void addb_d(const int u,const int v)
{
	if(u==v)return;
	tmp_d++;nxt_d[tmp_d]=head_d[u];head_d[u]=tmp_d;tow_d[tmp_d]=v;
}
int dp[maxn];
int mini[maxn],maxi[maxn];
void draw()
{
	for(rg int i=1;i<=n;i++)
		for(rg int j=head[i];j;j=nxt[j])
			addb_d(belo[i],belo[tow[j]]);
}
const LL mod=1000000007;
LL res;LL ans[maxn];
void calc(const int u)
{
	dp[u]=0;
	mini[u]=MIN[u],maxi[u]=MAX[u];
	for(rg int i=head_d[u];i;i=nxt_d[i])
	{
		const int v=tow_d[i];
		if(dp[v]==-1)calc(v);
		mind(mini[u],mini[v]),maxd(maxi[u],maxi[v]);
	}
	ans[u]=maxi[u]-mini[u]+1;
}
int main()
{	read(n);
	for(rg int i=1;i<=n;i++)read(x[i]),read(r[i]);
	for(rg int i=1;i<=n;i++)
	{
		l[i]=std::lower_bound(x+1,x+n+1,x[i]-r[i])-x;
		r[i]=std::upper_bound(x+1,x+n+1,x[i]+r[i])-x-1;
	}
	for(rg int i=1;i<=n;i++)
	{
		while(Top&&r[Stack[Top]]<i)Top--;
		if(Top)addb(Stack[Top],i);
		Stack[++Top]=i;
	}
	Top=0;
	for(rg int i=n;i>=1;i--)
	{
		while(Top&&l[Stack[Top]]>i)Top--;
		if(Top)addb(Stack[Top],i);
		Stack[++Top]=i;
	}
	for(beg=1;beg<=n;beg++)if(!DFN[beg])DFS(beg);
	draw();
	memset(dp,-1,sizeof(dp));
	for(rg int i=1;i<=n_d;i++)if(dp[i]==-1)calc(i);
	for(rg int i=1;i<=n;i++)res=(res+(LL)i*ans[belo[i]])%mod;
	print(res);
	return 0;
}

算法3(搬自https://blog.csdn.net/C_K_Y_/article/details/79980119)(假算法)

这个算法同样是O(n)O(n)的复杂度,代码量、常数都比算法2要优
先抛开建图缩点跑dp的思路,考虑之前贪心拓展的算法
为什么往两边分别找最远的点求出的答案不对呢?容易发现,你炸到的左边的点可能炸的比你还右边,也就是说两边会相互影响
而算法3通过扫两遍的方式解决了这个问题,第一遍维护了向左拓展对向右拓展的贡献,第二遍算出向右的最远值并更新右对左的影响
对于复杂度,容易发现,在每个while循环中最多循环2次,第一次是用到自己原有的数据,第二次是跟着拓展出去的点更新信息,容易发现更新出去后已经为最优,所以总复杂度是O(n)
然而不幸的是,这个算法虽然能过bzoj然而却是错的,数据

5
1 0
30 100
50 20
90 0
100 0

能叉掉这个算法
因为它每次都是向左跳一次,而这移动一格+一跳会跳过中间可能时答案更优的点,所以本来求的所谓向左、向右最远就是错的
然而,对于卡一般暴力向左、右分别跳的此种数据却能过:

4
60 0
90 20
100 10
110 50

(我一开始测了这个算法这个数据,发现过了,bzoj上也AC了,觉得真是很优越,然后最后却被告知这是一个假算法,深深感受到自己看别人的算法严谨性不足)

总结

这道题网上大多是算法1,而算法2在一定程度上优化了算法复杂度,算法3能通过此题,复杂度看起来十分优越,但是却是错的(数据好水啊)。整个优化算法的想法和思路都很有趣,所以用这篇博客来记录一下。现在的很多题都有多种解法,倘若能够优化算法复杂度、代码量与常数,那么你将可以感受到算法的乐趣(并且加大数据范围使题目变成毒瘤题)。

阅读更多

没有更多推荐了,返回首页