2020 ICPC 澳门(G,J,I)详解

链接:The 2020 ICPC Asia Macau Regional Contest

G Game on Sequence

题意

给定长度为 n n n 数组 a i a_i ai,A与G博弈,G先手,给定初始位置 k k k,若当前在 i i i 点转移到 j j j,满足 i < j i < j i<j,并且 a i , a j a_i, a_j ai,aj 二进制数位最多只有一位不同,谁不能移动了就输了。现在有两个操作,操作1:在数组末尾增加一个数 k k k. 操作2:询问从 k k k 出发二者谁赢。

思路

乍一看每次新增一个数似乎都要将前面的都更新一遍时间复杂度爆炸,但是对于博弈的这种题我们需要发现一些性质来入手。
我们知道平等组合游戏中,若当前点能转移到必败点,则当前点是必胜点。
2020 ICPC 澳门(G,J,I)详解_第1张图片

s g i = 0 / 1 sg_i = 0/1 sgi=0/1 分别代表从该点出发先手必败/必胜。

我们考虑以下情况若存在两个位置 a i = a j ( i < j ) a_i = a_j (i < j) ai=aj(i<j) a j a_j aj s g j sg_j sgj 值分类讨论:
s j = 1 s_j = 1 sj=1 则说明 a j a_j aj 后存在一个可转移的点 k k k s g k = 0 sg_k = 0 sgk=0,那么 a i a_i ai 同样可以转移到 k k k,所以 s g i = 1 sg_i = 1 sgi=1.
s j = 0 s_j = 0 sj=0 a i a_i ai 可以直接转移 j j j 点, s g i = 1 sg_i = 1 sgi=1.
所以若 a i a_i ai 后存在相同的值,则 a i a_i ai 必胜。

所以每次增加一个数从后往前更新,只需要更新能转移的数的最后一个位置就可以了,总共只有 0 ∼ 255 0\sim255 0255 种数。

具体见代码有详细注释。

代码

#include 
using namespace std;

const int N = 5e5 + 10, M = 300;

int sg[N], a[N];

int last[M];

void update(){
    vector<int> A;
    for(int i = 0; i <= 255; i ++){
        if(last[i]) A.push_back(last[i]);
    }
    sort(A.begin(), A.end()); // 排序,因为一定要从后向前更新
    for(int i = A.size() - 1; i >= 0; i --){
        sg[A[i]] = 0; // 先赋值为 0
        for(int j = 0; j < 8 && !sg[A[i]]; j ++){ // 重新将其更新
            int bi = (a[A[i]] ^ (1 << j));
            if(last[bi] > A[i] && !sg[last[bi]]) sg[A[i]] = 1;
        }
    }
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0); cout.tie(0);

    int n, m;
    cin >> n >> m;
    for(int i = 1; i <= n; i ++){
        cin >> a[i];
        if(last[a[i]]) sg[last[a[i]]] = 1; // 后面有相同值,该点必胜
        last[a[i]] = i;
    }

    for(int i = n; i >= 1; i --){ // 从后向前转移
        if(sg[i]) continue ; // 只从必败点转移
        for(int j = 0; j < 8; j ++){ // 枚举可以转移到此的数,且只更新最后一个该数
            int bi = (a[i] ^ (1 << j)); // 改变j数位上的数
            if(last[bi]) sg[last[bi]] = 1; // 有必败点则自己必胜
        }
    }

    for(int i = 1; i <= m; i ++){
        int op, k;
        cin >> op >> k;
        if(op == 1){
            if(last[k]) sg[last[k]] = 1; // 与自己相同的必胜
            last[k] = ++ n;
            a[n] = k;
            update(); // 更新所有值最后点的sg值
        }
        else{
            // printf("sg[%d] = %d\n",k, sg[k]);
            cout << (sg[k] == 1?"Grammy":"Alice") << "\n";
        }
    }
    return 0;
}

J Jewel Grab

题意

给定 n n n 个物品有颜色 c i c_i ci 和 价值 v i v_i vi,两个操作。操作1:将 x x x 号物品颜色价值改为 c j , v j c_j,v_j cj,vj. 操作2:从 s s s 位置开始,不允许拿走同色的物品,但可以跳过 k k k 次选择不拿,问最多能拿价值多少的物品。(拿走物品后,下一次操作前就会原样回复,但修改不会)。

思路

我们可以随便用线段树/树状数组维护价值。考虑主要的颜色问题。

对于和区间数字种类有关的问题,我们考虑维护一个 l a s t i last_i lasti,代表该位置前一个同色的物品的位置。用线段树维护 l a s t i last_i lasti 的区间最大值,因为 k k k 很小,第一次我们都在线段树区间 [ s , n ] [s,n] [s,n] 中二分找 l a s t i ≥ s last_i \geq s lastis 的最小的位置,每次询问过后得到 i d x idx idx(这就是我们第一个要跳过的颜色),下次询问的时候就扩展至在区间 [ i d x + 1 , n ] [idx + 1, n] [idx+1,n] 继续询问。 这样就能找到所有重复的颜色,减去同色的较小值,保留最大值,最后再用树状数组求一个区间求和即可。

具体见代码。

代码

/* 
对于找相同元素的问题,可以考虑维护last:上一个相同元素的位置
线段树维护最大的last,线段树上二分逐一找到10个重复元素
 */
#include 
using namespace std;

#define ls p << 1
#define rs p << 1 | 1
#define ll long long

const int N = 2e5 + 10, inf = 1e9;

int n, m;
ll rt[N]; // 树状数组维护区间求和单点修改
int lowbit(int x){ return x & -x; }
void update(int r, int k){
    for(int i = r; i <= n; i += lowbit(i)) rt[i] += k;
}
ll get_sum(int l, int r){
    ll ans = 0;
    for(int i = r; i; i -= lowbit(i)) ans += rt[i];
    for(int i = l - 1; i; i -= lowbit(i)) ans -= rt[i];
    return ans;
}

set<int> s[N]; // si:维护颜色i的下标
struct seg_tree{
    int l, r, max_last;
}tr[N * 4];

void build(int p, int l, int r){
    tr[p] = {l, r, 0};
    if(l == r) return ;
    int mid = (l + r) >> 1;
    build(ls, l, mid); build(rs, mid + 1, r);
}

void pushup(int p){
    tr[p].max_last = max(tr[ls].max_last, tr[rs].max_last);
}
void update(int p, int loc, int k){
    if(tr[p].l == tr[p].r){
        tr[p].max_last = k;
        return ;
    }
    int mid = tr[ls].r;
    if(loc <= mid) update(ls, loc, k);
    else update(rs, loc, k);
    pushup(p);
}

int query(int p, int k, int l, int r){ // 线段树上二分
    if(tr[p].max_last < k) return inf;
    if(tr[p].l == tr[p].r) return tr[p].l;
    
    if(l <= tr[p].l && tr[p].r <= r){
        if(tr[ls].max_last >= k) return query(ls, k, l, r);
        else return query(rs, k, l, r);
    }
    int mid = tr[ls].r, ans = inf;
    if(l <= mid) ans = query(ls, k, l, r);
    if(r > mid && ans == inf) ans = query(rs, k, l, r);
    return ans;
}

int c[N], v[N], last[N], nex[N], last_c[N]; // lasti:前一个同色的位置 nexi:后一个同色的位置

void solve(){
    int si, k;
    cin >> si >> k;
    vector<int> ci;

    ll sub = 0;
    int idx = si;
    // 此处last_c[i]: 中存的是需要跳过的颜色的最大价值
    for(int i = 1; i <= k && idx < n; i ++){
        idx = query(1, si, idx + 1, n);
        if(idx == inf) break;
		
		if(!last_c[c[idx]]) last_c[c[idx]] = v[last[idx]]; // 之前没有跳过该颜色,记录
        if(last_c[c[idx]] < v[idx]){ // 减去除了最大价值的同色的价值
            sub -= last_c[c[idx]];
            last_c[c[idx]] = v[idx];
        }
        else sub -= v[idx];
        
        ci.push_back(c[idx]);
    }

    for(auto x : ci) last_c[x] = 0;
    idx = (idx + 1 <= n) ? query(1, si, idx + 1, n) : inf; // 找到最近的不能跳过的点
    idx = min(idx - 1, n); 
    cout << get_sum(si, idx) + sub << "\n";
}

void update(){
    int x, ci, vi;
    cin >> x >> ci >> vi;

    update(x, vi - v[x]); v[x] = vi;
    if(ci == c[x]) return  ;
    
    if(last[x] && nex[x]){
        update(1, nex[x], last[x]); 
        last[nex[x]] = last[x];
        nex[last[x]] = nex[x];
    }
    else if(last[x]) nex[last[x]] = 0;
    else if(nex[x]){
        update(1, nex[x], 0);
        last[nex[x]] = 0;
    }
	
    s[c[x]].erase(x);
    auto it = s[ci].lower_bound(x);
    last[x] = nex[x] = 0;
    if(it != s[ci].end()){
        int nx = *it;
        update(1, nx, x);
        last[nx] = x;
        nex[x] = nx;
    }
    
    if(it != s[ci].begin()){
        int la = *prev(it);
        update(1, x, la);
        last[x] = la;
        nex[la] = x;
    }
    else update(1, x, 0);
    s[ci].insert(x);
    c[x] = ci;
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0); cout.tie(0);

    cin >> n >> m;
	
	build(1, 1, n);
	
    for(int i = 1; i <= n; i ++){ // 此处last_ci:颜色i的最后一个位置
        cin >> c[i] >> v[i];
        update(i, v[i]);

        int idx = last_c[c[i]];
        if(idx){
            last[i] = idx;
            nex[idx] = i;
        }
        last_c[c[i]] = i;
        if(last[i]) update(1, i, last[i]);
        s[c[i]].insert(i);
    }

    for(int i = 1; i <= n; i ++) last_c[i] = 0;

    for(int i = 1; i <= m; i ++){
        int op;
        cin >> op;
        if(op == 1) update();
        else solve();
    }
    return 0;
}

C Club Assignment

用的很麻烦的思路,以及臭长的代码,明天再补思路和注释,写吐我了。

题意

思路

代码

#include 
using namespace std;

typedef pair<int, int> pii;
const int N = 1e5 + 10, inf = (1 << 30);

struct DSU {
    std::vector<int> f;
    
    DSU() {}
    DSU(int maxn) {
        init(maxn);
    }
    void init(int maxn) {
        f.resize(++ maxn); // 重构容器大小到 n
        std::iota(f.begin(), f.end(), 0); // 批量递增赋值
    }
    int find(int x) {
        while (x != f[x]) {
            x = f[x] = f[f[x]];
        }
        return x;
    }
    bool same(int x, int y) {
        return find(x) == find(y);
    }
    bool merge(int x, int y) {
        x = find(x);
        y = find(y);
        if (x == y) {
            return false;
        }
        f[y] = x;
        return true;
    }
};

int son[N * 30][2], siz[N * 30], id[N * 30], tot;
void insert(int x, int ID){
    int p = 0;
    for(int i = 29; i >= 0; i --){
        int u = x >> i & 1;
        if(!son[p][u]) son[p][u] = ++ tot;
        p = son[p][u];
        siz[p] ++;
    }
    id[p] = ID;
}

int check(int x){
    int p = 0;
    for(int i = 29; i >= 0; i --){
        int u = x >> i & 1;
        if(!son[p][u]) return 0;
        p = son[p][u];
    }
    return siz[p];
}

pii get_min(int x, int s, int p){
    int ans = 0;
    for(int i = s; i >= 0; i --){
        int u = x >> i & 1;
        if(son[p][u]) p = son[p][u];
        else{
            p = son[p][!u];
            ans |= (1 << i);
        }
    }
    return {ans, id[p]};
}

struct edge{
    int u, v, w;
    bool operator < (const edge& A)const{
        return w < A.w;
    }
};

edge query(int p1, int x, int i, int p2, int s){ // 遍历子树小的一边
    edge res = {0, 0, inf};
    if(son[p1][0]) res = min(res, query(son[p1][0], x, i - 1, p2, s));
    if(son[p1][1]) res = min(res, query(son[p1][1], x + (1 << i - 1), i - 1, p2, s));
    if(res.w == inf){
        pii tmp = get_min(x, s, p2);
        res = {id[p1], tmp.second, tmp.first};
    }
    return res;
}

edge e[N];
int cnt;
void dfs(int p, int i){ // 最小异或生成树
    if(son[p][0]) dfs(son[p][0], i - 1);
    if(son[p][1]) dfs(son[p][1], i - 1);
    if(son[p][0] && son[p][1]) {
        if(siz[son[p][0]] < siz[son[p][1]]){
            e[++ cnt] = query(son[p][0], 0, i - 1, son[p][1], i - 2);
        }
        else{
           e[++ cnt] = query(son[p][1], 1 << (i - 1), i - 1, son[p][0], i - 2);
        }
        e[cnt].w |= (1 << i - 1);
    }
    return ;
}


DSU dsu;
int n, Enemy[N], c[N];
vector<int> g[N];

void init(int op = 0){ // 清空
    for(int i = 0; i <= tot; i ++){
        son[i][0] = son[i][1] = id[i] = siz[i] = 0;
    }
    tot = 0;
    if(op) return ;
    dsu.init(n);
    for(int i = 1; i <= n; i ++){
        Enemy[i] = c[i] = 0;
        g[i].clear();
    }
    cnt = 0;
}

void dfs(int u){ // 二分图染色
    for(auto v : g[u]){
    	if(c[v]) continue ;
        c[v] = c[u] % 2 + 1;
        dfs(v);
    }
}
int a[N];
void solve(){
    cin >> n;

    init();
    for(int i = 1; i <= n; i ++){
        cin >> a[i]; 
        insert(a[i], i);
    }

    dfs(0, 30);

    sort(e + 1, e + 1 + cnt);

    dsu.init(n);

    int ans = 0;
    for(int i = 1; i <= cnt; i ++){
        // auto [u, v, w] = e[i];
        int u = e[i].u, v = e[i].v, w = e[i].w;
        if(dsu.same(u, v)){
            // cout << "w = " << w << "\n";
            ans = w;
            break ;
        }
        g[u].push_back(v);
        g[v].push_back(u); // 建二分图

        if(!Enemy[u]) Enemy[u] = v;
        else dsu.merge(Enemy[u], v);

        if(!Enemy[v]) Enemy[v] = u;
        else dsu.merge(Enemy[v], u);
    }

    for(int i = 1; i <= n; i ++){
        if(Enemy[i]){
        	if(!c[i]) c[i] = 1, dfs(i);
        }
        else c[i] = 1;
    }
    
    for(int i = 1; i <= n; i ++){
        if(!c[i]) c[i] = 1;
    }
    
    if(!ans){
    	init(1);
    	ans = inf;
    	for(int i = 1; i <= n && ans; i ++){
    		if(c[i] == 1){
    			int sum = check(a[i]);
    			if(sum >= 1){
    				 c[i] = 2;
    				 continue ;
    			}
    		    if(!sum && tot){
    		    	ans = min(ans, get_min(a[i], 29, 0).first);
    		    }
    		    insert(a[i], i);
            }
    	}
        init(1);
    	for(int i = 1; i <= n && ans; i ++){
            if(c[i] == 2){
            	int sum = check(a[i]);
            	if(sum >= 1) ans = 0;
                if(!sum && tot){
                	ans = min(ans, get_min(a[i], 29, 0).first);
                }
                insert(a[i], i); 
            }
        }
    }

    cout << ans << "\n";
    for(int i = 1; i <= n; i ++) cout << c[i];
    cout << "\n";
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0); cout.tie(0);

    int t;
    cin >> t;
    while(t --){
        solve();
    }
    return 0;
}

你可能感兴趣的:(XCPC,博弈论,数据结构,c++)