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
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步