APIO 2017 考拉的游戏 题解

题目传送门

题目大意: 交互题,太长了不想写了qwq。

题解

task1

随便在一个物品旁放一个石子就好了,如果这个物品是最小的那么肯定会被考拉舍弃掉,否则一定把最小的物品的那个石子拿过来然后在这个物品旁放两个,那么只需要看其他物品中哪个没有石子就是最小的。

int minValue(int n, int w) {
	memset(B,0,sizeof(B));B[0]=1;
	playRound(B,R);
	if(R[0]<2)return 0;
    for(int i=1;i<n;i++)
    	if(!R[i])return i;
}
task2

注意到假如往每个物品旁都放一个石子,那么最后考拉一定会选择其中较大的一半物品,在每个物品旁放两个石子。

这种操作可以缩小最大值所在的区间,这启示我们用类似二分的操作不断缩小来找到它。

事实上,你只需要恰好四次就可以找到最大值,每一次如下:

  1. 区间长度为 100 100 100,每个物品旁放 1 1 1 个石子。
  2. 区间长度为 50 50 50,每个物品旁放 2 2 2 个石子。
  3. 区间长度为 25 25 25,每个物品旁放 4 4 4 个石子。
  4. 区间长度为 9 9 9,每个物品旁放 11 11 11 个石子。
  5. 找到最大值。

正确性的话只需要列一列不等式计算一下每次放了石子的物品中最大的几个会被考拉取走。最后区间长度为 9 9 9 然后能恰好找到最大值真的让我感觉很巧合……也许是出题人故意设计的吧。

代码如下:

int maxValue(int n, int w) {
	memset(B,0,sizeof(B));
    for(int i=0;i<n;i++)B[i]=1;
    
    playRound(B,R);
    memset(B,0,sizeof(B));
    for(int i=0;i<n;i++)
		if(R[i]>1)B[i]=2;
	playRound(B,R);
	memset(B,0,sizeof(B));
    for(int i=0;i<n;i++)
		if(R[i]>2)B[i]=4;
	playRound(B,R);
	memset(B,0,sizeof(B));
    for(int i=0;i<n;i++)
		if(R[i]>4)B[i]=11;
	playRound(B,R);
    for(int i=0;i<n;i++)
		if(R[i]>11)return i;
}
task3

task2对task3其实是有启示作用的,在task2中我们每次会选择一个石子数,然后在当前区间内的每个物品旁放这个数量的石子,这样就可以区分出区间内较大权值的物品和较小权值的物品。

于是我们猜想,一定存在一个 x x x 能使得在 0 , 1 0,1 0,1 号物品旁放 x x x 个石子,然后考拉只选择了其中一个物品(也就是较大的),那么此时我们就确定了他们间的大小关系。

显然 x ≤ 14 x\leq 14 x14,假如 x = 15 x=15 x=15,因为选取了其他 98 98 98 个物品后最多剩下两个石子,假如要用 x + 1 x+1 x+1 个石子取走 0 , 1 0,1 0,1 之中的一个物品,那么就必须从 98 98 98 个物品中舍弃掉 14 14 14 个物品,总价值至少为 ∑ i = 1 14 i = 105 \sum_{i=1}^{14} i=105 i=114i=105,舍弃他们显然是不优的。

考虑一手二分,于是就可以写出这样的代码:

int greaterValue(int n, int w) {
	memset(B,0,sizeof(B));
	int l=1,r=14;
	while(l<r){
		int mid=l+r>>1;
		B[0]=B[1]=mid;playRound(B,R);
		if(R[0]>mid&&R[1]<=mid)return 0;
		else if(R[0]<=mid&&R[1]>mid)return 1;
		else if(R[0]<=mid&&R[1]<=mid)r=mid-1;
		else l=mid+1;
	}
}

这时候你会问:怎么保证二分只询问 3 3 3 次?当 l = r l=r l=r 时为什么没有返回值?

这两个问题其实是互相关联的。显然当 0 , 1 0,1 0,1 两个物品的价值只相差 1 1 1 的时候是最难判断的,下面就以此为前提考虑,假设他们中较小那个物品的价值为 x x x

不妨简单打个表,当对于每个石子数,求出两个物品同时被取走时 x x x 的最大值 和 两个物品同时被舍弃时 x x x 的最小值:

石子数同时被取走时 x x x 的最大值 a a a同时被舍弃时 x x x 的最小值 b b b
1N/A4
2110
3319
4631
51045
61563
72184
828108

石子数在 8 8 8 以上就省略了,这些已经够看了。

对于每个石子数求出来的 a , b a,b a,b,同时表示着:如果往 0 , 1 0,1 0,1 上放这个数量的石子,那么当 x ∈ ( a , b ) x\in(a,b) x(a,b) 时,其中一个就会被取走,另一个会被舍弃。

对于 x x x 而言,假如一个石子数对应的 a , b a,b a,b 满足 x ∈ ( a , b ) x\in(a,b) x(a,b),那么不妨称这个石子数为 x x x答案石子数,不难发现每个 x x x 至少对应两个答案石子数。( x ∈ [ 85 , 100 ] x\in[85,100] x[85,100] 时,答案石子数显然不只有 8 8 8,比 8 8 8 大的 9 , 10 9,10 9,10 显然也是。)

也就是说,在二分的时候,我们总能在 l = r l=r l=r 之前就为 x x x 找到一个答案石子数,所以保证了二分的次数最多只有三次,并且是不存在 l = r l=r l=r 然后退出二分的情况的。

还有一个很特殊的情况: x = 1 x=1 x=1。他的答案石子数只有 1 1 1,但是简单模拟一下会发现二分到第三次时 m i d mid mid 会恰好等于 1 1 1,所以也是没问题的。

task4

一开始没注意到w=200,企图直接将task3的比较方法拿来跑个排序结果次数超了答案还是错的qaq。

事实上 w = 200 w=200 w=200 是个很好的性质,我们只需要在需要比较的两个物品上各放 100 100 100 个石子,那么考拉就一定只能选走其中一个了。

代码如下:

//利用这个函数+stable_sort排个序就做完了
//关于为什么不用sort:sort相当追求效率,在分治底层会变成插入排序,浪费大量询问,而stable_sort则是老老实实的归并
bool Cmp(int x,int y){
	memset(B,0,sizeof(B));
	B[x]=B[y]=100;playRound(B,R);
	if(R[x]>100)return 0;
	else return 1;
}
task5

上面我们反复用了一种操作:在一些物品上放若干个相同数量的石子,然后强迫考拉只能选其中一些,从而达到将物品划分为权值较小和较大的两堆的目的。

不难想到这里也是要这样做,利用这种操作做一个分治,最后只需要 99 99 99 次即可完成。

问题是每次我们要放的石子数量是多少呢?我们并不知道,只能暴力枚举,难道每次枚举完都问一次考拉吗?事实上,我们知道这个区间内所有物品的权值,只是不知道他们的顺序,顺序是不重要的,只要能划分成两堆即可。所以我们枚举出石子数后,把交互库里playRound函数的实现偷过来,自己跑一次看看能不能划成两堆就好了。

代码如下:

bool my_playRound(int l,int r,int Val){
	int cache[2][205],A[105];
    int num[2][205],i,j;
    char taken[105][205];
    for (i=0;i<205;++i) {
        cache[1][i] = 0;
        num[1][i] = 0;
    }
    int N=100,W=100;//注意要自己魔改一下
    for(i=0;i<N;i++)A[i]=i+1;
    memset(B,0,sizeof(B));
    for(i=l;i<=r;i++)B[i]=Val;
    for (i=0;i<N;++i) {
        int v = B[i]+1;
        int ii = i&1;
        int o = ii^1;
        for (j=0;j<=W;++j) {
            cache[ii][j] = cache[o][j];
            num[ii][j] = num[o][j];
            taken[i][j] = 0;
        }
        for (j=W;j>=v;--j) {
            int h = cache[o][j-v] + A[i];
            int hn = num[o][j-v] + 1;
            if (h > cache[ii][j] || (h == cache[ii][j] && hn > num[ii][j])) {
                cache[ii][j] = h;
                num[ii][j] = hn;
                taken[i][j] = 1;
            } else {
                taken[i][j] = 0;
            }
        }
    }
    int cur = W;
    for (i=N-1;i>=0;--i) {
        R[i] = taken[i][cur] ? (B[i] + 1) : 0;
        cur -= R[i];
    }
    int tot=0;
    for(i=l;i<=r;i++)tot+=(R[i]>Val);
    return tot!=0&&tot!=r-l+1;
}
void solve(int *id,int l,int r){
	if(l==r)return;
	memset(B,0,sizeof(B));
	int w;
	for(int i=1;;i++)
		if(my_playRound(l,r,i)){w=i;break;}
	memset(B,0,sizeof(B));
	for(int i=l;i<=r;i++)B[id[i]]=w;
	playRound(B,R);
	vector<int> tl,tr;
	for(int i=l;i<=r;i++)
		if(R[id[i]]>w)tr.push_back(id[i]);
		else tl.push_back(id[i]);
	int now=l;
	for(int i:tl)id[now++]=i;
	for(int i:tr)id[now++]=i;
	solve(id,l,l+tl.size()-1);solve(id,l+tl.size(),r);
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值