LOADING

加载过慢请开启缓存 浏览器默认开启

P9529 [JOISC2022] 一流团子师傅 题解

今天下午学长刚讲了交互题,写完交上去一发过,似乎还是最优解?
题目传送门

题意简述

\(n\) 种物品,每种有 \(m\) 个,现在这些物品被随机打乱了(你并不知道顺序)。你每次可以选择任意多个(即给出一个向量)进行询问,每次询问会告诉你这些物品中 \(1\)\(n\)\(n\) 种物品中,最少的那种物品的数量。最后你需要给出 \(m\)完全不同的向量,每个向量必须包含这 \(n\) 种物品,每种物品有且仅有 \(1\) 个,在不多于 \(50000\) 次询问下完成。

思路

注意到只能在 \(50000\) 次询问内完成,而物品最多有 \(n \times m\)\(10000\) 个,最后对于每个物品,我们还剩下了 \(5\) 次查询机会。因为 \(m \le 25\),所以 \(\log m < 5\),因此我们可以考虑二分(这个其实还是比较难想的?)

我们令 \(M\) 表示当前处理的需要提交的向量的数量,\(mid\) 变量表示二分的值,即$mid = $所以当 \(M=1\) 的时候,我们就可以提交当前这一个向量。我们每次将一个大向量从前往后一个个遍历(具体方法后面会讲),将其拆成两个小向量,再递归处理,递归到最后直接提交答案,可以计算最多需要询问 \(43600\) 次,时间复杂度也是 \(O(n \log m)\) 的,可以通过。

实现

最开始,我们将 \(n \times m\) 这么多个数全部扔进一个叫做 \(x\) 的向量中,我们要将 \(x\) 这个向量拆成两个小的向量 \(y\)\(z\)。使得 \(y\) 这个向量中有 \(mid\) 串未处理的物品,\(z\) 中有 \(M-mid\) 串未处理的物品。

根据查询的性质:返回 \(1\)\(n\)\(n\) 种物品中最少的一种物品的数量。因此一个向量可以组成多少个串,就是对这个向量进行询问后的返回值。

因此我们从 \(x\) 这个向量的第一个元素开始遍历,假如去掉这一个物品后,我们进行一次查询,发现返回值仍是大于等于 \(mid\) 的,那么就说明,去掉这个物品后,剩下的物品也至少也可以组成 \(mid\) 个串,那么这个物品就可以扔到 \(z\) 这个向量。反之当返回值小于 \(mid\) 的时候,就说明去掉这个物品后,剩下的物品不能够组成 \(mid\) 个串,因此这个物品是必须的,需要放到 \(y\) 向量。再依次对 \(y\)\(z\) 递归处理,当一个向量长度为 \(n\) 时,那么就说明这是一个合法的串,提交答案即可。

注意! 当去掉这个物品后,剩下的物品不能组成至少 \(mid\) 根串的时候,我们是需要把它扔进 \(y\) 这个向量的,但是它依然应该被保留在 \(x\) 这个向量中,否则在后面继续遍历的时候,无论如何返回的询问值总是小于 \(mid\) 的。因此在代码实现的时候,我们其实并不需要真的开两个向量,自然的当 \(x\) 中可以扔去 \(z\) 的扔完后,剩下的其实就是原先\(y\) 这个向量。

具体的,我们维护一个 \(pos\) 变量,记录当前应该取的是 \(x\) 这个向量中的哪个元素的位置,再另开一个变量记录一下这个元素的值。因为我们有时需要扔进另一个向量,有时需要继续保留。每次我们先将这个元素从向量中弹出,进行查询后根据返回值决定要放回去还是丢进另一个变量。在扔走的时候,\(pos\) 是不变的,当留下的时候,将 \(pos\) 增加 \(1\)

具体可以看下面几张图来理解:

为什么这样是正确的?因为我们在每次处理 \(x\) 这个向量时,都保证了处理后的 \(y\) 向量和 \(z\) 向量中,分别可以组合出 \(mid\) 个串和 \(M-mid\) 个串。当最后 \(M=1\) 的时候,也就自然的一定可以组合出 \(1\) 个串,因此这也就是答案。

最后处理一些细节,代码也很容易完成,似乎是最短的,就可以愉快地 AC 啦~

代码

#include <vector>
void Solve(int N, int M);
int Query(const std::vector<int> &x);
void Answer(const std::vector<int> &a);
void work(std::vector<int> &a,int m){//递归处理
    if(m==1){//得到答案
        Answer(a);
        return;
    }
    int mid=m/2,cache;
    std::vector<int> x;
    int pos=0;
    int len=a.size();
    for(int i=1;i<=len;i++){
        cache=a[pos];
        a.erase(a.begin()+pos);
        if(Query(a)<mid) a.insert(a.begin()+pos,cache),pos++;//返回值大于等于mid,扔进另一个向量
        else x.push_back(cache);//返回值小于mid,需要扔回去
    }
    work(a,mid);
    work(x,m-mid);//向下递归继续处理
}
void Solve(int N,int M){
    std::vector<int> s;
    for(int i=1;i<=N*M;i++) s.push_back(i);//将最开始的N*M个数全部丢进一个初始向量
    work(s,M);
}