HDU6579系列题解

题目链接

题目大意:

给你一个长度为n的数列,有m次操作,操作有两种:

  1. 在数列末尾插入一个数
  2. 查询区间的子集异或最大值

操作强制在线,n ≤ \le 5e5,m ≤ \le 5e5,所有数值域[0, 2 30 2^{30} 230]。

难度:Ag

分析:

线性基可以处理的操作是:

  1. 在数列末尾插入一个数
  2. 查询全局的子集异或最大值

由于线性基的长度很短,因此我们可以将数列所有前缀的线性基保存下来。1到x的线性基可以由1到x-1的线性基通过插入a[x]来求得,这样,我们就可以查询前缀区间的子集异或最大值。现在问题的关键在于,查询区间 [L, R] 时,如何避免 [1, L-1] 的干扰。

考虑线性基的插入过程,如果线性基当前位上已经有值,我们就不能把待插入的值放入这一位,因此线性基上每一位的数,都是对应位上在原数列最左侧的数字。现在我们改变策略,使得线性基上每一位的数,都变成对应位上在原数列最右侧的数字。实现这个策略的方法是:我们额外保存线性基上每一位数在原数列中的位置,插入的时候,如果对应位上的数在原数列中更靠左,就用待插入的数和它交换。基于这种策略,我们在查询区间 [L, R] 时,可以在区间 [1, R] 对应的线性基中查询,对于线性基上每一位的数,如果它在原数组中出现的位置比 L 更靠右,就考虑它对答案的贡献,否则直接跳过这一位。

这个做法的正确性也很显然,通过改变策略,使线性基上每一位数变成对应位上在原数列最右侧的数字,可以看成线性基插入数字的顺序变反,完全不影响线性基的性质。同时,将线性基上所有在原数组中的位置比 x 更靠左的数字删除,可以视为区间 [1, L-1] 的数字还没有被插入线性基。

复杂度:O((n + m) logx),n为初始数列长度,m为操作次数,x为值域大小。

代码:

# include 
# define MAXN 1000005
# define MAXM 35

using namespace std;

struct LB
{
	int n;	//当前已插入的数字个数
	int a[MAXN][MAXM];	//保存所有前缀区间的线性基
	int b[MAXN][MAXM];	//保存线性基上的数字在原数组上的对应位置

	void build()
	{
		n = 0;
	}

	void add(int x)
	{
		int cur = ++n;	//表示待插入的数字在原数组上的位置
		for (int i = 31; i >= 0; --i)
		{
			a[n][i] = a[n - 1][i];
			b[n][i] = b[n - 1][i];
		}
		for (int i = 31; i >= 0; --i)
			if (x >> i)
			{
				if (!a[n][i])
				{
					a[n][i] = x;
					b[n][i] = cur;
					break;
				}
				else
				{
					if (cur > b[n][i])	//如果待插入的数字在原数组上更靠右,则用线性基上的数与其交换
					{
						swap(a[n][i], x);
						swap(b[n][i], cur);	//位置值也要交换
					}
					x ^= a[n][i];
				}
			}
	}

	int query(int l, int r)
	{
		l = l % n + 1;
		r = r % n + 1;
		if (l > r)
			swap(l, r);	//处理强制在线
		int ret = 0;
		for (int i = 31; i >= 0; --i)
			if (b[r][i] >= l)	//只考虑在原数组中位置比l更靠右的数字的贡献
				ret = max(ret, ret ^ a[r][i]);
		return ret;
	}
};

LB b;

int main()
{
	int t;
	scanf("%d", &t);
	int n, m;
	while (t--)
	{
		b.build();
		scanf("%d %d", &n, &m);
		int tmp;
		for (int i = 0; i < n; ++i)
		{
			scanf("%d", &tmp);
			b.add(tmp);
		}
		int lastans = 0, opt, x, y;	//lastans用于处理强制在线
		for (int i = 0; i < m; ++i)
		{
			scanf("%d %d", &opt, &x);
			if (opt == 0)
			{
				scanf("%d", &y);
				printf("%d\n", lastans = b.query(x ^ lastans, y ^ lastans));
			}
			else
				b.add(x ^ lastans);
		}
	}

	return 0;
}

扩展1:

给你一个数列,有两种操作:

  1. 在数列末尾插入一个数
  2. 查询某个前缀区间的子集异或最大值

原题的弱化版,从原题的解法中,我们应该学到此题的两种解法。

第一种就是,我们保存该数列的所有前缀区间的线性基,这种做法的空间复杂度较高,存在被卡空间的可能。

第二种方法是,我们只保存一个线性基,每次向线性基插入一个数时,额外记录一下插入的数在原数列的位置,查询区间 [1, R] 的时候,对于线性基每一位上的数,如果它在原数列的位置比R更靠左,才考虑它对答案的贡献,否则直接跳过这一位。

扩展2:Codeforces 1100F

给你一个数列,有两种操作(操作可以离线):

  1. 在数列末尾插入一个数
  2. 查询区间的子集异或最大值

跟原题唯一的区别就是操作不强制在线。考虑离线做法,对于每次询问 [L, R] ,把询问按R从小到大排序,然后维护一个线性基。我们每次可以把所有右端点在x之前的询问处理完,再向线性基插入第x个数,所以问题的关键在于,查询区间 [L, R] 时,如何避免 [1, L-1] 的干扰,那么这就回到了原题的解法。

扩展3:洛谷 P4839

给你一排n个桶,每个桶能装任意个数,有两种操作:

  1. 往某个桶中放入一个数
  2. 查询一个区间内所有桶中数字的子集异或最大值

这个题并没有什么很好的做法,只能用线段树维护线性基,时间复杂度O(m * logn * log 2 ^2 2X),n为数列长度,m为操作次数,X为值域。

你可能感兴趣的:(题解)