「NOIP2020」移球游戏 题解 (构造,分治)
原本刚听完课时是不想写这道题的……
题目简介
\(N+1\)根柱子,其中 \(N\) 个上面各有\(M\)个小球,第 \(N+1\) 根柱子是空的。小球共 \(N\) 种颜色。
现在要使得每根柱子上的小球颜色相同,求移动次数与方法(不必最优)。
分析
考虑将第 \(1\) 根柱子全部放 \(1\) 色球,第 \(2\) 根柱子全部放 \(2\) 色球,\(\dots\),第 \(N\) 根柱子全部放 \(N\) 色球。
首先考虑 \(N=2\) 的情况:
现在第 \(1\) 根柱子中找到颜色为 \(2\) 的球的个数 \(k\),那么颜色为 \(1\) 的球有 \(M-k\) 个;
将第 \(2\) 根柱子顶端 \(k\) 个球移动到第三根柱子上,现在第 \(2\) 根柱子上有 \(M-k\) 个球,第 \(3\) 根柱子上有 \(k\) 个球;
将柱子 \(1\) 清空,颜色为 \(2\) 的球挂到 \(2\) 号柱上去,颜色为 \(1\) 的球挂在 \(3\) 号柱上去,现在第 \(1\) 根柱子没有球,第 \(2\) 和第 \(3\) 根柱子上都有 \(M\) 个球,且第 \(2\) 根柱子顶端 \(k\) 个球颜色都为 \(2\),第 \(3\) 根柱子顶端 \(M-k\) 个球颜色都为 \(1\)。
将第 \(3\) 根柱子顶端 \(M-k\) 个球放回 \(1\) 号柱,将第 \(2\) 根柱子顶端 \(k\) 个球放回 \(1\) 号柱。现在第 \(1\) 根柱子下方 \(M-k\) 个都是 \(1\) 色球,上方 \(k\) 个都是 \(2\) 色球。
将第 \(2\) 根柱子剩下的 \(M-k\) 个球全部放到 \(3\) 号柱上,再将第 \(1\) 根柱子上方 \(k\) 个 \(2\) 色球全部放到 \(2\) 号柱上,现在 \(1\) 号柱有 \(M-k\) 个 \(1\) 色球,\(2\) 号柱上有 \(k\) 个 \(2\) 色球,\(3\) 号柱上有 \(M\) 个杂色球。
清空 \(3\) 号柱,将 \(1\) 色球全部挂 \(1\) 号柱,\(2\) 色球全部挂 \(2\) 号柱,这样就完了。
在考虑 \(N>2\) 的情况:
考虑分治,将球的颜色分为 \(\leq mid\) 和 \(>mid\) 考虑。
对于区间 \([l,r]\) ,我们将颜色 \(\leq mid\) 的全部移动到 \([l,mid]\) , 将颜色 \(> mid\) 的全部移动到 \([mid+1,r]\),这个过程可以看作对两种颜色处理。
在区间 \([l,mid]\) 中寻找第一根含有颜色 \(>mid\) 的柱子,在 \([mid+1,r]\) 中寻找第一根含有颜色 \(\leq mid\) 的柱子,根据上述 \(N=2\) 的过程移动,注意:两根柱子上不合法的小球数量可能不一样,移动之后不一定能刚好解决,多余的放不下的小球,留在相反的柱子上下次解决(详见代码)。
处理完 \([l,r]\) 后,\([l,mid]\) 上全是颜色 \(\in [l,mid]\) 的小球,\([mid+1,r]\) 上全是颜色 \(\in [mid+1,r]\) 的小球,于是我们又可以分开处理区间 \([l,mid]\) 和区间 \([mid+1,r]\) 了。
可以由此将 \(N>2\) 和 \(N=2\) 的情况并成 \(N\geq 2\) 。
每一次操作都要移动 \(5M-k\) 次,总共移动约为 \(5MN\times \log N\) 次
代码实现较长,建议多用函数,注意细节:
\(AC\ Code\)
#include<cstdio>
#include<iostream>
using namespace std;
int read(){
int x=0;
char ch=getchar();
while(ch<'0'||ch>'9')ch=getchar();
while(ch>='0'&&ch<='9'){
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar();
}
return x;
}
int a[55][405];
int p[55];
int cnt[55];
const int Maxn=1e6+5;
struct Record{
int from;
int to;
}g[Maxn];
int tot;
int n,m;
void count(int l,int r){
const int mid=(l+r)>>1;
for(int i=l;i<=mid;i++){
cnt[i]=0;
for(int j=1;j<=p[i];j++)
if(a[i][j]>mid)cnt[i]++;
}
for(int i=mid+1;i<=r;i++){
cnt[i]=0;
for(int j=1;j<=p[i];j++)
if(a[i][j]<=mid)cnt[i]++;
}
}
inline void moving(int x,int y){
a[y][++p[y]]=a[x][p[x]--];
g[++tot].from=x;
g[tot].to=y;
}
void move(int lp,int rp,const int c){
int k=cnt[lp];
for(int i=1;i<=k;i++)moving(rp,n+1);
for(int i=1;i<=m;i++){
if(a[lp][p[lp]]<=c)moving(lp,n+1);
else moving(lp,rp);
}
for(int i=1;i<=m-k;i++)moving(n+1,lp);
for(int i=1;i<=k;i++)moving(rp,lp);
for(int i=1;i<=m-k;i++)moving(rp,n+1);
for(int i=1;i<=k;i++)moving(lp,rp);
cnt[lp]=cnt[rp]=0;
for(int i=1;i<=m;i++){
if(a[n+1][p[n+1]]<=c){
if(p[lp]<m)moving(n+1,lp);
else moving(n+1,rp),cnt[rp]++;
}else{
if(p[rp]<m)moving(n+1,rp);
else moving(n+1,lp),cnt[lp]++;
}
}
}
void solve(int l,int r){
if(l>=r)return ;
const int mid=(l+r)>>1;
count(l,r);
int lp=l,rp=r;
while(lp<rp){
while(!cnt[lp]&&lp<=mid)lp++;
while(!cnt[rp]&&rp>mid)rp--;
if(lp>mid||r<=mid)break;
move(lp,rp,mid);
}
solve(l,mid);
solve(mid+1,r);
}
int main(){
n=read();
m=read();
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
a[i][++p[i]]=read();
solve(1,n);
printf("%d\n",tot);
for(int i=1;i<=tot;i++)
printf("%d %d\n",g[i].from,g[i].to);
return 0;
}