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

6 篇文章 0 订阅
3 篇文章 0 订阅

链接: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 出发二者谁赢。

思路

乍一看每次新增一个数似乎都要将前面的都更新一遍时间复杂度爆炸,但是对于博弈的这种题我们需要发现一些性质来入手。
我们知道平等组合游戏中,若当前点能转移到必败点,则当前点是必胜点。
在这里插入图片描述

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 g j = 1 sg_j = 1 sgj=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 g j = 0 sg_j = 0 sgj=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 <bits/stdc++.h>
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;
    }

    update();

    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 <bits/stdc++.h>
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

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

题意

给定 n n n 个数 w i w_i wi,将其分成两组编号为 b i = 1 / 2 b_i = 1/2 bi=1/2. 最大化同组之间的最小任意异或值,即最大化 min ⁡ 1 ≤ i , j ≤ n w i ⨁ w j [ b i = b j ] \min_{1\leq i,j\leq n} w_i\bigoplus w_j[b_i=b_j] min1i,jnwiwj[bi=bj].

思路

考虑到我们肯定从最小的两者异或值开始,将这两者分到不同组,直接枚举肯定不行,于是我们就想到最小异或生成树算法,可以在 n l o g n nlogn nlogn 时间内找到将所有点连通的最小 n − 1 n - 1 n1 条边,于是我们将这 n − 1 n-1 n1 条边求出来后(形式为 [ u , v , w ] [u,v,w] [u,v,w]),要找到一个方法将 u , v u,v u,v 放在不同集合中。

是不是想到一个很经典的题目?没错将一对 u , v u,v u,v 看做敌人等于转化为了 关押罪犯 这道题目。于是按权值升序排序后就自顾自的写了个并查集判断到哪条边的时候无法分为两个集合就结束算法。再对可以分配的节点建图跑二分图染色给他们分配集合。

但是今天细想好像对于最小异或生成树求出来的这 n − 1 n-1 n1 条边一定可以分到两个不同集合中去。(感性的理解一下吧,虽然推了一下但还是有点解释不清),于是就可以省略一个并查集(如果不能理解当然也可以保留并查集的判断)。

接下来就是对于没分配的集合的节点任意分配,对两个集合分别求一次最小异或值即可。要注意一些小细节,例如( w i w_i wi 相同的点,最小异或生成树不会求出来)。

代码

#include <bits/stdc++.h>
using namespace std;

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

int son[N * 30][2], siz[N * 30], id[N * 30], tot;
void insert(int x, int ID){ // 01 trie
    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; // 记录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){ // 对x求最小异或值,并返回对应的编号
    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 ;
}

int 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 ;
    for(int i = 1; i <= n; 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);

    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;
        g[u].push_back(v);
        g[v].push_back(u); // 建二分图
    }

    for(int i = 1; i <= n; i ++){ // 二分图染色分配集合
        if(!g[i].empty()){
        	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; // 无集合的任意分配
    }
    
    int ans = inf;
    init(1); // 重新初始化01trie
    for(int i = 1; i <= n; i ++){
    	if(c[i] == 1){
    		int sum = check(a[i]);
    		if(sum >= 1){ // 当有相同的数就分配给另一个集合
    			c[i] = 2;
    			continue ;
    		}
    	    if(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; // 有相同的值直接为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;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值