Codeforces Round #765 (Div. 2) 部分题解D题

CodeForce1625D最大异或集合

问题描述

给定一个数组a[1-n],求解一个集合,使得集合内任意两个元素的异或值都不小于k

输入格式

第一行,包含两个整数 n , k n,k n,k

第二行,包含 n n n个整数,表示数组 a [ 1 − n ] a[1-n] a[1n]

0 ≤ a [ i ] < 2 30 , 2 ≤ n ≤ 3 × 1 0 5 , 0 ≤ k < 2 30 0\le a[i]<2^{30}, 2\le n\le 3 \times 10^5,0\le k < 2 ^ {30} 0a[i]<230,2n3×105,0k<230

输出格式

第一行,一个整数m表示最大异或集合的大小

第二行,m个整数,表示集合中的元素在a[1-n]中的下标

tags

位运算、DS、树、CF2300分

  • 从高位到低位,按位考虑

    • ,对于一个集合,如果当前位为0则将其划分给左半边的集合,反之则将其划分给右半边的集合
    • 反复这个划分操作,知道当前位k出现的最高位才停止划分(k的最高位也不划分)
  • 于是,我们经过上述操作的时候,就得到若干个集合了

  • 因为 n m a x = 3 × 1 0 5 n_{max}=3\times 10^5 nmax=3×105,所以最后划分出来的集合数量不会超过 2 log ⁡ n 2\log n 2logn个,即 36 36 36

  • 以下我就称这些集合为:位集合

    无意冒犯bitset

  • 划分所得到的位集合的性质

    • 不同集合之间的两个元素异或,答案一定大于等于k
      • 显然,按照我们上面的划分方式,因为他们一定是因为在某一位i不同,然后被划分到不同的集合中
      • 故在第i位,两个集合中取出的数 x , y x,y x,y,在第i位一定是一个0一个1相异或得到的值,至少为 2 i 2^i 2i,因为我们的划分规则是在k的最高位就停止,故必然有 x ⊕ y ≥ 2 i > k x\oplus y \ge 2^i > k xy2i>k
    • 同一个集合中选出到最后答案里面的数,最多不超过两个
      • 我们学过了一个集合内求出最大异或对01trie的做法,那么我们就可以将这个集合丢进01trie中,然后查询集合内的每一个数,若查询到a[i]最大异或值大于k,则将再线性遍历一遍集合(因为需要输出的是下标,而不是数值),找出跟a[i]配对的那个最大异或值的下标
      • 这样我们就找到了这个集合里对最后答案贡献的两个数
      • 那么是否有可能是三个数或者更多呢?
      • 答案是不可能是,既然他们在同一个位集合内,那么就说明他们在k的最高位以前全部相同,只有在最高位及其以后才会出现不同。它们异或出来的结果,在k的最高位必须为1,这样才有可能会不小于k,即一个数该位为0,一个数该位为1
      • 如果我们加入第三个数,鸽笼原理(抽屉原理、染色原理)得,必然存在一对该位相同,这样他们的异或值在k的最高位必然为0,即必然小于k
      • 故同一个集合中选出到最后答案里面的数,最多不超过两个
  • 本题中01trie维护的细节

    • 因为我们上次插入一个集合,又删掉它们的时候,有些节点已经建立完成了,导致他们的son值不为0,但是不存在于当前位集合中。
    • 每次用完后trie都是空的,但是memsetson数组效率过低,所以采用delete字符串的方式
    • 所以我们采用一下方式来解决:新加入num数组,节点存在且num值大于0才表示这个前缀存在于这个位集合中
    • 我们需要在路径上面的点,也给它们的num标志加上v,这样就能和不存在的点区分开来(num=0),才能继续向下迭代遍历
  • 最后注意边界条件k=0时候直接输出所有数字即可

  • 最后博主全部的代码大致如下

#include <bits/stdc++.h>
#include <bits/extc++.h>
#include <unordered_map>
#include <unordered_set>
using namespace std;
using namespace __gnu_cxx;
using namespace __gnu_pbds;

#define debug(x) cerr << #x << ": " << x << '\n';
#define bd cerr << "----------------------" << el;
#define el '\n'
#define cl putchar('\n');
#define pb push_back
#define eb emplace_back
#define x first
#define y second
#define rep(i, a, b) for (int i = (a); i <= (b); i++)
#define lop(i, a, b) for (int i = (a); i < (b); i++)
#define dwn(i, a, b) for (int i = (a); i >= (b); i--)
#define ceil(a, b) (a + (b - 1)) / b
#define ms(a, x) memset(a, x, sizeof(a))
#define INF 0x3f3f3f3f
#define db double
#define all(x) x.begin(), x.end()
#define reps(i, x) for (int i = 0; i < x.size(); i++)
#define cmax(a, b) a = max(a, b)
#define cmin(a, b) a = min(a, b)

typedef long long LL;
typedef long double LD;
typedef pair<int, int> PII;
typedef pair<db, db> PDD;
typedef vector<int> vci;

template <typename T>
inline void read(T &x)
{
    x = 0;
    T f = 1;
    char c = getchar();
    while (!isdigit(c))
    {
        if(c == '-')
            f = -1;
        c = getchar();
    }
    while (isdigit(c))
    {
        x = (x << 1) + (x << 3) + (c ^ 48);
        c = getchar();
    }
    x *= f;
}

constexpr int N = 3e5 + 5;

int T, n, m, k;
int a[N];
vci ans;

struct _01trie{
    
    int son[N * 16][2];//表面一个数有30个节点
    int num[N * 16]; int idx;
    void build()
    {
        ms(son, 0);
        ms(num, 0);
    }
    void add(int x, int v)
    {
        int u = 0;
        dwn(i, 29, 0)
        {
            int &s = son[u][x >> i & 1];
            if(!s) //儿子节点不存在
                s = ++ idx;
            u = s; 
            num[u] += v;
            //这句话得写在里面,不能写在外面
        }
       
    }
    int query(int x) //返回(x xor y)的最大值,而不是y
    {
        int u = 0, res = 0;
        dwn(i, 29, 0)
        {
            int s = x >> i & 1;
            if(son[u][!s] && num[son[u][!s]] > 0) //另一个儿子存在
            {//说明该位异或可以为1
                res += 1 << i;
                u = son[u][!s];
            }
            else //否则说明这一位只能强制跟原本的相同 
                u = son[u][s];
        }
        return res;
    }
}tr;

void work(vci v)//看一下该集合能出几个数
{//一个集合最多出两个数
    if(!v.size())//v为空则没必要计算了
        return ;
    for(auto i : v)
        tr.add(a[i], 1);//将当前集合插入
    bool ok = false;//该集合是否插入两个数
    for(auto i : v)
        if(tr.query(a[i]) >= k) //a[i]对应的最大异或的结果>=k
        {
            ok = true;
            for(auto j : v)
                if((a[i] ^ a[j]) >= k)//找到对应的异或对
                {
                    ans.pb(i), ans.pb(j);
                    break;
                }
            break;
        }
    if(!ok)//若不能插入两个数,则插入一个数
        ans.pb(v[0]);
    for(auto i : v)
        tr.add(a[i], -1);//删除掉
    return ;        
}

void divide(vci v, int bit) //划分位集合
{
    if( !v.size())
        return ;
    if(k & (1 << bit)) //到k的最高位1停止划分
    {
        work(v);
        return ;
    }
    vci v0, v1;
    for(auto i : v)
    {
        if(a[i] & (1 << bit))
            v1.pb(i);//a[i]的bit位是否为1
        else 
            v0.pb(i);
    }
    divide(v0, bit - 1);
    divide(v1, bit - 1);
    return ;
}

int main()
{
    read(n), read(k);
    vci v(n);
    iota(v.begin(), v.end(), 1);
    rep(i, 1, n)
        read(a[i]);
    if(k == 0)
    {
        cout << n << el;
        rep(i, 1, n)
            cout << i << ' ';
        return 0;
    }
    divide(v, 29);
    if(ans.size() <= 1)
        cout << -1 << el;
    else 
    {
        cout << ans.size() << el;
        for(auto it : ans)
            cout << it << ' ';
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值