1. 程式人生 > 其它 >2021“MINIEYE杯”中國大學生演算法設計超級聯賽(1)1006 / HDU6955. Xor sum(01Trie/好題)

2021“MINIEYE杯”中國大學生演算法設計超級聯賽(1)1006 / HDU6955. Xor sum(01Trie/好題)

Problem Description

Given a sequence of integers of length n, find the shortest consecutive subsequence witch XOR sum not less than k.

If there are multiple consecutive subsequences of the same length, print the consecutive subsequence with the smallest left end point.

If there are no consecutive subsequence witch XOR sum not less than k, just print "-1".

Input

The first line contains a single integer t (t<=100) representing the number of test cases in the input. Then t test cases follow.

The first line of each test case contains two integers n (1<=n<=100000) and k (0<=k<2^30), representing the length of sequence.

The second line of each test contains n integers ai (0<=ai<2^30), representing the integers in sequence.

The number of test witch n>1000 does not exceed 5.

Output

For each test case, print two integers in one line, representing the left end point and right end point of the consecutive subsequence.

If there are no consecutive subsequence witch XOR sum not less than k, print "-1" in one line.

Sample Input

2
3 2
1 2 2
9 7
3 1 3 2 4 0 3 5 1

Sample Output

2 2
5 7

感覺是很好的一道題,賽時寫的暴力二分+可持久化01trie直接t飛2333。但是補題的時候不管是看std還是好多大佬的部落格,要麼是直接按異或和最大做的要麼是像std那樣用了奇奇怪怪的貪心思想(?)總感覺都不一定能保證找到最優解...有明白的大佬還請幫本蒟蒻解答一下QAQ。唯一看到的一份程式碼https://blog.csdn.net/qq_39523236/article/details/118940552?utm_medium=distribute.pc_relevant.none-task-blog-2defaultbaidujs_utm_term~default-0.pc_relevant_baidujshouduan&spm=1001.2101.3001.4242感覺是最正確的了,本題解也是借鑑了這篇部落格的思路Orz

首先這個題提到了區間異或和毫無疑問想到用01trie來寫。因為題目要求的是在滿足異或和大於等於k的前提下找到長度最小的一段區間,因此不妨先求出來異或字首和(這樣求區間異或就可以O(1)來算了),然後列舉區間的右端點,目標是找到一個滿足條件的左端點並更新答案。因此可以維護一棵01trie,在列舉右端點的時候,先在裡面查詢異或和大於等於k的離右端點最近的左端點(作為query函式的返回值),然後把當前位置的異或字首和插入trie。那麼查詢函式怎麼寫呢?

回顧題目,要找的是滿足區間異或和大於等於k的情況下找最靠近右端點的左端點。不妨設列舉到的位置為x,那麼此時字典樹中插入的是sum[1], sum[2]....sum[x - 1],和之前常見的query寫法不同的是,每次查詢實際上執行的是對trie的一次dfs過程,在搜尋過程中,如果走到一個節點時發現接下來不論往哪棵子樹遍歷,最終得到的區間異或和總是大於等於k,那麼就直接返回覆蓋這個節點的最靠右的異或字首和的下標(這和std的思路是一樣的,實際上這也是遞迴的出口),如果最終得到的區間異或和總是小於k則直接返回-1(在當前子樹不可能找到),否則就遞迴遍歷左右子樹(如果存在的話)。

問題又來了:

  1. 怎麼知道覆蓋這個節點的最靠右的異或字首和的下標呢?答:在insert的時候維護一個數組mx即可。因為是從左往右遍歷,因此插入時對於01鏈上的節點直接更新就OK。
  2. 怎麼知道最終搜到的區間異或和是否一定大於等於k或者小於k呢?答:在不斷遞迴搜尋的過程中傳遞一個引數sum表示當前搜尋路徑上兩異或字首和異或得到的區間異或和的前面一部分的大小,然後將其後面若干位全變為0或變為1來比較判斷。描述比較費勁,不妨看一個例子:設右端點對應的異或字首和為01101,沿trie樹從高位往低位搜尋到的部分為00,此時高兩位異或得到01,還剩下三位沒有搜尋到,若k取小於等於01000即8的數,那麼最終無論如何都能滿足要求,反之若k取大於10000即16的數那麼最終必然不可能滿足要求。

這樣對於每個右端點就可以找到一個與之對應的最優左端點,根據題意更新答案即可。

注意:陣列不要開太大,以及初始化的時候不能無腦memset

#include <bits/stdc++.h>
using namespace std;
#define int long long
const int MAXN = 1e5 + 10;
int tol; //節點個數 
int val[32 * MAXN]; //點的值 
int ch[32 * MAXN][2]; //邊的值 
int mx[32 * MAXN];
int n, k, a[100005], sum[100005];
void init() {
    tol = 1;
    ch[0][0] = ch[0][1] = 0;
    for(int i = 0; i <= 32 * n; i++) mx[i] = 0;
}
void insert(int x, int pos)
{ //往 01字典樹中插入 x 
    int u=0;
    for(int i = 31; i >= 0; i--) {
        int v=(x>>i)&1;
        mx[u] = max(mx[u], pos);//維護mx陣列,順序更新實際上不需要max函式
        if(!ch[u][v])
        { //如果節點未被訪問過 
            ch[tol][0]=ch[tol][1]=0; //將當前節點的邊值初始化 
            val[tol]=0; //節點值為0,表示到此不是一個數 
            ch[u][v]=tol++; //邊指向的節點編號 
        }
        u=ch[u][v]; //下一節點 
    }
    val[u]=x; //節點值為 x,即到此是一個數 
    mx[u] = max(mx[u], pos);
}
int f(int p, int sum, int i, int k, int x) {
    int ans = -1;
    if(sum >= k) {
        //cout << "ok" << endl;
        return mx[p];//沒必要繼續往下走了
    }
    if(sum + ((1ll << (i + 1)) - 1) < k) {//這麼走下去不論怎麼選都不可行
        return -1;
    }
    if(ch[p][0]) {
        ans = max(ans, f(ch[p][0], sum + (x & (1 << i)), i - 1, k, x));
    }
    if(ch[p][1]) {
        ans = max(ans, f(ch[p][1], sum + ((x & (1 << i)) ^ (1 << i)), i - 1, k, x));
    }
    return ans;
}
int query(int x, int k) { 
    return f(0, 0, 31, k, x);
}
signed main() {
    ios::sync_with_stdio(false);
    int T;
    cin >> T;
    while (T --) {
        init();
        cin >> n >> k;
        insert(0, 0);
        int ansl = 0, ansr = 0, minlen = 0x3f3f3f3f;
        for(int i = 1; i <= n; i++) {
            cin >> a[i];
            sum[i] = sum[i - 1] ^ a[i];
        }
        for(int i = 1; i <= n; i++) {
            if(a[i] >= k) {
                ansl = ansr = i;
                minlen = 1;
                break;
            }
            int lpos = query(sum[i], k);
            if(lpos != -1 && (sum[i] ^ sum[lpos]) >= k) {
                int len = i - lpos;
                if(len < minlen) {
                    minlen = len;
                    ansl = lpos + 1, ansr = i;
                } else if(len == minlen) {
                    if(ansl > lpos + 1) {
                        ansl = lpos + 1;
                        ansr = i;
                    }
                }
            }
            insert(sum[i], i);
        }
        if(minlen != 0x3f3f3f3f) {
            cout << ansl << " " << ansr << endl;
        } else {
            cout << -1 << endl;
        }
    }
    return 0;
}