自学内容网 自学内容网

[Luogu 5675] GZOI2017 取石子游戏

[Luogu 5675] GZOI2017 取石子游戏

批话

winsun 又来水蓝题了,不过 Nim 和线性基都是九级。。。

原题是个暴力 DP O ( n 2 V ) O(n^2V) O(n2V),如果是模拟赛毒瘤搬题人,这部分只给 30 p t s 30\mathtt{pts} 30pts

对于 100 % 100\% 100% 的数据, 1 ≤ n ≤ 1 0 5 1 \le n \le 10^5 1n105 1 ≤ a i < 2 31 1 \le a_i < 2^{31} 1ai<231,开 2 秒不卡常。

这个数据范围仍然可做,不过也许就要升紫了。

01 Trie 可以做按位贪心(2024 省选,加法定向加强),简化后的线性基也可以。

问题转化

不难发现,若石子集合为 { b 1 , b 2 , ⋯   , b m } \{b_1, b_2, \cdots, b_m\} {b1,b2,,bm},其 xor ⁡ \operatorname{xor} xor 和为 s s s,先手从 b i b_i bi 中取,先手必败,当且仅当 s ⊕ b i ≥ b i s \oplus b_i \ge b_i sbibi

欲使后手进入必败态,显然这次取完之后剩下的数的异或和为 0 0 0。而排除 b i b_i bi 之后集合内其它数的异或和为 s ⊕ b i s \oplus b_i sbi(总的异或掉自己,异或消去律),那么本轮取完之后第 i i i 个数应当恰好剩下 s ⊕ b i s \oplus b_i sbi。而 s ⊕ b i ≥ b i s \oplus b_i \ge b_i sbibi 时,取了至少一个,剩的竟然还比原来多,显然做不到使后手失败,先手只能认输。

钦定先手第一次取了第 i i i 堆石子,考虑计算使先手必败的方案数。

显然问题转化为,对于下标位于 [ 1 , i − 1 ] ∪ [ i + 1 , n ] [1, i-1] \cup [i+1, n] [1,i1][i+1,n] 的所有数,每个数都可以选或者不选,使得已选的若干个数的异或和大于等于 b i b_i bi 的方案数。

考虑线性基

V V V 为位运算可能得到的最大值,原题中 255 255 255

设线性基中有 t o t tot tot 个位置上的数非零,那么余下的 n − t o t − 1 n - tot - 1 ntot1 个数都一定可以被线性基表出(否则它应当被插入线性基)。

考虑暴力枚举每个 x ∈ [ b i , V ] x \in [b_i, V] x[bi,V],若 x x x 能够被它的线性基表出,那么答案应当累加 2 n − t o t − 1 2^{n-tot-1} 2ntot1。(如果加入了某个能被表出的数,那么线性基中表出它的数选与不选的状态取反即可。)

这个方法就是 David_Mercury 的题解 中的做法。它的时间复杂度是 O ( n V log ⁡ V ) O(n V \log V) O(nVlogV) 的。

考虑按位贪心优化

按照 XOR 第 k k k 大的方法,用高斯消元把线性基化简。

// s 是存线性基的数组。
void insert(int x) {
    for (int k = 7; ~k; --k) {
        if ((x>>k)&1) {
            if (s[k]) x ^= s[k];
            else { ++tot; s[k] = x; break; }
        }
    }
}
void simplify() {
    for (int i = 0; i < 8; ++i) {
        if (!s[i]) continue;
        for (int j = i+1; j < 8; ++j) {
            if ((s[j]>>i)&1) s[j] ^= s[i];
        }
    }
}

观察 insertsimplify,存在一个事实:

如果 s i s_i si 非空,那么 ∀ j ≠ i \forall j \ne i j=i s j s_j sj 的第 i i i 位必然为 0 0 0,因为更高位在高斯消元的时候被消掉了,更低位在插入的时候也被消掉了。

根据上面事实以及位运算经典按位贪心的经验,有一个单次 O ( log ⁡ V ) O(\log V) O(logV) 可以求出 [ x , V ] [x, V] [x,V] 内能被表出的数的个数的方法。

int query(int x) {
    int res = 0, cur = 0, cnt = 0;
    for (int k = 7; ~k; --k) {
        if (s[k]) ++cnt;
        if ((x>>k)&1) {
            if (s[k]) cur ^= s[k];
            else if (!((cur>>k)&1)) return res; // GG
            // Otherwise, nothing matters.
        } else {
            if (s[k]) res += 1 << (tot - cnt);
            else if ((cur>>k)&1) return res += 1 << (tot - cnt);
            // Case 2: it's always greater than x.
            // Otherwise, nothing matters.
        }
    }
    return ++res;
}

对于第 k k k 位:

  • 如果 x x x 的当前位为 1 1 1
    • 如果线性基的当前位有值,为了保证大于等于,线性基当前位的数必须选。
    • 如果当前位没有值,且已选的异或和 c u r cur cur 里面,当前位是 0 0 0,再往下怎样都做不到 ≥ x \ge x x,死棋了,当场 return
    • 如果当前位没有值,且已选的异或和 c u r cur cur 里面,当前位是 1 1 1,那么没有影响,还能继续。
  • 如果 x x x 的当前位是 0 0 0
    • 如果线性基的当前位有值,选了就一定大于 x x x,答案加上 选了当前位,后面的位随便选或不选的方案数,然后不选当前位,进入下面的计算。
    • 如果当前位没有值,且已选的异或和 c u r cur cur 里面,当前位是 0 0 0,那么没有事情发生。
    • 如果当前位没有值,且已选的异或和 c u r cur cur 里面,当前位是 1 1 1,再往下不管怎么选都一定大于 x x x,答案加上后面的位随便选或不选的方案数,然后返回。

如果上面这个过程的途中没有 return,说明最后顺利进入了恰好等于 x x x 的情况,答案应当加 1 1 1

完整代码

#include <cstdio>
#include <cstring>
#include <cmath>
#include <vector>
#include <queue>
#include <algorithm>
#include <utility>
#define fi first
#define se second
using namespace std;
using LL = long long;
using LLL = __int128;
const int MAXN = 205;
const int mod = 1e9+7;

template <typename Tp> void read(Tp &res) {
    char ch; bool op = 0; res = 0;
    do ch = getchar(), op |= ch == '-'; while (ch < '0' || ch > '9');
    do res = (res<<3)+(res<<1)+(ch&15), ch = getchar(); while(ch >= '0' && ch <= '9');
    if (op) res = -res;
}
int n, a[MAXN];

struct Base {
    int s[8], tot;
    Base() {
        tot = 0; for (int i = 0; i < 8; ++i) s[i] = 0;
    }
    void insert(int x) {
        for (int k = 7; ~k; --k) {
            if ((x>>k)&1) {
                if (s[k]) x ^= s[k];
                else { ++tot; s[k] = x; break; }
            }
        }
    }
    void simplify() {
        for (int i = 0; i < 8; ++i) {
            if (!s[i]) continue;
            for (int j = i+1; j < 8; ++j) {
                if ((s[j]>>i)&1) s[j] ^= s[i];
            }
        }
    }
    int query(int x) {
        int res = 0, cur = 0, cnt = 0;
        for (int k = 7; ~k; --k) {
            if (s[k]) ++cnt;
            if ((x>>k)&1) {
                if (s[k]) cur ^= s[k];
                else if (!((cur>>k)&1)) return res; // GG
            } else {
                if (s[k]) res += 1 << (tot - cnt);
                else if ((cur>>k)&1) return res += 1 << (tot - cnt);
            }
        }
        return ++res;
    }
} pre[MAXN], suf[MAXN];

Base operator+(const Base& A, const Base& B) {
    Base res = A;
    for (int i = 0; i < 8; ++i) {
        if (!B.s[i]) continue;
        res.insert(B.s[i]);
    }
    return res;
}

int qpow(int x, int y) {
    int res = 1;
    for (; y; y >>= 1) {
        if (y & 1) res = 1ll * res * x % mod; 
        x = 1ll * x * x % mod;
    }
    return res;
}

int main() {
    #ifndef ONLINE_JUDGE
    freopen("lg5675.in", "r", stdin);
    freopen("lg5675.out", "w", stdout);
    #endif
    // code
    read(n);
    for (int i = 1; i <= n; ++i) read(a[i]);
    for (int i = 1; i <= n; ++i) {
        pre[i] = pre[i-1];
        pre[i].insert(a[i]);
    }
    for (int i = n; i; --i) {
        suf[i] = suf[i+1];
        suf[i].insert(a[i]);
    }
    int ans = 0;
    for (int i = 1; i <= n; ++i) {
        Base cur = pre[i-1] + suf[i+1];
        cur.simplify();
        ans = (ans + 1ll * cur.query(a[i]) * qpow(2, n-1-cur.tot)) % mod;
    }
    printf("%d\n", ans);
    return 0;
}

原文地址:https://blog.csdn.net/winsunboy/article/details/143052532

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!