非常好玩的一道题!
首先不难想到首先要找到一对括号,然后再次基础上询问其它的。简单拆分一下询问次数, 550 = 2 × 20 + 500 + eps 550 = 2 \times 20 + 500 + \text{eps} 550=2×20+500+eps,发现在找到括号后需要询问一次处理两个括号。至于为什么是 2 × 20 2 \times 20 2×20,原因便是在 [ l , r ] [l,r] [l,r] 中即使有左右括号,它可能以 () \texttt{()} () 或 )( \texttt{)(} )( 的形式出现,因此需要正反各询问一次。
接下来考虑如何一次询问处理两个括号。其实方法很多,适合手玩,用上 substring 这个条件,尽量构造不对称括号序列即可。我构造的是 ()??() \texttt{()??()} ()??(),有以下几种情况:
a n s = { 1 ((((() 2 (()(() 3 ((()() 4 (())() ans = \begin{cases} 1\ \ \ \ \texttt{((((()} \\ 2\ \ \ \ \texttt{(()(()} \\ 3\ \ \ \ \texttt{((()()} \\ 4\ \ \ \ \texttt{(())()} \\ \end{cases} ans=⎩ ⎨ ⎧1 ((((()2 (()(()3 ((()()4 (())()
设 L , R L,R L,R 为二分找到的左/右括号位置,以第一个为例子,查询的写法为 query ({L,L,x,y,L,R}
。注意,如果剩下未确定的数量是奇数,需要特殊考虑。
询问次数变为 200 200 200 以内,需要一次至少测 6 6 6 个。
如果接着从上一节的询问出发,直接赋值询问序列,会发现情况并不会显著增长,原因就是受到相互影响。所以进一步思考如何才能独立考虑。
尝试构造 (?((?((?(... \texttt{(?((?((?(...} (?((?((?(... 的序列。具体来说,用二进制数的思想,第 i i i 个待检测位置出现 2 i − 1 2^{i - 1} 2i−1 次,即 (x((y((y((z((z((z((z(... \texttt{(x((y((y((z((z((z((z(...} (x((y((y((z((z((z((z(...。得到答案后,从低往高第 i i i 位为 1 1 1,则说明第 i i i 个待检测的位置为 ) \texttt{)} ),否则为 ( \texttt{(} (。当然,若剩余待检测位置的数量不足,直接用 L L L 补齐即可,处理的时候特判。
最后检验一下合法性。若一次检测 x x x 个,询问的长度为 3 ( 2 0 + 2 1 + ⋯ + 2 x − 1 ) = 3 ( 2 x − 1 ) 3(2^0 + 2^1 + \cdots + 2^{x - 1}) = 3(2^x - 1) 3(20+21+⋯+2x−1)=3(2x−1)。由于长度不能超过 1000 1000 1000,因此 x max = 8 x_{\max} = 8 xmax=8,可以通过此题。
询问次数变为 100 100 100 以内,需要一次至少测 13 13 13 个。
接着上一节的想法,尝试构造互不影响的序列。尝试构造序列 (x((y(y((... \texttt{(x((y(y((...} (x((y(y((...,组内用 ( \texttt{(} ( 分隔,组间用 (( \texttt{((} (( 分隔。设第 i i i 个待检测的位置出现 d i g i dig_i digi 次,则对答案的贡献为 s u m i = ( d i g i + 1 ) d i g i 2 sum_i = \frac{(dig_i + 1)dig_i}{2} sumi=2(digi+1)digi。进一步来说,只要满足下式条件即可:
∄ i ∈ I s.t. s u m i = ∑ j ∈ J s u m j \nexists i \in I \quad \text{s.t.} \quad sum_i = \sum_{j \in J} sum_j ∄i∈Is.t.sumi=j∈J∑sumj
其中 I = { 1 , 2 , ⋯ , ∣ s u m ∣ } I = \{1,2,\cdots,|sum|\} I={1,2,⋯,∣sum∣}, J ⊆ I ∖ { i } J \subseteq I \setminus \{i\} J⊆I∖{i},且满足 ∀ j , k ∈ J , j ≠ k \forall j,k \in J, j \neq k ∀j,k∈J,j=k。
容易发现,当 2 s u m i − 1 ≤ s u m i 2sum_{i - 1} \le sum_i 2sumi−1≤sumi 时可以满足条件,通过暴力打表发现,最大的符合题目限制的 d i g dig dig 集合为 { 1 , 2 , 4 , 6 , 9 , 13 , 19 , 28 , 40 , 57 , 81 , 115 } \{1,2,4,6,9,13,19,28,40,57,81,115\} {1,2,4,6,9,13,19,28,40,57,81,115}。但是此时 ∣ d i g ∣ = 12 |dig| = 12 ∣dig∣=12,按照原来的二分方式会超过限制。仔细思考可以发现原来的二分每一次的 check 需要花费 2 2 2 次询问,但是我们只想要知道序列中是否存在左右括号以及括号的方向。因此,当方向未确定时,我们将询问的序列正反拼接,得到有无左右括号的信息。一旦在某次询问中得知存在左右括号,直接花费额外的一次询问定方向。在得知方向后,每次二分只需要一次询问去缩小范围。二分的询问次数的上确界为 ⌈ log ( 1 0 3 ) + 1 ⌉ = 11 \lceil{\log (10^3)} + 1 \rceil = 11 ⌈log(103)+1⌉=11 次,总询问次数的上确界为 11 + ⌈ 1 0 3 12 ⌉ = 11 + 84 = 95 11 + \lceil \frac{10^3}{12} \rceil = 11 + 84 = 95 11+⌈12103⌉=11+84=95 次,同时单次询问长度不会超过 1000 1000 1000,已经可以通过此题。
但能否进一步优化呢?按照之前的互不影响的条件去构造基,可以尝试写一个 O ( k 2 2 k ) O(k^2 2^k) O(k22k) 的状压构造并手动调整,在此不过多赘述,直接给出一组长度为 13 13 13 的可行构造: { 1 , 2 , 4 , 5 , 8 , 11 , 16 , 23 , 33 , 57 , 74 , 105 , 150 } \{1,2,4,5,8,11,16,23,33,57,74,105,150\} {1,2,4,5,8,11,16,23,33,57,74,105,150},可以证明不存在比 13 13 13 更大且满足条件限制的基。此时总询问次数的上确界可以降为 11 + ⌈ 1 0 3 13 ⌉ = 11 + 77 = 88 11 + \lceil \frac{10^3}{13} \rceil = 11 + 77 = 88 11+⌈13103⌉=11+77=88 次,理论上应该已经达到最优解了。
完整代码如下:
cpp
#include <bits/stdc++.h>
#define init(x) memset (x,0,sizeof (x))
#define ll long long
#define ull unsigned long long
#define INF 0x3f3f3f3f
#define pii pair <int,int>
using namespace std;
const int MAX = 1e5 + 5;
const int MOD = 1e9 + 7;
const int UP = 13;
inline int read ();
int dig[] = {1,2,4,5,8,11,16,23,33,57,74,105,150};
int query (vector <int> lst)
{
printf ("? %d ",(int)lst.size ());
for (auto v : lst) printf ("%d ",v);
puts ("");fflush (stdout);
return read ();
}
void solve ()
{
int n = read ();
int l = 1,r = n,L = -1,R = -1,sure = -1;
vector <char> ans (n + 1);
auto check = [&] (int l,int r) -> bool
{
vector <int> lst;
for (int i = l;i <= r;++i) lst.push_back (i);
if (sure == 0) return query (lst);
else if (sure == 1)
{
reverse (lst.begin (),lst.end ());
return query (lst);
}
for (int i = r;i >= l;--i) lst.push_back (i);
if (!query (lst)) return false;
for (int i = l;i <= r;++i) lst.pop_back ();
if (query (lst)) sure = 0;
else sure = 1;
return true;
};
while (1)
{
if (l + 1 == r)
{
if (sure == -1)
{
if (query ({l,r})) L = l,R = r;
else L = r,R = l;
}
else if (sure == 0) L = l,R = r;
else L = r,R = l;
break;
}
int mid = (l + r) >> 1;
if (check (l,mid)) r = mid;
else l = mid;
}
ans[L] = '(';ans[R] = ')';
vector <int> p;
for (int i = 1;i <= n;++i)
if (i != L && i != R) p.push_back (i);
int pos = 0,sz = p.size ();
while (pos < sz)
{
vector <int> nw (UP,L);
for (int i = 0;i < UP;++i)
if (pos + i < sz) nw[i] = p[pos + i];
vector <int> lst;
for (int i = 0;i < UP;++i)
{
for (int j = 0;j < dig[i];++j) lst.push_back (L),lst.push_back (nw[i]);
lst.push_back (L);
}
int res = query (lst);
for (int i = UP - 1;~i;--i)
{
if (nw[i] == L) continue;
if (res >= (1 + dig[i]) * dig[i] / 2) ans[nw[i]] = ')',res -= (1 + dig[i]) * dig[i] / 2;
else ans[nw[i]] = '(';
}
pos += UP;
}
printf ("! ");
for (int i = 1;i <= n;++i) printf ("%c",ans[i]);
puts ("");fflush (stdout);
}
int main ()
{
int t = read ();
while (t--) solve ();
return 0;
}
inline int read ()
{
int s = 0;int f = 1;
char ch = getchar ();
while ((ch < '0' || ch > '9') && ch != EOF)
{
if (ch == '-') f = -1;
ch = getchar ();
}
while (ch >= '0' && ch <= '9')
{
s = s * 10 + ch - '0';
ch = getchar ();
}
return s * f;
}