[十二省联考2019] 异或粽子

mac2022-06-30  125

[十二省联考2019] 异或粽子

题意:

题目传送门

题解:

没有做过异或之和超级钢琴,但是这几道题的做法似乎还是非常好想的。首先做前缀异或和,这样问题转化成了个给定序列,找出\(K\)对数字对\((i, j)\)使这几对数字的异或的值之和最大。考虑如果我们确定右端点\(r\),那么异或最大值是很好确定的,直接在\(Trie\)上查找即可。假如我们要查找第\(K\)大的异或值的话,就可以使用可持久化\(Trie\)树。虽然以前没有写过,但写法就和主席树差不多。每当找到\(r\)为右端点的最大值之后,把这个异或值删除,再次查找的就是第二大的了,以此类推就可以得到第\(K\)大的了。

然后我们用堆维护每一个点为右端点时的异或最大值,然后每次取出堆顶的值,更新次大值即可。总共会修改\(K\)次,时间复杂度为\(KlogK\),空间复杂度为\(KlogK\),跑的似乎非常慢。。

Code:

#pragma GCC optimize(2, "inline", "Ofast") #include <bits/stdc++.h> using namespace std; const int N = 5e6 + 50; typedef long long ll; int n, k; ll a[N], s[N], nrt[N]; ll res; namespace PerTrie { int rt[N], cnt[N << 3], ch[N << 3][2]; int tot = 0; queue<int> rec; void Init() { for(int i = 1; i < (N << 3); i++) rec.push(i); } int Newnode() { int o = rec.front(); rec.pop(); cnt[o] = 0; ch[o][0] = ch[o][1] = 0; return o; } void Insert(int &o, int rt, ll x, int Bit) { if(!o || o == rt) o = Newnode(); cnt[o] = cnt[rt] + 1; ch[o][0] = ch[rt][0]; ch[o][1] = ch[rt][1]; if(Bit == -1) return ; int c = (1ll << Bit) & x ? 1 : 0; Insert(ch[o][c], ch[rt][c], x, Bit - 1); return ; } void Delete(int &o, int rt, ll x, int Bit) { if(!o || o == rt) o = Newnode(); cnt[o] = cnt[rt] - 1; ch[o][0] = ch[rt][0]; ch[o][1] = ch[rt][1]; if(Bit == -1) { if(cnt[o] == 0) rec.push(o), o = 0; return ; } int c = (1ll << Bit) & x ? 1 : 0; Delete(ch[o][c], ch[rt][c], x, Bit - 1); if(cnt[o] == 0) rec.push(o), o = 0; return ; } void Query(int o, ll x, int Bit) { if(Bit == -1) return ; int c = (1ll << Bit) & x ? 1 : 0; if(ch[o][c ^ 1]) Query(ch[o][c ^ 1], x, Bit - 1), res ^= (1ll << Bit); else Query(ch[o][c], x, Bit - 1); } } struct node { int R; ll val; bool operator < (const node &rhs) const { return val < rhs.val; }; }; priority_queue<node> Q; int main() { scanf("%d%d", &n, &k); PerTrie::Init(); for(int i = 1; i <= n; i++) scanf("%lld", &a[i]); for(int i = 1; i <= n; i++) s[i] = s[i - 1] ^ a[i]; PerTrie::Insert(PerTrie::rt[0], PerTrie::rt[n + 1], 0, 33); for(int i = 1; i <= n; i++) PerTrie::Insert(PerTrie::rt[i], PerTrie::rt[i - 1], s[i], 33), nrt[i] = i; for(int i = 1; i <= n; i++) { res = 0; PerTrie::Query(PerTrie::rt[i - 1], s[i], 33); Q.push( (node) { i, res }); } ll ans = 0; int tot = n + 1; while(k--) { node o = Q.top(); Q.pop(); ans += o.val; ll V = o.val ^ s[o.R]; PerTrie::Delete(PerTrie::rt[++tot], PerTrie::rt[nrt[o.R - 1]], V, 33); nrt[o.R - 1] = tot; if(PerTrie::rt[nrt[o.R - 1]] == 0) continue; res = 0; PerTrie::Query(PerTrie::rt[nrt[o.R - 1]], s[o.R], 33); Q.push( (node) { o.R, res } ); } printf("%lld\n", ans); return 0; }

2019.4.9 UPD:今天机房放出这道题目的加强版,数据范围如下:\(n \leq 7.5 * 10 ^ 5, k \leq \frac{n * (n + 1)}{2}, a_i < 2 ^ {32}\)\(T L: 2s\)于是我们就有了一个\(O(nlogn)\)的优秀做法,暂时在\(loj\)\(luogu\)\(rank1\)……马上就会放上来的……

2019.4.10 UPD:完了我被日爆了……果然我常数还是大啊……

这里继续讲一个复杂度为\(O(nlogn)\)的做法,并且需要的只是一棵\(Trie\)树。

首先我们还是做一遍前缀异或和,记这个数组为\(b\)。题目转化成点对异或前\(K\)大值之和。但是这是有序对,所以我们将\(K * 2\),即转化成了无序对,最后答案除以\(2\)即可。考虑到\(K\)非常的大,我们不能枚举\(K\)来得到答案,那么比较容易想到的就是枚举每一位计算贡献。在这之前,我们需要找出异或值第\(K\)大的数的值,之后要求的就是所有大于这个值的异或值之和。

考虑逐位确定第\(K\)大的每一位,假设我们这一位取的是\(1\),那么对于某一个\(b_i\),假设当前所在节点为\(x\),当前这一位的值为\(c\),那么只有在\(ch[x][c\) ^ \(1]\)这个节点的子树中的\(b_j\)才能够在该位产生贡献。我们记\(cnt[x]\)表示在\(Trie\)树上每个节点的子树中,有多少个实节点(即这个子树内包含多少个叶子节点)。那么对于当前这一位的答案,假设\(b_i\)\(Trie\)树上的当前位置是\(pos_i\),那么假设当前这一位上取\(1\),则对于\(K\)的贡献就是\(s = \sum_i cnt[ch[pos_i][c_i\) ^ \(1]]\)\(c_i\)表示第\(i\)个前缀异或值这一位上是多少)。

类似树上二分,假设\(s \leq k\),说明这一位都取\(1\)是合法的,那么我们让答案\(K\)减去\(s\),然后对于\(b_i\)当前所在位置\(pos_i\),我们在$ch[pos_i][c_i $ ^ $ 1]\(这个节点上打上\)b_i\(这样一个标记,表示这个节点的子树中所有的\)b_j$ ^ \(b_i\)是会对答案产生贡献的,至于贡献我们最后一起算。然后\(pos_i \to ch[pos_i][c_i]\),即朝下一个地方走下去,并且我们记录\(ans[i]\)表示二进制下答案的这一位上会有多少个\(1\),那么此时\(ans[bit] += s\)\(bit\)表示当前在第几位)。 如果\(s > k\),说明这一位不能全部都取\(1\),只能有\(k\)个取到\(1\),那么此时答案\(ans[bit] += k\),然后每个节点的\(pos[i] \to ch[pos_i][c_i\) ^ \(1]\),即往不会产生贡献的方向走。

这样我们在最后\(Dfs\)一遍\(Trie\)树,处理每个标记,把贡献加到答案里即可。但是由于我们处理的是类似于这样的\(b_i\) ^ \(b_j + b_i\) ^ $b_k + \dots $ 这样的答案,所以我们处理每个标记的时候都需要拆位来计算。那么处理一个标记的时间就是\(O(log)\)的,总的复杂度就是\(O(nlog^2)\)的,仍然不够优秀。

继续考虑到在一个点上打的标记是怎样的形式,很容易发现,能够在某个相同的点上打的标记的这些数的集合\(\{ b_i, b_j, \cdots, b_m\}\),一定是都在某一棵子树里的。于是我们的标记转化成了\(x\)\(y\)这两棵子树中所有的\(b_i\)两两异或的值之和作为贡献。考虑仍然是拆位做,这样我们需要知道某一棵子树中某一位上\(1\)的个数,有一种方法就是在插入\(Trie\)数之前,将\(b\)从小到大排序一遍,那么\(Trie\)树上的一个子树对应的就是数组中的一段区间,这样就非常好处理了。

复杂度之所以是\(O(nlog)\)的,在于这样\((x,y)\)这样的标记个数不会超过\(O(n)\)对,因为标记只会在三度点上被打上,而\(Trie\)数上叶子节点个数为\(O(n)\)个,那么三度点的个数也是\(O(n)\)个,所以标记个数就是\(O(n)\)个。由于这个性质,我们还会发现一个节点\(x\),只会至多和另一个节点\(y\)打上标记,这样就不用标记去重了。

至于如何打这个标记呢,由于我们需要\(b_i\)在按照\(Trie\)树上正常走之后,第\(bit\)位时,在什么位置。于是我们记\(rpos_i\)表示这个位置,那么每次\(rpos_i\)都往\(ch[rpos_i][c_i]\)这个方向走,打标记是,就相当于打上\((ch[rpos_i][c_i], ch[pos_i][c_i\) ^ \(1])\)这样的一对标记。我们将这样的标记存下来,最后一起算即可。注意这个时候\(K\)比较大,所以答案也会爆\(long long\),记得开__\(int128\),并且别忘记除以2。

Code:

#pragma GCC optimize (3, "inline", "Ofast") #include <bits/stdc++.h> using namespace std; const int N = 7.5E5 + 50; const int M = 1.1E7 + 50; typedef long long ll; typedef pair<int, int> P; #define fi first #define se second #define mk make_pair typedef vector<int> Vec; typedef vector<P> VecP; int n, tot; ll k, s; ll a[N], b[N], ans[33]; int ch[M][2], l[M], r[M], dep[M], c[M]; int p1[N], p2[N], sum[N][33], gg[N], vis[M]; VecP t; void Insert(ll x, int idx) { int nw = 0; for(int i = 32; ~i; i--) { int son = (x >> i) & 1; if(!ch[nw][son]) ch[nw][son] = ++tot, dep[tot] = i, l[tot] = r[tot] = idx; nw = ch[nw][son]; l[nw] = min(l[nw], idx); r[nw] = max(r[nw], idx); c[nw] ++; } return ; } void print(__int128 x) { if(!x) return ; print(x / 10); putchar( (char)( x % 10 + '0') ); } void Solve() { for(int i = 0; i < (int)t.size(); i++) { int x = t[i].fi, y = t[i].se; for(int j = 0; j < dep[x]; j++) { int c1 = sum[r[x]][j] - sum[max(0, l[x] - 1)][j], c2 = sum[r[y]][j] - sum[max(0, l[y] - 1)][j]; ans[j] += (ll) c1 * (c[y] - c2) + (ll) c2 * (c[x] - c1); } } __int128 res = 0; for(int i = 0; i <= 32; i++) { if(!ans[i]) continue; res = res + (__int128) (ans[i] / 2) * (1ll << i); } if(res == 0) puts("0"); else print(res), puts(""); return ; } int main() { scanf("%d%lld", &n, &k); k = k * 2; for(int i = 1; i <= n; i++) scanf("%lld", &a[i]), b[i] = b[i - 1] ^ a[i]; sort(b, b + 1 + n); for(int i = 0; i <= n; i++) Insert(b[i], i); for(int i = 1; i <= n; i++) { for(int j = 0; j <= 32; j++) { sum[i][j] = sum[i - 1][j] + ((b[i] >> j) & 1); } } for(int i = 32; ~i; i--) { s = 0; for(int j = 0; j <= n; j++) { if(gg[j]) continue; int son = (b[j] >> i) & 1; s += c[ch[p1[j]][son ^ 1]]; p2[j] = ch[p2[j]][son]; } if(s <= k) { k -= s; ans[i] += s; for(int j = 0; j <= n; j++) { if(gg[j]) continue; int son = (b[j] >> i) & 1; if(ch[p1[j]][son ^ 1] && !vis[p2[j]]) { t.push_back( mk(p2[j], ch[p1[j]][son ^ 1])); vis[p2[j]] = ch[p1[j]][son ^ 1]; } p1[j] = ch[p1[j]][son]; if(!p1[j]) gg[j] = 1; } } else { ans[i] += k; for(int j = 0; j <= n; j++) { if(gg[j]) continue; int son = (b[j] >> i) & 1; p1[j] = ch[p1[j]][son ^ 1]; if(!p1[j]) gg[j] = 1; } } } Solve(); return 0; }

转载于:https://www.cnblogs.com/Apocrypha/p/10666886.html

最新回复(0)