1. 程式人生 > 實用技巧 >CodeForces 665E. Beautiful Subarrays(字典樹)(貪心)(異或字首和)

CodeForces 665E. Beautiful Subarrays(字典樹)(貪心)(異或字首和)

題意:給定一個長度為n(1 <= n <= 1e6)的陣列a[i](0 <= a[i] <= 1e9)和k(1 <= k <= 1e9)。求有多少個區間[l, r]是合法的。我們認為一個區間是合法的,當且僅當\(a[l]\oplus a[l + 1]\oplus a[l + 2] \oplus ... a[r] >= k\)

分析:對於一個區間是否合法,我們可以先求出異或的字首和,對於一個區間[l, r]合法,代表者\(sum[l - 1] \oplus sum[r] >= k\),我們列舉每個當前的字首和,然後用一個數據結構\(trie樹\)維護,查詢之前多有少個字首和和當前的字首和異或起來>=k

,然後我們分析一下,如何在查詢的時候求出和當前\(sum[r]\)異或起來大於>=k的字首和。我們從根節點出發,從每個數的二進位制最高位開始統計,比較sum[r]和k的最高位,如果k的這一位代表0,我們就直接累加起來和sum[r]異或起來當前為1的那棵子樹中存在的字首和數量,即\(+cnt[tr[p][!u]]\),u表示sum[r]當前位的數,那麼不管之後怎麼走,異或起來都大於k,然後我們再統計和當前sum[r]異或起來為0的字首和數量,直接繼續走就可以,因為這棵子樹中還存在一些字首和,和當前字首和異或起來>=k的數。然後當k的這一位等於1的時候,我們無法直接判斷,直接繼續走就可以。

字典樹的結點個數要開多少呢,可以看出\(a[i]\in{[0, 1e9]}\)\(log(1e9) == 30\),那麼我們可以開\(1e6 * 30 = 3e7\)個結點。

字典樹不僅能在葉子結點維護插入的數的數量,而且能在每個結點上累加,代表這裡曾今被插過的數的數量。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <vector>
#include <algorithm>

using namespace std;
using LL = long long;
const int N = 30 * 1000005;
const int M = 1000005;
int tr[N][2], idx;
int cnt[N];
int a[M];
int sum[M];

int n, k;

void insert(int val)
{
	int p = 0;
	for (int i = 30; i >= 0; --i)
	{
		int u = (val >> i) & 1;
		if (!tr[p][u]) tr[p][u] = ++idx;
		p = tr[p][u];
		++cnt[p];
	}
}

int query(int val)
{
	int p = 0, sum = 0;
	for (int i = 30; i >= 0; --i)
	{
		int u = (val >> i) & 1;
		int c = (k >> i) & 1;
		if (c == 0)
		{
			int k = tr[p][!u];
			sum += cnt[k];
			if (tr[p][u] == 0) return sum;
			p = tr[p][u];
		}
		else
		{
			p = tr[p][!u];
			if (p == 0) return sum;
		}
	}
	return sum + cnt[p];
}

int main()
{
	scanf("%d%d", &n, &k);
	for (int i = 1; i <= n; ++i)
	{
		scanf("%d", &a[i]);
		sum[i] = sum[i - 1] ^ a[i];
	}

	LL res = 0;
	for (int i = 1; i <= n; ++i)
	{
		res += query(sum[i]);
		insert(sum[i]);
	}

	for (int i = 1; i <= n; ++i)
	{
		if (sum[i] >= k) ++res;
	}

	printf("%lld\n", res);

	return 0;
}