P7115 [NOIP2020] 移球游戏
很有意思的题目,并没有想象的那么难。
首先,为了方便起见,我们可以认为只有两种颜色的球,记为 \(0/1\)。考虑如何将 \(0/1\) 分开,之后多次重复这一过程,每次将部分颜色的球看作 \(1\) 即可。
考虑到要将所有颜色移到同一列中,首先要将将一个目标列中的 \(0/1\) 给“分离”出来。
方法很简单:考虑该列中 \(1\) 的个数为 \(x\),则如下操作:
- 将一个满列上方的 \(x\) 个球移至空列 (\(x\) 步)
- 将目标列的 \(0\) 放入空列,\(1\) 放入满列(分离)(\(m\) 步)
- 将这些球“倒回”目标列( \(m\) 步)
- 将空列球还原(\(x\) 步)
一共有 \(2m+2x\) 步。这是还原的基本操作,称为“提取”。
如果直接利用该过程暴力执行,进行一些优化可以获得 \(70pts\)
然后,就有几种不同的思路:
一 (感谢 QwQcOrZ 提供)
可以发现,这种方法操作次数与限制次数相差常数级别。
原来的做法还有优化空间,因为我们发现其实用 \(m+x\) 已经成功分离了该列的 \(0/1\)。因此,考虑如何“不还原”。方法很简单:让所借助的列为全 \(0\) 列。
因为 \(0\) 的数量远大于 \(1\) 所以构造很简单:分离第一列后再分离第二列即可(前两列中 \(0\) 的个数 \(\geq m\))。
这样,常数就优化了一半,上界 \(60,0000\) 次。
当然,要特判 \(n=2\) 的情况,具体操作可在方法二中找到。
二 (大多数题解的分治方法)
考虑将“颜色”的编号设一个阈值,超过为 \(0\),未超过为 \(1\)。
考虑每次将序列划成两段,左边为 \(0\),右边为 \(1\),再分治左右两边。
只需考虑“划分 \(0/1\)”即可。用类似双指针的过程每只考虑两列。
考虑以多补少。用错误的球较多的一列( \(i\) )补齐较少的一列( \(j\) )。
对 \(j\) 进行“提取”操作(错误球在上方),并将错误球放入空列,再分离 \(i\) 列(将与 \(j\) 同色的球放入 \(j\) 列,其余放入 \(n+1\) 列),最后将 \(n+1\) 中的球还原到 \(i\) 的位置。
用官方数据测试没有超过 \(47,0000\) 次的。
#include <cstdio>
#include <cctype>
#include <vector>
#include <algorithm>
using namespace std;
char buf[1<<14],*p1=buf,*p2=buf;
#define GetC() ((p1==p2)&&(p2=(p1=buf)+fread(buf,1,1<<14,stdin),p1==p2)?EOF:*p1++)
struct Ios{}io;
template <typename _tp>
Ios &operator >>(Ios &in,_tp &x){
x=0;int w=0;char c=GetC();
for(;!isdigit(c);w|=c=='-',c=GetC());
for(;isdigit(c);x=x*10+(c^'0'),c=GetC());
if(w) x=-x;
return in;
}
const int N=55,M=405;
int a[N][M];
int col[N][M];
int t[N];
int top[N];
int bel[N];
int n,m;
vector<pair<int,int> > v;
void move(int i,int j,int k=1){
while(k--){
v.push_back(make_pair(i,j));
a[j][++top[j]]=a[i][top[i]--];
int x;
x=col[j][top[j]]=col[i][top[i]+1];
t[i]-=(x==bel[i]);
t[j]+=(x==bel[j]);
}
}
void tiqu(int i,int j){//use j to sort i
int x=t[i];
move(j,n+1,x);
while(top[i]>0){
if(col[i][top[i]]==bel[i]) move(i,j);
else move(i,n+1);
}
move(j,i,x);move(n+1,i,m-x);
move(n+1,j,x);
}
void work(int i,int j){
if(m-t[i]<m-t[j]) swap(i,j);
tiqu(j,i);
move(j,n+1,m-t[j]);
while(top[j]!=m){
if(col[i][top[i]]==bel[j]) move(i,j);
else move(i,n+1);
}
move(n+1,i,top[n+1]);
}
void solve(int l,int r){
if(l==r) return ;
int mid=(l+r)>>1;
for(int i=l;i<=r;++i){
t[i]=0;
if(i<=mid) bel[i]=0;
else bel[i]=1;
for(int j=1;j<=m;++j){
col[i][j]=(a[i][j]>mid);
t[i]+=(col[i][j]==bel[i]);
}
}
int i=l,j=mid+1;
while(i<=mid&&j<=r){
while(i<=mid&&t[i]==m) ++i;
while(j<=r&&t[j]==m) ++j;
if(i>mid||j>r) break;
work(i,j);
if(t[i]==m) ++i;
if(t[j]==m) ++j;
}
solve(l,mid);solve(mid+1,r);
}
int main(){
io>>n>>m;
for(int i=1;i<=n;++i)
for(int j=1;j<=m;++j)
io>>a[i][j];
for(int i=1;i<=n;++i) top[i]=m;
solve(1,n);
printf("%d\n",(int)v.size());
for(auto tmp:v) printf("%d %d\n",tmp.first,tmp.second);
return 0;
}