题解 P7115 【移球游戏(洛谷民间数据)】
QwQcOrZ
2020-12-07 14:53:22
_此题解纪念我在考场上通过了本题_
考虑先枚举颜色,然后将此颜色的所有球移动到同一个柱子上
### Step. 0 问题的转换
假设原来是这么一组数据:
![](https://cdn.luogu.com.cn/upload/image_hosting/cwpdg8i2.png)
为了方便,我们将每个柱子看作一列,从左到右标号为 $1..n$
记 $now$ 表示当前枚举到的颜色,令所有 $a_{i,j}=now$ 的位置为 $1$,$a_{i,j}\not=now$ 的位置为 $0$
记 $t_i$ 表示第 $i$ 列 $1$ 的个数
当 $now=1$ 时,那么图就变成了这个样子:
![](https://cdn.luogu.com.cn/upload/image_hosting/jibk41sr.png)
这时问题的目标就转化为了将所有的 $1$ 移到同一列上
### Step. 1 制造全 $0$ 列
考虑对于每个颜色先制造出一个全 $0$ 列,然后再制造全 $1$ 列
至于为什么要先制造全 $0$ 列我后面会讲,这里先讲构造方法:
0. 还是为了方便,我们强制第 $n+1$ 列是空的(没有任何球)
具体操作时可以维护一个数组 $p$,其中 $p_i$ 表示当前第 $i$ 列的柱子在最开始时的编号为 $p_i$
这样就可以通过 `swap` $p$ 中的两个数来使柱子的排列方便我们构造
1. 设 $tot=t_1$,将第 $n$ 列最上面的 $tot$ 个球移动到第 $n+1$ 列
![](https://cdn.luogu.com.cn/upload/image_hosting/wjhhyj9g.png)
(这里用的还是 `Step. 0` 中的例子)
2. 将第 $1$ 列中的 $1$ 全部移动到第 $n$ 列,$0$ 全部移动到第 $n+1$ 列
![](https://cdn.luogu.com.cn/upload/image_hosting/myjlbseh.png)
具体过程就是如果第一列的最上面为 $1$ 就移到第 $n$ 列,否则移到第 $n+1$ 列
此时可以发现第一列中的 $0$ 和 $1$ 被我们分离了
3. 将第 $n+1$ 列中的 $m-tot$ 个 $0$ 全部移回第 $1$ 列(其中 $tot$ 为第一步中的 $t_1$)
![](https://cdn.luogu.com.cn/upload/image_hosting/1q1m0423.png)
4. 将第 $2$ 列中的 $0$ 分离到第 $1$ 列,$1$ 分离到第 $n+1$ 列(如果第一列塞不下 $0$ 了就往第 $n+1$ 列丢)
![](https://cdn.luogu.com.cn/upload/image_hosting/bjmjj427.png)
此时我们就构造出了一个全 $0$ 列!
至于第四步中为什么 $0$ 的数量一定够,是因为前两列中 $1$ 的个数一定不大于 $m$,所以 $0$ 的个数一定 $\geq 2m-1的数量$ ,即 $\geq m$。而这四步的本质也就是将前两列中的 $0$ 全部分离到第一列中,所以一定能构造出一个全 $0$ 列
`Step. 1` 实际上就是用第 $1,2,n,n+1$ 列构造了一个全 $0$ 列
#### Step. 2 构造全 $1$ 列
我们在 `Step. 1` 中构造全 $0$ 列的目的自然是为了方便将所有的 $1$ 移到同一列
依然是为了方便,我们强制在构造全 $1$ 列时第 $n$ 列要为全 $0$,第 $n+1$ 列要为空
所以在构造了全 $0$ 列的之后需要 $swap(p_1,p_n),swap(p_2,p_{n+1})$ (因为这时第一列为全 $0$,第二列为空)
然后自然是通过全 $0$ 列和全空列构造全 $1$ 列了(不然上一步构造全 $0$ 列干啥)
0. 开始的时候图长这样
![](https://cdn.luogu.com.cn/upload/image_hosting/rclsxwrb.png)
1. 像 `Step. 1` 中的 1.2. 那样将第 $1$ 列分解到第 $n,n+1$ 列
![](https://cdn.luogu.com.cn/upload/image_hosting/fras5a3j.png)
然后你就会发现第 $1$ 列中的 $1$ 全部被移到了第 $n$ 列的最上方,然后第 $n+1$ 列构成了一个新的全 $0$ 列
所以我们第一步中构造全 $0$ 列的目的就是保证在这步中,第 $n$ 列除了最上面的第一列中的 $1$,其它都为 $0$(否则第 $n$ 列的下方可能出现其它的 $1$,第 $n+1$ 列也不一定是全 $0$)
然后你又能发现你可以用第 $n+1$ 列新产生的全 $0$ 列和第 $2$ 列继续操作,再用新的全 $0$ 列和第 $3,4,...,n-1$ 列操作
所有操作完成后,所有的 $1$ 都被移到了柱子的最上方,这时就可以将所有 $1$ 都移到空行,然后再用全 $0$ 列去填充产生的空当
然后我们发现当前枚举的颜色已经合法,剩下的相当于要将 $n-1$ 根柱子的颜色分离,用同样的方法求解即可
### Step. 3 $n=2$
也许你以为本题的做法到这里就结束了?其实并没有
你会发现这个做法根本过不了样例
因为当 $n=2$ 时,第 $2$ 列和第 $n$ 列是同一列,也就是说不能用上面的方法制造全 $0$ 列
所以要特判 $n=2$ 的情况
这个应该挺好做的,这里简单介绍一下我的做法:
0. 假设开始的图是这样的:
![](https://cdn.luogu.com.cn/upload/image_hosting/kf54txnp.png)
1. 还是像 `Step. 1` 中的 1.2. 那样将第 $1$ 列分解到第 $2,3$ 列
![](https://cdn.luogu.com.cn/upload/image_hosting/5y49k8ws.png)
2. 然后再将分离后的丢回第 $1$ 列(先 $1$ 后 $2$)
![](https://cdn.luogu.com.cn/upload/image_hosting/qdncepqk.png)
这两步操作相当于将第一列排了个序
3. 将第三列移回第二列,然后将第一列上面的 $2$ 移到第三列
![](https://cdn.luogu.com.cn/upload/image_hosting/rupoz1bv.png)
4. 然后类似 `Step. 1` 中的 1.2. 那样将第 $2$ 列分解到第 $1,3$ 列,就能得到全 $1$ 列和全 $2$ 列了
### Step. 4 操作次数分析
最外层枚举颜色一个 $n$ ,每次构造全 $1$ 列时需要 $nm+m$(因为 $1$ 的总个数为 $m$,所以分解时的第一步均摊的总次数为 $m$)
因为列数随着颜色一个一个处理完会减小,所以 $nm+m$ 中的 $n$ 其实是个等差数列,也就是 $\sum\limits_{i=1}^n im+m$。所以有个 $1/2$ 的常数
构造全 $0$ 列时上限需要 $4m$ 次,所以操作次数上限为 $\sum\limits_{i=1}^n im+5m$,极限数据满打满算要操作 $600,000$ 次,可以轻松通过本题
复杂度 $=$ 操作次数,所以不需要管它
### Step. 5 一些小细节
1. 所有对第 $i$ 列的操作实际上都是对 $p_i$ 进行的,因为上面说第 $i$ 列只是方便思考,实际上时对 $p_i$ 进行的操作
2. 每次移完球需要动态更新 $p$ 数组,否则会惨烈爆蛋
3. 祝大家省选 rp++!
$Code\ Below$
```cpp
#include<bits/stdc++.h>
using namespace std;
const int N=55;
const int M=405;
const int OPT=820005;
int read()
{
int s=0;
char c=getchar(),lc='+';
while (c<'0'||'9'<c) lc=c,c=getchar();
while ('0'<=c&&c<='9') s=s*10+c-'0',c=getchar();
return lc=='-'?-s:s;
}
void write(int x)
{
if (x<0)
{
putchar('-');
x=-x;
}
if (x<10) putchar(x+'0');
else
{
write(x/10);
putchar(x%10+'0');
}
}
void print(int x=-1,char c='\n')
{
write(x);
putchar(c);
}
int L[OPT],R[OPT],CNT=0,n,m;
int a[N][M],cnt[N],tot[N],p[N];
void move(int x,int y)
{
++CNT;
L[CNT]=x;
R[CNT]=y;
a[y][++cnt[y]]=a[x][cnt[x]--];
}
int count(int x,int y)
{
int ret=0;
for (int i=1;i<=m;i++) ret+=a[x][i]==y;
return ret;
}
inline int top(int x)
{
return a[x][cnt[x]];
}
int main()
{
freopen("ball.in","r",stdin);
freopen("ball.out","w",stdout);
n=read();
m=read();
for (int i=1;i<=n;i++)
{
cnt[i]=m;
for (int j=1;j<=m;j++) a[i][j]=read();
}
cnt[n+1]=0;
for (int i=1;i<=n+1;i++) p[i]=i;
//empty:n+1
//full :n
for (int now=n;now>=3;now--)
{
int tmp=count(p[1],now);
for (int i=1;i<=tmp;i++) move(p[now],p[now+1]);
for (int i=1;i<=m;i++)
if (top(p[1])==now) move(p[1],p[now]);
else move(p[1],p[now+1]);
for (int i=1;i<=m-tmp;i++) move(p[now+1],p[1]);
for (int i=1;i<=m;i++)
if (top(p[2])==now||cnt[p[1]]==m) move(p[2],p[now+1]);
else move(p[2],p[1]);
//构造全 0 列
swap(p[1],p[now]);
swap(p[2],p[now+1]);
for (int k=1;k<now;k++)
{
tmp=count(p[k],now);
for (int i=1;i<=tmp;i++) move(p[now],p[now+1]);
for (int i=1;i<=m;i++)
if (top(p[k])==now) move(p[k],p[now]);
else move(p[k],p[now+1]);
swap(p[k],p[now+1]);
swap(p[k],p[now]);
}
for (int i=1;i<now;i++) while (top(p[i])==now) move(p[i],p[now+1]);
for (int i=1;i<now;i++) while (cnt[p[i]]<m) move(p[now],p[i]);
//构造全 1 列
}
int tmp=count(p[1],1);
for (int i=1;i<=tmp;i++) move(p[2],p[3]);
for (int i=1;i<=m;i++)
if (top(p[1])==1) move(p[1],p[2]);
else move(p[1],p[3]);
for (int i=1;i<=tmp;i++) move(p[2],p[1]);
for (int i=1;i<=m-tmp;i++) move(p[3],p[1]);
while (cnt[p[3]]) move(p[3],p[2]);
for (int i=1;i<=m-tmp;i++) move(p[1],p[3]);
for (int i=1;i<=m;i++)
if (top(p[2])==1) move(p[2],p[1]);
else move(p[2],p[3]);
//特判 n=2
print(CNT);
for (int i=1;i<=CNT;i++)
{
print(L[i],' ');
print(R[i]);
}
return 0;
}
```