P5208 [WC2019] I 君的商店 题解
第一道黑题,发个题解。
很好玩的一道交互题。
题意
-
有一个长为 \(n\) 的 01 字符串,保证至少有一个 1,且已知 1 的数量的奇偶性。
-
每次可以询问两个下标集合,返回哪个下标集合中 1 的个数更多(相同则可能返回其中任意的一个)。
-
求该字符串,查询次数有限。
题解
我们约定 a,b,c
等字母表示下标,\(ans_a\) 表示 \(a\) 处的值。
Subtask 3
我们首先注意到这个特殊的 subtask 3:\(N\leq 10^5,M=100\),保证序列单调(不降或不增)。
这意味着这个序列一定是形如 0000…1111
或者 1111…0000
这样的形式。
我们很自然的想到可以二分出 0
和 1
的分界点,可是该如何二分呢?
首先虽然原题说可以让我们查询任意长度的数列,但是只有以下两种查询有用,我们把它记作 query1
和 query2
。
query1(a,b)
: 只查询a
和b
两个位置的量(query({a},1,{b},1)
) ,这样可以确定a
与b
之间的相对大小关系(若返回0
,则 \(ans_a\ge ans_b\) ; 否则 \(ans_a\le ans_b\) ) ;query2(a,b,c)
:查询两个数{a,b}
和c
的关系(query({a,b},2,{c},1)
),这样可以确定{a,b}
中有多少个 1。
既然序列保证单调并且一定至少有一个 1,那么我们就可以先进行一次 query1(0,n-1)
判断序列是单调不增还是不降(若返回 0
,那么一定单调不增(为 1111…0000
形式);否则一定单调不降(为0000…1111
形式))。为了叙述方便,我们认为它单调不增。
接下来确定了 1
的位置,我们就可以愉快的二分了!我们认为 l
是最右且必定为 1
的位置,r
为最左且必定为 0
的位置 -2(否则会死循环)。
记now
为必定为 1
的位置,进行query2(mid,mid+1,now)
,如果返回 1
,那么 \(ans_{mid},ans_{mid+1}\) 中至多有一个为 1
,又因为单调不增,所以 \(ans_{mid+1}\) 一定为 0
。如果返回 0
,那么\(ans_{mid},ans_{mid+1}\) 中至少有一个为 1
,又因为单调不增,所以 \(ans_{mid}\) 一定为 1
。
这样我们可以二分出除了最中间的分界点以外的所有点,只有 \(ans_{l+1}\) 不知道。这时通过题目给的我们的奇偶性判断即可。
二分每次消耗 3 个体力,一开始消耗 2 点,最多消耗体力值为\(2+3\times\log_2 10^5 \approx 52 < 100\) ,可以通过。
Code:(这个代码不好写,调试技巧)
#include<bit/stdc++.h>
int query(int *S, int nS, int *T, int nT);
int query1(int a, int b) {
int tmp1[1] = {a}, tmp2[1] = {b};
return query(tmp1, 1, tmp2, 1);
}
int query2(int a, int b, int c) {
int tmp1[2] = {a, b}, tmp2[1] = {c};
return query(tmp1, 2, tmp2, 1);
}
vector<int>vec;
void find_price(int task_id, int n, int k, int ans[]) {
vec.clear();
if (task_id == 3||n<=2) {
int bigger = query1(0, n - 1);
int now = bigger ? (n - 1) : 0;
ans[now] = 1;
for (int i = 0; i < n; i++) vec.push_back(i);
if (now)
reverse(vec.begin(), vec.end()); // 从大到小 111...000
int l = 0, r = n - 1;
while (l < r) { // 3*lg(1e5)=50
int mid = (l + r + 1) >> 1;
if (mid + 1 < n && query2(vec[mid], vec[mid + 1], now) ==
1) { // 最少 1 个为 1 因为单调不增,所以ansmid=1
r = mid - 1; // 最左且必定为 0 的位置-2
} else
l = mid; // l 表示最右且必定为 1 的位置
// cout<<l<<" "<<r<<endl;
}
for (int i = 0; i <= l; i++)
ans[vec[i]] = 1;
ans[vec[l + 1]] = (l&1)==k ? 0 : 1;
// for(int i=0;i<n;i++)cout<<ans[i]<<" ";
return;
}
}
Subtask 1 2 4 5
首先我们发现对于两个数 x
,y
(进行一次 query1
保证 \(ans_x\le ans_y\)),把他们与 1 比较(执行query2({x,y},2,{1},1)
),如果返回 0
,那么 \(ans_y=1\) ;否则 \(ans_x=0\) 。这样我们每次花 5 个代价就可以确定一位。
证明显然。
所以我们就可以先找到 1 个 1
, 然后执行上面的操作直到只剩 1 个数,用奇偶性判断即可。
那么该如何找到 1
呢?
我们可以考虑二分的思想,每次查询 \([l,mid]\) 和 \([mid+1,r]\) 哪边 1
多,因为至少有一个 1
,所以最后一定能找到。代价约为 \(2n\)。
总代价为 \(5n+2n=7n\)。
Subtask 6
我们可以优化找 1
的过程:具体的说,我们不需要一开始找到一个 1
, 随便找一个数 m
进行上面的操作:
对于两个数 x
,y
(进行一次 query1
保证 \(ans_x\le ans_y\)),把他们与 m
比较(执行query2({x,y},2,{m},1)
),如果返回 1
,那么 \(ans_x=0\) ;否则 : 若 \(ans_m=1\), 则 \(ans_y\) 一定等于 \(1\),所以一定有 \(ans_y \ge ans_m\)。
我们用 y
替换 m
,记录这个操作,并继续进行。
最终我们会得到一堆 0
和一条链,记录了这些数相对的大小情况。另外还剩下 1 个数,和链的最大值比较一次得到一个 1,最后在链上按照 Subtask 3 的规则二分即可。
总代价 \(5n+\mathcal O(\log_2{n})\)。
Code:AC记录
// WC2019 I君的游戏
#include <bits/stdc++.h>
using namespace std;
int query(int *S, int nS, int *T, int nT);
int query1(int a, int b) {
int tmp1[1] = {a}, tmp2[1] = {b};
return query(tmp1, 1, tmp2, 1);
}
int query2(int a, int b, int c) {
int tmp1[2] = {a, b}, tmp2[1] = {c};
return query(tmp1, 2, tmp2, 1);
}
#define MAXX 1145141919
vector<int> vec;
void find_price(int task_id, int n, int k, int ans[]) {
vec.clear();
if (task_id == 3||n<=2) {
int bigger = query1(0, n - 1);
int now = bigger ? (n - 1) : 0;
ans[now] = 1;
for (int i = 0; i < n; i++) vec.push_back(i);
if (now)reverse(vec.begin(), vec.end());
int l = 0, r = n - 1;
while (l < r) {
int mid = (l + r + 1) >> 1;
if (mid + 1 < n && query2(vec[mid], vec[mid + 1], now) ==
1) {
r = mid - 1;
} else
l = mid;
}
for (int i = 0; i <= l; i++)ans[vec[i]] = 1;
ans[vec[l + 1]] = (l + k ^ 1) & 1;
return;
}
int z = 0, t, x = 1, y, s;
for (int i = 2; i < n; i++) {
y = i;
t = query1(x, y);
if (!t) swap(x, y);
t = query2(x, y, z);
if (t) {
ans[x] = 0;
x = y;
} else {
vec.push_back(z);
z = y;
}
}
t = query1(x, z);if (!t)swap(x, z);
ans[z] = 1;
if (!vec.size()) {
ans[x] = k ^ 1;
return;
}
vec.push_back(z);
reverse(vec.begin(), vec.end());
int nnnn = vec.size();
int l = 0, r = nnnn - 1;
while (l < r) {
int mid = (l + r + 1) >> 1;
if (mid + 1 < nnnn && query2(vec[mid], vec[mid + 1], z) ==
0) {
l=mid ;
} else
r=mid-1;
}
for (int i = 0; i <= l; i++)
ans[vec[i]] = 1;
int c = vec[l + 1];
if(l+1==vec.size())goto AAA;//可能会越界
if (query1(c, x))
swap(c, x);
if (!query2(c, x, z)) {
ans[c] = 1;
l++;
} else {
ans[x] = 0;
x = c;
}
AAA: ans[x] = (l + 1 ^ k) & 1;
return;
}
完结撒花!
本文作者:wangyishan,转载请注明原文链接:https://www.cnblogs.com/wang-yishan/p/17995182