2019 HDU 多校训练第一场 B-Operation

题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=6579

题意: 有一个长度为n的整数序列a,有两种操作:
0 l r:从[l,r]中选择一些数字,使它们的异或和最大,输出最大值;
1 x:将x附加到序列的末尾,并让n=n+1。
题目要求强制在线,lastans表示最后一个操作0的答案,最初为零。
对于每一个操作0,让l=(l xor lastans)mod n+1,r=(r xor lastans)mod n+1,如果l>r两元素交换。
对于每个操作1,让x=x xor lastans。

比赛的时候,读完这题,看到异或和最大,第一时间想到的就是线性基。线性基区间异或和最大,感觉不是很好处理,突然想到线性基有合并这个操作,能不能把线性基搬到线段树上呢?于是尝试着写了如下的(错解)代码:

//这是一份错解
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1000005;
int n, q, a[MAXN];
struct L_B
{
    int a[32];
    void init()
    {
        memset(a, 0, sizeof(a));
    }
    bool insert(int val)
    {
        for (int i = 30; i >= 0; --i)
        {
            if (val & (1 << i))
            {
                if (!a[i])
                {
                    a[i] = val;
                    break;
                }
                else
                    val ^= a[i];
            }
        }
        return val > 0;
    }
    int query_max()
    {
        int ret = 0;
        for (int i = 30; i >= 0; --i)
        {
            if ((ret ^ a[i]) > ret)
                ret ^= a[i];
        }
        return ret;
    }
    L_B merge(L_B m)
    {
        L_B ret;
        for (int i = 0; i < 31; i++)
            ret.a[i] = a[i];
        for (int i = 0; i < 31; i++)
        {
            for (int j = i; j >= 0; j--)
            {
                if (m.a[i] & (1 << j))
                {
                    if (ret.a[j])
                        m.a[i] ^= ret.a[j];
                    else
                    {
                        ret.a[j] = m.a[i];
                        break;
                    }
                }
            }
        }
        return ret;
    }
} tr[MAXN * 4];
void build(int l, int r, int root)
{
    if (l == r)
    {
        tr[root].init();
        tr[root].insert(a[l]);
        return;
    }
    int mid = (l + r) >> 1;
    build(l, mid, root << 1);
    build(mid + 1, r, root << 1 | 1);
    tr[root] = tr[root << 1].merge(tr[root << 1 | 1]);
}
void add(int l, int r, int pos, int root, int ans)
{
    if (l == pos && r == pos)
    {
        tr[root].init();
        tr[root].insert(ans);
        return;
    }
    int mid = (l + r) >> 1;
    if (pos > mid)
        add(mid + 1, r, pos, root << 1 | 1, ans);
    else
        add(l, mid, pos, root << 1, ans);
    tr[root] = tr[root << 1].merge(tr[root << 1 | 1]);
}
L_B query(int L, int R, int l, int r, int root)
{
    if (L <= l && r <= R)
        return tr[root];
    int mid = (l + r) >> 1;
    if (L <= mid && mid < R)
        return query(L, R, l, mid, root << 1).merge(query(L, R, mid + 1, r, root << 1 | 1));
    if (L <= mid)
        return query(L, R, l, mid, root << 1);
    if (R > mid)
        return query(L, R, mid + 1, r, root << 1 | 1);
}
int main()
{
    int T;
    scanf("%d", &T);
    while (T--)
    {
        memset(a, 0, sizeof(a));
        scanf("%d %d", &n, &q);
        for (int i = 1; i <= n; ++i)
            scanf("%d", &a[i]);
        int len = n + q;
        build(1, len, 1);
        int lastans = 0, op;
        L_B ans;
        for (int i = 1; i <= q; ++i)
        {
            scanf("%d", &op);
            if (op == 0)
            {
                int l, r;
                scanf("%d %d", &l, &r);
                l = (l ^ lastans) % n + 1;
                r = (r ^ lastans) % n + 1;
                if (l > r)
                    swap(l, r);
                ans.init();
                ans = query(l, r, 1, len, 1);
                lastans = ans.query_max();
                printf("%d\n", lastans);
            }
            else if (op == 1)
            {
                int x;
                scanf("%d", &x);
                x = x ^ lastans;
                n++;
                add(1, len, n, 1, x);
            }
        }
    }
    return 0;
}

写完代码后确认没问题,尝试交了一发,TLE。反复检查自己的代码,找不到超时的地方,想办法优化后还是TLE,又重新算了一下时间复杂度,突然发现线性基合并这个操作是O(900)的,并不像线段树的操作是O(1)。之后的时间复杂度越算越不对,开始怀疑自己的思路是否正确。(这场比赛我已经没了.jpg)

题解:
暴力的做法可以用数据结构维护区间线性基,但肯定过不了。(哭)
贪心地维护序列的前缀线性基 (上三角形态),对于每个线性基,将出现位置靠右的数字尽可能地放在高位,也就是说在插入新数字的时候,要同时记录对应位置上数字的出现位置,并且在找到可以插入的位置的时候,如果新数字比位置上原来的数字更靠右,就将该位置上原来的数字向低位推。
在求最大值的时候,从高位向低位遍历,如果该位上的数字出现在询问中区间左端点的右侧且可以使答案变大,就异或到答案里。
对于线性基的每一位,与它异或过的线性基更高位置上的数字肯定都出现在它右侧 (否则它就会被插入在那个位置了),因此做法的正确性显然。

解释:
实际上题解是用了一个贪心的想法,对于一个线性基来说,每个位置的数字贡献的都是它的最高位,那么我们可以对序列中的每个位置都建一个线性基,线性基中每个位置的数字都尽可能往右取,同时记录下序列中每个线性基中每一位贡献的位置。当某个位置上插入新数字前,它的线性基与前一个线性基相同,然后找到新数字能够贡献的位置,新数字插入后只会贡献最高的一位,而它取代的数字也可能会更新线性基后面的几位,因此对取代的数字继续进行更新。
而在求最大值时,只要将该位线性基从高位到低位进行遍历,如果该位上的数字出现在询问中区间左端点的右侧且可以使答案变大,就异或到答案里。

代码如下:

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1000005;
int a[MAXN], L_B[MAXN][31], pos[MAXN][31];
void add(int p, int x)
{
    int k = p;
    for (int i = 30; i >= 0; --i)
    {
        L_B[p][i] = L_B[p - 1][i];
        pos[p][i] = pos[p - 1][i];
    }
    for (int i = 30; i >= 0; --i)
    {
        if (x >> i)
        {
            if (!L_B[p][i])
            {
                L_B[p][i] = x;
                pos[p][i] = k;
                break;
            }
            else
            {
                if (k > pos[p][i])
                {
                    swap(k, pos[p][i]);
                    swap(x, L_B[p][i]);
                }
                x ^= L_B[p][i];
            }
        }
    }
}
int main()
{
    int T;
    scanf("%d", &T);
    while (T--)
    {
        int n, q, op, ans = 0;
        scanf("%d %d", &n, &q);
        for (int i = 1; i <= n; ++i)
        {
            scanf("%d", &a[i]);
            add(i, a[i]);
        }
        while (q--)
        {
            scanf("%d", &op);
            if (op == 1)
            {
                scanf("%d", &a[++n]);
                a[n] ^= ans;
                add(n, a[n]);
            }
            else
            {
                int l, r;
                scanf("%d %d", &l, &r);
                l = (l ^ ans) % n + 1;
                r = (r ^ ans) % n + 1;
                if (l > r)
                    swap(l, r);
                ans = 0;
                for (int j = 30; j >= 0; --j)
                    if ((ans ^ L_B[r][j]) > ans && pos[r][j] >= l)
                        ans ^= L_B[r][j];
                printf("%d\n", ans);
            }
        }
        for (int i = 1; i <= n; ++i)
        {
            for (int j = 30; j >= 0; --j)
            {
                L_B[i][j] = 0;
                pos[i][j] = 0;
            }
        }
    }
    return 0;
}
posted @ 2019-07-30 13:31  redleaves  阅读(...)  评论(...编辑  收藏
Live2D