CF1100F - Ivan and Burgers 题解
什么是前缀线性基,爬罢
首先有个显然的线段树和 st 表维护线性基的做法。前者是预处理 2log 询问 3log,后者是预处理 3log 询问 2log 都过不了。它明显 2log 都不想让你过了。考虑发明一个 1log 的做法。
考虑把询问离线下来,按照 \(r\) 分类,扫描线,时刻维护对每个 \(l\) 的 \([l,r]\) 线性基。
线性基有个跟 gcd 很相似的概念:从一处往右扩展,相等段的数量是 log 的。原理也跟 gcd 差不多,一开始线性基大小为 \(0\),往右移一格就可能加一或者大小不变。大小不变说明没插进去,那就整个都没变了。
那就时刻维护对当前 \(r\) 的关于 \(l\) 的 log 个线性基相等段。考虑 \(r\) 右移一位的更新?新加一个 \(([r,r],\varnothing)\) 相等段,然后把 \(a_r\) 插进前面 log 个相等段。我们需要合并新的大小相同的线性基的相等段,这样才能保证以后一直都是 log 段。但是要知道一个集合的线性基(仅化为阶梯型)与插入顺序有关。而上述操作显然都是从左往右插入的,而只有从右往左插入才能保证大小相同的线性基相同。但其实一点关系都没有,因为生成空间相等……根据的是一个(有限维)线性空间的等维子空间只有本身。那就随便选一个线性基就好啦。
那这样是 2log 的,瓶颈在于每次都要试图插入 log 次。其实对固定的 \(r\),插入的成功性是单调的,必定是连续段序列的一个后缀。那如果到第一个失败就 break
,那其实成功插入的复杂度可以由势能支付掉,因为每个位置最多插入成功(大小增加)log 次,那么总共 2log,一次插入 log,总复杂度就是 3log,前面不乘以 \(n\) 相当于常数。而每个位置只会失败一次,所以总复杂度 1log。这样一来合并也不用写的那么精细,因为合并次数也能由势能支付掉,随便暴力枚举可合并相邻对就行了 vector
的 erase
也随便用,不用管那么多。
(但是好像跑得很慢,我好菜啊)
code
#include<bits/stdc++.h>
using namespace std;
#define pb push_back
#define mp make_pair
#define X first
#define Y second
const int N=500010;
int n,qu;
int a[N];
vector<pair<int,int> > qry[N];
struct base{
int sz,b[30];
base(){sz=0;memset(b,0,sizeof(b));}
void insert(int x){
for(int i=20;~i;i--)if(x>>i&1)
if(b[i])x^=b[i];
else{sz++,b[i]=x;break;}
}
int ask(){
int res=0;
for(int i=20;~i;i--)if(!(res>>i&1))res^=b[i];
return res;
}
};
vector<pair<pair<int,int>,base> > v;
int ans[N];
int main(){
cin>>n;
for(int i=1;i<=n;i++)scanf("%d",a+i);
cin>>qu;
for(int i=1;i<=qu;i++){
int l,r;
scanf("%d%d",&l,&r);
qry[r].pb(mp(l,i));
}
for(int i=1;i<=n;i++){
v.pb(mp(mp(i,i),base()));
for(int j=v.size()-1;~j;j--){
int sz=v[j].Y.sz;
v[j].Y.insert(a[i]);
if(v[j].Y.sz==sz)break;
}
while(true){
bool flg=false;
for(int j=0;j+1<v.size();j++)if(v[j].Y.sz==v[j+1].Y.sz){
flg=true;
int r=v[j+1].X.Y;
v.erase(v.begin()+j+1);
v[j].X.Y=r;
break;
}
if(!flg)break;
}
for(int j=0;j<qry[i].size();j++)for(int k=0;k<v.size();k++)
if(v[k].X.X<=qry[i][j].X&&qry[i][j].X<=v[k].X.Y)ans[qry[i][j].Y]=v[k].Y.ask();
}
for(int i=1;i<=qu;i++)printf("%d\n",ans[i]);
return 0;
}