洛谷題單指南-字串-P4735 最大異或和

五月江城發表於2024-10-21

原題連結:https://www.luogu.com.cn/problem/P4735

題意解讀:已知長度為n的陣列a[],要在l~r範圍找到一個p,使得a[p]^a[p+1]^...^a[n]^x最大,求這個最大的異或值。

解題思路:

1、利用字首和將問題轉化

設s[]是a[]的字首異或陣列,要計算a中一段範圍l~r的異或,可以藉助於s

由於s[r] = a[0]^a[1]^...a[l-1]^a[l]^...^a[r],s[l-1] = a[0]^a[1]^...^a[l-1]

因此s[r] ^ s[l-1] = a[l]^a[l+1]^...^a[r]

對於a[p]^a[p+1]^...^a[n]^x,可以轉化為s[n]^x^s[p-1],即問題轉化為在s[l-1]~s[r-1]範圍內找到一個s[p-1]使得s[n]^x與其異或值最大。

要計算與一個數異或最大的值,可以藉助於Trie樹,但是這裡限制條件是要在一個範圍內找到與某個數異或的最大值,需要引入一種新的資料結構-持久化Trie樹。

2、持久化Trie樹介紹

所謂持久化Trie,一般用於01Trie,是指對每一個數字進行加入trie操作時,都建立一個新的根節點,從該根節點可以查詢到之前新增的所有的數,具體如何實現呢?我們以2 6 4 3 為例進行Trie新增操作:

洛谷題單指南-字串-P4735 最大異或和

新增的過程有以下特點:

  • 每新增一個數s[i],都建立一個根節點root[i],從root[i]可以查詢到s[1]~s[i]所有數
  • 當新增s[i]時,對於s[i]的每一個二進位制位都新建立節點,對於不在s[i]中的二進位制位,複製前一個根的節點
  • 如新增6時,trie[6][0]=trie[1][0]是複製前一個根節點,而trie[6][1]=7是新建節點

查詢的過程:

要在L~R範圍內查詢與x異或最大的結果,可以在根為root[R]中去找1~R範圍的數,同時用一個maxid[]來記錄Trie樹中每一個節點所經過的數的最大id,這樣在查詢最大異或值時,只需要判斷相反位節點存在且maxid[]>=L即說明是L~R範圍的數,不存在則走相同位的節點。

Trie樹操作核心程式碼:

//將s[id]新增到持久化Trie
void add(int id)
{
    root[id] = ++idx; //建立當前根節點
    int cur = root[id]; //當前根
    int pre = root[id - 1]; //前一個根
    for(int i = 23; i >= 0; i--)
    {
        int v = s[id] >> i & 1;
        trie[cur][!v] = trie[pre][!v]; //相反位複製上一個根
        trie[cur][v] = ++idx; //對s[i]每個二進位制位新建節點
        maxid[cur] = id; //更新maxid
        cur = trie[cur][v], pre = trie[pre][v]; //根沿著s[i]的二進位制位往下走
    }
    maxid[cur] = id; //更新maxid
}

//在l~r範圍內查詢與val異或的最大值
int find(int l, int r, int val)
{
    int res = 0;
    int u = root[r]; //根
    for(int i = 23; i >= 0; i--)
    {
        int v = val >> i & 1;
        if(maxid[trie[u][!v]] >= l) //如果相反位的節點存在且最大編號>=l
        {
            u = trie[u][!v];
            res = res * 2 + 1; //異或後1對結果的貢獻
        }
        else
        {
            u = trie[u][v];
            res = res * 2; //異或後0對結果的貢獻
        }
    }
    return res;
}

3、最終問題求解

第一步:計算字首異或

第二步:建立持久化Trie

第三步:範圍查詢最大異或值

100分程式碼:

#include <bits/stdc++.h>
using namespace std;

const int N = 600005, M = N * 24;
int s[N]; //字首異或陣列
int root[N]; //所有版本trie的根節點
int trie[M][2], idx, maxid[M]; //trie樹,maxid[]儲存經過某個節點的數的最大s編號
int n, m;
char op;
int l, r, x;

//將s[id]新增到持久化Trie
void add(int id)
{
    root[id] = ++idx; //建立當前根節點
    int cur = root[id]; //當前根
    int pre = root[id - 1]; //前一個根
    for(int i = 23; i >= 0; i--)
    {
        int v = s[id] >> i & 1;
        trie[cur][!v] = trie[pre][!v]; //相反位複製上一個根
        trie[cur][v] = ++idx; //對s[i]每個二進位制位新建節點
        maxid[cur] = id; //更新maxid
        cur = trie[cur][v], pre = trie[pre][v]; //根沿著s[i]的二進位制位往下走
    }
    maxid[cur] = id; //更新maxid
}

//在l~r範圍內查詢與val異或的最大值
int find(int l, int r, int val)
{
    int res = 0;
    int u = root[r]; //根
    for(int i = 23; i >= 0; i--)
    {
        int v = val >> i & 1;
        if(maxid[trie[u][!v]] >= l) //如果相反位的節點存在且最大編號>=l
        {
            u = trie[u][!v];
            res = res * 2 + 1; //異或後1對結果的貢獻
        }
        else
        {
            u = trie[u][v];
            res = res * 2; //異或後0對結果的貢獻
        }
    }
    return res;
}

int main()
{
    cin >> n >> m;
    maxid[0] = -1; //不存在的節點maxid=-1,便於find中比較
    add(0); //第一個數新增0,使得便於區間計算
    for(int i = 1; i <= n; i++)
    {
        cin >> s[i];
        s[i] = s[i - 1] ^ s[i]; //計算字首異或
        add(i); //新增到持久化Trie
    } 
    while(m--)
    {
        cin >> op;
        if(op == 'A')
        {
            cin >> x;
            s[n + 1] = s[n] ^ x;
            n++;
            add(n);
        }
        else
        {
            cin >> l >> r >> x;
            cout << find(l - 1, r - 1, x ^ s[n]) << endl;
        }
    }
}

相關文章