題意:
一個長為\(n\)的序列\(a\),初始全為零。
\(n\)個操作,第\(i\)個操作形如給\(a_1,\cdots,a_{x_i}\)加上\(y_i\)。
\(m\)次查詢,給定\(l,r\),求對\(a\)執行第\(l\sim r\)個操作後數列\(a\)的全域性最大值。
\(1\le n,m\le 5\cdot 10^5,1\le x_i,|y_i|\le n,1\le l\le r\le n\),時間限制\(\texttt{10s}\),空間限制\(\texttt{1024MB}\)。
分析:
將所有操作按\(x_i\)排序,那麼每個操作有時間戳\(t_i\),詢問變為僅考慮時間戳在\([l,r]\)之間的操作時的最大值。
注意到\(a_i\)僅由排序後一段字尾的操作決定,考慮分塊。每\(B\)個操作為一組,那麼塊前的操作無影響,塊後的操作可以用字首和解決,而塊內只有\(B\)種不同的時間戳,離散化後本質不同的詢問只有\(B^2\)種,即下文的\(f\)陣列。
令\(f_{i,j}\)為僅考慮時間戳排在塊內第\([i,j]\)名的操作時的字尾和最大值,如果能預處理出\(f\)陣列,那麼每塊可以做到\(O(1)\)回答每個詢問。
列舉\(i\),掃描\(j\),用線段樹維護單點加、字尾最大值,可以做到\(O(B^2\log B)\)預處理。
但這並未充分利用操作已經按\(x\)排序的性質,考慮分治。
在solve(l,r)
的過程中,僅考慮按\(x\)排序的第\([l,r]\)個操作,\(f_{i,j}\)表示考慮在這些操作中時間戳排在第\([i,j]\)名的操作時的字尾和最大值。
合併時對取最大值的字尾起點分類討論,如果在左邊就需要加上右邊的貢獻,用字首和處理,如果在右邊就不用管。
劃重點:\(x_i\)可能相同是一件很煩人的事情,這意味著僅有從特定位置開始的字尾是合法的,將其它位置的初始值設定為
-inf
。另外如果前面有合法的字尾開頭但\([i,j]\)中沒有仍然可以轉移,理論上需要特判,只不過訪問到該位置剛好是零所以程式碼中體現不出來。
分治過程中\(T(n)=2T(\frac n2)+\mathcal O(n^2)\),由主定理\(T(B)=\mathcal O(B^2)\)。
時間複雜度\(\mathcal O(\frac nB(B^2+m))\),取\(B=\sqrt n\)則時間複雜度為\(\mathcal O((n+m)\sqrt n)\)。
下面是經過喪心病狂卡常後的程式碼,可以在洛谷上透過,但是可讀性幾乎沒有,所以更推薦去\(\texttt{Loj}\)上看卡常前有註釋版本的程式碼,link。
#include<bits/stdc++.h>
#define ll long long
#define getchar() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<23,stdin),p1==p2)?EOF:*p1++)
using namespace std;
const int B=708,maxn=5e5+5;
const ll inf=1e18;
int d,m,n;
int b[maxn],s[maxn],pl[maxn],pr[maxn];
int y[maxn],rid[maxn];
ll s1[maxn],s2[maxn];
ll f[B+5][B+5],g[B+5][B+5];
char buf[1<<23],*p1=buf,*p2=buf,obuf[1<<23],*O=obuf;
struct oper
{
int x,y,t;
bool operator<(const oper &c)
{
return x!=c.x?x<c.x:t<c.t;
}
}a[maxn],c[maxn];
struct quer
{
int l,r,id;
ll res;
bool operator<(const quer &c)
{
return l!=c.l?l<c.l:r<c.r;
}
}q[maxn];
int read()
{
int q=0,f=1;char ch=getchar();
while(!isdigit(ch)) f=ch!='-'?1:-1,ch=getchar();
while(isdigit(ch)) q=10*q+ch-'0',ch=getchar();
return q*f;
}
void write(ll x)
{
if(x<0) return putchar('-'),write(-x);
if(x>=10) write(x/10);
putchar(x%10+'0');
}
void chmax(ll &a,ll b)
{
if(a<b) a=b;
}
void solve(int l,int r)
{
if(l==r) return f[l-d][r-d]=b[l]?a[l].y:-inf,void();
int m=(l+r)>>1;
solve(l,m),solve(m+1,r),s2[m]=0;
for(int j=m+1;j<=r;j++) s2[j]=s2[j-1]+a[j].y;
pl[l-1]=l-1,pr[l-1]=m;
for(int i=l,j=m+1,k=l;i<=m||j<=r;)
{
c[k]=j>r||(i<=m&&a[i].t<a[j].t)?a[i++]:a[j++];
pl[k]=i-1,pr[k]=j-1,k++;
}
for(int k=l;k<=r;k++) a[k]=c[k];
for(int i=l;i<=m;i++) for(int j=i;j<=m;j++) g[i-d][j-d]=f[i-d][j-d];
for(int i=m+1;i<=r;i++) for(int j=i;j<=r;j++) g[i-d][j-d]=f[i-d][j-d];
for(int i=l;i<=r;i++)
for(int j=i;j<=r;j++)
{
int l1=pl[i-1]+1,l2=pl[j],r1=pr[i-1]+1,r2=pr[j];
f[i-d][j-d]=-inf;
if(s[m]-s[l-1]) chmax(f[i-d][j-d],g[l1-d][l2-d]+s2[r2]-s2[r1-1]);
if(s[r]-s[m]) chmax(f[i-d][j-d],g[r1-d][r2-d]);
}
}
void work(int l,int r)
{
d=l-1;
for(int i=l;i<=r;i++) for(int j=i;j<=r;j++) f[i-d][j-d]=-inf;
solve(l,r);
pl[0]=l-1;
for(int i=1;i<=n;i++)
{
pl[i]=pl[i-1]+(pl[i-1]<r&&a[pl[i-1]+1].t<=i);
s1[i]=s1[i-1]+(rid[i]>r?y[i]:0);
}
for(int i=1;i<=m;i++)
{
int ql=pl[q[i].l-1]+1,qr=pl[q[i].r];
if(s[r]-s[l-1]) chmax(q[i].res,f[ql-d][qr-d]+s1[q[i].r]-s1[q[i].l-1]);
}
}
int main()
{
n=read(),m=read();
for(int i=1;i<=n;i++) a[i].x=read(),y[i]=a[i].y=read(),a[i].t=i;
for(int i=1;i<=m;i++) q[i].l=read(),q[i].r=read(),q[i].id=i,q[i].res=-inf;
sort(a+1,a+n+1),a[n+1]={n,0,n+1},n++;
for(int i=1;i<=n;i++) rid[a[i].t]=i;
for(int i=1;i<=n;i++) b[i]=a[i].x!=a[i-1].x,s[i]=s[i-1]+b[i];
sort(q+1,q+m+1);
for(int l=1,r=B;l<=n;l+=B,r+=B) work(l,min(r,n));
for(int i=1;i<=m;i++) rid[q[i].id]=q[i].res;
for(int i=1;i<=m;i++) write(rid[i]),putchar('\n');
return 0;
}
總結:
- 如果操作具有可加性,可以將\(\mathcal O(n^2)\)變成\(\mathcal O(\frac nB\cdot B^2)\)從而有效降低複雜度。
- \(\mathcal O(n\sqrt n)\)次
cache miss
真的很可怕!