【解题报告】博弈专场(CF2200~2400)后五题

F:Arpa and a game with Mojtaba | CF850C

题意

  • Arpa and a game with Mojtaba
    n n n 个数字,第 i i i 个为 a i a_i ai
    双方轮流操作。每次选择一个质数 p p p 和一个正整数 k k k,要求至少要有一个数字 a i a_i ai 满足 p k ∣ a i p^k|a_i pkai
    然后,把所有满足上述要求的 a i a_i ai 变为 a i p k \frac{a_i}{p^k} pkai
    不能操作的人输。问先手胜负情况。
  • 1 ≤ n ≤ 100 1\le n\le 100 1n100
    1 ≤ a i ≤ 1 0 9 1\le a_i\le 10^9 1ai109

思路

  • 首先可以想到,对于不同质数,每个数字可以写成这个质数的 x x x 次方。对于不同质数来说是独立的游戏,所以我们只用考虑对于某个质数 p p p,有一个集合 S S S,存储着有些数字是 p p p 的多少次方。
  • 但是这个转移非常不好做。我们假设使用状压做。
    记集合为 S S S,每次选择一个 k k k,就是要把这其中所有 > k >k >k 的元素都 − k -k k,在二进制表示就相当于把 S S S 变为了 ( S > > k + 1 ) ∣ ( S & ( ( 1 < < k ) − 1 ) ) (S>>k+1) | (S\&((1<<k)-1)) (S>>k+1)(S&((1<<k)1))
    然后求出所有后续状态的 S G SG SG 值,就能获得当前状态的 S G SG SG 值。由于 S S S 的范围可以达到 2 e 9 2e9 2e9,我们用 m a p map map 去记忆化存储。

代码

  • 时间复杂度:感觉可能是 O ( 2 e 9 × 30 × o ( m a p ) ) O(2e9\times 30\times o(map)) O(2e9×30×o(map)),但是其实是 O ( 飞 快 ) O(飞快) O()
#include <bits/stdc++.h>
#define IOS ios::sync_with_stdio(false);cin.tie(NULL);cout.tie(NULL);
using namespace std;
typedef long long ll;
void show(){std::cerr << endl;}template<typename T,typename... Args>void show(T x,Args... args){std::cerr << "[ " << x <<  " ] , ";show(args...);}

const int MAX = 2e6+50;
const int MOD = 1e9+7;
const int INF = 0x3f3f3f3f;
const ll LINF = 0x3f3f3f3f3f3f3f3f;
const double EPS = 1e-5;

int tot;
map<int,int>M;
vector<int>V[MAX];

map<int,int>sg;

int get_sg(int x){

    if(x == 0)return 0;
    if(sg.count(x))return sg[x];

    map<int,bool>tmp;

    for(int j = 0;j < 31;++j){
        if(x < (1LL << j))break;
        int nxt = (x >> j + 1) | (x & ((1LL << j) - 1));
        tmp[get_sg(nxt)] = 1;
    }

    for(int j = 0;;++j){
        if(!tmp[j]){
            return sg[x] = j;
        }
    }
}

int main()
{
    int n;
    cin >> n;
    for(int i = 1;i <= n;++i){
        int t;cin >> t;
        for(int j = 2;j * j <= t;++j){
            if(t % j == 0){
                int cnt = 0;
                while(t % j == 0){
                    t /= j;
                    cnt++;
                }
                int bh;
                if(!M[j])M[j] = ++tot;
                bh = M[j];
                V[bh].push_back(cnt);
            }
        }
        if(t > 1){
            int bh;
            if(!M[t])M[t] = ++tot;
            bh = M[t];
            V[bh].push_back(1);
        }
    }
    int mex = 0;
    for(int i = 1;i <= tot;++i){
        int S = 0;
        for(auto it : V[i])
            S |= (1<<it-1);
        mex ^= get_sg(S);
    }
    puts(mex?"Mojtaba":"Arpa");
    return 0;
}

G:Tokitsukaze and Duel | CF1190C

题意

  • Tokitsukaze and Duel | CF1190C
    给定一个长度为 n n n 01 01 01 S S S,以及一个 k k k
    两个人轮流操作。每次选择一个长度为 k k k 的子串,把他们都变成 1 1 1 或者 0 0 0
    某人操作之后,串全部为 0 0 0 或者全部为 1 1 1 就赢了。
    求先手胜负情况,或者平局。
  • 1 ≤ k ≤ n ≤ 1 0 5 1\le k\le n\le 10^5 1kn105

思路

  • 又是感觉策略不好选的题目。但是凭感觉就像一个巧克力博弈的题目。
    首先,如果先手某次选完之后,直接赢了,那么他肯定是必胜的。
    关键就在于什么时候是输,什么时候是平局。
  • 题目给了一个关键样例 n = 4 , k = 2 , S = { 0101 } n=4,k=2,S=\{0101\} n=4,k=2,S={0101}
    注意到,先手不管怎么操作,都能搞出三个连续的数字,然后后手可以轻松获胜。
    所以我们想到,如果先手不管怎么操作,都能怎么怎么样,那么就是输,否则就是平局。
  • 首先想一想,这个怎么怎么样是什么。
    显然,如果不管怎么操作,子串两侧的串如果都是一边全是 1 1 1,一边全是 0 0 0,且 2 k ≥ n 2k\ge n 2kn,那么明显先手必败。
    如果 2 k < n 2k<n 2k<n,且上述条件达到了的话,后手是无法在一步之内获胜的。
    如果不能在一步之内获胜,那么一定是平局,因为后手可以重复先手的操作,让串不发生变化。

代码

  • 时间复杂度: 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 = 1e5+50;
const int MOD = 1e9+7;

char ss[MAX];
int pre[MAX];


int one(int l,int r){
    if(l > r)return 0;
    return pre[r] - pre[l-1];
}
int zero(int l,int r){
    if(l > r)return 0;
    return r - l + 1 - one(l,r);
}

int main()
{
    int n,k;
    scanf("%d%d%*c",&n,&k);
    scanf("%s",ss + 1);
    for(int i = 1;i <= n;++i){
        pre[i] = pre[i-1] + (ss[i] == '1');
    }

    bool canwin = false;
    bool alwaysfail = true;
    bool both = false;

    for(int i = 1;i + k - 1 <= n;++i){
        int Lone = one(1,i-1);
        int Lzero= zero(1,i-1);
        int Rone = one(i+k,n);
        int Rzero= zero(i+k,n);

        if(Lone + Rone == n - k){		// 先手赢的两种情况
            canwin = true;
        }
        if(Lzero + Rzero == n - k){
            canwin = true;
        }

        if(Lone && Rone)both = true;	// 如果操作后两边都有 1 或者 0,那么就不会输,至少是平局的
        if(Lzero && Rzero)both = true;

    }

    if(canwin){
        puts("tokitsukaze");
    }else if(k * 2 >= n && !both){
        puts("quailty");
    }else{
        puts("once again");
    }


    return 0;
}

H:Warrior and Archer | CF594A

题意

  • Warrior and Archer
    2 n 2n 2n 个在坐标轴上的位置 x i x_i xi
    A l i c e Alice Alice B o b Bob Bob 轮流删除这上面的位置,直到只有两个位置,这俩位置之间的距离为 S S S
    A l i c e Alice Alice 希望最小化 S S S B o b Bob Bob 希望最大化 S S S。你需要求出最后的 S S S
  • 2 ≤ 2 n ≤ 2 × 1 0 5 2\le 2n\le 2\times 10^5 22n2×105
    0 ≤ x i ≤ 1 0 9 0\le x_i\le 10^9 0xi109

思路

  • 容易想到, A l i c e Alice Alice 只会删除所有位置中的两侧的位置。她删除中间的位置只会是更劣的。
    其次,更容易想到 B o b Bob Bob 只会删除最终中间的位置。也就是说,如果最后剩下的两个位置为 i , j i,j i,j,那么 B o b Bob Bob 一定是删除 [ i + 1 , j − 1 ] [i+1,j-1] [i+1,j1] 的所有位置,而 A l i c e Alice Alice 删除的就是 [ 1 , i − 1 ] , [ j + 1 , 2 n ] [1,i-1],[j+1,2n] [1,i1],[j+1,2n] 的位置。
  • 经过这样的思考,我们直接枚举 A l i c e Alice Alice 最左边删除了几个点,最右边删除了几个点,那么答案就是这中间的所有点的最两侧的位置的距离。取这个距离的最小值即可。

代码

  • 时间复杂度: 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;

int aa[MAX];

int main()
{
    int n;scanf("%d",&n);
    for(int i = 1;i <= n;++i)scanf("%d",&aa[i]);
    sort(aa+1,aa+1+n);
    int ans = INF;
    for(int i = 0,ed = (n - 2) / 2;i <= ed;++i){
        int zuo = 1 + i;
        int you = n - (ed - i);
        ans = min(ans,aa[you] - aa[zuo]);
    }
    printf("%d",ans);

    return 0;
}

F:Cookies | CF1099F

题意

  • Cookies
    给定一颗树,树上每个节点有 x [ i ] x[i] x[i] 个饼干,这个节点中,吃掉一个饼干需要花 t [ i ] t[i] t[i] 时间。走每条边需要花费 l [ i ] l[i] l[i] 的时间。
    首先人在 1 1 1 节点,也就是根节点处。
  • 首先, A l i c e Alice Alice 选择结束游戏或者继续往某个儿子走。
    如果继续走,走到那个儿子后, B o b Bob Bob 在当前节点的所有儿子节点中,删除一个儿子节点。然后仍然 A l i c e Alice Alice 再操作。
  • 如果选择结束,那么他从该节点一直向根节点走,经过的每个节点都可以选择相应数量的饼干吃。最终走到根节点,问你最多吃几个饼干。
  • 2 ≤ n ≤ 1 0 5 2\le n\le 10^5 2n105
    1 ≤ T , l i ≤ 1 0 18 1\le T,l_i\le 10^{18} 1T,li1018
    1 ≤ x i , t i ≤ 1 0 6 1\le x_i,t_i\le 10^6 1xi,ti106

思路

  • 考虑步骤解耦。我们最终走到某个节点 x x x ,花费的时间为 S S S,也就是说我们最终问你的是 T − 2 S T-2S T2S 的时间,对于这个节点到根的链上,你最多吃几个饼干,即答案为 d p [ x ] dp[x] dp[x]
  • 考虑最终的答案为 d p 2 [ x ] dp2[x] dp2[x]。如果是根节点,儿子是任意走的,答案是 max ⁡ { d p [ x ] , d p f i r s t [ x ] } \max\{dp[x],dp_{first}[x]\} max{dp[x],dpfirst[x]}
    如果是叶子结点,答案就是 d p [ x ] dp[x] dp[x]。如果是中间的结点,答案是 max ⁡ { d p [ x ] , d p s e c o n d [ x ] } \max\{dp[x],dp_{second}[x]\} max{dp[x],dpsecond[x]},因为 B o b Bob Bob 会把最优的答案给删掉。
  • 那我们的唯一难点是求 d p [ x ] dp[x] dp[x]。考虑到时间是 1 0 6 10^6 106,我们对于给定的饼干,最优吃法肯定是先吃花费时间最少的。我们可以用线段树去做,写一个线段树上二分即可。

代码

  • 时间复杂度: 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;
#define ls (p<<1)
#define rs (p<<1|1)
#define md ((l+r)>>1)
const int MAX = 1e6+50;
const int MOD = 1e9+7;
const int INF = 0x3f3f3f3f;

int x[MAX],t[MAX];
ll tim[MAX*4];
ll cok[MAX*4];

inline void push_up(int p){
    tim[p]=tim[ls]+tim[rs];
    cok[p]=cok[ls]+cok[rs];
}
inline void build(int p,int l,int r){
    if(l == r){
        tim[p] = 0;
        cok[p] = 0;
        return;
    }
    build(ls,l,md);
    build(rs,md+1,r);
    push_up(p);
}
inline void add(int p,int l,int r,ll s,ll t){
    cok[p] += s;
    tim[p] += s * t;
}
inline void update(int p,int l,int r,int ux,int uy,ll s,ll t){
    if(ux <= l && uy >= r){
        add(p,l,r,s,t);
        return;
    }
    if(ux <= md)update(ls,l,md,ux,uy,s,t);
    if(uy >  md)update(rs,md+1,r,ux,uy,s,t);
    push_up(p);
}
inline ll query(int p,int l,int r,ll T){	// 一个简单的线段树上二分
    if(l == r) return min(cok[p],T / l);

    if(T >= tim[ls]) return cok[ls] + query(rs,md+1,r,T - tim[ls]);
    else return query(ls,l,md,T);
}



vector<pair<int,ll> >V[100050];

ll dp[100050];

void dfs(int now,ll T){
    update(1,1,1000000,t[now],t[now],x[now],t[now]);	// 多了一些饼干
    dp[now] = query(1,1,1000000,T);
    for(auto it : V[now]){
        int y = it.first;
        ll cost = it.second;
        if(T >= 2 * cost)dfs(y,T - 2 * cost);			// 可用时间减少 2 * cost 因为要考虑来回
    }
    update(1,1,1000000,t[now],t[now],-x[now],t[now]);	// 回溯回去
}

ll dp2[100050];

void dfs2(int now){
    ll mx = 0,sc = 0;
    for(auto it : V[now]){
        int y = it.first;

        dfs2(y);
        if(dp2[y] >= mx){
            sc = mx;
            mx = dp2[y];
        }else if(dp2[y] > sc){
            sc = dp2[y];
        }
    }
//    show(now,mx,sc);
    if(now == 1)dp2[now] = max(dp[now],mx);
    else dp2[now] = max(dp[now],sc);
}

int main()
{
    int n;ll T;
    scanf("%d%lld",&n,&T);
    for(int i = 1;i <= n;++i)scanf("%d",&x[i]);
    for(int i = 1;i <= n;++i)scanf("%d",&t[i]);
    for(int i = 2;i <= n;++i){
        int p,l;scanf("%d%d",&p,&l);
        V[p].push_back(make_pair(i,l));
    }
    build(1,1,1000000);
    dfs(1,T);
    dfs2(1);

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

    printf("%lld",dp2[1]);
    return 0;
}

J:Game | CF277C

题意

  • Game
    给定一个长度为 n × m n\times m n×m 的矩形网格纸
    其中已经被切割了 k k k 刀,给出每刀的切割坐标(一定是水平或竖直切,端点都在格点处)
    两个人轮流操作。每次切一刀,要求也是一定是水平或竖直切,端点都在格点处,且至少切到一条网格中的边(当然切割的地方不能是原来网格纸的最外面的四条边)
    谁不能操作了,谁就输了。求先手胜负情况。如果先手必胜,输出一种先手的操作方案。
  • 1 ≤ n , m ≤ 1 0 9 1\le n,m\le 10^9 1n,m109
    0 ≤ k ≤ 1 0 5 0\le k\le 10^5 0k105

思路

  • 非常经典的题目(但是我之前不会)。
    首先,发现横切和竖切是独立的游戏,我们只考虑一个即可。
  • 对于横切,相当于我们每次选择一个 y y y,然后切割这个 y y y 的线段集 S S S
    想到,我们至少要切割到一条边,且可以把所有的都切掉,相当于一个 N i m Nim Nim 博弈,这一堆中的石头我们至少拿一个,可以全拿光。这是一个很重要的转化!
    所以我们发现,题目就是一个简单的 N i m Nim Nim 博弈。如果不考虑原有的 k k k 刀,就是 n − 1 n-1 n1 m m m 个石头,以及 m − 1 m-1 m1 n n n 个石头的博弈。
    当然,切了一些只是让某些堆的石头少一点,本质是一样的。
  • 现在一个问题,就是怎么输出先手必胜的方案。
    假设游戏的 S G = s g SG=sg SG=sg,如果 s g > 0 sg>0 sg>0,先手是必胜的。我们需要找到某个堆,拿走一些石头,来让 s g = 0 sg=0 sg=0,怎么操作呢?
    假设我们现在有一堆石头有 a i a_i ai 个,我们拿走 x x x 个,这样我们的 S G = s g ⊕ a i ⊕ x = 0 SG=sg\oplus a_i \oplus x=0 SG=sgaix=0
    这里,唯一可行的答案即 x = s g ⊕ a i x=sg \oplus a_i x=sgai
    当然,拿的石头数不能超过这堆石头的总数,所以我们需要满足 x = ( s g ⊕ a i ) ≤ a i x=(sg\oplus a_i) \le a_i x=(sgai)ai 即可。

代码

  • 非常板的题,不需要代码,思考才是最重要的(懒得写啦!)
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值