zoukankan      html  css  js  c++  java
  • @atcoder


    @description@

    给定一个含 N 个数的序列,Alice 与 Bob 在博弈。Alice 先手,轮流进行 N 次操作。

    每一次操作会选择一个之前未选中的数,且与上一个玩家选择的数相邻。
    如果是第一次或者上一次选择的数周围没有未被选中的数,则可以任意选择一个数。

    两个人都想要最大化自己所选择的数之和,且都采取最优策略,求最后 Alice 选择的数之和与 Bob 选择的数之和。

    原题连接。

    @solution@

    首先考虑第一次操作对应的几种可能性。
    第一,先手可以直接取最左边/最右边,则接下来的方案唯一。
    第二,先手选择一个中间的数 x,后手决定选 x 的左边还是右边。

    考虑第二种情况,如果 x 的左边/右边有奇数个数,先手可能会被动地变成后手。
    而此时,后手存在一种可能的取数方法,对应着先手一开始取最左边/最右边的方案。
    也就是说此时后手的最优策略一定不劣于先手一开始取最左边/最右边的方案,这对先手而言不利,所以先手绝对不会让出主动权。

    那么这意味着如果先手选择第二类情况,那么选择的那个 x 的左边/右边都应有偶数个数。
    而 N 为偶数时这是不可能办到的,即 N 为偶数时先手一开始只能选择最左边/最右边。

    我们接下来考虑 N 为奇数。
    此时流程变为:"先手取走一个偶数位置的数" -> "后手选择左边/右边,先手取走偶数位置的数,后手取走奇数位置的数" -> "迭代到区间的子问题" -> ... -> "先手取走区间内奇数位置的数(取走最左边/最右边的数),后手取走偶数位置的数"。

    假如最后奇数位置上的数在区间 [l, r] 内,那么先手取走的数应是 "[1...l-1] 中的偶数位置" + "[l...r] 中的奇数位置" + "[r+1...N] 的偶数位置"。
    不妨看成先选择全部偶数位置的数,然后选择一个区间 [l, r] 将其奇偶位置的选择状况反转。
    先手需要最大化 [l, r] 中奇数位置的和 - 偶数位置的和,可以通过一些处理写成前缀和 s[r] - s[l-1] 的形式。

    考虑一下二分答案:假如 s[r] - s[l-1] >= x,我们就可以知道哪些区间是合法。
    可以通过手玩发现如果有些合法区间首尾相接,即存在 [l1, r1], [l2, r2], ..., [lk, rk] 使得 (r_i + 1 = l_{i+1} - 1),那么先手就可以最终落到某一个区间中,视为检验成功
    归纳法即可证。

    怎么快速检验呢?可以使用 dp。记 dp[i](此处默认 i 为偶数)表示 [1, i] 是否能划分成若干合法区间,则枚举 j < i 且 dp[j] 为真,如果有 s[i-1] - s[j] >= x 即可转移。
    我们可以维护 dp[j] 为真的 min{s[j]} 来方便实现转移,这样一来甚至连 dp 都不存下来。

    @accepted code@

    #include <cstdio>
    #include <algorithm>
    using namespace std;
    
    const int MAXN = 300000;
    const int INF = (1 << 30);
    
    int s[2], sum[MAXN + 5], a[MAXN + 5], N;
    bool check(int x) {
    	int k = 0;
    	for(int i=2;i<=N;i+=2) {
    		if( sum[i-1] - k >= x )
    			k = min(k, sum[i]);
    	}
    	return sum[N] - k >= x;
    }
    
    int main() {
    	scanf("%d", &N);
    	for(int i=1;i<=N;i++)
    		scanf("%d", &a[i]), s[i & 1] += a[i];
    	if( N % 2 == 0 )
    		printf("%d %d
    ", max(s[0], s[1]), min(s[0], s[1]));
    	else {
    		for(int i=1;i<=N;i++) sum[i] = sum[i-1] + (i & 1 ? a[i] : -a[i]);
    		int le = 0, ri = s[0] + s[1];
    		while( le < ri ) {
    			int mid = (le + ri + 1) >> 1;
    			if( check(mid) ) le = mid;
    			else ri = mid - 1;
    		}
    		printf("%d %d
    ", s[0] + le, s[1] - le);
    	}
    }
    

    @details@

    我才不会说我一开始想到了二分答案随后就把它叉掉了。
    最后写了个神奇的线段树做法,发现过不了,然后把它叉掉过后又想起了二分答案其实可以做。

    感觉脑子需要修理一下,最近短路的现象太频繁了。

  • 相关阅读:
    jsp-servlet(2)响应HTML文档-书籍管理系统
    jsp-servlet(1)环境搭建(Tomcat和myeclipse)和基本概念
    MySQL(2)数据库 表的查询操作
    MySQL(1) 基本操作(MySQL的启动,表的创建,查询表的结构和表的字段的修改)
    Java 构造器Constructor 继承
    数据结构:链表的操作
    C++指针易错点梳理
    C++操作符重载
    无重复字符的最长子串-LeetCode-第3题-C++
    计算几何模板存储
  • 原文地址:https://www.cnblogs.com/Tiw-Air-OAO/p/12158108.html
Copyright © 2011-2022 走看看