zoukankan      html  css  js  c++  java
  • [十二省联考2019] 异或粽子

    [十二省联考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
    ", 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 o ch[pos_i][c_i]),即朝下一个地方走下去,并且我们记录(ans[i])表示二进制下答案的这一位上会有多少个(1),那么此时(ans[bit] += s)(bit)表示当前在第几位)。
    如果(s > k),说明这一位不能全部都取(1),只能有(k)个取到(1),那么此时答案(ans[bit] += k),然后每个节点的(pos[i] o 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;
    }
    
  • 相关阅读:
    【Jenkins】之自动化测试持续集成
    【shell】正则表达式
    【openwrt】systemctl详解
    STM32(三十九)RS485串口通信
    ucos(十)信号量优先级反转
    ucos(九)互斥锁和死锁
    【线程】pthread介绍
    git push发现本地 代码没有更新到最新版本,但是已经commit怎么办?
    reset按键和ipget按键在openwrt中的处理逻辑
    用openwrt编译工具链编译一个可执行文件
  • 原文地址:https://www.cnblogs.com/Apocrypha/p/10666886.html
Copyright © 2011-2022 走看看