LGP11661 无聊 学习笔记
LGP11661 无聊 学习笔记
前言
无聊,给我一个连APIO2025都没去成,但是甚至还板刷了若干道根号分治和极值分治的,一眼盯真直接把解胡了出来,发现只有五十分部分分的,最后发现还有一坨沟槽的复杂度优化的,我觉得我是的职业选手都看笑了。
题意简述
给出两个长为 \(n\) 的数列 \(A,B\)。
求 \(b_l\equiv b_r\pmod{\max_{l\le i\le r}a_i}\) 的 \((l,r)\) 个数(\(l\le r\))。
\(n,V\le 5\times 10^5\),\(a_i,b_i\le V\)。
做法解析
\(n,V\) 同阶,所以可能有 \(n,V\) 混用,见谅。
首先你只要刷过几道极值分治就一定能看出来这有个极值分治的形式,建笛卡尔树,请。
然后呢?由于你需要保证复杂度正确,所以你要采取某种启发式合并思想,对于每一个有某数作为极值的区间,他以极值所在处为中心,会往左右各延伸一段,设两段长度分别为 \(x,y\) 且设 \(x\le y\),你要让预处理的复杂度在短的区间那边,这样由于 \(\sum x=n\log n\),复杂度就有所保证。
然后现在的问题变成了若干个如此的询问:给定 \(n\) 个区间 \([l_i,r_i]\),此段的极值是 \(a_i\)。问你在此区间内有多少 \(l_i\le l_j\le r_j\le r_i\) 满足 \(b_{l_j}\equiv b_{r_j}\pmod {a_i}\)。
看到这种模条件下大小关系,只要你刷过这种题就一眼根号分治。设 \(p=b_i\)。显然 \(p<\sqrt{n}\) 的时候你直接把询问离线出来前缀和秒了,复杂度很容易做到 \(O(n\sqrt{V})\) 左右。\(p>\sqrt{n}\) 时,因为满足条件的 \(b\) 只有 \(O(\frac{V}{p})\) 量级,我们用总计 \(O(n)\) 或者 \(O(n\log n)\) 的启发式合并状物获得 \(y\) 段的桶,然后就可以暴力查询了,这样处理每个询问的复杂度是 \(O(x\sqrt{V})\) 的。
看着挺好?但是题目的数据范围是 \(5\times 10^5\) 级别的,所以一根号加一劳嗝过不去。怎么办呢?
你发现实际上我们通过类似map的东西,对每个询问还有一种 \(O(x+y)\) 的暴力。所以我们在 \(p>\sqrt{n}\) 时,我们择优选取暴力。
令人惊讶的是这对复杂度是有质变的。
感性理解就是:对于长且极值点较为居中的区间,\(O(m)\) 的暴力是更优秀的,因为此时 \(x\) 和 \(m\) 同阶。我们可以构造数据使得这样的区间总长度为 \(O(n\log V)\) 级别,此时用 \(O(x\sqrt{V})\) 的做法总复杂度是 \(O(n\sqrt{V}\log V)\) 的,而 \(O(m)\) 的暴力就可以干掉其中的 \(\sqrt{V}\) 从而把这部分复杂度降下去。
最终总复杂度是 \(O(n\sqrt{V})\) 的,可以通过。
代码实现
代码参考了Alex_Wei的实现。对笛卡尔树叶子结点直接 ans++,执行 \(O(x\sqrt{V})\) 暴力的把 i 遍历到 u,执行 \(O(x+y)\) 暴力的也判到了,这样不重不漏数完了所有的 \((l,l)\) 对。
dfs 总是会在末尾将自己区间内较短的那一侧和极值下标的所有 B[i] 放进桶里;在递归的时候它会把两边都递归并抹掉较短侧的桶,这样就可以得到当前较长侧的桶,最后在 dfs 结束的时候恰好补全这段区间的桶,复杂度正确,非常巧妙。
经过实验,根号分治阈值设在 \(0.3\sqrt{n}\) 是比较优秀的。另外在处理阈值下那些询问的时候把不存在询问的 \(p\) 跳过是一个幅度很大的剪枝。
#include <bits/stdc++.h>
using namespace std;
using namespace obasic;
const int MaxN=5e5+5;
int N,A[MaxN],B[MaxN],C[MaxN],F[MaxN],V;
lolo ans;int stk[MaxN],ktp,ls[MaxN],rs[MaxN];
int ksiz,buc1[MaxN],buc2[MaxN];
struct quer{int x,l,r,k;};vector<quer> Q[MaxN];
bool cmpx(quer a,quer b){return a.x<b.x;}
void badd(int cl,int cr,int k){for(int i=cl;i<=cr;i++)buc1[B[i]]+=k;}
void dfs(int cl,int cr,int u){
if(cl>cr)return;
if(cl==cr){ans++,buc1[B[u]]++;return;}
int llen=u-cl,rlen=cr-u,flag=0;
auto qadd=[&](int xl,int xr,int yl,int yr)->void {
if(xl>xr||yl>yr)return;
Q[A[u]].push_back({yr,xl,xr,1});
Q[A[u]].push_back({yl-1,xl,xr,-1});
};
if(llen<=rlen){
dfs(cl,u-1,ls[u]),badd(cl,u-1,-1),dfs(u+1,cr,rs[u]);
if(A[u]<=ksiz){qadd(cl,u-1,u+1,cr),qadd(u,u,cl,cr);}
else if(llen*(V/A[u])<llen+rlen){
int urm=B[u]%A[u];
for(int i=cl,crm;i<=u;i++){
crm=B[i]%A[u],ans+=(urm==crm);
for(int j=crm;j<=V;j+=A[u])ans+=buc1[j];
}
}
else flag=1;
badd(cl,u,1);
}
else{
dfs(u+1,cr,rs[u]),badd(u+1,cr,-1),dfs(cl,u-1,ls[u]);
if(A[u]<=ksiz){qadd(u+1,cr,cl,u-1),qadd(u,u,cl,cr);}
else if(rlen*(V/A[u])<llen+rlen){
int urm=B[u]%A[u];
for(int i=u,crm;i<=cr;i++){
crm=B[i]%A[u],ans+=(urm==crm);
for(int j=crm;j<=V;j+=A[u])ans+=buc1[j];
}
}
else flag=1;
badd(u,cr,1);
}
if(flag){
int urm=B[u]%A[u];
for(int i=u;i<=cr;i++)C[i]=B[i]%A[u],buc2[C[i]]++,ans+=(urm==C[i]);
for(int i=cl;i<u;i++)C[i]=B[i]%A[u],ans+=buc2[C[i]];
for(int i=u;i<=cr;i++)buc2[C[i]]--;
}
}
int main(){
readi(N);ksiz=sqrt(N)*0.3+1;
for(int i=1;i<=N;i++)readi(A[i]);
for(int i=1;i<=N;i++)readi(B[i]),maxxer(V,B[i]);
for(int i=1;i<=N;i++){
while(ktp&&A[stk[ktp]]<=A[i])ls[i]=stk[ktp--];
rs[stk[ktp]]=i,stk[++ktp]=i;
}
dfs(1,N,stk[1]);
for(int i=1,res;i<=ksiz;i++){
if(Q[i].empty())continue;
sort(Q[i].begin(),Q[i].end(),cmpx);
fill(buc1,buc1+i+1,0);
for(int j=1;j<=N;j++)F[j]=(i==1?0:B[j]%i);
int cp=1;for(auto [x,l,r,k] : Q[i]){
res=0;for(;cp<=x;cp++)buc1[F[cp]]++;
for(int j=l;j<=r;j++)res+=buc1[F[j]];
ans+=res*k;
}
}
writi(ans);
return 0;
}
后记
一眼极值分治,二眼根号分治,发现 \(5\times 10^5\) 卡掉了一根号一老葛,然后加一个小优化附一坨复杂度证明。
现在知道这题名字为什么叫“无聊”了吧。
浙公网安备 33010602011771号