LG3777 [APIO2017] 考拉的游戏 详细题解

LG3777 [APIO2017] 考拉的游戏

本题解提供了一种子任务 \(3\) 的不同做法,并将子任务 \(4\),子任务 \(5\) 做法统一了起来,分别使用询问 \(84\) 次和 \(71\) 次,子任务 \(3\) 期望询问 \(1.75\) 次。


一些基本的观察

  1. 研究下发的 grader.cpp\(R\) 数组的给出算法,发现如果 Koala 想要选择某个物品 \(i\),他只会放 \(R_i=B_i+1\) 个石子在旁边,尽管他可能可以放更多石子。
  2. 假设你为物品 \(i,j\) 分配了 \(B_i=B_j\) 即相同的石子数,但 Koala 最后只为 \(i\) 物品放石子,那么显然 \(P_i>P_j\)。这一条是后面区分序列 \(P\) 的核心。
  3. 石子权重 \(P\) 相差不大时且 \(B\) 较小时,Koala 倾向多选 \(B_i\) 较小的石子,即使 \(P_i\) 也较小,例如我们有三块石头 \(P=\{60,50,40\},B=\{1,0,0\},W=2\),koala 会选择后两块石头而不是第一块最大的石头。为了减少这种模糊的选择倾向,我们在分配石子时尽量减少 \(B\) 数值的种数。

SubTask1

实现函数 minValue(N,W),返回权值最小的物品的标号 \(i\),即 \(P_i=1\),单个函数最多调用 \(2\) 次询问。\(N=W=100\)

如果存在一种查询方式,能够让 Koala 任意从 \(100\) 个物品中最多任选 \(99\) 个物品,那么没选的那个物品权值最小。怎么做,我们给任意一个物品 \(B_i:=1\),其余 \(B_i:=0\),即可。没选的物品 \(R_i=0\)

int minValue(int N, int W) {
    for(int i=0;i<=N;++i)a[i]=0;
    a[0]=1;
    playRound(a,b);
    for(int i=0;i<N;++i){
        if(b[i]==0)
            return i;
    }
    return 0;
}

SubTask2

实现函数 maxValue(N,W),返回权值最大的物品的标号 \(i\),即 \(P_i=N\),单个函数最多调用 \(4\) 次询问。\(N=W=100\)

可以说是本题的核心了,后面 \(3\) 问我都使用此子问题的基本方法。

我们先将所有 \(B_i:=1\),这样 Koala 每次选物品必须花费 \(2\) 个石子,结果就是我们可以知道较大的 \(50\) 个物品和较小的 \(50\) 个物品共两个集合。

现在回顾观察 \(2\),我们想要找较大的 \(50\) 个物品中较大的一些物品,怎么做?首先需要赋予这些权值较大的物品相同的石子数,然后查询 Koala 选择的情况,放了石子的就是这 \(50\) 个权值大的物品中更大的。

放多少个石子呢?感性上权值较大集合的物品上越多越好,其余集合的物品上越少越好(不放石子),只有这样 Koala 在选择的时候才会尽量少选这较大的集合中的元素,达到最少次数询问出最大元素的要求。

这样我们的实现就呼之欲出了,假设目前找到了最大的 \(num\) 个物品,那么我们给这些物品分配 \(\lfloor W/num\rfloor\) 个石子,其余物品分配 \(0\) 个物品,进行查询,将这 \(num\) 物品中 Koala 选择的物品作为新的一个权值更大物品集合,这个集合大小作为新的 \(num\)。直到只剩 \(1\) 个物品,就是我们找的最大物品。实现如下

int maxValue(int N, int W) {
    for(int i=0;i<N;++i)tag[i]=0;
    int num=N;
    for(int t=1;num>1;t=W/num){
        for(int i=0;i<=N;++i){
            if(tag[i])a[i]=0;
            else a[i]=t;
        }
        playRound(a,b);
        for(int i=0;i<N;++i){
            if(b[i]<t+1){
                if(!tag[i])--num;
                tag[i]=1;
            }
        }
    }
    for(int i=0;i<N;++i)
        if(!tag[i])return i;
    return 114514;
}

看起来很通用,但他并不是万能的,比如 \(N=W=10\) 的情况,当然这是后话了。

SubTask3

实现函数 greaterValue(N,W),返回物品 \(0\) 和物品 \(1\) 中权值较大物品的编号,可以理解成比较两个元素大小。单个函数最多调用 \(3\) 次询问,\(N=W=100\)

直接讲我的神秘做法吧,反正我看到 \(3\) 次询问就直接想分类讨论了。

核心思路就是先找一些 Koala 必须选的数,使得选完这些数之后,最后石子数只够 Koala 在物品 \(0\) 和物品 \(1\) 中选择一个,看他选了那个即可,同时需要保证同时选物品 \(0\) 和物品 \(1\) 一定更劣。

首先模仿 SubTask2,找出较大的 \(50\) 个数和较小的 \(50\) 个数共两个集合,如果两物品分属两个集合,可直接判断大小。

为方便表述,设两物品标号 \(x,y\),权值 \(P_x,P_y\),然后下面废话有点多,可以直接看代码。

  • 如果两物品权值均 \(\leq 50\),首先我们令所有 \(B_i:=1\),然后找到任意一个 \(P_i>50\) 的物品,进行 \(B_i:=0\)。此时 Koala 会花费 \(99\) 个石子,选择出最大的 \(50\) 个物品。然后我们将 \(B_x:=0,B_y:=0\),Koala 别无选择,只能从这俩物品中选个大的。由于一个 \(P_i>50\) 的物品 \(B_i=0\) ,所以 Koala 不会同时选物品 \(x,y\)。花费一个询问。
  • 如果两物品权值均 \(> 50\),我们还是先令所有 \(B_i=1\),由于我们需要在 \(x,y\) 中只选一个物品,并且他们本来选的倾向就很大,所以要把他们权值变大 \(B_x:=2,B_y:=2\)。只这样不行,因为 Koala 可以花费 \(2\times2\) 个石子选择 \(P=50,49\) 两物品。为此,我们任选一个 \(P_i>50\) 的物品 \(i\neq x,y\),执行 \(B_i:=0\),那么 Koala 现在会选择 \((P=50),i,x/y\) 三个物品,Koala 花费 \(2+1+3\) 个石子。 但这样还不行,因为一开始使用了 \(101\) 个石子,为了不影响最后的选取,找到 \(P=1\) 的物品,将其 \(B:=0\),这样既节省了 \(1\) 块石头,又避免了这个省出来的物品被选取。花费两个询问。
int greaterValue(int N, int W) {
	int loc0=minValue(N,W);//只在一种情况中出现,故这里调用次数可以优化
	for(int i=0;i<N;++i)tag[i]=0;
	for(int i=0;i<N;++i)a[i]=1;
	playRound(a,b);
	for(int i=0;i<N;++i){
		if(b[i]==0)tag[i]=0;
		if(b[i]==2)tag[i]=1;
	}
	if(loc0<=1)return loc0^1;//最小值在这两个物品中间,也可以判断
	if(tag[0]!=tag[1])return tag[0]<tag[1];

	for(int i=0;i<N;++i)a[i]=1;//两种情况共同的代码
	for(int i=2;i<N;++i){
		if(tag[i]){
			a[i]=0;
			break;
		}
	}
	if(tag[0]==0){
		a[0]=a[1]=0;
	}
	if(tag[0]==1){
		a[0]=a[1]=2;
		a[loc0]=0;
	}
	playRound(a,b);
	return b[0]<b[1];
}

SubTask5

实现函数 allValues(N,W,P),确定整个排列,将权值排列 \(P\) 存放在数组中,单个函数最多调用 \(100\) 次询问,\(N=W=100\)

我们现在能用 \(4\) 次询问找到最大值,同时前 \(3\) 次询问并没有浪费。假设我们某次询问知道了最大的 \(k\) 个物品有哪些,那么我们在日后如果查询出来了最大的 \(k-1\) 个物品,第 \(k\) 大的物品也就呼之欲出。 换句话说,假如我们第 \(j\) 次查询都能知道最大的 \(k_j\) 个物品有哪些(同时 \(k\) 互不相同),我们在未来的某个时刻总会知道第 \(k_j\) 个物品是那个,也就知道了排列。

则我们从大到小找物品,找到最大物品位置 \(l_{100}\) 后,在以后的询问中恒设置 \(B_{l_{100}}:=0\),那么这个最大物品必选,koala 还可以使用 \(99\) 个石头选其他物品,那么我们故意少放,也只在剩下 \(99\) 个物品上至多放 \(99\) 个石头,则转化成了一个更小的几乎相同的子问题,似乎我们至多用 \(4\times 100\) 次询问就能解决问题了(毕竟找 \(99\) 个物品中最大的花费的次数应该不会大于 \(100\) 个物品吧)。

为什么说几乎相同,还记得 SubTask2 在结尾说的吗,Task2 的做法并不能保证所有 \(N\) 都有解。比如 \(N=10,num=2\) 时,\(2\) 个大物品 \(B=5\),其余物品 \(B=0\),koala 会选择所有 \(B=0\) 的权值较小的物品。为了避免这种情况,我们需要缓慢的增加这 \(num\) 个物品每个所分配的石头的数量 \(t\),避免出现不选的情况。这样做尽管我们无法像之前一样 \(4\) 次得出最大值,但只要我们每次都能得到一个全新\(k\),表示最大的 \(k\) 个物品有哪些,还是能在 \(N\) 次内得出答案。

假设我们能预知\(n\) 个物品,\(m\) 个较大物品已知,每个较大物品分配 \(t\) 个石子,\((n-m)\) 个较小物品不分配石子,koala 针对这个情况共分配 \(w\) 个他的石子,最后 koala 会在 \(m\) 个大物品中选择了 \(k\) 个。如果这个函数 \(f(n,m,t,w)=k\) 与先前的 \(k\) 重复,那么我们此次无需真的分配石子,就能得到这 \(k\) 个物品是哪些(先前已经询问出来了),此次询问跳过。

剩下的就是实现问题,关于函数 \(f(n,m,t,w)\),直接使用 \(O(n)\) 暴力计算即可。先前已经得到的 \(k\) 只需要使用一个 set 即可;然后需要维护一个标记数组,第 \(j\) 次得出最大的 \(k_j\) 个物品时,将这 \(k_j\) 个物品的标记全部 \(+1\),那么假如某个物品的标记与其他物品均不相同,则我们就已经得出了这个物品是第几个。

至此我们已经可以使用 \(99\) 次询问求解问题,但是如果我们利用一下没利用的信息,可以达到 \(71\) 次。我们查询时还能够知道较小的 \((n-m)\) 个物品中较大的若干个物品,这些物品被 koala 分配了 \(1\) 个石子,假如这些元素有 \(k_2\) 个,那么我们就能知道最大的 \(N-(n-m-k_2)\) 个元素有哪些。这样一次询问就有可能在未来得到两个物品的排名。

具体实现见代码。

int predict(int n,int m,int t,int w){//题解中 f 函数
	int ans=(n-m+1)*(n-m)/2,mxl=0;
	for(int i=1;i<=m&&i*(t+1)<=w;++i){
		int tmp=(n+n-i+1)*i/2,num=std::min(w-(t+1)*i,n-m);
		if(num)tmp+=(n-m+n-m-num+1)*num/2;
		if(tmp>ans)ans=tmp,mxl=i;
	}
	return mxl;
}

std::set<int>si;
std::set<std::pair<int,int>>lis;
void allValues1(int N,int W,int *P,int flag){//flag=0,case=5; flag=1,case=4;
	for(int i=0;i<N;++i)P[i]=0;
	for(int i=0;i<N;++i)tag[i]=0,lis.insert({tag[i],i});
	si.insert(0);
	for(int x=N;x>=1;--x){
		int num=x,t=1+flag;
		if(si.find(x-1)!=si.end())num=1;
		while(num!=1){
			int exp=predict(x,num,t,x*(1+flag)),cnt=0,tot2=0,tot1=0;
			while(num==exp)++t,exp=predict(x,num,t,x*(1+flag));
			if(si.find(x-exp)!=si.end()){
				num=exp;continue;
			}
			for(auto [val,i]:lis){
				++cnt;
				if(cnt<=x-num)a[i]=0;
				else a[i]=t;
			}
			playRound(a,b);
			lis.clear();
			for(int i=0;i<N;++i){
				if(P[i]){tag[i]+=2;continue;}
				if(a[i]==t)++tag[i];
				if(b[i]>=t+1){// a[i]=t && b[i]>=t+1
					++tag[i];
					++tot2;
				}else if(b[i]>=1){// a[i]=0
					++tag[i];
					++tot1;
				}
				lis.insert({tag[i],i});
			}
			si.insert(x-tot2);
			si.insert(x-num-tot1);
			num=tot2;
		}
		int id=lis.rbegin()->second;
		P[id]=x,a[id]=0+flag;
		lis.erase(--lis.end());
	}
}

void allValues(int N, int W, int *P) {
	printf("%d\n",predict(100,66,2,200));
	if (W == 2*N) {
		allValues1(N,W,P,1);
	} else {
		allValues1(N,W,P,0);
	}
}

SubTask4

实现函数 greaterValue(N,W),返回物品 \(0\) 和物品 \(1\) 中权值较大物品的编号,可以理解成比较两个元素大小。单个函数最多调用 \(3\) 次询问,\(N=100,W=200\)

一开始进行分配 \(f(100,100,2,200)=66\),就可以得到 \(66\) 个较大的物品,只要我们像之前一样不断改变分配石子的数量,最后依然可以得到最大的元素,以及在这个过程中较大的 \(k\) 个元素有哪些。

最大的物品位置 \(l_{100}\)找到后,设置 \(B_{l_{100}}=1\),那么 koala 必须花费 \(2\) 个石子选他(看似与观察 \(3\) 矛盾,但是要联系 \(f\) 函数的过程),余 \(198\) 石子,剩下的 \(99\) 个物品我们也只分配 \(198\) 个石子,即可缩小子问题。

因此只需要改变 SubTask5 中的部分参数即可,具体见上方代码。

后记

花费了很长时间写这样一篇废话很多还算详细以及做法较为不同的题解,虽然肯定没啥人看,还是希望多多资瓷本菜鸡AP。

posted @ 2025-12-12 23:55  BigSmall_En  阅读(0)  评论(0)    收藏  举报