Codeforces 1290D - Coffee Varieties(分块暴力+完全图的链覆盖)
Easy version:Codeforces 题面传送门 & 洛谷题面传送门
Hard version:Codeforces 题面传送门 & 洛谷题面传送门
发现自己交互题烂得跟 sh*t 一样……于是不管三七二十一先来两道再说(
首先考虑最 trivial 的情况,也就是 \(k=1\) 和 \(k=n\) 两种情况,对于 \(k=1\) 你就 \(\mathcal O(n^2)\) 地检查一遍所有的 pair,具体来说我们枚举所有 \(i,j(i<j)\),然后依次询问 \(i,j\),如果询问到 \(j\) 时发现队列中存在 \(i\),就说明在 \(j\) 前面有存在与 \(a_j\) 相同的值,此时 \(j\) 不会对答案产生贡献,因此我们考虑开一个数组 \(is_i\) 表示 \(i\) 是否对答案产生贡献,每访问到这样的 \(j\) 就将 \(is_j\) 设为 \(0\) 即可。最后统计 \(\sum\limits_{i=1}^nis_i\) 即可,总询问次数 \(\dbinom{n}{2}·2=n(n-1)\)
\(k=n\) 的情况更容易,直接扫一遍,如果插入一个 \(i\) 时发现 \(i\) 已经存在,就将 \(is_i\) 设为 \(0\) 即可,总查询次数 \(n\)。
现在继续思考更一般的情形,也就是 \(k\ne 1\) 且 \(k\ne n\) 的情形,我们考虑将两个暴力结合一下,也就是人们常说的分块暴力,我们考虑将每 \(\dfrac{k}{2}\) 个元素形成一块,询问时对每两个块 \(i,j(i<j)\) 跑一遍上面 \(k=n\) 的暴力然后清空队列,这样对于每一对满足 \(x<y\) 且 \(a_x=a_y\) 的 \((x,y)\),如果 \(x,y\) 在同一块中那只要扫到 \(x\) 所在的块就会统计到这个二元组,否则在遍历 \(x\) 所在的块与 \(y\) 所在的块时会遍历到该二元组,这也就证明了上述算法的正确性。
算下询问次数,总共 \(\dfrac{2n}{k}\) 个块,遍历次数 \(\dbinom{2n/k}{2}=\mathcal O(\dfrac{2n^2}{k^2})\),而每次遍历要检验 \(k\) 个元素,因此总操作次数 \(\dfrac{2n^2}{k}\),可以通过 easy version。
const int MAXN=1<<10;
int n,k,is[MAXN+5];
bool query(int x){
printf("? %d\n",x);fflush(stdout);
char c;cin>>c;return (c=='Y');
}
int main(){
scanf("%d%d",&n,&k);int siz=max(k>>1,1);
for(int i=1;i<=n;i++) is[i]=1;int cnt=n/siz;
for(int i=1;i<=cnt;i++) for(int j=i+1;j<=cnt;j++){
for(int l=(i-1)*siz+1;l<=i*siz;l++) is[l]&=!query(l);
for(int l=(j-1)*siz+1;l<=j*siz;l++) is[l]&=!query(l);
printf("R\n");fflush(stdout);
} int sum=0;for(int i=1;i<=n;i++) sum+=is[i];
printf("! %d\n",sum);fflush(stdout);
return 0;
}
接下来考虑 hard version,不难发现上述算法的瓶颈在于,每次遍历两块时都要重新清空队列,效率太低。考虑优化。我们考虑将所有待检验的二元组 \((i,j)\) 看作一个边,那么显然会连成一个 \(\dfrac{2n}{k}\) 个点的完全图(注:这里可能有人会有疑问,在之前的算法中我们不是强制钦定 \(i\) 必须小于 \(j\) 吗?那这里不应该也有 \(i<j\) 吗?为什么会得到一张完全图呢?事实上我们并不一定非得要 \(i<j\),如果我们采取这样一个思想:维护一个集合表示目前没有值与其重复的元素,对于某个 \(a_i\),如果我们发现在该集合中存在某个元素,满足值与其相同,就将 \(a_i\) 从集合中删除,如此进行下去直到集合中无法再删除元素。此时 \(is_i\) 的定义要变为 \(a_i\) 是否还在集合中。不难发现这两个思路是等价的,而后者并不需要用到 \(i<j\) 这个性质),我们的目标即是覆盖这些边。不难发现对于完全图中的一条长度为 \(l\) 的链 \(a_1\to a_2\to a_3\to\cdots\to a_l\),覆盖它们所需的最小代价为 \(l·\dfrac{k}{2}\),具体步骤就是对于这条链上所有点一一询问即可。而显然我们不会重复覆盖相同的边,因此对于某个链覆盖的方案,覆盖整张图所需的最少代价就是 \(\dfrac{k}{2}·(\dbinom{2n/k}{2}+\text{使用链的个数})\)。而对于一个 \(n\) 个点(\(n\) 是偶数)的完全图而言有一个性质,就是覆盖它的边集所用的最少的链的个数是 \(\dfrac{n}{2}\),具体构造是我们枚举所有 \(s\in[1,\dfrac{n}{2}]\),然后 \(s\to s+1\to s-1\to s+2\to s-2\) 这样之字形反复横跳,如果超过 \(n\) 就回到 \(1\),如果小于 \(1\) 就回到 \(n\)。可以证明这样操作之后必然可以覆盖所有边,大概证明方式就是,对于每一条边 \((i,j)\),显然恰好存在两个点 \(s\) 满足从 \(s\) 开始能够遍历到这个边,而这两个 \(s\) 刚好差 \(\dfrac{n}{2}\),因此必定恰好有一个 \(s\) 在 \([1,\dfrac{n}{2}]\) 中。具体细节留给读者自己思考。操作次数 \(\dfrac{k}{2}·(\dbinom{2n/k}{2}+\dfrac{2n}{2k})=\dfrac{k}{2}·(\dfrac{n}{k}·(\dfrac{2n}{k}-1)+\dfrac{n}{k})=\dfrac{n^2}{k}\),可以通过 hard version。
const int MAXN=1<<10;
int n,k,siz,cnt,is[MAXN+5];
bool query(int x){
printf("? %d\n",x);fflush(stdout);
char c;cin>>c;return (c=='Y');
}
void deal(int x){
for(int i=(x-1)*siz+1;i<=x*siz;i++) if(is[i]&&query(i)) is[i]=0;
}
int main(){
scanf("%d%d",&n,&k);siz=max(k>>1,1);cnt=n/siz;
for(int i=1;i<=n;i++) is[i]=1;
for(int i=1;i<=n/k;i++){
int cur=0;
for(int j=1;j<=cnt;j++) deal((i+cur+cnt)%cnt+1),cur=(cur<=0)-cur;
printf("R\n");fflush(stdout);
} int sum=0;for(int i=1;i<=n;i++) sum+=is[i];
printf("! %d\n",sum);fflush(stdout);
return 0;
}

浙公网安备 33010602011771号