题目描述
给定 \(n+1\) 个栈,栈的高度限制为 \(m\)。初始时前 \(n\) 个上每个有 \(m\) 个球,最后一个为空。球分为 \(n\) 种颜色,每种恰好 \(m\) 个。一次操作可以把一个栈顶的元素弹出放到一个另一个栈顶,但是不可以使栈溢出或下溢。现要把同种颜色的球移动到同一个栈上,你需要构造一个在 \(820000\) 次操作内的方案。
题目分析
题目总共分为 \(4\) 个 \(subtask\) 解决。
Sub1
这个部分 \(n\le 2,m\le 20\),通过模拟发现:
两个满栈 \(1,2\) 与一个空栈 \(3\) 可以实现将一个栈中的颜色聚拢在一起,并放在栈底。具体操作为统计栈 \(1\) 中颜色为 \(1\) 的球的个数 \(cnt\),从栈 \(2\) 中弹出 \(m-cnt\) 个球放入栈 \(3\) 中,再把栈 \(1\) 中颜色为 \(1\) 的球弹出放入 \(3\) 中,颜色为 \(2\) 的球弹出放入栈 \(2\) 中,最后将栈 \(3\) 中的球全部弹出放入栈 \(1\) 中,这样栈 \(1\) 中颜色为 \(1\) 球就到了栈 \(1\) 的栈底了。同理可将颜色为 \(2\) 的球放到栈 \(2\) 的底端。这样一次操作代价为 \(3 \times m - cnt\) 步。反复交换执行两个操作,直到两个栈均满足条件。
期望 \(10pts\),实际 \(10pts\)。
Sub2
这个部分 \(n\le 10,m\le 20\),显然还是可以使用刚才的方法,将两个栈 \(1,2\) 换成栈 \(X,Y\) 即可。 每次操作选择两个未满足条件的栈,进行上述操作,直到所有栈都满足条件。对于其中任意的连续两次操作要求存在 \(x_i\not=x_{i+1}\) 或 \(y_i\not= y_{i+1}\),否则后一次操作会无效,凭空增大步数。可以使用队列完成选取过程。
期望 \(25pts\),实际 \(45pts\),测试点中步数最多为\(1.6M\)。
点击查看代码
namespace sub2{
int count(int p){//记录一个栈p中有多少个颜色非p的球
int cnt=0;
for(int i=1;i<=m;i++){
if(a[p][i]!=p){
cnt++;
}
}
return cnt;
}
int check(int p){//判断一个栈是否符合条件
for(int i=1;i<=m;i++){
if(a[p][i]!=p){
return 0;
}
}
return 1;
}
void work(int x,int y){//对两个栈进行操作
int num=count(x);
for(int i=1;i<=num;i++){
ans.pb(mp(y,n+1));
a[n+1][++tot[n+1]]=a[y][tot[y]--];//Y中弹出相应个数的球
}
while(num){/*因为目的是将颜色为x的球放到最底下,所以下面没有其它球的颜色为x的球不需要弹出
这是个重要的优化,25->45*/
if(a[x][tot[x]]!=x){//颜色非x的球弹入Y中
num--;
ans.pb(mp(x,y));
a[y][++tot[y]]=a[x][tot[x]];
}
else{//否则弹入Z中
a[n+1][++tot[n+1]]=a[x][tot[x]];
ans.pb(mp(x,n+1));
}
tot[x]--;
}
while(tot[n+1]){//弹回
a[x][++tot[x]]=a[n+1][tot[n+1]--];
ans.pb(mp(n+1,x));
}
}
void solve(){
queue<int>q;//用队列维护还未符合条件的栈
for(int i=1;i<=n;i++){
if(!check(i)){
q.push(i);
}
}
while(q.size()!=1){
int x=q.front();
q.pop();
int y=q.front();
work(x,y);//对队列最前的两个栈进行操作
if(!check(x)){//不符合条件的栈才放回队列中
q.push(x);
}
}
}
}
Sub3
这个部分 \(n\le 50,m\le 85\ \text{和}\ m\le 300\)。回到上一个做法,我们发现主要消耗步数的地方在于不停的将重复执行某个操作 \((x,y)\)。其实,颜色为 \(x\) 的球不一定要全部放在栈 \(X\) 中,只要所有颜色为 \(x\) 的球上面都没有其它颜色的球,这样所有的球都可以放入空栈中,再将栈中剩余球数最小的栈清空,这样步数就会减少许多。
那如何将球移到栈顶呢?只需要将步骤改动一下,放到 \(Y\) 栈顶的不是非颜色 \(c\) 的球,是颜色为 \(c\) 的球。将非颜色 \(c\) 的球弹进空栈 \(Z\) 中,最后 \(Z\) 中先弹 \(tot_Z-cnt\) 个球到 \(X\) 中,\(Y\) 将最上方的颜色为 \(c\) 的球弹回 \(X\) 中,最后将 \(Z\) 中剩余的 \(cnt\) 个球弹入 \(Y\) 中,这样就完成了“拉起”操作。此时 \(Y\) 中球的个数与顺序均不变,\(X\) 中的颜色为 \(c\) 的球均在栈顶,一次“拉起”操作步数为 \(2\times(m+cnt)\)。
期望 \(70pts\),实际 \(80pts\),测试点中步数最多的为 \(900K\)。
点击查看代码
namespace sub3{
bool f,b[60];//b[i]=1表示栈i已经符合条件
int empty_place;//记录空栈位置
int count(int p,int c){//统计栈p中颜色为c的球的个数
int cnt=0;
for(int i=1;i<=m;i++){
if(a[p][i]==c){
cnt++;
}
}
return cnt;
}
void push_up(int p,int c){//“拉起”操作
if(b[p]||p==empty_place){
return;
}
int cnt=count(p,c),y=(p+1)%(n+2);
if(cnt==0){
return;
}
if(cnt==m){
f=b[p]=1;
return;
}
while(y==empty_place||y==p||y==0){
y=(y+1)%(n+2);
}//找到一个可以用的栈Y
for(int i=1;i<=cnt;i++){
ans.pb(mp(y,empty_place));
a[empty_place][++tot[empty_place]]=a[y][tot[y]--];
}//弹出Y顶端cnt个球
int num=cnt;
while(num){
if(a[p][tot[p]]==c){
num--;
ans.pb(mp(p,y));
a[y][++tot[y]]=a[p][tot[p]--];
}
else{
ans.pb(mp(p,empty_place));
a[empty_place][++tot[empty_place]]=a[p][tot[p]--];
}
}//弹到X中没有颜色为c的球
while(tot[empty_place]>cnt){
ans.pb(mp(empty_place,p));
a[p][++tot[p]]=a[empty_place][tot[empty_place]--];
}//将栈X填到只剩cnt个空
for(int i=1;i<=cnt;i++){
ans.pb(mp(y,p));
a[p][++tot[p]]=a[y][tot[y]--];
}//将颜色为c的球填回栈顶
while(tot[empty_place]){
ans.pb(mp(empty_place,y));
a[y][++tot[y]]=a[empty_place][tot[empty_place]--];
}//Z中剩下的球全部填回Y中
}
void solve(){
empty_place=n+1;
for(int i=1;i<=n;i++){
f=0;
for(int j=1;j<=n+1;j++){
push_up(j,i);
}
if(f){
continue;
}
for(int j=1;j<=n+1;j++){
if(j==empty_place||b[j]){
continue;
}
while(a[j][tot[j]]==i){
ans.pb(mp(j,empty_place));
a[empty_place][++tot[empty_place]]=a[j][tot[j]--];
}
}//将在栈顶的所有颜色为i的球全部放入空栈中
int num=0;
for(int j=1;j<=n+1;j++){
if(j==empty_place||b[j]){
continue;
}
if(!num||tot[j]<tot[num]){
num=j;
}
}
for(int j=1;j<=n+1;j++){
if(j==num){
continue;
}
while(tot[j]<m){
ans.pb(mp(num,j));
a[j][++tot[j]]=a[num][tot[num]--];
}
}//清空球数最少的栈
empty_place=num;
}
}
}
Sub4
还有 \(90K\) 的步数怎么消掉呢?再次回到方案上,发现新的方案耗步数最多的地方在于不停地回弹,那能不能不回弹呢,答案是可以的。发现若 \(Y\) 里面没有颜色为 \(c\) 的球(后文称为“白栈”),那么将颜色为 \(c\) 的球弹入 \(Y\) 中后,\(Y\) 中的颜色为 \(c\) 的球就全在栈顶了,若将 \(X\) 中剩余的球全部弹入 \(Z\) 中,栈 \(X\) 变成了空栈,栈 \(Z\) 变成了白栈。目的仍然达成,且栈中情况基本不变,仍是 \(1\) 白栈,\(1\) 空栈,消耗步数为 \(m+cnt\)。
构造白栈方法也很简单,选择一个不符合条件的栈 \(X\),将其中的颜色为 \(c\) 的球全部“拉起”,再放入栈 \(Z\) 中,从 \(Y\) 中弹出相应个数的颜色不为 \(c\) 的球到 \(X\) 中,再将 \(Z\) 中的球弹入 \(Y\) 中。步数消耗为 \(4\times(m+cnt)\)。
期望得分 \(100pts\),实际 \(100pts\),测试点中步数最多的点为 \(700K\) 步。
点击查看代码
namespace sub4{
int empty_place,last,b[60],f;//last:白栈
int count(int p,int c){
int cnt=0;
for(int i=1;i<=m;i++){//同上
if(a[p][i]==c){
cnt++;
}
}
return cnt;
}
void make(int p,int c){//造白栈
int cnt=count(p,c),y;
for(int i=n+1;i>=1;i--){
if(i==p||i==empty_place||b[i]){
continue;
}
y=i;
break;
}//随意找一个栈当中介
for(int i=1;i<=cnt;i++){
ans.pb(mp(y,empty_place));
a[empty_place][++tot[empty_place]]=a[y][tot[y]--];
}
while(tot[p]){
if(a[p][tot[p]]==c){
ans.pb(mp(p,y));
a[y][++tot[y]]=a[p][tot[p]--];
}
else{
ans.pb(mp(p,empty_place));
a[empty_place][++tot[empty_place]]=a[p][tot[p]--];
}
}
while(tot[empty_place]>cnt){
ans.pb(mp(empty_place,p));
a[p][++tot[p]]=a[empty_place][tot[empty_place]--];
}
for(int i=1;i<=cnt;i++){
ans.pb(mp(y,p));
a[p][++tot[p]]=a[y][tot[y]--];
}
while(tot[empty_place]){
ans.pb(mp(empty_place,y));
a[y][++tot[y]]=a[empty_place][tot[empty_place]--];
}//拉起p中颜色为c的球
for(int i=1;i<=cnt;i++){
ans.pb(mp(p,empty_place));
a[empty_place][++tot[empty_place]]=a[p][tot[p]--];
}
while(tot[p]<m){
if(a[y][tot[y]]==c){
ans.pb(mp(y,empty_place));
a[empty_place][++tot[empty_place]]=a[y][tot[y]--];
}
else{
ans.pb(mp(y,p));
a[p][++tot[p]]=a[y][tot[y]--];
}
}
while(tot[empty_place]){
ans.pb(mp(empty_place,y));
a[y][++tot[y]]=a[empty_place][tot[empty_place]--];
}//弹掉p中的颜色为c的球
last=p;
}
void push_up(int p,int c){//将栈p中的颜色为c的球拉到栈顶,哪个栈不重要
if(b[p]||p==empty_place||p==last){
return;
}
int cnt=count(p,c);
if(cnt==0||cnt==m){
return;
}
for(int i=1;i<=cnt;i++){
ans.pb(mp(last,empty_place));
a[empty_place][++tot[empty_place]]=a[last][tot[last]--];
}
while(tot[p]){
if(a[p][tot[p]]==c){
ans.pb(mp(p,last));
a[last][++tot[last]]=a[p][tot[p]--];
}
else{
ans.pb(mp(p,empty_place));
a[empty_place][++tot[empty_place]]=a[p][tot[p]--];
}
}
last=empty_place;
empty_place=p;
}//将p中的颜色为c的球弹到白栈中,空栈变为白栈,栈p变为空栈
void solve(){
empty_place=n+1;
int num;
for(int i=1;i<=n;i++){
last=num=f=0;
for(int j=1;j<=n+1;j++){
if(!b[j]&&j!=empty_place){
num=j;
break;
}
}//寻找可以变为白栈的栈
make(num,i);
for(int j=1;j<=n+1;j++){
if(j==empty_place){
continue;
}
push_up(j,i);
}
for(int j=1;j<=n+1;j++){
if(j==empty_place||b[j]){
continue;
}
while(a[j][tot[j]]==i){
ans.pb(mp(j,empty_place));
a[empty_place][++tot[empty_place]]=a[j][tot[j]--];
}
}//将所有颜色为i的球放入空栈
b[empty_place]=i;//标记符合条件
num=0;
for(int j=1;j<=n+1;j++){
if(j==empty_place||b[j]){
continue;
}
if(tot[j]<m){
num=j;
break;
}
}
for(int j=1;j<=n+1;j++){
if(j==num||b[j]){
continue;
}
while(tot[j]<m){
ans.pb(mp(num,j));
a[j][++tot[j]]=a[num][tot[num]--];
}
}
empty_place=num;//清空一个栈
}
}
}