zoukankan      html  css  js  c++  java
  • [JZOJ4684] 【GDOI2017模拟8.11】卡牌游戏

    题目

    描述

    在这里插入图片描述

    题目大意

    112n2n牌,一开始分别给两个人,每人nn张。
    轮流出牌,给出对手出牌的顺序,若自己的牌更大,就记一分。
    在中间的某个时刻可以改变游戏规则。
    问最大的分数。


    思考历程

    显然,一定是把大的放分界点左边,把小的放右边。
    那可以枚举分界点,两边分别计算就可以了。时间复杂度为O(n2)O(n^2),朴素的暴力算法。
    接下来我就想有没有什么数据结构可以在枚举分界点的时候维护左右两边的答案。
    想不出……
    换种简单的思路。
    当分界点从左向右移动的时候,左边的答案会增,右边的答案回减。
    于是我就天真地想到了三分(如果画成函数图像就是一座山峰),打了个三分上去。
    最终沮丧地发现三分是错的,不过还好,可以水到50分。
    但实际上这并不是一座山峰,而是连绵起伏,没有什么规律……
    莫名其妙地想到模拟退火,可不可以用它来AC这题?


    正解

    其实这题的解法比较多样,先说题解做法(我AC的做法):
    枚举分界点,用数据结构维护左右两边的答案。
    如何维护呢?
    现在我们只考虑左边,右边的可以分开处理,方法是一模一样的。
    我们现在有个大小为2n2n的桶,枚举到某个分界点时,这个桶里面有一些点有值,表示这个点存在。这个值还表示它是敌方还是我方。
    接下来敌方点要分别在右边匹配我方点,使得匹配数最大。
    然后就有一个比较粗暴的思路:可以考虑匹配最近的点。
    用个线段树来维护,对于每个节点,记录当前区间内的我方点和敌方点的个数。
    合并区间的时候,用左区间的敌方点配对右区间的我方点(尽量配对),加入答案,然后将剩余的加在一起。
    这样就使得近的点先匹配到一起。
    这个思路是正确的。不妨想想,对于每个敌方点,它们只能匹配在右边的我方点。显然从右到左,它们可以选择的集合的大小是递增的。既然要让匹配数最大,就应当尽量让左边的点在它的集合以内,并且右边的点的集合以外的,这样就不会影响右边的点的选择。所以匹配的最好方式是选择最近的点。
    但合并的过程是从小区间到大区间,顺序是乱的,有没有可能出问题呢?出问题的原因在于两个点右边最近的点重合了。如果这样,就其中一个选点,然后另一个会再往后面找。我们不需要关心到底是哪个先选点,因为这样是等价的,我们没有必要关心这些。
    有了这个数据结构之后扫一遍,记录答案就可以做出来了,时间复杂度O(nlgn)O(nlg n)

    还有一个DYP发明的不同算法。主要思路是在桶中枚举,然后用数据结构来修改答案(把答案看成一个序列)。
    先把左右两边分开计算(一下以左边为准)。开一个2n2n大小的桶。
    根据贪心策略,从右到左,然后能匹配就匹配的方案一定是最优的。
    从大到小在桶中扫,用一个变量来记录扫到的我方的数量。
    遇见一个敌方的点,就开始搞事情:它可能会匹配右边的点。如果右边有点,那就可以匹配,它出现的时间那里的答案加一。如果右边的点,那就不可以匹配,就不加一。
    我们维护的是整个答案序列。显然答案序列是递增的。所以可以在数据结构中二分出一个尽量后的地方,使得它的答案小于我方的数量,这就意味着在它后面时间中,那右边的点被占满了。所以就将它出现的时间和二分出来的时间形成的这段区间加一。
    做完这些后就记录左边和右边的答案,然后合并。在之前我们可以记录当前的这个答案用到的最右(最左)的我方点位置,表示选取我方点的区间。合并的时候,如果区间重合,那鱼与熊掌不可得兼,减去重合部分就是真正的答案了。


    代码

    题解方法

    using namespace std;
    #include <cstdio>
    #include <cstring>
    #include <algorithm>
    #define N 50010
    int n;
    int a[N],b[N*2];
    bool flag[N*2];
    struct Node{
    	int p,d,w;
    } d[N*8];
    inline void update1(int k){
    	int newp=min(d[k<<1].w,d[k<<1|1].d);//两边的最大匹配数
    	d[k]={d[k<<1].p+d[k<<1|1].p+newp,d[k<<1].d+d[k<<1|1].d-newp,d[k<<1].w+d[k<<1|1].w-newp};
    }
    inline void add1(int k,int l,int r,int x){
    	if (l==r){
    		if (flag[x])
    			d[k]={0,0,1};
    		else
    			d[k]={0,1,0};
    		return;
    	}
    	int mid=l+r>>1;
    	if (x<=mid)
    		add1(k<<1,l,mid,x);
    	else
    		add1(k<<1|1,mid+1,r,x);
    	update1(k);
    }
    inline void update2(int k){
    	int newp=min(d[k<<1].d,d[k<<1|1].w);
    	d[k]={d[k<<1].p+d[k<<1|1].p+newp,d[k<<1].d+d[k<<1|1].d-newp,d[k<<1].w+d[k<<1|1].w-newp};
    }
    inline void add2(int k,int l,int r,int x){
    	if (l==r){
    		if (flag[x])
    			d[k]={0,0,1};
    		else
    			d[k]={0,1,0};
    		return;
    	}
    	int mid=l+r>>1;
    	if (x<=mid)
    		add2(k<<1,l,mid,x);
    	else
    		add2(k<<1|1,mid+1,r,x);
    	update2(k);
    }
    int ans1[N],ans2[N];
    int main(){
    	scanf("%d",&n);
    	for (int i=1;i<=n;++i)
    		scanf("%d",&a[i]),flag[a[i]]=1;
    	for (int i=1,j=0;i<=n*2;++i)
    		if (!flag[i])
    			b[++j]=i;
    	for (int i=0;i<n;++i){
    		ans1[i]=d[1].p;
    		add1(1,1,n<<1,a[i+1]);
    		add1(1,1,n<<1,b[n-i]);
    	}
    	ans1[n]=d[1].p;
    	memset(d,0,sizeof d);
    	for (int i=n;i>0;--i){
    		ans2[i]=d[1].p;
    		add2(1,1,n<<1,a[i]);
    		add2(1,1,n<<1,b[n-i+1]);
    	}
    	ans2[0]=d[1].p;
    	int ANS=0;
    	for (int i=0;i<=n;++i)
    		ANS=max(ANS,ans1[i]+ans2[i]);
    	printf("%d
    ",ANS);
    	return 0;
    }
    

    总结

    其实这是一个分治思路,只是用数据结构来动态实现罢了。
    所以还是可以往分治方面想……

  • 相关阅读:
    04邻接表深度和广度遍历DFS_BFS
    03邻接矩阵的深度和广度遍历的C语言实现
    02邻接表创建的C语言实现
    01邻接矩阵的创建C语言实现
    GUN的相关使用
    JAVA学习笔记
    排序的C语言实现
    线索二叉树的C语言实现
    maven配置logback
    多线程概念
  • 原文地址:https://www.cnblogs.com/jz-597/p/11145222.html
Copyright © 2011-2022 走看看