@loj - 2985@ 「WC2019」I 君的商店


@description@

V 君、I 君和 Y 君是好朋友。

I 君最近开了一家商店,商店里准备了 N 种物品(编号为 0~N-1 中的整数),每种物品均有无限个可供出售,每种物品的单价是 0 或者 1。

V 君想知道每个物品的价格,他已经通过某种超自然力量知道,这 N 个物品里,价格是 1 的物品恰好有奇数/偶数个,且至少存在一个物品的价格是 1

然而, V 君并不想自己去问 I 君,他选择了这样一种方法:他准备了 +∞ 的钱给 Y 君。然后让 Y 君帮他跑腿:每一次,他会给 Y 君指定两个非空物品集合 \(S, T\)(同一个集合内的物品必须两两不同,即每种物品在每个集合类最多包含一个),Y 君会跑到商店,分别买下这两个集合的物品,把他们送回来,并告诉 V 君哪个集合的物品价格之和更高。但是,当两集合价格之和相等的时候,Y 君会按照 I 君的指示来回答 V 君。

带着很多物品跑腿是一个很累的事情,因此,我们定义一次跑腿的体力消耗是 S + T。其中,S 表示集合 \(S\) 包含的物品个数。

你的任务是:写一个程序,帮助 V 君决定如何合理地让 Y 君跑腿,从而推算出每种物品的价值。Y 君的体力有限,你当然不能让他过于劳 累,也即,你不能让他的总体力消耗超过某个预设的阈值 M。

实现细节
你不需要,也不应该实现主函数,你只需要实现下列函数:

find_price(task_id, N, K, ans)
其中 task_id 表示子任务编号(见限制与约定)。N 表示物品个数,K 的意义为:
若 K=0,表示有偶数个物品价值为 1;
若 K=1,表示有奇数个物品价值为 1。
你需要将计算出的物品价格放在数组 ans[] 中,其中 ans[i] 表示编号为 i 的物品的价格。

你可以通过调用如下函数来向交互库发出询问:

query(S, nS, T, nT)
这里 nS=S,nT=T, 数组 S [0…(nS − 1)] 和数组 T [0…(nT − 1)] 分别描述两个集合,你需要保证:
nS, nT>0;
∀0≤i<nS,0≤S[i]<N;
∀0≤i<nT,0≤T[i]<N;
调用此函数一次的时间复杂度为 Θ(nS + nT)。它的返回值为 0 或 1,返回值的意义为:
若集合 S 的物品价格和更大,返回 0;
若集合 T 的物品价格和更大,返回 1;
否则,按照某种未知规则返回 0 或 1。
如题面所述,我们定义这样一次调用的代价为 nS+ nT。

评测时,交互库可能会调用 find_price 多次(不超过 10 次),每次调用代表一次新的猜价格游戏,所有的物品的价格都会被重新设定。

子任务
我们令代价之和的上界为 M,记答案数组为 ans[]:
子任务 1:N≤5,M=100;
子任务 2:N≤103,M=106;
子任务 3:N≤10^5,M=100,保证 ∀i<j<k,若 ans[i]=ans[k] 则必有 ans[j]=ans[i]。
子任务 4:N≤104,M=2×105;
子任务 5:N≤5×10^4,M=350100;
子任务 6:N≤10^5,M=500100。

提示
I 君可能并不愿意让 V 君知道每件物品的价格,在物品价格相等时,他会按照他自己的某种方式来回答问题。

@solution@

做交互题的第一步:首先应该先观察子任务分别的数据范围
注意到一点:子任务 3 是无论任何都无法与其他子任务合并的。
我们就从子任务 3 下手。

注意到子任务 3 要么是 00...011...1 的形式,要么是 11...100...0 的形式。
不难想到去找这个分界点。而询问次数如此小(甚至不足 N),排列形式如此具有单调性,不难想到或许可以二分。

做这样的题还有一个原则:尽量简化问题。对应到这道题就是尽量少塞数在 S 与 T 中
比如,如果令 nS = nT = 1,则可以得到 S 与 T 中的元素哪个大哪个小(但不能得到严格大于还是严格小于)。
我们可以通过一次 nS = nT = 1 的操作,比较序列开头与末尾,将子任务 3 统一转为 00...011...1 的形式。

注意到题目中提到序列中必定有一个 1,故我们的序列中最后一个元素一定是 1。
此时我们发现如果继续 nS = nT = 1 的操作,我们可能无法得到任何有用信息。我们就稍微扩大一点:令 nS = 2, nT = 1。
我们取相邻的两个数 a, b,则 a <= b。如果 a + b <= 1,则有 a 以及 a 之前的都为 0;否则如果 a + b >= 1,则有 b 以及 b 之后的都为 1。
因此我们就不断二分,取相邻的两个数 a, b 与最后那个 1 比较。
注意二分到最后可能剩一个无法判断,需要通过奇偶性来搞。

解决完子任务 3,我们再来看看其他子任务。
二分与什么最相配?排序。我们可以通过 nS = nT 的操作将这个序列 sort 一遍,再进行二分。
这样理论上能过前 4 个子任务,但实测好像第 4 个过不了。

看子任务 5,M = 7N + 100。后面那个 100 肯定是用来二分的,我们只需要考虑怎么能够在 7N 的次数内进行排序。
当 nS = nT = 1 时,是基于比较的排序,复杂度下限是 O(nlogn)。我们再次使用 nS = 2, nT = 1。
上面二分时使用的结论这里还可以用:当 a <= b 时,如果 a + b <= 1,则有 a 为 0;否则如果 a + b >= 1,则有 b 为 1。
因此我们先花 2*N 找到一个 1(找最大值),然后每次花 2 得到 a <= b 还是 a >= b,再花 3 得到 a + b 与 1 的关系。
这样就可以花 5 次确定 1 个数是 0 还是 1,最后剩下单独一个数无法判断,直接塞到 0 与 1 之间,然后二分。

看子任务 6,M = 5N + 100。
注意到我们子任务 5 中几乎就可以确定整个序列了,二分的作用微乎其微。
而子任务中 7N = 5N + 2N,2N 是找 1 的复杂度。我们是否可以省掉找 1 的过程?或者说,我们可以动态去找 1,边找 1 边排序?
如果 a + b <= c 且 a <= b,不管 c 是不是 1,一定有 a = 0。
如果 a + b >= c 且 a <= b,不管 c 是不是 1,一定有 b >= c。b 更有可能成为 1,因此接下来的比较中不等式右边就放 b。
由于 c 是之前的最大值,所以最终排好序后 c 一定可以在 b 的前一个(也就是说 c,b 相邻)。我们就确定了 c 与 b 的相对位置。

这样下来,我们得到了若干 = 0 的,和一条有序的链(互相之间确定了相对位置的数构成的链),和单独剩一个无法判断。
我们对这条链二分,将链分为若干 = 0 的,若干 = 1 的,一个无法判断的。
剩下两个无法判断的再根据奇偶性以及几次比较就可以判断出来了。

@accepted code@

#include<cstdio>
#include "shop.h"
#include<vector>
#include<algorithm>
using namespace std; 
const int MAXN = 100000;
int num[MAXN + 5];
int S[MAXN + 5], T[MAXN + 5];
void solve(int N, int K, int ans[]) {
	ans[num[N - 1]] = 1;
	int L = 0, R = N - 1;
	while( L + 1 < R ) {
		int mid = (L + R + 1) >> 1;
		S[0] = num[mid], S[1] = num[mid - 1], T[0] = num[N - 1];
		if( !query(S, 2, T, 1) ) {
			for(int i=mid;i<R;i++)
				ans[num[i]] = 1;
			R = mid;
		}
		else {
			for(int i=L;i<mid;i++)
				ans[num[i]] = 0;
			L = mid;
		}
	}
	if( L != R ) {
		if( (N - R) % 2 == K )
			ans[num[L]] = 0;
		else ans[num[L]] = 1;
	}
}
bool cmp(int a, int b) {
	S[0] = a, T[0] = b;
	return query(S, 1, T, 1);
}
vector<int>v;
void find_price(int task_id, int N, int K, int ans[]) {
	if( task_id == 3 ) {
		for(int i=0;i<N;i++)
			num[i] = i;
		S[0] = 0, T[0] = N - 1;
		if( !query(S, 1, T, 1) )
			reverse(num, num + N);
		solve(N, K, ans);
	}
	else if( task_id <= 3 ) {
		for(int i=0;i<N;i++)
			num[i] = i;
		sort(num, num + N, cmp);
		solve(N, K, ans);
	}
	else if( task_id <= 5 ) {
		int lp = 0, rp = N - 1, mx = 0;
		for(int i=0;i<N;i++)
			if( cmp(mx, i) ) mx = i;
		num[rp--] = mx;
		int p, q;
		for(p=0,q=N-1;p<q;) {
			if( p == mx ) p++;
			else if( q == mx ) q--;
			else {
				S[0] = p, S[1] = q, T[0] = mx;
				if( query(S, 2, T, 1) ) {
					if( cmp(p, q) )
						num[lp++] = p++;
					else num[lp++] = q--;
				}
				else {
					if( cmp(p, q) )
						num[rp--] = q--;
					else num[rp--] = p++;
				}
			}
		}
		num[lp++] = p;
		solve(N, K, ans);
	}
	else {
		v.clear();
		int p, q, lp = 0, mx = N - 1; v.push_back(N - 1);
		for(p=0,q=N-2;p<q;) {
			if( p == mx ) p++;
			else if( q == mx ) q--;
			else {
				S[0] = p, S[1] = q, T[0] = mx;
				if( query(S, 2, T, 1) ) {
					if( cmp(p, q) )
						num[lp++] = p++;
					else num[lp++] = q--;
				}
				else {
					if( cmp(p, q) )
						v.push_back(q), mx = q--;
					else v.push_back(p), mx = p++;
				}
			}
		}
		int rp = N - 1;
		for(int i=v.size()-1;i>=0;i--)
			num[rp--] = v[i];
		ans[num[N - 1]] = 1;
		int L = rp + 1, R = N - 1;
		while( L + 1 < R ) {
			int mid = (L + R + 1) >> 1;
			S[0] = num[mid], S[1] = num[mid - 1], T[0] = num[N - 1];
			if( !query(S, 2, T, 1) ) {
				for(int i=mid;i<R;i++)
					ans[num[i]] = 1;
				R = mid;
			}
			else {
				for(int i=L;i<mid;i++)
					ans[num[i]] = 0;
				L = mid;
			}
		}// rp + 1 ~ L - 1 : 0
		// R ~ N - 1 : 1
		if( L != R ) {
			if( (N - R) % 2 == K ) {
				S[0] = num[L], S[1] = p, T[0] = num[N - 1];
				if( query(S, 2, T, 1) ) // S <= T
					ans[num[L]] = ans[p] = 0;
				else ans[num[L]] = ans[p] = 1;
			}
			else {
				S[0] = num[L], T[0] = p;
				if( query(S, 1, T, 1) ) // S <= T
					ans[num[L]] = 0, ans[p] = 1;
				else ans[num[L]] = 1, ans[p] = 0;
			}
		}
		else {
			if( (N - R) % 2 == K )
				ans[p] = 0;
			else ans[p] = 1;
		}
	}
}

@details@

考场上想到二分,没想到排序,错失金牌,GG。

NOI 之前想写一下这道题练练手,结果没写出来,注定了我 NOI 只能打银的结局,GG。

现在总算是根据当时讲题的记忆勉强写了出来。。。

posted @ 2019-10-21 11:45  Tiw_Air_OAO  阅读(268)  评论(0编辑  收藏  举报