【线性基】【离线处理】codeforces1100F Ivan and Burgers
F. Ivan and Burgers
time limit per test3 seconds
memory limit per test256 megabytes
inputstandard input
outputstandard output
Ivan loves burgers and spending money. There are n burger joints on the street where Ivan lives. Ivan has q friends, and the i-th friend suggested to meet at the joint li and walk to the joint ri (li≤ri). While strolling with the i-th friend Ivan can visit all joints x which satisfy li≤x≤ri.
For each joint Ivan knows the cost of the most expensive burger in it, it costs ci burles. Ivan wants to visit some subset of joints on his way, in each of them he will buy the most expensive burger and spend the most money. But there is a small issue: his card broke and instead of charging him for purchases, the amount of money on it changes as follows.
If Ivan had d burles before the purchase and he spent c burles at the joint, then after the purchase he would have d⊕c burles, where ⊕ denotes the bitwise XOR operation.
Currently Ivan has 22100−1 burles and he wants to go out for a walk. Help him to determine the maximal amount of burles he can spend if he goes for a walk with the friend i. The amount of burles he spends is defined as the difference between the initial amount on his account and the final account.
Input
The first line contains one integer n (1≤n≤500000) — the number of burger shops.
The next line contains n integers c1,c2,…,cn (0≤ci≤106), where ci — the cost of the most expensive burger in the burger joint i.
The third line contains one integer q (1≤q≤500000) — the number of Ivan’s friends.
Each of the next q lines contain two integers li and ri (1≤li≤ri≤n) — pairs of numbers of burger shops between which Ivan will walk.
Output
Output q lines, i-th of which containing the maximum amount of money Ivan can spend with the friend i.
Examples
inputCopy
4
7 2 3 4
3
1 4
2 3
1 3
outputCopy
7
3
7
inputCopy
5
12 14 23 13 7
15
1 1
1 2
1 3
1 4
1 5
2 2
2 3
2 4
2 5
3 3
3 4
3 5
4 4
4 5
5 5
outputCopy
12
14
27
27
31
14
25
26
30
23
26
29
13
13
7
Note
In the first test, in order to spend the maximum amount of money with the first and third friends, Ivan just needs to go into the first burger. With a second friend, Ivan just go to the third burger.
In the second test for a third friend (who is going to walk from the first to the third burger), there are only 8 options to spend money — 0, 12, 14, 23, 12⊕14=2, 14⊕23=25, 12⊕23=27, 12⊕14⊕23=20. The maximum amount of money it turns out to spend, if you go to the first and third burger — 12⊕23=27.
昨天CF div2的最后一题,因为起初过的人比较多,所以先看了这题。后来才发现用到了自己没学过的知识——线性基。于是今天学习了一下。
传送门:https://www.cnblogs.com/vb4896/p/6149022.html
个人认为已经这篇文章讲解得极其详细了,所以没必要再多说。1.一组数的所有异或值等价于线性基的异或值,2.线性基没有子集异或为0,或者说线性基是满足第一个条件的最小集合。想到异或运算还有这种基的概念,真实挺有意思的。线性代数真是无处不在呀。从理解方面来说,把每个数看做二进制向量组,类比普通的向量,可以推导出,这些二进制向量对于异或运算,也有秩,线性相关、无关,极大线性无关组,基等一系列的概念。这种类比的方式学习,非常重要。
极大无关组是一组基,如果是求极大线性无关组,那么类同普通向量组,把每个二进制数写成列向量,再对矩阵作初等行变换(当然这里没有数乘,只有交换和异或)。当然大部分时候我们不一定要求得的线性基在原来的数字中,因此,有更简单高效的方法去求基,效率为,M为数字最大值。
void Guass()
{
for (int i=1;i<=n;i++)
for (int j=M;j>=0;j--)
if (A[i]>>j&1)
if(base[j])
A[i]^=base[j];
else
{
base[j]=A[i];
break;
}
}
以上代码的原理,是因为如果基中含有a,b,那么把a替换成a xor b同样也是一组基。这里base[i]如果非0,那么储存了一个最高位在第i位的基。每加入一个A[i],把它高位的1不断用已有的基消去,实在消不掉了,显然应该加在base里面。具体的说明参看以上的网址,这样得到的base从上到下排列出每一个数的二进制,可以产生一个上三角阵,有时还需要进一步用的时间消元,让每一列只有唯一的1,称之为标准化。
现在让我们回到这道题上来。题意是给你五十万个数和五十万个询问,询问区间里子集异或的最大值。如果我们求出了区间的线性基,那么标准化之后,所有基的异或和当然就是最大值。既然是区间问题,那就用线段树咯~,所以我开开心心先写了个线段树。
#include<cstdio>
#include<algorithm>
#include<cstring>
#define kl (k<<1)
#define kr (k<<1|1)
#define M (L+R>>1)
#define lin L,M
#define rin M+1,R
using namespace std;
int T[1<<20][21],n,c[500005],ans[21],q,tmp[21],l,r;
void merge(int *a, int *b, int *c) //求出两个集合并集的线性基,从a和b合并到c。
{
memcpy(tmp,a,sizeof(tmp));
for(int i=20,t;i>=0;i--)
{
t=b[i];
for(int j=i;j>=0;j--)
if(t>>j&1)
if(tmp[j])
t^=tmp[j];
else
{
tmp[j]=t;
break;
}
}
memcpy(c,tmp,sizeof(tmp));
}
void build_tree(int k, int L, int R)
{
if(L==R)
{
for(int i=20;i>=0;i--)
if(c[L]>>i&1)
{
T[k][i]=c[L];
break;
}
return ;
}
build_tree(kl,lin);
build_tree(kr,rin);
merge(T[kl],T[kr],T[k]);
}
void query(int k, int L, int R)
{
if(l<=L&&R<=r)
{
merge(ans,T[k],ans); //小区间合并出答案
return ;
}
if(l<=M)
query(kl,lin);
if(r>M)
query(kr,rin);
}
void read(int &x)
{
int f=1;x=0;char s=getchar();
while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
x*=f;
}
int main()
{
read(n);
for(int i=1;i<=n;i++)
read(c[i]);
build_tree(1,1,n);
read(q);
while(q--)
{
read(l),read(r);
query(1,1,n);
for(int i=0;i<=20;i++)
if(ans[i])
for(int j=i+1;j<=20;j++)
if(ans[j]>>i&1)
ans[j]^=ans[i];
printf("%d\n",accumulate(ans,ans+21,0,[&](int &a, int &b){return a^b;}));
memset(ans,0,sizeof(ans));
}
return 0;
}
这段代码在五十万的数据面前超时了。本机大概要八九秒吧。其实也可以理解,merge用了,总效率是,摁一下计算器,早该超时了。
那该怎么办呢?查看了一些人的代码,有的是一些奇妙的数据结构,有的是用了整体二分(现在还不会,哭了)。其中第一名的代码很引人注目。我看了好一段时间才明白过来。稍微改写了一下。
#include<cstdio>
#include<vector>
#include<algorithm>
using namespace std;
struct item
{
int l,pos;
};
vector<item> E[500005];
int n,a[500005],ans[500005],q,l,r,base[21],pos[21];
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
scanf("%d",&q);
for(int i=1;i<=q;i++)
{
scanf("%d%d",&l,&r);
E[r].push_back((item){l,i});
}
for(int i=1;i<=n;i++)
{
int x=a[i],p=i;
for(int j=20;j>=0;j--)
if(x>>j&1)
if(base[j])
{
if(pos[j]<p)
swap(base[j],x),swap(pos[j],p);
x^=base[j];
}
else
{
base[j]=x,pos[j]=p;
break;
}
for(auto &t:E[i])
for(int j=20;j>=0;j--)
if(pos[j]>=t.l)
if((ans[t.pos]^base[j])>ans[t.pos])
ans[t.pos]^=base[j];
}
for(int i=1;i<=q;i++)
printf("%d\n",ans[i]);
return 0;
}
这段代码中,首先的思想就是离线。把询问按照r从小到大排序,然后逐步地把a[i]添加进来,求出1~r的基。最有意思的地方来了,如果只是普通的一组基,那么还是没办法回答询问,因为不知道把1……l-1的a[i]给删掉,会为基带来什么样的影响。但是作者认为,只要找到基的最靠右边的等价极大无关组S(这个最靠右边,指的是下标的最小值最大,同时也可以证明它等价于从右到左贪心找到的极大无关组),那么l……r的极大无关组,就是S中位置大于或等于l的向量了。那么怎么做到这一点呢,就要好好品味代码中关键的两个swap了。真是神来之笔!相信以后这种神操作还会继续引导我思考。
另外,求最大值时基可以不用标准化,直接从高位到低位贪心就行了,因为相当于反正从大到小考虑每个位,能从0变1就变1(增大),肯定是最好的。
第一名的代码真是有趣呢!