P8111 [Cnoi2021] 区间

发布时间 2023-12-02 20:17:57作者: Hanx16Msgr

[Cnoi2021] 区间

题目背景

Cirno 有一个区间 \([a,b](1\le a \le b \le n)\),而你的任务是在规定的次数内帮 Rumia 猜出这个区间。

每次,你可向Cirno询问一个数字 \(k\),而 Cirno 会告诉你这个数字与区间 \([a,b]\) 的关系。

题目描述

为了猜到这个区间,你需要实现一个函数 std::pair<int,int> Guess(int n,int c),这个函数的作用是在不超过 \(c\) 次询问中猜对 \([1,n]\) 中的一个子闭区间 \([a,b]\),返回值为你最终确定的区间,以 std::pair<int,int> 的形式返回。

你可以调用交互库中一个叫做 Query 的函数,其原型为 int Query(int x),返回值为:

  • \(x < a\),返回 \(-1\)
  • \(x \in [a,b]\),返回 \(0\)
  • \(x > b\),返回 \(1\)

你调用 Query 函数的次数不超过 \(c\) 才能得到这个点的分数,否则这个点为 \(0\) 分。有关该函数的调用请参考「说明/提示」部分。

在一个测试点中,你的 Guess 函数可能被调用多次,最多不超过 \(5000\) 次。为了保证你的程序不会超时,你需要额外实现一个函数 void init(),这个函数只会在开始时被交互库调用一次。当然,它的实现可以为空。

\(100\) points: \(n\le 1.5\times 10^3, c=20\)

Bonus: \(1\le n\le 10^6\)\(c=\lfloor\log_2 n\rfloor+\lfloor\log_2 \frac{4n}{3}\rfloor\)

Solution

很妙的一个题。

最简单的方式就是分别二分确定 \(l\)\(r\),这样询问次数为 \(2\lceil\log 1500\rceil=22\),可以获得 70pts。

会发现其实二分 \(l\)\(r\) 的时候有些询问重复了。因此可以开始的时候询问一次 \(\text{mid}\),就可以把 \(l\)\(r\) 的范围减小一半,这样数量变成了 \(21\) 次,还是没法得到 100pts。

假设第一步询问的位置是 \(p\),那么可以将答案表示成为 \(\lceil\log p\rceil+\lceil\log(n-p)\rceil\),发现 \(p=\text{mid}\) 并非是最优的,实际上 \(p\) 的位置大概是 \(\dfrac 1 3\) 的时候更优,因为 \(\lceil\log 500\rceil+\lceil\log 1000\rceil=20\),这样可以得到 100pts,但是因为数据加强了而过不去 Bonus。

个人觉得不是很好写。

Code(100pts)
#include <bits/stdc++.h>
using namespace std;
int Query(int x);
void init() {}
pair<int, int> Guess(int N, int total) {
    if (N == 1) return make_pair(1, 1);
    int L = 1, R = N;
    int l = 1, r = N, mid = (l + r) / 3, nl, nr;
    int d = Query(mid);
    if (d == -1) l = mid + 1;
    else r = mid - 1, L = mid;
    if (d != 1) nl = mid + 1, nr = N, R = mid;
    else nr = mid - 1, nl = 1;
    if (d == -1) {
        mid = (l + r) >> 1;
        d = Query(mid);
        if (d != 1) nl = mid + 1, nr = N, R = mid;
        else nr = mid - 1, nl = l;
        if (d == -1) l = mid + 1;
        else r = mid - 1, L = mid;
    }
    while (l <= r) {
        mid = (l + r) >> 1;
        if (Query(mid) == -1) l = mid + 1;
        else r = mid - 1, L = mid;
    }
    l = nl, r = nr;
    while (l <= r) {
        mid = (l + r) >> 1;
        if (Query(mid) != 1) l = mid + 1, R = mid;
        else r = mid - 1;
    }
    return make_pair(L, R);
}

Bonus

考虑 DP,设 \(f_i\) 表示 \(n=i\) 的最小询问次数,那么枚举询问的点 \(p\),按照 \(p\) 的返回值进行分类讨论。

  • 返回 \(-1\),那么最优的询问方法一定是 \(f_{i-p}\)
  • 返回 \(1\),那么最优的询问方法一定是 \(f_{p-1}\)
  • 返回 \(0\),那么 \(l\)\(r\) 相当于是按照 \(p\) 分割成了两半,相互独立,因此直接在左侧和右侧二分就是最优解。

也就是:

\[f_i=\min\limits_{p=1}^i {\Large\{}\max\{f_{p-1},f_{i-p},\lceil\log p\rceil+\lceil\log (i-p+1)\rceil\}{\Large\}} \]

时间复杂度 \(\mathcal O(n^2)\),只需要记录每次 DP 的决策点 \(p_i\) 即可获得 100pts。

Code(100pts)
#include <bits/stdc++.h>
#define For(i, a, b) for (int i = (a); i <= (b); ++i)
#define Rof(i, a, b) for (int i = (a); i >= (b); --i)
using namespace std;
int Query(int x);
const int _N = 1500 + 5;
int f[_N], p[_N];
void init() {
    memset(f, 0x3f, sizeof f);
    f[1] = 0, p[1] = 1;
    f[0] = 0, p[0] = 1;
    For(i, 2, 1500) {
        For(j, 1, i) {
            int cnt = ceil(log2(j)) + ceil(log2(i - j + 1));
            int val = max({f[j - 1], f[i - j], cnt}) + 1;
            if (val < f[i])
                f[i] = val, p[i] = j;
        }
    }
}
pair<int, int> Solve2(int L, int R, int pos) {
    int l, r, mid;
    pair<int, int> res;
    l = L, r = pos - 1;
    while (l <= r) {
        mid = (l + r) >> 1;
        if (Query(mid) == -1) l = mid + 1;
        else r = mid - 1;
    }
    res.first = l;
    l = pos + 1, r = R;
    while (l <= r) {
        mid = (l + r) >> 1;
        if (Query(mid) == 1) r = mid - 1;
        else l = mid + 1;
    }
    res.second = r;
    return res;
}
pair<int, int> Solve1(int L, int R) {
    if (L == R) return {L, R};
    int pos = p[R - L + 1] + L - 1;
    int res = Query(pos);
    if (res == -1) return Solve1(pos + 1, R);
    if (res == 1) return Solve1(L, pos - 1);
    return Solve2(L, R, pos);
}
pair<int, int> Guess(int N, int C) {
    return Solve1(1, N);
}

容易发现复杂度瓶颈其实是在 DP 部分,考虑观察决策点有什么性质。打表发现,记 \(t\) 是满足 \(2^t\le i\) 的最大值,有 \(p_i=(i\bmod 2^t)+1\)。那么就可以 \(\mathcal O(1)\) 的时间计算 \(p_i\) 的值。可以通过 Bonus 数据。

Code(101pts)
#include <bits/stdc++.h>
using namespace std;
int Query(int x);
void init() {}
inline int GetPos(int i) {
    int lim = 1 << __lg(i);
    return (i & ((lim >> 1) - 1)) + 1;
}
pair<int, int> Solve2(int L, int R, int pos) {
    int l, r, mid;
    pair<int, int> res;
    l = L, r = pos - 1;
    while (l <= r) {
        mid = (l + r) >> 1;
        if (Query(mid) == -1) l = mid + 1;
        else r = mid - 1;
    }
    res.first = l;
    l = pos + 1, r = R;
    while (l <= r) {
        mid = (l + r) >> 1;
        if (Query(mid) == 1) r = mid - 1;
        else l = mid + 1;
    }
    res.second = r;
    return res;
}
pair<int, int> Solve1(int L, int R) {
    if (L == R) return {L, R};
    int pos = GetPos(R - L + 1) + L - 1;
    int res = Query(pos);
    if (res == -1) return Solve1(pos + 1, R);
    if (res == 1) return Solve1(L, pos - 1);
    return Solve2(L, R, pos);
}
pair<int, int> Guess(int N, int C) {
    return Solve1(1, N);
}