[NOIP2020] 移球游戏 解题报告

题目描述

给定 \(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;//清空一个栈
        }
    }
}
posted @ 2023-06-18 14:27  luo_shen  阅读(135)  评论(0)    收藏  举报