1. 程式人生 > >01字典樹專題 (解決異或最大值問題)不斷更新ing~

01字典樹專題 (解決異或最大值問題)不斷更新ing~

以前一直以為字典樹沒有多少用,但是最近一直碰到(難道是以前刷題太少的原因麼),其中有一類問題叫做01字典樹問題,它是用來解決xor的有力武器,通常是給你一個數組,問你一段連續的異或和最大是多少,正常思路貪心dp啥的都會一頭霧水,但是用01字典樹就能很快的解決,實現起來也十分方便。

將要插入的數的二進位制位倒著建樹(為什麼?因為異或時高位儘量大,結果才儘量大),即高位在深度低的節點上

貼一個01字典樹的普遍模版

#define Memset(x, a) memset(x, a, sizeof(x))
typedef long long ll;
const int maxn = 100000 + 5;//集合中的數字個數
int ch[32*maxn][2];         //節點的邊資訊
ll val[32*maxn];            //節點儲存的值
int sz;                     //樹中當前節點個數

void init(){
    Memset(ch[0],0);           //樹清空
    sz=1;
}

void _insert(ll a){//在字典樹中插入 a
                  //和一般字典樹的操作相同 將X的二進位制插入到字典樹中
    int u=0;
    for(int i=32;i>=0;i--){
        int c=((a>>i)&1);
        if(!ch[u][c]){
            Memset(ch[sz],0);
            val[sz]=0;
            ch[u][c]=sz++;
        }
        u=ch[u][c];
    }
    val[u]=a;     //最後的節點插入value
}

ll query(ll a){   //在字典樹中查詢和a異或的值最大的元素b 返回b的值
    int u=0;
    for(int i=32;i>=0;i--){
        int c=((a>>i)&1);
        if(ch[u][c^1]) u=ch[u][c^1];//c=0,b=c^1=1,b^c=1;c=1,b=c^1=0,b^c=1;
        else u=ch[u][c];
    }
    return val[u];
}

中間的細節可以自己修改,比如有時可能會刪除某個數,就需要記錄這個節點走了多少次,如果次數為0,就不往下走,陣列大小應該開32(64,如果是LL)*陣列元素個數。
廢話不多說,來看題把。

題意:O(-1)

思路:01字典樹入門題,每個數都插入字典樹中,然後查詢即可AC。

#include <bits/stdc++.h>
#define Memset(x, a) memset(x, a, sizeof(x))
using  namespace  std;
typedef long long ll;
const int maxn = 100000 + 5;//集合中的數字個數
int ch[32*maxn][2];         //節點的邊資訊
ll val[32*maxn];            //節點儲存的值
int sz;                     //樹中當前節點個數

void init(){
    Memset(ch[0],0);           //樹清空
    sz=1;
}

void _insert(ll a){//在字典樹中插入 a
                  //和一般字典樹的操作相同 將X的二進位制插入到字典樹中
    int u=0;
    for(int i=32;i>=0;i--){
        int c=((a>>i)&1);
        if(!ch[u][c]){
            Memset(ch[sz],0);
            val[sz]=0;
            ch[u][c]=sz++;
        }
        u=ch[u][c];
    }
    val[u]=a;     //最後的節點插入value
}

ll query(ll a){   //在字典樹中查詢和a異或的值最大的元素b 返回b的值
    int u=0;
    for(int i=32;i>=0;i--){
        int c=((a>>i)&1);
        if(ch[u][c^1]) u=ch[u][c^1];//c=0,b=c^1=1,b^c=1;c=1,b=c^1=0,b^c=1;
        else u=ch[u][c];
    }
    return val[u];
}

int  main(){
  int T,n,m;
  scanf("%d",&T);
  for(int cas=1; cas<=T; cas++){
    init();  //初始化
    scanf("%d%d",&n,&m);
    ll a;
    for(int i=0; i<n; i++){
      scanf("%lld",&a);
      _insert(a);
    }
    printf("Case #%d:\n",cas);
    for(int i=0 ; i<m; i++){
      scanf("%d",&a);
      printf("%lld\n",query(a));
    }
  }
  return 0;
}

題意:O(-1)

思路:一道01字典樹的題。將要插入的數的二進位制位倒著建樹(為什麼?因為異或時高位儘量大,結果才儘量大),即高位在深度低的節點上。用一個數組記錄經過各個節點的數的個數,插入時,每經過一個點,將節點的這個值加一,刪除時,則減一。查詢時,當前節點的這個值大於0,說明有數經過。對於要查詢的這個數的高位,如果是1,要使異或值儘量大,那麼就要往0的地方走,反之,往1的地方走,實在沒辦法走,只有按原路徑走啦。詳見程式碼。注意:0永遠在樹中。

#include<bits/stdc++.h>
using namespace std;
#define Memset(x, a) memset(x, a, sizeof(x))
typedef long long ll;
const int maxn = 222222;//集合中的數字個數
int ch[32*maxn][2];         //節點的邊資訊
ll val[32*maxn];            //節點儲存的值
int sz,q;                     //樹中當前節點個數
ll num;

void init(){
    Memset(ch[0],0);           //樹清空
    sz=1;
}

void _insert(ll a){
    int u=0;
    for(int i=32;i>=0;i--){
        int c=((a>>i)&1);
        if(!ch[u][c]){
            Memset(ch[sz],0);
            ch[u][c]=sz++;
        }
        u=ch[u][c];
        ++val[u];
    }
}

void _delete(ll a){
  int u=0;
  for (int i=32; i>=0; --i){
      int c=((a>>i)&1);
      u=ch[u][c];
      --val[u];
  }
}

ll query(ll a){   
    int u=0;
    ll ans=0;
    for(int i=32;i>=0;i--){
        int c=((a>>i)&1);
        if (c==1){
            if (ch[u][0] && val[ch[u][0]]){
                ans+=1<<i;
                u=ch[u][0];
            }
            else
                u=ch[u][1];
        }
        else{
            if (ch[u][1] && val[ch[u][1]]){
                ans+=1<<i;
                u=ch[u][1];
            }
            else
                u=ch[u][0];
        }
    }
    return ans;
}

int main(){
  ios::sync_with_stdio(false);
  cin.tie(0);

  string op;
  init();
  cin>>q;
  _insert(0);
  while(q--){
    cin>>op>>num;
    if(op[0]=='+')_insert(num);
    else if(op[0]=='-')_delete(num);
    else cout<<query(num)<<endl;
  }
  return 0;
}



總結:
總的來說如果碰到連續異或和,一般都是01字典樹,並且01字典樹也很容易,頂多就是加個刪除,這個用加標記當前節點用過多少次,刪除的時候num--;,恢復的時候num++;即可,十分簡單。
xor的還有一種套路就是按位列舉的貪心構造,一般就是幾個零散的數的xor和最大或者最小,反正不要求連續的,可以往按位列舉考慮,不過按位列舉也更難。