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;
}
上一篇:PAT乙级1006,用C语言进行编程,换个格式输出整数


下一篇:Android资料!一线互联网移动架构师NDK模块开发!真香!