题解:P5380 [THUPC 2019] 鸭棋

题解:P5380 [THUPC 2019] 鸭棋

前置芝士:模拟。

推荐题目:

  1. P2482 [SDOI2010] 猪国杀
  2. P4911 河童重工的计算机
  3. P7147 [THUPC 2021 初赛] 麻将模拟器

写在前面

这道题是本人第一道独立完成的大模拟,前后一共用了 \(15\) 个小时左右,特写此文以示纪念。

在完成的过程中,我有好几次卡壳的地方:

  1. 绊脚的位置(马,象,鸭各一次)。
  2. 判断将军(一次)。
  3. 坐标的位置(反了一次)。
  4. 判断游戏结束(忘记一次)。

这些等会儿会重点讲。

以下正文中,如出现 \(cpp\) 框,要么是代码,要么是模拟棋盘。模拟棋盘中,\(wu\) 表示空格,\(ba\) 表示绊脚,其他棋子用拼音中的前两个字母表示,如鸭就是 \(ya\),马就是 \(ma\)

那么我们废话不多说,开始进入正文。

题意解析

鸭棋是一款与象棋高度相似的博弈棋类游戏,大部分规则与象棋相同,不同的部分如下列举:

  1. 将象棋中的炮转换为鸭(整个程序里最烦的棋子),有两个绊脚点,位置分布为:
wu wu wu wu
wu wu ba wu
ya ba wu wu

即对于任意 \(s_x,s_y\in \left\{ 1,-1\right\}\),分别有:

  • 如果位置 \(\left(x+s_x\times 2 ,y+s_y \times 1\right),\left(x+s_x\times 1 ,y\right)\) 上均无任意一方的棋子停留,则 \(\left( x+s_x \times 3,y+s_y \times 2\right)\) 为一个可达的位置。
  • 如果位置 \(\left(x+s_x \times 1 ,y+ s_y \times 2 \right),\left(x ,y+ s_y \times 1 \right)\) 上均无任意一方的棋子停留,则 \(\left( x+s_x \times 2,y+s_y \times 3\right)\) 为一个可达的位置。
  1. 象棋中的兵只有四个方向,但鸭棋中的兵有八个方向。
  2. 鸭棋中没有限制王和士移动的九宫格,因此无需判断王和士是否走出九宫格(还算良心)。

在鸭棋中,输入的部分为四个坐标,前两个坐标 \((x_1,y_1)\) 表示棋子原来的坐标,后两个坐标 \((x_2,y_2)\) 表示棋子移动后的坐标,我们要根据这些棋子的信息判断游戏是否结束,移动是否合法,是否有将军情况。

输出的部分分为两种:

  • 如果该操作为不合法操作,则请输出 Invalid command
  • 如果为合法操作,则依次回答如下的四个问题:
    • 被移动的棋子用 [颜色] [类型](注意中间包含空格)来描述(类型要求使用英文名称)。如,红象为 red elephant,蓝王为 blue captain
    • 被移出游戏的棋子的描述方式与上面类似。特别地,如果无棋子被移出游戏,则该问题的答案为 NA
    • yesno 分别表示形成、不形成将军局面。
    • yesno 分别表示游戏结束、游戏不结束。
    • 特别的,用 ;(分号)将所有问题的答案隔开。

如还有不理解的地方,可以多次细读题目,或者去讨论区问问,希望大家都可以理解题目。

思路详解

接下来,我们将一步步将卡壳攻克,带领大家 AC 此题。

棋盘存储

首先,我们要先解决棋盘的存储。作者身边的很多神犇都是用了二维字符数组,方便省力。但作者却运用了别的方法:用序号标注每一个棋子,将每一个棋子的信息都精准无误的存在结构体里,包括坐标,存活状态,名字,虽然烦了些,但也更加清晰明了(吗?)。

特别的,作者一开始将坐标看成了左上角为 \((1,1)\),所以写的代码都是以左上角为 \((1,1)\) 的。

结构体:

struct duck {
    int dx,dy,xh;
    //dx 和 dy 分别是 x 坐标和 y 坐标。
    string zl,ys;
    //zl 是棋子的种类,ys 是颜色。
    bool sm=true;
    //sm 是是否存活。
}a[110];

结构体初始化函数

void fuzhi() {
    for(int i=1;i<=n;i++) {
        //n 是棋子的总个数(32)。
        if(i<=n/2) {
            a[i].ys="blue";
        }
        else {
            a[i].ys="red";
        }
        if(i==1||i==9||i==24||i==32) {
            //初始化车。
            if(i==1) {
                a[i].dx=1;
                a[i].dy=1;
            }
            else if(i==9) {
                a[i].dx=1;
                a[i].dy=9;
            }
            else if(i==24) {
                a[i].dx=10;
                a[i].dy=1;
            }
            else {
                a[i].dx=10;
                a[i].dy=9;
            }
            a[i].zl="car";
        }
        if(i==2||i==8||i==25||i==31) {
            //初始化马。
            if(i==2) {
                a[i].dx=1;
                a[i].dy=2;
            }
            else if(i==8) {
                a[i].dx=1;
                a[i].dy=8;
            }
            else if(i==25) {
                a[i].dx=10;
                a[i].dy=2;
            }
            else {
                a[i].dx=10;
                    a[i].dy=8;
            }
            a[i].zl="horse";
        }
        if(i==3||i==7||i==26||i==30) {
            //初始化象。
            if(i==3) {
                a[i].dx=1;
                a[i].dy=3;
            }
            else if(i==7) {
                a[i].dx=1;
                a[i].dy=7;
            }
            else if(i==26) {
                a[i].dx=10;
                a[i].dy=3;
            }
            else {
                a[i].dx=10;
                a[i].dy=7;
            }
            a[i].zl="elephant";
        }
        if(i==4||i==6||i==27||i==29) {
            //初始化士。
            if(i==4) {
                a[i].dx=1;
                a[i].dy=4;
            }
            else if(i==6) {
                a[i].dx=1;
                a[i].dy=6;
            }
            else if(i==27) {
                a[i].dx=10;
                a[i].dy=4;
            }
            else {
                a[i].dx=10;
                a[i].dy=6;
            }
            a[i].zl="guard";
        }
        if(i==5||i==28) {
            //初始化王。
            if(i==5) {
                a[i].dx=1;
                a[i].dy=5;
            }
            else {
                a[i].dx=10;
                a[i].dy=5;
            }
            a[i].zl="captain";
        }
        if(i>=12&&i<=21) {
            //初始化兵。
            if(i<=16) {
                a[i].dx=4;
            }
            else {
                a[i].dx=7;
            }
            if(i==12||i==17) {
                a[i].dy=1;
            }
            else if(i==13||i==18) {
                a[i].dy=3;
            }
            else if(i==14||i==19) {
                a[i].dy=5;
            }
            else if(i==15||i==20) {
                a[i].dy=7;
            }
            else {
                a[i].dy=9;
            }
            a[i].zl="soldier";
        }
        if(i==10||i==11||i==22||i==23) {
            //初始化鸭。
            if(i==10) {
                a[i].dx=3;
                a[i].dy=1;
            }
            else if(i==11) {
                a[i].dx=3;
                a[i].dy=9;
            }
            else if(i==22) {
                a[i].dx=8;
                a[i].dy=1;
            }
            else {
                a[i].dx=8;
                a[i].dy=9;
            }
            a[i].zl="duck";
        }
        //初始化序号。
        a[i].xh=i;
    }
}

主函数与辅助函数

初始完结构体,再来看主函数,作者将轮次变换放在了主函数:

signed main() {
    cin>>q;
    n=32;
    fuzhi();
    while(q--) {
        solve();
        //solve() 函数是最重要的判断部分,后期重点详解。
        if(f) {
            //lun 指当前轮次是那个颜色的棋子移动,方便后续判断。
            if(lun=="red") {
                lun="blue";
            }
            else {
                lun="red";
            }
        }
    }
}

在正式进入操作前,我们还需要以下变量的初始化和一些判断函数。

  1. 判断游戏是否结束的函数,每一次操作前都要优先判断:
string jieshu() {
    //序号 5 是蓝色王的编号,序号 28 是红色王的编号。
    if(!a[5].sm) {
        //蓝王死了,红方赢。
        return "redwin!";
    }
    else if(!a[28].sm) {
        //红王死了,蓝方赢。
        return "bluewin!";
    }
    else {
        //都没死,谁也没赢。
        return "nowin!";
    }
}
  1. 更新坐标的函数,将合法操作的棋子由旧坐标钻尾新坐标:
void move(int i,int qx,int qy) {
    //qx 和 qy 是新的 x 坐标和 y 坐标。
    a[i].dx=qx;
    a[i].dy=qy;
}
  1. 判断每次输入的四个坐标是否合法的两个函数:

判断前两个坐标的函数:

bool check(int x,int y) {
    for(int i=1;i<=n;i++) {
        //有任意一颗与当前轮次同颜色棋子,在将被移动的位置就返回 true。
        if(a[i].sm&&a[i].dx==x&&a[i].dy==y&&a[i].ys==lun) {
            return true;
        }
    }
    //没有任意一颗与当前轮次同颜色棋子,在将被移动的位置就返回 false。
    return false;
}

判断后两个坐标的函数:

bool check1(int x,int y) {
    for(int i=1;i<=n;i++) {
         //有任意一颗与当前轮次同颜色的棋子,在将被移动到的位置就返回 false。
        if(a[i].sm&&a[i].dx==x&&a[i].dy==y&&a[i].ys==lun) {
            return false;
        }
    }
    //没有任意一颗与当前轮次同颜色的棋子,在将被移动到的位置就返回 true。
    return true;
}
  1. 由于作者开始时将坐标看错了,所以还需要一个转换坐标的函数:
int zhhu(int x) {
    //左上角和右上角坐标的转换为 10-x。
    return 10-x;
}
  1. 主要操作中需要遇到的变量:
int x,y,x1,y1,p;
bool f=false,yi=false;
//p 表示本次移动的棋子序号,f 表示操作是否合法,yi 表示是否有棋子死亡。

主要操作

从现在开始,我们正式进入主要操作,这是这道题中,难度最大,码量最大,细节最多的部分。


转换坐标与初步判断合法

在转换坐标中,\(x\) 可以通过之前写过的转换函数进行转换,\(y\) 坐标只需 \(+1\) 即可(\(0\)\(1\) 的差为 \(1\)),代码如下:

cin>>x>>y>>x1>>y1;
x=zhhu(x);
x1=zhhu(x1);
y++;
y1++;

初步判断需要用到题目中的不合法说明,具体有以下几点:

  1. 游戏已经结束了。
  2. \((x,y)\)\((x_1,y_1)\) 相同。
  3. \((x,y)\)\((x_1,y_1)\) 超出了棋盘的范围。
  4. 违反了两个判断函数中的条件。

代码实现很简单,具体如下:

if(jieshu()=="bluewin!"||jieshu()=="redwin!") {
    cout<<"Invalid command\n";
    return;
}
if(x==x1&&y==y1) {
    cout<<"Invalid command\n";
    return;
}
if(x>jx||y>jy||x1>jx||y1>jy||x<1||y<1||x1<1||y1<1) {
    //jx 和 jy 为棋盘的极限范围,jx 为 10,jy 为 9。
    cout<<"Invalid command\n";
    return;
}
//以下是刚刚提到的两个判断函数。
if(!check(x,y)) {
    cout<<"Invalid command\n";
    return;
}
if(!check1(x1,y1)) {
    cout<<"Invalid command\n";
    return;
}

具体判断每一颗棋子

可以从 \(1\)~\(n\) 的序号中找到棋子的对应序号,再根据每一个棋子的特点进行判断。

王(captain)

王的移动为上下左右四格,要么 \(x=x_1\)\(|y_1-y|=1\);要么 \(|x_1-x|=1\)\(y=y_1\)。代码简单易懂。

if(a[i].zl=="captain") {
    if((abs(x1-x)==1&&!abs(y1-y))||(!abs(x1-x)&&abs(y1-y)==1)) {
        //判断条件。
        f=true;
        //此次操作为合法。
        move(i,x1,y1);       
        //进行移动。         
        p=i;
        //记录序号。
    }
    else {
        cout<<"Invalid command\n";
        return;
    }
}

之后的代码中 fmove(i,qx,qy)p 的含义不再赘述,上文皆有提及。

士(guard)

士的移动为斜向的移动,判断条件相较于王更为简单,只要 \(|y_1-y|=1\)\(|x_1-x|=1\),那么移动就是合法的。代码如下;

if(a[i].zl=="guard") {
    if(abs(x1-x)==1&&abs(y1-y)==1) {
        f=true;
        move(i,x1,y1);
        p=i;
    }
    else {
        cout<<"Invalid command\n";
        return;
    }
}
象(elephant)

从这里开始,问题才真正变得棘手起来,绊脚点的加入让判断变得复杂多变。但我们可以通过四个大类来逐一攻克:

  1. \(x_1-x=2\)\(y_1-y=2\),只要 \((x+1,y+1)\) 处没有棋子,此次操作就是合法的。
  2. \(x_1-x=2\)\(y_1-y=-2\),只要 \((x+1,y-1)\) 处没有棋子,此次操作就是合法的。
  3. \(x_1-x=-2\)\(y_1-y=2\),只要 \((x-1,y+1)\) 处没有棋子,此次操作就是合法的。
  4. \(x_1-x=-2\)\(y_1-y=-2\),只要 \((x-1,y-1)\) 处没有棋子,此次操作就是合法的。

每一次判断的时间复杂度都是 \(O(n)\),虽然比前两个的 \(O(1)\) 慢了一些,但对于这个数据范围来说还是极为充足的,代码如下:

if(a[i].zl=="elephant") {
    if(abs(x1-x)==2&&abs(y1-y)==2) {
        //大的判断条件。
        if(x1-x==2&&y1-y==2) {
            //判断可能的条件 1。
            for(int j=1;j<=n;j++) {
                if(i==j) {
                    continue;
                }
                if(a[j].sm&&a[j].dx==x+1&&a[j].dy==y+1) {
                    cout<<"Invalid command\n";
                    return;
                }
            }
        }
        else if(x1-x==-2&&y1-y==2) {
            //判断可能的条件 2。
            for(int j=1;j<=n;j++) {
                if(i==j) {
                    continue;
                }
                if(a[j].sm&&a[j].dx==x-1&&a[j].dy==y+1) {
                    cout<<"Invalid command\n";
                    return;
                }
            }
        }
        else if(x1-x==2&&y1-y==-2) {
            //判断可能的条件 3。
            for(int j=1;j<=n;j++) {
                if(i==j) {
                    continue;
                }
                if(a[j].sm&&a[j].dx==x+1&&a[j].dy==y-1) {
                    cout<<"Invalid command\n";
                    return;
                }
            }
        else {
            //判断可能的条件 4。
            for(int j=1;j<=n;j++) {
                if(i==j) {
                    continue;
                }
                if(a[j].sm&&a[j].dx==x-1&&a[j].dy==y-1) {
                    cout<<"Invalid command\n";
                    return;
                }
            }
        }
        f=true;
        move(i,x1,y1);
        p=i;
    }
    else {
        cout<<"Invalid command\n";
        return;
    }
}
马(horse)

马的复杂程度是仅次于鸭的。它的合法情况一共有 \(8\) 种:

  1. \(x_1-x=1\)\(y_1-y=2\),只要 \((x,y+1)\) 处没有棋子,此次操作就是合法的。
  2. \(x_1-x=1\)\(y_1-y=-2\),只要 \((x,y-1)\) 处没有棋子,此次操作就是合法的。
  3. \(x_1-x=-1\)\(y_1-y=2\),只要 \((x,y+1)\) 处没有棋子,此次操作就是合法的。
  4. \(x_1-x=-1\)\(y_1-y=-2\),只要 \((x,y-1)\) 处没有棋子,此次操作就是合法的。
  5. \(x_1-x=2\)\(y_1-y=1\),只要 \((x+1,y)\) 处没有棋子,此次操作就是合法的。
  6. \(x_1-x=2\)\(y_1-y=-1\),只要 \((x+1,y)\) 处没有棋子,此次操作就是合法的。
  7. \(x_1-x=-2\)\(y_1-y=1\),只要 \((x-1,y)\) 处没有棋子,此次操作就是合法的。
  8. \(x_1-x=-2\)\(y_1-y=-1\),只要 \((x-1,y)\) 处没有棋子,此次操作就是合法的。

代码实现细节较多,大家可以写一步检查一步,不要最后写完了才查出错(别问我是怎么知道的)。

if(a[i].zl=="horse") {
    if((abs(x1-x)==1&&abs(y1-y)==2)||(abs(x1-x)==2&&abs(y1-y)==1)) {
        //大的判断条件。
        if(x1-x==1&&y1-y==2) {
            //判断可能的条件 1。
            for(int j=1;j<=n;j++) {
                if(i==j) {
                    continue;
                }
                if(a[j].sm&&a[j].dx==x&&a[j].dy==y+1) {
                    cout<<"Invalid command\n";
                    return;
                }
            }
        }
        else if(x1-x==1&&y1-y==-2) {
            //判断可能的条件 2。
            for(int j=1;j<=n;j++) {
                if(i==j) {
                    continue;
                }
                if(a[j].sm&&a[j].dx==x&&a[j].dy==y-1) {
                    cout<<"Invalid command\n";
                    return;
                }
            }
        }
        else if(x1-x==-1&&y1-y==2) {
            //判断可能的条件 3。
            for(int j=1;j<=n;j++) {
                if(i==j) {
                    continue;
                }
                if(a[j].sm&&a[j].dx==x&&a[j].dy==y+1) {
                    cout<<"Invalid command\n";
                    return;
                }
            }
        }
        else if(x1-x==-1&&y1-y==-2) {
            //判断可能的条件 4。
            for(int j=1;j<=n;j++) {
                if(i==j) {
                    continue;
                }
                if(a[j].sm&&a[j].dx==x&&a[j].dy==y-1) {
                    cout<<"Invalid command\n";
                    return;
                }
            }
        }
        else if(x1-x==2&&y1-y==1) {
            //判断可能的条件 5。
            for(int j=1;j<=n;j++) {
                if(i==j) {
                    continue;
                }
                if(a[j].sm&&a[j].dx==x+1&&a[j].dy==y) {
                    cout<<"Invalid command\n";
                    return;
                }
            }
        }
        else if(x1-x==2&&y1-y==-1) {
            //判断可能的条件 6。
            for(int j=1;j<=n;j++) {
                if(i==j) {
                    continue;
                }
                if(a[j].sm&&a[j].dx==x+1&&a[j].dy==y) {
                    cout<<"Invalid command\n";
                    return;
                }
            }
        }
        else if(x1-x==-2&&y1-y==1) {
            //判断可能的条件 7。
            for(int j=1;j<=n;j++) {
                if(i==j) {
                    continue;
                }
                if(a[j].sm&&a[j].dx==x-1&&a[j].dy==y) {
                    cout<<"Invalid command\n";
                    return;
                }
            }
        }
        else {
            for(int j=1;j<=n;j++) {
            //判断可能的条件 8。
                if(i==j) {
                    continue;
                }
                if(a[j].sm&&a[j].dx==x-1&&a[j].dy==y) {
                    cout<<"Invalid command\n";
                    return;
                }
            }
        }
        f=true;
        move(i,x1,y1);
        p=i;
    }
    else {
        cout<<"Invalid command\n";
        return;
    }
}
posted @ 2026-02-06 16:55  Mirror_victor  阅读(2)  评论(0)    收藏  举报