Loading

二次离线莫队学习笔记

做了两天二离,还是lxl那个Ynoi毒瘤,卡常卡了一天啊

理论部分

其实也没啥好说的,我对二离的理解就是加速莫队端点的移动。

但是目前看起来,二离的局限性在于维护的信息必须可减。

举个例子,比如我们现在需要移动端点 \([l,r]\to [l,r']\) ,那么正常莫队的写法是每移动一次就修改一次贡献。

当移动端点不是 \(O(1)\) 的时候,由于总的移动路径长是 \(O(m\sqrt n)\) 的,复杂度会再乘上移动复杂度,不那么优秀。

二离就是来优化这个过程的。

我们发现我们只需要预处理出端点移动时产生的贡献就可以 \(O(1)\) 移动端点,然后端点移动时产生的贡献就成功被平衡掉了。

接下来简略讲讲怎么平衡。

一般都是拆前缀和的形式。比如 \([l,r]\to [l,r+1]\) 的时候,我们统计 \(r+1\)\([1,l-1]\) 的贡献以及 \([1,r]\) 的贡献,减一下就好了。 \(r+1\)\([1,r]\) 的贡献很特殊,可以预处理。这样我们只需算出 \([1,l-1]\)\(r\) 的贡献即可,所以要支持 \(O(\sqrt n)\) 修改一个集合,完成 \(O(1)\) 查询一个端点对集合的答案即可做到 \(O(n\sqrt n)\) 了(因为总移动路径长是 \(O(n\sqrt n)\) 的)。

当然复杂度再高就会导致二离成为瓶颈而非莫队,这时候要小心。不过到现在为止做到的题都是刚好平衡成莫队复杂度。

大概口胡的部分就这么点,剩下的还是要看题目,很多东西要在实践中发现。

P5047 [Ynoi2019模拟赛]Yuno loves sqrt technology II

区间逆序对,要求空间 \(O(n)\)

\(O(n\sqrt{n}\log n)\) 应该谁都会做,而且谁都知道被卡了。

先把所有询问按照莫队的方法排序。

考虑按照上面的说法拆询问。

首先预处理 \([1,x]\)\(x+1\) 的逆序对数,这个树状数组跑一趟就好了。

记上面那个东西的前缀和为 \(pre_x\)

\(f([l,r],x)\) 表示区间 \([l,r]\)\(x\) 的贡献。

\([l,r]\to [l,r+1]\) : 每一个询问拆成 \(f([1,r],r)-f([1,l-1],r)\)

那么 \([l,r]\to[l,r'](r'>r)\) 拆成 \(pre_{r'}-pre_{r}\) 减去 \([1,l-1]\)\([r+1,r']\) 间每一个点的贡献,询问挂到 \(l-1\) 上。

注意这里的 \(r\) 并不用减一,因为贡献是从 \(r+1\) 开始算的。

\([l,r]\to [l-1,r]\) :发现前缀好像不够,于是我又搞了个后缀 \(suf\) 表示 \(x\)\([x+1,n]\) 的贡献 (事实上是我zz了,后面会讲怎么不用后缀,但是现在这样好理解)。那么询问拆成 \(f([l,n],l-1)-f([r+1,n],l-1)\)

那么 \([l,r]\to[l',r](l'<l)\) 拆成 \(suf_{l'}-suf_{l}\) 减去 \([r+1,n]\)\([l',l-1]\) 间每一个点的贡献,询问挂到 \(r+1\) 上。

这里我把扩展区间写到了前面,因为先删除区间可能出现 \(l>r\) 的情况无意义,我也不知道会出啥事,可能也能AC吧,反正能避掉这种无意义情况就避掉了。

剩下就很类似了:

\([l,r]\to[l,r'](r>r')\) 拆成 \(pre_{r}-pre_{r'}\) 减去 \([1,l-1]\)\([r'+1,r]\) 的贡献。注意这个对于答案的系数是 \(-1\) ,而 \(r'\) 的贡献不应该被减去。

\([l,r]\to[l',r](l'<l)\) 拆成 \(suf_{l}-suf_{l'}\) 减去 \([l',r]\)\([l,l'-1]\) 的贡献,对答案的贡献还是 \(-1\)

于是我们有了 \(2m\) 个二次离线的询问,每一个都是 \([1,x]\)\([l,r]\) 贡献的形式(后缀同理)

这时候我们不能带 \(\log\) 。有个非常强大的东西:分块。这东西博大精深。lxl最清楚。

我们对于值域分块(值域太大就离散化,反正具体大小不影响逆序对,相对大小才重要)

现在我们面对的是单点修改前缀和,单点查值的问题,要求查询 \(O(1)\)

\(x\) 属于块 \(bel_x\) ,对于整块打标记,每一个块内再记前缀和。

对于小于 \(bel_x\) 的块直接打标记,\(O(\dfrac{n}{S})\) 完成,设这一块的标记为 \(tag_x\)

对于 \(x\) 所在块的左端点到 \(x\) ,再修改块内前缀和 \(sum_x\)

那么查询的时候 \(sum_x+tag_x\) 就是答案了,\(O(1)\) 了。

我们从 \(1\to i\) 遍历所有离线下来的询问,先修改这一位,然后处理所有挂在 \(i\) 上的询问。

综上,我们会先离线出 \(2m\) 个询问,然后利用莫队性质遍历 \(n\sqrt n\) 级别的长度 \(O(1)\) 回答每一个询问,然后再跑一次莫队,然而指针的移动已经 \(O(1)\) 了,只需要 \(O(m)\)

当然这个也可以改成前缀和,只不过在离线的时候要多记一个系数。其实这个东西本质是处理相邻两个询问的差。

#include<bits/stdc++.h>
using namespace std;
#define fi first
#define se second
#define mkp(x,y) make_pair(x,y)
#define pb(x) push_back(x)
#define sz(v) (int)v.size()
typedef long long LL;
typedef double db;
template<class T>bool ckmax(T&x,T y){return x<y?x=y,1:0;}
template<class T>bool ckmin(T&x,T y){return x>y?x=y,1:0;}
#define rep(i,x,y) for(int i=x,i##end=y;i<=i##end;++i)
#define per(i,x,y) for(int i=x,i##end=y;i>=i##end;--i)
inline int read(){
    int x=0,f=1;char ch=getchar();
    while(!isdigit(ch)){if(ch=='-')f=0;ch=getchar();}
    while(isdigit(ch))x=x*10+ch-'0',ch=getchar();
    return f?x:-x;
}
const int N=100005;
const int S=320;
const int B=N/S+5;
int n,m,num,a[N],pre[N],suf[N],L[B],R[B],bel[N];
LL sp[N],ss[N],res[N<<1];
LL ans[N];
int lsh[N],len;
int tr[N];
int tol,tor;
struct QUE{
	int l,r,id;
	inline bool operator< (const QUE&t)const{return bel[l]!=bel[t.l]?l<t.l:bel[l]&1?r<t.r:r>t.r;}
}q[N];
struct ASK{
	int pos,l,r,id;
	inline bool operator < (const ASK&t)const{return pos<t.pos;}
}ql[N],qr[N];
namespace BLOCK1{
int sum[N],tag[B];
void update(int x,int d){
	for(int i=1;i<bel[x];++i)tag[i]+=d;
	for(int i=L[bel[x]];i<=x;++i)sum[i]+=d;
}
int query(int x){
	return sum[x]+tag[bel[x]];
}
}
namespace BLOCK2{
int sum[N],tag[B];
void update(int x,int d){
	for(int i=num;i>bel[x];--i)tag[i]+=d;
	for(int i=x;i<=R[bel[x]];++i)sum[i]+=d;
}
int query(int x){
	return sum[x]+tag[bel[x]];
}
}

signed main(){
	n=read(),m=read(),num=(n-1)/S+1;
	rep(i,1,num)L[i]=R[i-1]+1,R[i]=i*S;R[num]=n;
	rep(i,1,num)rep(j,L[i],R[i])bel[j]=i;
	rep(i,1,n)a[i]=lsh[i]=read();
	sort(lsh+1,lsh+n+1),len=unique(lsh+1,lsh+n+1)-lsh-1;
	rep(i,1,n)a[i]=lower_bound(lsh+1,lsh+len+1,a[i])-lsh;
	rep(i,1,n){
		int res=0;
		for(int j=a[i]+1;j<=n;j+=j&-j)res+=tr[j];
		pre[i]=res;
		for(int j=a[i];j>0;j-=j&-j)++tr[j];
	}
	fill(tr,tr+n+1,0);
	per(i,n,1){
		int res=0;
		for(int j=a[i]-1;j>0;j-=j&-j)res+=tr[j];
		suf[i]=res;
		for(int j=a[i];j<=n;j+=j&-j)++tr[j];
	}
	rep(i,1,m)q[i].id=i,q[i].l=read(),q[i].r=read();
	sort(q+1,q+m+1);
	for(int i=1,l=1,r=0;i<=m;++i){
		if(l>q[i].l)ql[++tol].pos=r+1,ql[tol].l=q[i].l,ql[tol].r=l-1,ql[tol].id=q[i].id<<1,l=q[i].l;
		if(r<q[i].r)qr[++tor].pos=l-1,qr[tor].l=r+1,qr[tor].r=q[i].r,qr[tor].id=q[i].id<<1|1,r=q[i].r;
		if(l<q[i].l)ql[++tol].pos=r+1,ql[tol].l=l,ql[tol].r=q[i].l-1,ql[tol].id=q[i].id<<1,l=q[i].l;
		if(r>q[i].r)qr[++tor].pos=l-1,qr[tor].l=q[i].r+1,qr[tor].r=r,qr[tor].id=q[i].id<<1|1,r=q[i].r;
	}
	sort(ql+1,ql+tol+1),sort(qr+1,qr+tor+1);
	for(int i=1,j=1;i<=n;++i){
		while(j<=tor&&qr[j].pos<i)++j;
		BLOCK1::update(a[i],1),sp[i]=sp[i-1]+pre[i];
		for(;j<=tor&&qr[j].pos==i;++j){
			for(int k=qr[j].l;k<=qr[j].r;++k)
				res[qr[j].id]+=BLOCK1::query(a[k]+1);
		}
	}
	for(int i=n,j=tol;i>=1;--i){
		while(j>=1&&ql[j].pos>i)--j;
		BLOCK2::update(a[i],1),ss[i]=ss[i+1]+suf[i];
		for(;j>=1&&ql[j].pos==i;--j){
			for(int k=ql[j].l;k<=ql[j].r;++k)
				res[ql[j].id]+=BLOCK2::query(a[k]-1);
		}
	}
	LL now=0;
	for(int i=1,l=1,r=0;i<=m;++i){
		if(l>q[i].l)now+=ss[q[i].l]-ss[l]-res[q[i].id<<1],l=q[i].l;
		if(r<q[i].r)now+=sp[q[i].r]-sp[r]-res[q[i].id<<1|1],r=q[i].r;
		if(l<q[i].l)now-=ss[l]-ss[q[i].l]-res[q[i].id<<1],l=q[i].l;
		if(r>q[i].r)now-=sp[r]-sp[q[i].r]-res[q[i].id<<1|1],r=q[i].r;
		ans[q[i].id]=now;
	}
	rep(i,1,m)printf("%lld\n",ans[i]);
	return 0;
}

\(O(n\sqrt n)\)\(2\) 倍常数还能 150ms 跑完挺令人吃惊的。

P5501 [LnOI2019]来者不拒,去者不追

有了上一道题,这题应该很板子了吧!万事开头难,理解了二离这种题应该随便切了。

移动端点的贡献显然是:一个区间内小于 \(a_x\) 的数的个数 乘 \(a_x\) ,加上大于 \(a_x\) 的数的和。

这个东西直接套路值域分块即可,没啥好讲的。

#include<bits/stdc++.h>
using namespace std;
#define fi first
#define se second
#define mkp(x,y) make_pair(x,y)
#define pb(x) push_back(x)
#define sz(v) (int)v.size()
typedef long long LL;
typedef double db;
template<class T>bool ckmax(T&x,T y){return x<y?x=y,1:0;}
template<class T>bool ckmin(T&x,T y){return x>y?x=y,1:0;}
#define rep(i,x,y) for(int i=x,i##end=y;i<=i##end;++i)
#define per(i,x,y) for(int i=x,i##end=y;i>=i##end;--i)
#define getchar() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++)
char buf[1<<21],*p1=buf,*p2=buf;
inline int read(){
    int x=0,f=1;char ch=getchar();
    while(!isdigit(ch)){if(ch=='-')f=0;ch=getchar();}
    while(isdigit(ch))x=x*10+ch-'0',ch=getchar();
    return f?x:-x;
}
const int N=500005;
const int M=100005;
int n,m,a[N],S,V,num;
int bel[N],L[N],R[N];
int pr1[N],sf1[N],t1[M];
LL  pr2[N],sf2[N],t2[M];
LL sp1[N],ss1[N],sp2[N],ss2[N];
LL res1[N<<1],res2[N<<1],ans[N];
int tl,tr;

struct QUE{
	int id,l,r;
	QUE(){l=r=id=0;}
	inline bool operator < (const QUE&t)const{return bel[l]!=bel[t.l]?l<t.l:bel[l]&1?r<t.r:r>t.r;}
}q[N];
struct ASK{
	int pos,id,l,r;
	ASK(){pos=id=l=r=0;}
	inline bool operator < (const ASK&t)const{return pos<t.pos;}
}ql[N],qr[N];

namespace BLO{

int num,S,L[N],R[N],bel[N];
int sum1[N],tag1[N];
LL sum2[N],tag2[N];
void init(){
	S=sqrt(V-1)+1,num=(V-1)/S+1;
	rep(i,1,num)L[i]=R[i-1]+1,R[i]=i*S;R[num]=V;
	rep(i,1,num)rep(j,L[i],R[i])bel[j]=i;
}
void clear(){
	memset(sum1,0,sizeof(sum1)),memset(tag1,0,sizeof(tag1));
	memset(sum2,0,sizeof(sum2)),memset(tag2,0,sizeof(tag2));	
}
void update1(int x,int d){
	for(int i=num;i>bel[x];--i)tag1[i]+=d;
	for(int i=R[bel[x]];i>=x;--i)sum1[i]+=d;
}
int query1(int x){return sum1[x]+tag1[bel[x]];}
void update2(int x,int d){
	for(int i=1;i<bel[x];++i)tag2[i]+=d;
	for(int i=L[bel[x]];i<=x;++i)sum2[i]+=d;
}
LL query2(int x){return sum2[x]+tag2[bel[x]];}
}

signed main(){
	n=read(),m=read(),
	S=sqrt(n-1)+1,num=(n-1)/S+1;
	rep(i,1,num)L[i]=R[i-1]+1,R[i]=i*S;R[num]=n;
	rep(i,1,num)rep(j,L[i],R[i])bel[j]=i;
	rep(i,1,n)ckmax(V,a[i]=read());
	rep(i,1,m)q[i].l=read(),q[i].r=read(),q[i].id=i;
	sort(q+1,q+m+1);
	
	for(int i=1;i<=n;++i){
		int r1=0;LL r2=0;
		for(int j=a[i]-1;j>0;j-=j&-j)r1+=t1[j];
		for(int j=a[i]+1;j<=V;j+=j&-j)r2+=t2[j];
		pr1[i]=r1,pr2[i]=r2,sp1[i]=sp1[i-1]+r1,sp2[i]=sp2[i-1]+r2;
		for(int j=a[i];j<=V;j+=j&-j)++t1[j];
		for(int j=a[i];j>0;j-=j&-j)t2[j]+=a[i];
	}
	memset(t1,0,sizeof(t1)),memset(t2,0,sizeof(t2));
	for(int i=n;i>=1;--i){
		int r1=0;LL r2=0;
		for(int j=a[i]-1;j>0;j-=j&-j)r1+=t1[j];
		for(int j=a[i]+1;j<=V;j+=j&-j)r2+=t2[j];
		sf1[i]=r1,sf2[i]=r2,ss1[i]=ss1[i+1]+r1,ss2[i]=ss2[i+1]+r2;
		for(int j=a[i];j<=V;j+=j&-j)++t1[j];
		for(int j=a[i];j>0;j-=j&-j)t2[j]+=a[i];
	}

	rep(i,1,n)sp1[i]=sp1[i-1]+1ll*a[i]*(pr1[i]+1);
	rep(i,1,n)sp2[i]=sp2[i-1]+pr2[i];
	per(i,n,1)ss1[i]=ss1[i+1]+1ll*a[i]*(sf1[i]+1);
	per(i,n,1)ss2[i]=ss2[i+1]+sf2[i];

	for(int i=1,l=1,r=0;i<=m;++i){
		// cerr<<q[i].id<<' '<<q[i].l<<' '<<q[i].r<<' '<<l<<' '<<r<<'\n';
		if(l>q[i].l)ql[++tl].pos=r+1,ql[tl].l=q[i].l,ql[tl].r=l-1,ql[tl].id=q[i].id<<1,l=q[i].l;
		if(r<q[i].r)qr[++tr].pos=l-1,qr[tr].l=r+1,qr[tr].r=q[i].r,qr[tr].id=q[i].id<<1|1,r=q[i].r;
		if(l<q[i].l)ql[++tl].pos=r+1,ql[tl].l=l,ql[tl].r=q[i].l-1,ql[tl].id=q[i].id<<1,l=q[i].l;
		if(r>q[i].r)qr[++tr].pos=l-1,qr[tr].l=q[i].r+1,qr[tr].r=r,qr[tr].id=q[i].id<<1|1,r=q[i].r;
	}
	sort(ql+1,ql+tl+1),sort(qr+1,qr+tr+1);

	BLO::init();
	for(int i=1,j=1;i<=n;++i){
		while(j<=tr&&qr[j].pos<i)++j;
		BLO::update1(a[i],1),BLO::update2(a[i],a[i]);
		for(;j<=tr&&qr[j].pos==i;++j){
			int st=qr[j].l,ed=qr[j].r,id=qr[j].id;
			for(int k=st;k<=ed;++k)
				res1[id]+=1ll*BLO::query1(a[k]-1)*a[k],res2[id]+=BLO::query2(a[k]+1);
			// cerr<<"qr:"<<i<<' '<<qr[j].l<<' '<<qr[j].r<<' '<<res1[id]<<' '<<res2[id]<<'\n';
		}
	}
	BLO::clear();
	for(int i=n,j=tl;i>=1;--i){
		while(j>=1&&ql[j].pos>i)--j;
		BLO::update1(a[i],1),BLO::update2(a[i],a[i]);
		for(;j>=1&&ql[j].pos==i;--j){
			int st=ql[j].l,ed=ql[j].r,id=ql[j].id;
			for(int k=st;k<=ed;++k)
				res1[id]+=1ll*BLO::query1(a[k]-1)*a[k],res2[id]+=BLO::query2(a[k]+1);
			// cerr<<"ql:"<<i<<' '<<ql[j].l<<' '<<ql[j].r<<' '<<res1[id]<<' '<<res2[id]<<'\n';
		}
	}

	LL now=0;
	for(int i=1,l=1,r=0;i<=m;++i){
		int id=q[i].id;
		if(l>q[i].l)now+=ss1[q[i].l]-ss1[l]-res1[id<<1]+ss2[q[i].l]-ss2[l]-res2[id<<1],l=q[i].l;
		if(r<q[i].r)now+=sp1[q[i].r]-sp1[r]-res1[id<<1|1]+sp2[q[i].r]-sp2[r]-res2[id<<1|1],r=q[i].r;
		if(l<q[i].l)now-=ss1[l]-ss1[q[i].l]-res1[id<<1]+ss2[l]-ss2[q[i].l]-res2[id<<1],l=q[i].l;
		if(r>q[i].r)now-=sp1[r]-sp1[q[i].r]-res1[id<<1|1]+sp2[r]-sp2[q[i].r]-res2[id<<1|1],r=q[i].r;
		ans[id]=now;
	}
	rep(i,1,m)printf("%lld\n",ans[i]);
	return 0;
}

P4887 【模板】莫队二次离线(第十四分块(前体))

讲一个重要的优化:两次变一次。即只用处理前缀,不用处理后缀,这样常数会小一些。为下一道终极Boss铺垫。

我们使用后缀的原因就是因为左端点移动。但是后缀和也可以用前缀和算啊。于是:

左端点左移:\([l,r]\to[l-1,r]\) ,拆成 \(f([l,r],l-1)=f([1,r],l-1)-f([1,l-1],l-1)\)

注意后面那个东西与我们之前统计的 \(f([1,x-1],x)\) 是有差别的,要重新处理一遍。(就一个修改前统计,一个修改完统计)

再来看看这题具体怎么搞。

\(cnt[a_i\bigoplus a_j]=k\)\(cnt\) 表示这个数二进制下有几个 \(1\)) 等价于 \(cnt[a_i\bigoplus x]=a_j,cnt[x]=k\)

每次加入一个数就暴力枚举所有 \(cnt[x]=k\) 的数,加到桶里面就好了。

你可能好奇我的码风为啥忽然变了,因为我忽然回来搞二离纯粹是因为觉得过个板子啥用都没有,于是重新搞7个月之前学的算法。其实这才是我最先做的二离,这代码是7个月前的。。。

注意,对于挂询问我们 不应该vector ,常数太大了,还不如排序,反正才 \(O(m)\) 个。远古代码懒得改了。

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
#define rint register int
#define getchar() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++)
char buf[1<<21],*p1=buf,*p2=buf;
inline int rd() {
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)) {if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch))x=x*10+(ch^48),ch=getchar();
	return x*f;
}
const int N=100010;
int n,m,k,a[N];
int k_2[4000],cnt;
int size,bel[N];
int bin[N];
LL sum1[N],sum2[N];
LL res[N<<1];
LL ans[N],cur;
struct QUE {
	int l,r,id;
	QUE(){}
	QUE(int _l,int _r,int _i):l(_l),r(_r),id(_i){}
}q[N];
vector<QUE>qn[N];
bool cmp(const QUE &a,const QUE &b) {
	return bel[a.l]!=bel[b.l]?a.l<b.l:bel[a.l]&1?a.r<b.r:a.r>b.r;
}
#define pb push_back
#define ITv vector<QUE>::iterator
signed main() {
	n=rd(),m=rd(),k=rd(),size=sqrt(n);
	for(rint i=0;i<16384;++i) {
		int x=i,t=0;
		while(x)x^=x&-x,++t;
		if(t==k)k_2[++cnt]=i;
	}
	for(rint i=1;i<=n;++i)a[i]=rd(),bel[i]=(i-1)/size+1;
	for(rint i=1;i<=m;++i)q[i].id=i,q[i].l=rd(),q[i].r=rd();
	sort(q+1,q+m+1,cmp);
	for(rint i=1,l=q[1].r+1,r=q[1].r;i<=m;++i) {
		if(l<q[i].l)qn[r].pb(QUE(l,q[i].l-1,q[i].id<<1));
		else if(l>q[i].l)qn[r].pb(QUE(q[i].l,l-1,q[i].id<<1));
		l=q[i].l;
		if(r<q[i].r)qn[l-1].pb(QUE(r+1,q[i].r,q[i].id<<1|1));
		else if(r>q[i].r)qn[l-1].pb(QUE(q[i].r+1,r,q[i].id<<1|1));
		r=q[i].r;
	}
	for(rint i=1;i<=n;++i) {
		sum1[i]=sum1[i-1]+bin[a[i]];
		for(rint j=1;j<=cnt;++j)++bin[a[i]^k_2[j]];
		sum2[i]=sum2[i-1]+bin[a[i]];
		for(ITv j=qn[i].begin();j!=qn[i].end();++j)
			for(rint k=j->l;k<=j->r;++k)
				res[j->id]+=bin[a[k]];
	}
	for(rint i=1,l=q[1].r+1,r=q[1].r;i<=m;++i) {
		if(l<q[i].l)cur+=sum2[q[i].l-1]-sum2[l-1]-res[q[i].id<<1];
		else if(l>q[i].l)cur+=sum2[q[i].l-1]-sum2[l-1]+res[q[i].id<<1];
		l=q[i].l;
		if(r<q[i].r)cur+=sum1[q[i].r]-sum1[r]-res[q[i].id<<1|1];
		else if(r>q[i].r)cur+=sum1[q[i].r]-sum1[r]+res[q[i].id<<1|1];
		r=q[i].r,ans[q[i].id]=cur;
	}
	for(rint i=1;i<=m;++i)printf("%lld\n",ans[i]);
	return 0;
}

P5398 [Ynoi2018]GOSICK

hoho!卡常卡了一天。话说 \(\color{black}{\texttt{z}}\color{red}{\texttt{houkangyang}}\) 好像把lxl的标算卡T了,所以,虽然你们现在看到的是 3s,说不定过不了多久就4s了,就不用卡常了!

这题真正的难点并非二离,而是如何处理离线下来的询问。准确来说是卡常

首先显然的一点,一个数加入的贡献为:区间中,它作为倍数的次数 加上 它作为因数的次数。

它作为因数很好处理,加入每一个数的时候开值域上的桶,加入它的所有因数即可,这东西严格小于 \(\sqrt n\) ,而且预处理之后跑不满。

它作为倍数有些困难。很容易想到阈值分治。设阈值 \(S\)

大于 \(S\) 的数加入的时候暴力枚举倍数加到上面的桶里就是 \(O(\dfrac{n}{S})\) 的了。

小于 \(S\) 的就很鬼畜了。我一开始让想不出来,问同学,让同学支持的操作是:往集合里加一个数。查询某个数作为倍数的出现次数,询问有 \(n\sqrt n\) 个。于是被认为不可做题。

其实二离要灵活用,我们要注意询问是被离线成区间的形式的,而且区间只有 \(O(m)\) 个。

而一个数作为倍数我们可以换种方法,统计某一个数作为因数的出现次数。

一个数作为 \(\le S\) 的数的倍数的贡献,转化成 \(\le S\) 的数作为因数的贡献,这个直接枚举每一个小于 \(S\) 的数就好了,统计一个前缀和就可以 \(O(1)\) 回答一个区间的答案,复杂度是 \(O(nS)\)

于是我们阈值分治之后复杂度上限是 \(O(n\sqrt n+\dfrac{n^2}{S}+nS)\)

看起来 \(S=\sqrt n\) 就做完了。对,这是看起来。

写完后发现这个东西跑的贼慢,大概是大量cache miss造成的,而且本身 \(5e5\sqrt{5e5}\), 就很吃力,后面虽然是 \(nS\) ,但是 \(S\) 开到200就几乎吃不消了。

这时候前面 \(\dfrac{n^2}{S}\) 的复杂度可以直接被卡飞。

于是点开了题解。

发现有个叫做动态寻找最优阈值东西非常nb。

具体来说,我们发现一个阈值的贡献是 \(nS+\sum [a_i>S]\dfrac{V}{a_i}\)\(V\) 是值域)

而且我们还可以对于 \(nS\) 部分带一个常数来平衡cache miss造成的常数增大。

于是这东西有效减小了常数,然后还是被卡常了。

对了,如果你TLE0了,不应该认定是你写挂了,因为正确复杂度很有可能就是0分。我一开始还是前后扫两遍的,就TLE0了。

说起来我怎么AC的题也真离谱,86分的代码不断交,差不多把一天中各个时段都试了一遍就AC了。没想到还是高峰期AC的。原因是洛谷忽然不明原因开始随机RE,而且评测机忽然变快了。后来随机RE刚结束赶紧交了一发,果然变快了,然后最大点2.98sAC。。。

给一个疑似卡lxl标算的提交链接。link

第一个点是随机的,第二个点是非常强劲的hack数据,第三个点是针对DPair卡的。

大概只有srz能过了,因为他不是动态块长的,而且最大点2.5s不到/fad。

upd:srz也跑了3s+。目前没有代码能跑进3s

貌似GOSICK现在的数据阈值直接开70就能过/fad

const int N=500005;
int n,m,a[N];
int V,S,bin[N],bel[N];
LL pre[N],ans[N];
int tot;
int cnt[N],tim[N];
LL suf[N];
vector<int>d[N];
struct QUE{
	int id,l,r;
	QUE(){id=l=r=0;}
	inline bool operator < (const QUE&t)const{return bel[l]!=bel[t.l]?l<t.l:bel[l]&1?r<t.r:r>t.r;}
}q[N];
struct ASK{
	int pos,id,op,l,r;
	ASK(){pos=l=r=op=id=0;}
	inline bool operator < (const ASK&t)const{return pos<t.pos;}
}qn[N<<1];
void init(){
	for(int i=1;i<=n;++i)suf[a[i]]+=V/a[i];
	for(int i=V;i>=1;--i)suf[i]+=suf[i+1];
	LL mx=1e15;
	for(int i=1;i*i<=V;++i)if(ckmin(mx,5ll*i*n+suf[i+1]))S=i;
	for(int i=1;i<=V;++i)for(int j=1;i*j<=V;++j)d[i*j].pb(i);
}
signed main(){
	clock_t ST=clock(),ED;
	n=read(),m=read();
	int blocksize=sqrt(n-1)+1;
	rep(i,1,n)ckmax(V,a[i]=read()),bel[i]=(i-1)/blocksize+1;
	init();
	cerr<<S<<'\n';
	for(int i=1;i<=n;++i){
		int x=a[i],res=bin[x];
		for(int j=0,up=sz(d[x]);j<up;++j){
			if(d[x][j]>S)break;
			res+=tim[d[x][j]];
		}
		pre[i]=pre[i-1]+res;
		for(int j=0,up=sz(d[x]);j<up;++j)++bin[d[x][j]];
		if(x>S)for(int j=x;j<=V;j+=x)++bin[j];
		else ++tim[x];
	}
	rep(i,1,m)q[i].l=read(),q[i].r=read(),q[i].id=i;
	sort(q+1,q+m+1);
	for(int i=1,l=1,r=0;i<=m;++i){
		if(l>q[i].l){
			if(r<=n)qn[++tot].pos=r,qn[tot].id=q[i].id,qn[tot].l=q[i].l,qn[tot].r=l-1,qn[tot].op=1;
			ans[q[i].id]-=pre[l-1]-pre[q[i].l-1]+(l-q[i].l)*2,l=q[i].l;
		}
		if(r<q[i].r){
			if(l>1)qn[++tot].pos=l-1,qn[tot].id=q[i].id,qn[tot].l=r+1,qn[tot].r=q[i].r,qn[tot].op=-1;
			ans[q[i].id]+=pre[q[i].r]-pre[r],r=q[i].r;
		}
		if(l<q[i].l){
			if(r<=n)qn[++tot].pos=r,qn[tot].id=q[i].id,qn[tot].l=l,qn[tot].r=q[i].l-1,qn[tot].op=-1;
			ans[q[i].id]+=pre[q[i].l-1]-pre[l-1]+(q[i].l-l)*2,l=q[i].l;
		}
		if(r>q[i].r){
			if(l>1)qn[++tot].pos=l-1,qn[tot].id=q[i].id,qn[tot].l=q[i].r+1,qn[tot].r=r,qn[tot].op=1;
			ans[q[i].id]-=pre[r]-pre[q[i].r],r=q[i].r;
		}
	}
	sort(qn+1,qn+tot+1);
	memset(bin,0,sizeof(bin));
	for(int i=1,j=1;i<=n;++i){
		int x=a[i];
		for(int k=0,up=sz(d[x]);k<up;++k)++bin[d[x][k]];
		if(x>S)for(int k=x;k<=V;k+=x)++bin[k];
		for(;j<=tot&&qn[j].pos==i;++j){
			int st=qn[j].l,ed=qn[j].r; LL sum=0;
			for(int k=st;k<=ed;++k)sum+=bin[a[k]];
			ans[qn[j].id]+=1ll*sum*qn[j].op;
		}
	}
	ED=clock(),cerr<<1.*(ED-ST)/CLOCKS_PER_SEC<<'\n';
	for(int v=1;v<=S;++v){
		for(int i=1;i<=n;++i)cnt[i]=cnt[i-1]+(a[i]%v==0),tim[i]=tim[i-1]+(a[i]==v);
		for(int i=1;i<=tot;++i)ans[qn[i].id]+=1ll*(cnt[qn[i].r]-cnt[qn[i].l-1])*tim[qn[i].pos]*qn[i].op;
	}
	rep(i,1,m)ans[q[i].id]+=ans[q[i-1].id];
	rep(i,1,m)ans[q[i].id]+=q[i].r-q[i].l+1;
	rep(i,1,m)write(ans[i],'\n');
	// rep(i,1,m)printf("%lld\n",ans[i]);
	fwrite(cltout,1,oh-cltout,stdout),oh=cltout;
	ED=clock(),cerr<<1.*(ED-ST)/CLOCKS_PER_SEC<<'\n';
	return 0;
}

代码直接给吧,直接交保证你AC不掉。

upd:又交了一发AC了大草,于是只保留主要部分/yun

posted @ 2020-12-19 22:36  zzctommy  阅读(394)  评论(0编辑  收藏  举报