【解题报告】博弈专场 (CF 2000~2200)前五题

A:Fox and Card Game | CF388C

题意

  • Fox and Card Game | CF388C
    n n n 堆牌,第 i i i 堆有 a i a_i ai 张牌,每堆牌从上往下,给出每张牌的分数
    两个人轮流拿牌, A l i c e Alice Alice 选一堆牌,拿走这堆牌的最上面那张牌; B o b Bob Bob 则是选一堆,拿走这堆牌的最下面那张牌。
  • 选完后,计算每个人的分数和,大的获胜。求每个人的最终分数。
    ∑ a i ≤ 1 0 4 \sum a_i\le 10^4 ai104

思路

  • 一开始感觉是无从下手的,选择决策有很多。
    如果靠近牌顶有收益很大的,肯定是 A l i c e Alice Alice 能拿到的。但是她可以不一开始就去拿。当 B o b Bob Bob 拿了这堆的底的时候,她再来拿这堆的顶即可,有点像围棋的下法和思路。
  • 这么一看,每堆牌就是一个独立的游戏。那么我们只考虑一堆牌。如果是偶数牌, A l i c e Alice Alice 拿走前一半, B o b Bob Bob 拿走后一半。如果是奇数,那么中间那张牌会被先手拿走
  • 如果考虑多堆奇数牌,先手拿走中间的某张牌后,先手就会变成后手,后手会拿走另一堆中间的某张牌。所以我们把奇数牌堆的中间那张牌的收益降序,然后双方轮流拿即可。

代码

  • 时间复杂度: O ( n log ⁡ n ) O(n\log n) O(nlogn)
#include<bits/stdc++.h>
using namespace std;
void show(){std::cerr << endl;}template<typename T,typename... Args>void show(T x,Args... args){std::cerr << "[ " << x <<  " ] , ";show(args...);}
typedef long long ll;
const int MAX = 5e5+50;
const int MOD = 1e9+7;
const int INF = 0x3f3f3f3f;
const ll LINF = 0x3f3f3f3f3f3f3f3f;


vector<int>V;

int main()
{
    int n;cin >> n;
    ll sumA = 0,sum = 0;
    ll mx = 0;
    for(int i = 1;i <= n;++i){
        int s;cin >> s;
        for(int j = 1;j <= s;++j){
            ll t;cin >> t;
            if(s & 1 && j == (s + 1) / 2)V.push_back(t);		// 中间的拿出来排序
            else if(j <= s / 2)sumA += t;
            sum += t;
        }
    }
    sort(V.begin(),V.end(),greater<ll>());
    int sz = V.size();
    for(int i = 0;i < sz;++i){
        if(i%2==0)sumA += V[i];		// 双方轮流拿
    }
    cout << sumA + mx << " " << sum - (sumA + mx);
    return 0;
}

B:Berzerk | CF786A

题意

  • Berzerk
    n n n 个球,编号 1 ∼ n 1\sim n 1n,环形排成一圈,第 n n n 个球是黑洞。
    A l i c e Alice Alice 有一个前进集合 A A A B o b Bob Bob 有一个前进集合 B B B
    对于每一个开始位置 p p p,双方轮流选择他集合中的一个数,相当于他顺时针走几步。如果走到黑洞,他就赢了。
  • 求对于每一个 p p p,对于每个人先手的情况,是必胜还是必输,或者是平局。
    2 ≤ n ≤ 7000 2\le n\le 7000 2n7000

思路

  • 会跑循环,貌似挺麻烦的。考虑到多起点单终点,我们倒着跑是一个不错的选择。
    我们可以定义 d p [ i ] [ 0 / 1 ] dp[i][0/1] dp[i][0/1] 表示在这个位置,谁先手,的必胜态怎么样。
    如果下一步,都走到对方的必胜态,自己就是必败态。如果下一步,有一步走到对方的必败态,自己就是必胜态。
    否则,就是平局。
  • 感觉写起来挺麻烦的。倒着走,都走到必胜态,我们可以用一个 c n t cnt cnt 数组去做。
    平局,我们可以使用一个 v i s vis vis 数组去做。

代码

  • 时间复杂度: O ( n 2 ) O(n^2) O(n2)
#include<bits/stdc++.h>
using namespace std;
void show(){std::cerr << endl;}template<typename T,typename... Args>void show(T x,Args... args){std::cerr << "[ " << x <<  " ] , ";show(args...);}
typedef long long ll;
const int MAX = 7e3+50;
const int MOD = 1e9+7;


int n;
vector<int>V[2];
bool vis[MAX][2];
int mex[MAX][2];
int cnt[MAX][2];

void dfs(int x,int who){
    if(vis[x][who])return;
    vis[x][who] = 1;
    int opp = who ^ 1;
    for(auto it : V[opp]){
        int xp = (x - it + n - 1) % n + 1;		// 上一步
        if(xp == 1)continue;					// 上一步是黑洞,不可能
        if(!mex[x][who]){						// 这一步必败,上一步必胜
            mex[xp][opp] = 1;
            dfs(xp,opp);
        }else if(++cnt[xp][opp] == V[opp].size()){		// 上一步的下一步全是必胜,上一步必败
            mex[xp][opp] = 0;
            dfs(xp,opp);
        }
    }
}

int main()
{
    cin >> n;
    int s;
    cin >> s;
    while(s--){
        int t;cin >> t;
        V[0].push_back(t);
    }
    cin >> s;
    while(s--){
        int t;cin >> t;
        V[1].push_back(t);
    }
    dfs(1,0);dfs(1,1);

    for(int i = 2;i <= n;++i)
    cout << (vis[i][0] ? mex[i][0] ? "Win" : "Lose" : "Loop") << " ";

    puts("");

    for(int i = 2;i <= n;++i)
    cout << (vis[i][1] ? mex[i][1] ? "Win" : "Lose" : "Loop") << " ";


    return 0;
}

C:Ithea Plays With Chtholly | CF896B

题意

  • Ithea Plays With Chtholly | CF896B
    n n n 张纸,一开始没有都是空。
    m m m 轮,每一次给定一个 [ 1 , c ] [1,c] [1,c] 的数字。
    给你数字之后,你需要选择把它写在那一张纸上,若纸上之前写过数字,则覆盖。
    你需要摆完一个数字,他才给你下一个数字。
    问你怎么摆,让最后 n n n 张纸上的数字非单调递减。
  • n , m ≥ 2 n,m\ge 2 n,m2
    1 ≤ c ≤ 1000 1\le c\le 1000 1c1000
    1 ≤ n ⋅ ⌈ c 2 ⌉ ≤ m ≤ 1000 1\le n \cdot \lceil \frac{c}{2}\rceil \le m\le 1000 1n2cm1000

思路

  • 非常神奇的交互题。也是一开始感觉摆数字让最后非单调递减,策略比较复杂的感觉。
    但是仔细想想,假设它最开始给了你个 1 1 1,你摆在最左边是毫无疑问的。
    假设最开始给了你个 2 2 2,你摆在最左边好像也可以,但如果再给了你个 1 1 1,你摆成 [ 2 , 1 ] [2,1] [2,1] 就很劣,不如把 2 2 2 替换成 1 1 1
  • 经过上述非常离散的思考,我们感觉一直维护一个非单调递减的序列就可以了,做法有点类似求最长上升子序列的过程。
  • 但是这样 W A WA WA了。再次仔细考虑,我们的次数超过了 m m m 轮了。假设它给你 [ 1 , c ] [1,c] [1,c] 范围内的数字,给你 c c c 的话,你放在最右位置是毫无疑问的。
    于是,凭感觉,我们把值分成小值和大值,序列从左往右维护一个小值的非递减序列,从右往左维护一个大值的非递增序列。这样,次数就够了。
  • 严格分析轮数的话,考虑最劣情况的维护非递增序列的次数,即给定序列为 [ x , x − 1 , x − 2 , ⋯   , 1 , x , x − 1 , ⋯   ] [x,x-1,x-2,\cdots,1,x,x-1,\cdots] [x,x1,x2,,1,x,x1,],即可以分析得到轮数的最劣情况就是 n ⋅ ⌈ c 2 ⌉ n \cdot \lceil \frac{c}{2}\rceil n2c

代码

  • 时间复杂度: O ( n log ⁡ n ) O(n\log n) O(nlogn)
#include<bits/stdc++.h>
using namespace std;
void show(){std::cerr << endl;}template<typename T,typename... Args>void show(T x,Args... args){std::cerr << "[ " << x <<  " ] , ";show(args...);}
typedef long long ll;
const int MAX = 1e3+50;
const int MOD = 1e9+7;
const int INF = 0x3f3f3f3f;

int arr[MAX];
int arr_iv[MAX];
int main()
{
    int n,m,c;cin >> n >> m >> c;

    fill(arr+1,arr+n+1,INF);
    fill(arr_iv+1,arr_iv+n+1,INF);

    while(1){
        int t;cin >> t;

        if(t <= c / 2){
            int p = upper_bound(arr+1,arr+1+n,t) - arr;
            cout << p << endl;
            arr[p] = t;
            arr_iv[n - p + 1] = -t;
            fflush(stdout);

        }else{
            int p = upper_bound(arr_iv+1,arr_iv+1+n,-t) - arr_iv;
            cout << n - p + 1 << endl;
            arr[n - p + 1] = t;
            arr_iv[p] = -t;
            fflush(stdout);
        }

//        for(int i = 1;i <= n;++i){
//            show(i,arr[i],arr_iv[i]);
//        }


        bool can = true;
        for(int i = 1;i <= n;++i){
            if(arr[i] == INF){
                can = false;
                break;
            }
            if(arr[i] < arr[i-1]){
                can = false;
                break;
            }
        }

        if(can)break;

    }
    return 0;
}

D:Lieges of Legendre | CF603C

题意

  • Lieges of Legendre | CF603C
    n n n 堆石头,每堆有 a i a_i ai 个石头。
    两个人轮流拿石头。每次可以:
    (1) 选择一堆,拿走其中一个石头
    (2)选择一堆,这堆的石头个数为 2 x 2x 2x,把它用 k k k 堆石头,每堆 x x x 个石头替换
    谁不能拿就输了。求先手是否必胜。
  • 1 ≤ n ≤ 1 0 5 1\le n\le 10^5 1n105
    1 ≤ a i , k ≤ 1 0 9 1\le a_i,k\le 10^9 1ai,k109

思路

  • 石头,考虑 S G SG SG 值。
    f ( x ) f(x) f(x) 表示 x x x 个石头的 S G SG SG 值。
    一个可以转移到 f ( x − 1 ) f(x-1) f(x1)
    x x x 是偶数,可以变成 k k k f ( x / 2 ) f(x/2) f(x/2) 的异或值。
  • 容易想到按照 k k k 的奇偶性分类讨论。
  • 如果 k k k 是偶数,我们能算出来 f [ 0 , 1 , 2 ] = { 0 , 1 , 2 } f[0,1,2]=\{0,1,2\} f[0,1,2]={0,1,2}
    如果 x x x 是奇数,那么可以得到 f ( x ) = m e x { f ( x − 1 ) } f(x)=mex\{f(x-1)\} f(x)=mex{f(x1)}
    如果 x x x 是偶数,那么可以得到 f ( x ) = m e x { f ( x − 1 ) , 0 } f(x)=mex\{f(x-1),0\} f(x)=mex{f(x1),0},因为 k k k f ( x / 2 ) f(x/2) f(x/2) 的异或值为 0 0 0
    我们可以直接得到 f ( x ) = 0 f(x)=0 f(x)=0,如果 x x x 是大于 2 2 2 的奇数; f ( x ) = 1 f(x)=1 f(x)=1,如果 x x x 是大于 2 2 2 的偶数。
  • 如果 k k k 是奇数,我们能算出来 f [ 0 , 1 , 2 , 3 , 4 ] = { 0 , 1 , 0 , 1 , 2 } f[0,1,2,3,4]=\{0,1,0,1,2\} f[0,1,2,3,4]={0,1,0,1,2}
    如果 x x x 是奇数,仍然 f ( x ) = m e x { f ( x − 1 ) } f(x)=mex\{f(x-1)\} f(x)=mex{f(x1)}
    如果 x x x 是偶数,那就 f ( x ) = m e x { f ( x − 1 ) , f ( x / 2 ) } f(x)=mex\{f(x-1),f(x/2)\} f(x)=mex{f(x1),f(x/2)} 因为 k k k f ( x / 2 ) f(x/2) f(x/2) 的异或值为 f ( x / 2 ) f(x/2) f(x/2)
    我们能得到一个结论,如果 x x x 是大于 4 4 4 的奇数,那么 f ( x ) = 0 f(x)=0 f(x)=0。否则, f ( x ) > 0 f(x)>0 f(x)>0,我们可以归纳法去做,假设成立,然后根据 m e x { f ( x − 1 ) } mex\{f(x-1)\} mex{f(x1)} 可以证明。
    我们接下来只要递归计算 f ( x ) f(x) f(x) x x x 是偶数的情况即可。每次都除以 2 2 2,是在 log ⁡ \log log 的数量级可以求出来的。
  • 当然有人说:结论怎么想得出来的?你可以打表找规律,但是记得按 k k k 的奇偶性去看。

代码

  • 时间复杂度: O ( n log ⁡ n ) O(n\log n) O(nlogn)
#include<bits/stdc++.h>
using namespace std;
void show(){std::cerr << endl;}template<typename T,typename... Args>void show(T x,Args... args){std::cerr << "[ " << x <<  " ] , ";show(args...);}
typedef long long ll;
const int MAX = 1e3+50;
const int MOD = 1e9+7;
const int INF = 0x3f3f3f3f;

const int pre[] = {0, 1, 0, 1, 2};

int sg(int x,int k){
    if(k & 1){
        if(x < 5)return pre[x];
        if(x & 1)return 0;
        return sg(x / 2,k) == 1 ? 2 : 1;
    }else{
        if(x <= 2) return x == 1 ? 1 : 2;
        return (x % 2) ^ 1;
    }
}


int main()
{
    int n,k;
    scanf("%d%d",&n,&k);
    int mex = 0;
    for(int i = 1;i <= n;++i){
        int t;scanf("%d",&t);
        mex ^= sg(t,k);
    }
    puts(mex?"Kevin":"Nicky");
    return 0;
}

E:Funny Game | CF731E

题意

  • Funny Game | CF731E
    n n n 个数,两个人轮流操作。
    每一次,选择至少两个,把最左边的 x x x 个数字合并成一个新的数字,值为这 x x x 个数字的和 y y y,操作的人获得 y y y 分。
    选完后,第一个人的分数减去第二个人的分数的差值为 C C C,第一个人希望让 C C C 最大,第二个人希望让 C C C 最小,你需要求出这个 C C C
  • 2 ≤ n ≤ 2 × 1 0 5 2\le n\le 2\times 10^5 2n2×105

思路

  • 每次就是选择一个前缀和,设 d p [ i ] [ 0 / 1 ] dp[i][0/1] dp[i][0/1] 表示已经选择好前 i i i 个数了,接下来由哪个人开始选。
    写下状态转移方程:
    d p [ i ] [ 0 ] = max ⁡ { d p [ j ] [ 1 ] + p r e [ j ] } dp[i][0]=\max\{dp[j][1] + pre[j]\} dp[i][0]=max{dp[j][1]+pre[j]}
    d p [ i ] [ 1 ] = min ⁡ { d p [ j ] [ 0 ] + p r e [ j ] } dp[i][1]=\min\{dp[j][0] + pre[j]\} dp[i][1]=min{dp[j][0]+pre[j]}
    然后考虑暴力做时间不够的,只要记录一个最大值和最小值,直接转移即可。
  • 注意至少选择两个,也就是说我们的答案是 d p [ 1 ] [ 0 ] dp[1][0] dp[1][0] 而不是 d p [ 0 ] [ 0 ] dp[0][0] dp[0][0]
  • 还有别的更简单的做法。考虑每个操作的人都是希望最大化自己的分数的,所以可以写成:
	ll ans = pre[n];
    for(int i = n-1;i >= 2;--i)
        ans = max(ans,pre[i] - ans);		// -ans 是因为差值对于不同双方是取反的
    cout << ans;

也是非常的妙。

代码

  • 时间复杂度: O ( n ) O(n) O(n)
#include<bits/stdc++.h>
using namespace std;
void show(){std::cerr << endl;}template<typename T,typename... Args>void show(T x,Args... args){std::cerr << "[ " << x <<  " ] , ";show(args...);}
typedef long long ll;
const int MAX = 2e5+50;
const int MOD = 1e9+7;
const int INF = 0x3f3f3f3f;
const ll LINF = 0x3f3f3f3f3f3f3f3f;

ll pre[MAX];
ll dp[MAX][2];
int main()
{
    int n;
    scanf("%d",&n);
    for(int i = 1;i <= n;++i){
        ll t;scanf("%lld",&t);
        pre[i] = pre[i-1] + t;
    }

    ll mx = pre[n];

    ll mn = -pre[n];

    for(int i = n - 1;i >= 0;--i){
        dp[i][0] = mx;
        dp[i][1] = mn;

        if(dp[i][0] - pre[i] < mn){
            mn = dp[i][0] - pre[i];
        }

        if(dp[i][1] + pre[i] > mx){
            mx = dp[i][1] + pre[i];
        }
//        show(i,dp[i][0],dp[i][1]);
    }

    printf("%lld",dp[1][0]);


    return 0;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值