KismetOI Round 7「春影赛」A题 思考与总结记录
题面如下:
T724881 [KismetOI 2026 I] oo_vv_oo
题目描述
有一个长度为 n 的序列 A。
你每次可以给交互库一个下标序列 i1,i2,i3,…,ik。需保证 1≤ix≤n,但可以有相同的。
交互库会返回 j1,j2,…,jK。其中 jx 表示 Aijx>max(Aijx−1,Aijx+1),显然 1<jx<k。
现在已知 n,并且 A 中存在唯一的最大值。同时一定有 A1=An=0。请在 m 次询问内回答最大值的下标。
请注意:查询的 k 不能超过 n,否则可能出现未知错误情况。
输入格式
首先输入一行两个整数 n,m。
对于你的每次查询,交互库会按如下格式输出一行:首先输出一个整数 K,接下来输出 K 个整数表示返回的 j 序列,用空格隔开。
输出格式
你可以以如下格式进行交互:
输出一行
? k i[1] i[2] ... i[k]进行题目描述中给出的查询。输出一行
! ans表示你确定了最大值的下标 ans 并输出,请在此后自行结束程序运行以防止获得不可预知的结果。
请在每次执行完一次查询或给出答案的输出后,输出一个换行并清空缓冲区。
你可以使用如下语句来清空缓冲区:
对于 C/C++:
fflush(stdout);对于 C++:
std::cout << std::flush;对于 Java:
System.out.flush();对于 Python:
stdout.flush();对于 Pascal:
flush(output);对于其他语言,请自行查阅对应语言的帮助文档。
请保证你的程序的交互过程符合格式要求,否则你可能得到不可预知的错误结果。
如果您有更多疑问,可以参阅样例与样例解释。
思考过程
其实一开始看到题目,我真的是一脸???
因为我重来没有写过这种类型的题,看到样例更是云里雾里,反复读了4遍题目才看懂题目意思,就是你输出一些编号,交互库回还给你这些编号中的峰值,如 a[i-1] <a[i]>a[i+1],就会返回 下标i 在你的询问序列中的下标,说的有点抽象,如下图示





暴力写法
于是我想出了一个暴力写法,先判断返回的峰值个数是否唯一,如果唯一就直接退出,输出i在询问列表中的下标,不唯一则将返回的第一个峰值位置即i-1到最后一个峰值的位置i+1再次输出
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StreamTokenizer;
import java.util.ArrayList;
import java.util.List;
public class oo_vv_oo {
public static StreamTokenizer sz = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
public static int sc() throws IOException {
sz.nextToken();
return (int) sz.nval;
}
public static void main(String... args) throws IOException {
int n = sc(), m = sc();
// 特判 n = 3 的情况
if (n == 3) {
System.out.println("! 2");
System.out.flush();
return;
}
// 维护当前查询区间的左边界 l 和右边界 r
int l = 1, r = n;
// 1. 第一次查询:? n 1 2 ... n
StringBuilder out = new StringBuilder();
out.append('?').append(" ").append(n);
for(int i=1;i<=n; i=-~i) out.append(" ").append(i);
System.out.println(out);
System.out.flush();
int cur = -1;
while (m-- > 0) {
cur = sc();
// 当只剩 1 个峰值时,读取其相对位置,算出真实下标并输出答案
if (cur == 1) {
int pos = sc(); // 读入唯一峰值在当前区间内的相对位置
System.out.println("! " + (l + pos - 1));
System.out.flush();
return;
}
// 读取本次返回的所有峰值(相对位置)
List<Integer> list = new ArrayList<>();
for (int i = 0; i < cur; i = -~i) list.add(sc());
// 将相对位置换算为原数组的真实下标
int p1 = l+list.get(0) - 1;
int p2 = l+list.get(list.size() - 1) - 1;
// 缩小下一次查询的区间范围 [l, r]
l = Math.max(1, p1 - 1);
r = Math.min(n, p2 + 1);
// 构建并发送下一次查询
out = new StringBuilder();
int len = r-l+1;
out.append('?').append(" ").append(len);
for(int i=l;i<=r;i=-~i) out.append(" ").append(i);
System.out.println(out);
System.out.flush();
}
}
}不用想肯定超时和WA(因为我交了一发),那就想怎么优化
第一个优化-对比优化
当区间内出现多个峰值时,额外花 1 次询问直接 PK 最左峰值与最右峰值的大小,根据结果斩断较小者那一侧的整个区间
题目不允许直接查询某个下标的数值,但利用护栏 $A_1 = 0$ 和 $A_n = 0$,我们可以构造一次特殊的 4 元素查询如
发送询问:? 1 p1(最左峰值) p2(最右峰值) n
交互库内部提取出的数值序列为:[A1, Ap1, Ap2, An] = [0, Ap1, Ap2, 0]。
然后进行裁剪


代码如下
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StreamTokenizer;
import java.util.ArrayList;
import java.util.List;
public class oo_vv_oo {
public static StreamTokenizer sz = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
public static int sc() throws IOException {
sz.nextToken();
return (int) sz.nval;
}
public static void main(String... args) throws IOException {
int n = sc(), m = sc();
if (n == 3) {
System.out.println("! 2");
System.out.flush();
return;
}
int l = 1, r = n;
while (m > 0) {
// 1. 查询当前连续区间 [l, r]
int len = r - l + 1;
StringBuilder out = new StringBuilder();
out.append('?').append(" ").append(len);
for (int i = l; i <= r; i = -~i) {
out.append(" ").append(i);
}
System.out.println(out);
System.out.flush();
m--;
int cur = sc();
if (cur == 1) {
int pos = sc();
System.out.println("! " + (l + pos - 1));
System.out.flush();
return;
}
// 读取所有峰值相对位置并换算为真实下标
List<Integer> list = new ArrayList<>();
for (int i = 0; i < cur; i = -~i) list.add(sc());
int p1 = l + list.get(0) - 1;
int p2 = l + list.get(list.size() - 1) - 1;
// 2. 额外询问 ? 4 1 p1 p2 n 比较 p1 和 p2 的大小
System.out.println("? 4 1 " + p1 + " " + p2 + " " + n);
System.out.flush();
m--;
sc(); // 读入返回数量 k (必定为 1)
int pos = sc(); // 2 表示 p1 胜出,3 表示 p2 胜出
// 3. 根据比较结果强行将区间砍半,彻底避免死循环
if (pos == 2) {
r = p2 - 1; // p1 更大,裁掉 p2 及其右侧
} else {
l = p1 + 1; // p2 更大,裁掉 p1 及其左侧
}
}
}
}这个也超时了,嘻嘻,所以这个也是错的,还要优化
既然对比优化每一轮要消耗 2 次询问,面对 N = 10^6, m = 20 的极限限制,最多只能缩减 10 轮,超时(TLE)也是意料之中。
离散化
核心逻辑
彻底放弃连续区间:区间中间那些递减或平庸的数字全是废料,我们根本不需要它们,只保留上一轮返回的“峰值点”。
加入
1和N护栏:因为交互库规定序列的首尾元素绝不会被当成峰值返回,如果全局最大值刚好落在集合的最左或最右,就会被误杀。我们在两侧分别加上 A1 = 0 和 An = 0 充当安全护栏,组装成新序列 Q = [1, g1, g2, ..., g_k, n]。两相邻必灭一:在新序列中,任意两个相邻的旧峰值 $g_i$ 与 $g_{i+1}$ 互相作为邻居。较小的那一个由于旁边存在更大的数,在这一轮绝对不可能再被选为峰值!
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StreamTokenizer;
import java.util.ArrayList;
import java.util.List;
public class Main {
public static StreamTokenizer sz = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
public static int sc() throws IOException {
sz.nextToken();
return (int) sz.nval;
}
public static void main(String... args) throws IOException {
int n = sc(), m = sc();
if (n == 3) {
System.out.println("! 2");
System.out.flush();
return;
}
StringBuilder out = new StringBuilder();
out.append('?').append(" ").append(n);
for (int i = 1; i <= n; i = -~i) out.append(" ").append(i);
System.out.println(out);
System.out.flush();
int cur = sc();
List<Integer> list = new ArrayList<>();
for (int i = 0; i < cur; i = -~i) list.add(sc());
while (list.size() > 1) {
int len = list.size() + 2;
int[] q = new int[len];
q[0] = 1;
for (int i = 0; i < list.size(); i = -~i) q[i + 1] = list.get(i);
q[len - 1] = n;
out = new StringBuilder();
out.append('?').append(" ").append(len);
for (int i = 0; i < len; i = -~i) out.append(" ").append(q[i]);
System.out.println(out);
System.out.flush();
cur = sc();
List<Integer> next = new ArrayList<>();
for (int i = 0; i < cur; i = -~i) {
int pos = sc();
next.add(q[pos - 1]);
}
list = next;
}
System.out.println("! " + list.get(0));
System.out.flush();
}
}其实走了很多弯路,想了很多没有用的方法,但是对与本蒟蒻来说能写出来还是有点成就感的