[Snoi2017]炸彈
題目相關
題面:
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的子孫,時間和空間複雜度均為
,所以不是很優。考慮一個點的的子孫集合一定是一段連續的區間,所以在dp的時候記子節點的最大、最小編號即可,空間複雜度轉成
演算法2
這道題事實上能做到
,先講一個並不是網上看到的
演算法,考慮連邊的時候不使用線段樹優化連邊,而是對於每一個點找到其左側和右側分別離自己最近的能炸到自己的點,將這兩個點向自己連邊。由於連邊需要的只要先讓能直接炸到自己的點向自己連邊,容易發現,在左邊的點如果能炸到自己,那麼也一定能炸到左邊連邊的那個點,右邊同理,所以這個演算法正確,由於每個點只連出2條邊,複雜度
這個演算法我自己寫過驗證的,所以貼出一波程式碼
總複雜度由於離散化所以是
的,但是其實可以通過基數排序來使複雜度變為
優於演算法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)(假演算法)
這個演算法同樣是
的複雜度,程式碼量、常數都比演算法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能通過此題,複雜度看起來十分優越,但是卻是錯的(資料好水啊)。整個優化演算法的想法和思路都很有趣,所以用這篇部落格來記錄一下。現在的很多題都有多種解法,倘若能夠優化演算法複雜度、程式碼量與常數,那麼你將可以感受到演算法的樂趣(並且加大資料範圍使題目變成毒瘤題)。