补题ing… 真的不应该错过这场比赛的 qwq
1100F. Ivan and Burgers
Tags:异或线性基 贪心
题目描述
给定 n n n 个数,有 q q q 次询问,每次询问 [ l , r ] [l,r] [l,r] 区间。
对于每次询问,可以选区间内任意多个数,求选数的最大异或和。
输入
1 1 1 组。第一行输入 n n n ,第二行输入 n n n 个数,第三行输入 q q q,
接下来 q q q 行每行两个数 l l l 和 r r r,代表每次询问
范围: 1 ≤ n ≤ 5 × 1 0 5 , 1 ≤ q ≤ 5 × 1 0 5 , 0 ≤ a i ≤ 1 0 6 1 \le n \le 5 \times 10^5,1 \le q \le 5 \times 10^5,0 \le a_i \le 10^6 1≤n≤5×105,1≤q≤5×105,0≤ai≤106
输出
对于每次询问,输出一行一个数,即对应区间任意选数的最大异或和
输入样例 1
4
7 2 3 4
3
1 4
2 3
1 3
输出样例 1
7
3
7
输入样例 2
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
输出样例 2
12
14
27
27
31
14
25
26
30
23
26
29
13
13
7
分析
异或线性基
看到区间最大异或和,马上应该想到的是 异或线性基
这货和高代里面的“极大线性无关组”差不多,就是用这么些东西就可以表示整个空间中的一切其他值,同时内部又不能互相表示自己。异或线性基的意思就是,在一批数中选那么几个数,使得这几个数互相进行任意多次异或可以表示出没选中的任何数,并且又不能表示出自己。
要做到这一点,不妨按位考虑。比如有一组数,二进制是 0001 , 0010 , 0011 , 0100 , 0101 , 1000 , 1011 , 1100 , 1110 0001,0010,0011,0100,0101,1000,1011,1100,1110 0001,0010,0011,0100,0101,1000,1011,1100,1110。显然,我们随便拿一个最高位 1 1 1 是第四位的、再拿一个最高位 1 1 1 是第三位的、再拿一个最高位 1 1 1 是第二位的、再拿一个最高位 1 1 1 是第一位的,用这四个数就绝对可以表示出其他任何数了。
也就是我们拿的四个数应该长得像这个样子(星号表示为什么都可以)
1***
01**
001*
0001
比如拿 1000 , 0100 , 0010 , 0001 1000,0100,0010,0001 1000,0100,0010,0001 显然可以,并且拿 1011 , 0101 , 0011 , 0001 1011,0101,0011,0001 1011,0101,0011,0001 等等拿法也都可以。
为什么?因为通过异或:
- 最高位 1 1 1 是第一位的这个数可以控制任何数的第一位
- 最高位 1 1 1 是第一位的这个数和最高位 1 1 1 是第二位的这个数一起可以控制任何数的前两位
- 最高位 1 1 1 是第一位的这个数和最高位 1 1 1 是第二位的这个数和最高位 1 1 1 是第三位的这个数一起可以控制任何数的前三位
- …
因此我们只要把每一位都拿一个最高位在这个位置的就行了。
那要是这一组数某些位都是 0 0 0 怎么办?那就直接不管这一位了。
(比如 10010 , 10011 , 10001 , 00011 , 00001 10010,10011,10001,00011,00001 10010,10011,10001,00011,00001,那就分别拿最高位 1 1 1 是第五位的、是第二位的、是第一位的这么三个数即可。第三第四位大家都是 0 0 0,就不用管了)
那编程要怎么实现呢?首先可以确定的是,某组数的异或线性基的大小应该 ≤ log 2 max { a i } \le \log_2 \max\{a_i\} ≤log2max{ai}(最多只有这么多个位出现了 1 1 1)
然后我们开一个数组记录线性基的每一个数。很显然地,第 i i i 个数应该就是表示最高位 1 1 1 在第 i i i 位的那个数。
于是我们逐个遍历数组,如果发现当前数的最高位是 i i i,并且线性基的那个位置还是空的话,那正好就收录这个数。
那如果那个位置已经有数了的话,我们是不是就直接抛弃这个数了呢?不行,因为这个数还是可能有利用价值的。我们拿这个数和线性基对应位置的数异或,得到的新数的最高位 1 1 1 一定在低于 i i i 的位(设为 j j j 吧),然后如果线性基的 j j j 位置是空的话,显然我们可以把这个异或值给放进去(这个异或值虽然不是原来就存在的数,但是是当前遍历数和线性基里面已经有的数异或得到的,它当然也可以作为线性基的一部分。)
而如果我们抛弃这个数,就有可能没法得到线性基了。
比如 1111 , 1100 , 1110 , 0010 1111,1100,1110,0010 1111,1100,1110,0010,如果我们采取“抛弃”方法,最后线性基里面只有 1111 1111 1111 和 0010 0010 0010,显然没法表示完全。
而如果采取正确的方式,线性基里面就有 1111 , 0011 ( 1111 ⊕ 1100 ) , 0001 ( 1111 ⊕ 1110 ) 1111,0011(1111\oplus 1100),0001(1111\oplus 1110) 1111,0011(1111⊕1100),0001(1111⊕1110),显然谁都可以表示。
解题思路
再回到题目。我们要求的是区间最大异或值。
首先假设我们已经拿到了某个区间的异或线性基,我们怎么求出最大值呢?
一个 贪心 的想法是,我们从线性基最大的那个数(最高位 1 1 1 最高的那个数)开始往后遍历,如果异或上这个数可以让异或和变大我们就异或。最后累计的异或和就是最大异或和。
为什么这样是对的?首先,根据线性基的性质,我们一定不会遗漏某些异或值。因为一组线性基通过任意组合就已经能表示原来那组数的一切异或和了。
其次,为什么这样做得到的就是最大异或和呢?首先显然我们必须拿线性基最大的那个数。因为只有这个数才拥有那个最高最高位的 1 1 1,其他数的这一位都是 0 0 0。
然后接下来还是按位考虑,假设刚才选中了最高位 1 1 1 在第 i i i 位的线性基,现在考虑最高位 1 1 1 在第 i − 1 i-1 i−1 位的要不要也选上。显然,如果当前累计异或和的第 i − 1 i-1 i−1 位是 0 0 0,那么不管第 i − 2 i-2 i−2 及以后的位是什么,我们都必须拿线性基的最高位 1 1 1 在第 i − 1 i-1 i−1 位的这个数;而如果当前累计异或和的第 i − 1 i-1 i−1 位是 1 1 1,我们也一定不拿这个数。(因为以后再也没法影响这一位了)
但是这样设计代码就有点啰嗦了。而我们发现这个时候用 i f ( s u m ⊕ a [ i ] > s u m ) if(sum\oplus a[i]\ >\ sum) if(sum⊕a[i] > sum) 语句可以起到等价的逻辑功能,所以这种贪心的求法就是正确的啦。
好,那么最后一个问题就是应该怎么应对多次询问了。借鉴 莫队 的思想,我们可以把询问全部收集起来然后按右端点排序,然后再按右端点升序动态插入维护线性基,然后回答每个询问就行啦。这样时间复杂度就不会爆炸了。
时间复杂度
- 排序询问 O ( q log q ) O(q\log q) O(qlogq) 的。
- 循环 q q q 次动态维护线性基,每次插入和回答都是 O ( log ( max { a i } ) ) O(\log(\max\{a_i\})) O(log(max{ai})) 的,总的 O ( q log ( max { a i } ) ) O(q\log(\max\{a_i\})) O(qlog(max{ai})) 的。
- 总时间复杂度: O ( q ( log ( max { a i } ) + log q ) ) O(\ q(\ \log(\max\{a_i\})+\log q\ )\ ) O( q( log(max{ai})+logq ) ) 的。
AC代码
#include<cstdio>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<ctime>
#include<string>
#include<algorithm>
#include<utility>
#include<vector>
#include<list>
#include<map>
#include<set>
#include<unordered_map>
#include<unordered_set>
#include<bitset>
#include<cctype>
#include<climits>
#define GC getchar()
#define _SN(x) {char _c=GC,_v=1;for(x=0;_c<48||_c>57;_c=GC)if(_c==45)_v=-1;for(;_c>=48&&_c<=57;x=(x<<1)+(x<<3)+_c-48,_c=GC);if(_v==-1)x=-x;}
#define _SAN(a,n) {auto _i=0,_n=n;for(;_i<_n;++_i)_SN(a[_i])}
#define _SA(a,l,r) {auto _i=l,_r=r;for(;_i<_r;++_i)_SN(a[_i])}
#define _gS(_1, _2, _3, _sc, ...) _sc
#define sc(...) _gS(__VA_ARGS__,_SA,_SAN,_SN, ...)(__VA_ARGS__)
#define _G1(_1) int _1;sc(_1)
#define _G2(_1,_2) int _1,_2;sc(_1)sc(_2)
#define _G3(_1,_2,_3) int _1,_2,_3;sc(_1)sc(_2)sc(_3)
#define _gG(_1,_2,_3,_get, ...) _get
#define get(...) _gG(__VA_ARGS__,_G3,_G2,_G1, ...)(__VA_ARGS__)
#define _F0N(i,n) for(auto i=0;i<n;++i)
#define _FLR(i,l,r) for(auto i=l,_r=r;i<_r;++i)
#define _gF(_1, _2, _3, _F, ...) _F
#define F(...) _gF(__VA_ARGS__,_FLR,_F0N, ...)(__VA_ARGS__)
#define _FD0(i,n) for(auto i=0;i<=n;++i)
#define _FDL(i,l,r) for(auto i=l,_r=r;i<=_r;++i)
#define _gFD(_1, _2, _3, _FD, ...) _FD
#define FD(...) _gFD(__VA_ARGS__,_FDL,_FD0, ...)(__VA_ARGS__)
#define OPER1(T,x1,b1) inline bool operator<(const T&o)const{return x1 b1 o.x1;}
#define OPER2(T,x1,b1,x2,b2) inline bool operator<(const T&o)const{return x1 b1 o.x1||x1==o.x1&&x2 b2 o.x2;}
#define OPER3(T,x1,b1,x2,b2,x3,b3) inline bool operator<(const T&o)const{return x1 b1 o.x1||x1==o.x1&&(x2 b2 o.x2||x2==o.x2&&x3 b3 o.x3);}
#define LL long long
#define ULL unsigned long long
#define PC putchar
template<class T>
void PRT(const T _){if(_<0){PC(45),PRT(-_);return;}if(_>=10)PRT(_/10);PC(_%10+48);}
template<class T>
void UPRT(const T _){if(_>=10)UPRT(_/10);PC(_%10+48);}
#define CON constexpr
#define T_CASE int CASE;sc(CASE)for(int __=1;__<=CASE;++__)
#define Tjj int T;sc(T)while(T--)
#define qjj int q;sc(q)while(q--)
#define cincout std::cin.tie(nullptr),std::cout.tie(nullptr),std::ios::sync_with_stdio(false);
#define eps 1e-8
#define PI 3.141592653589793
#define MAX_INT 2147483647
#define MIN_INT -2147483648
#define MAX_LL 9223372036854775807
#define MIN_LL -9223372036854775808
#define INF 0x3f3f3f3f
#define LINF 0x3f3f3f3f3f3f3f3fLL
#define endl '\n'
#define priority_queue priority_queue
#define PQ std::priority_queue
#define PR std::pair
#define vector vector
#define VI std::vector<int>
#define MII std::map<int,int>
#define MLI std::map<LL,int>
#define MSI std::map<std::string,int>
#define PII std::pair<int,int>
#define PLI std::pair<LL,int>
#define PSI std::pair<std::string,int>
#define MPFD(k) auto it=mp.find(k)
#define MIN(a, b) ((a)<(b)?(a):(b))
#define MIN3(a, b, c) (MIN(a, MIN(b, c)))
#define MAX(a, b) ((a)>(b)?(a):(b))
#define MAX3(a, b, c) (MAX(a, MAX(b, c)))
#define ABS(a) ((a)>0?(a):-(a))
#define FABS(a) ((a)>0?(a):-(a))
#define log2n(x) (log(x)/0.69314718055995)
#define MHD(p1, p2) ((p1.x>p2.x?p1.x-p2.x:p2.x-p1.x)+(p1.y>p2.y?p1.y-p2.y:p2.y-p1.y))
#define PB emplace_back
#define EB emplace_back
#define BRK else break
#define ALL(X) (X).begin(),(X).end()
#define SORT(X) std::sort(ALL(X))
#define SORTD(X) std::sort(ALL(X),std::greater<decltype((X)[0])>())
#define SWAP(a, b) do{auto _t=a; a=b; b=_t;}while(0)
#define mem0(a) memset(a,0,sizeof(a))
#define memf1(a) memset(a,-1,sizeof(a))
#define meminf(a) memset(a,0x3f,sizeof(a))
using bint = unsigned;
CON bint ONE(1);
CON int MN(5e5+7), MB(20);
struct Node
{
int id;
int l, r;
OPER1(Node, r, <)
} q[MN];
bint a[MN];
int pos[MB];
bint base[MB];
bint ans[MN];
bool insert(bint v, int p)
{
for (int i=MB; i>=0; --i)
{
if (v & (ONE << i))
{
if (base[i])
{
if (pos[i] < p)
/* 更新,替换旧的、更靠前的基。因为:
1.异或后产生的新的基的pos应该是两个异或加数的pos较小者
2.扩大当前i位置的基的可用范围。就是说我们维护线性基的每一项的最大pos,这样在做get_max时才不会遗漏
*/
{
SWAP(pos[i], p);
SWAP(base[i], v);
}
v ^= base[i]; // 尝试找下一个基
}
else
{
base[i] = v;
pos[i] = p;
break;
}
}
}
return v;
}
bint get_max(const int l)
{
bint max = 0;
for (int i=MB; i>=0; --i)
if (pos[i]>=l && (max^base[i])>max)
max ^= base[i];
return max;
}
int main()
{
get(n)
sc(a, n)
get(k)
F(i, k)
{
sc(q[i].l)--q[i].l;
sc(q[i].r)--q[i].r;
q[i].id = i;
}
std::sort(q, q+k);
int top = 0;
F(i, k)
{
while (top <= q[i].r)
{
insert(a[top], top);
++top;
}
ans[q[i].id] = get_max(q[i].l);
}
F(i, k)
UPRT(ans[i]), PC(10);
return 0;
}