【洛谷P4997】不围棋【并查集】【模拟】

题目大意:

题目链接:https://www.luogu.org/problemnew/show/P4997

「不围棋」是一种非常有趣的棋类游戏。

大家都知道,围棋的「气」是指一个棋子所在的联通块相邻的空格。两粒棋如果在棋盘上线段的两端就认为是相邻的,也就是在同一个连通块里。比如在图中,白子为四个独立的连通块,黑子构成一个连通块,绿色点是黑子连通块唯一的「气」:

在这里插入图片描述

「提子」是指将没有「气」的棋子提出棋盘,在上图中,如果白方走绿点,那么就可以将黑子全部提走。

在围棋中,我们想尽量多地占领地盘、提走对方棋子。然而,不围棋恰恰相反——不围棋是一种非常和平的游戏,双方的走子不能产生任何提子,也就是说,任何一次走子不能让棋盘上任何一个棋子所在的连通块没有气。比如,白方在上图中不能走绿点。

在你的某一步棋后,对方无棋可走,那么你就赢了。


小 F 对不围棋特别感兴趣,不过他经常输,所以他想做出一个 AI 来替他完成这局游戏。

不过造 AI 实在是太困难啦,小 F 千辛万苦写出来的 AI 被同学们的 AI 锤爆啦!

现在,他想请你帮他实现一个 AI 中一部分的功能——随机模拟,因为他相信你写的程序非常优秀,一定能优化他的 AI。

给你一个 n × n n \times n n×n 的棋盘,上面或许已经有一些棋子了,但此时局面一定是合法的,即不存在没有气的连通块;此时轮到黑棋下棋,因此棋盘上黑白棋子的数量一定是相等的。

你的任务是,依次为黑棋和白棋随意指定一个可行的走子位置,直到某一步游戏无法进行,决出胜负为止。

在正式的不围棋比赛还存在一些禁手规则。不过由于小 F 玩的是一种棋盘大小可变的新型不围棋,我们只用考虑上面提到的气的规则就好。


思路:

AC的第一道大模拟题祭orz \color{blue}\texttt{AC的第一道大模拟题祭orz} AC的第一道大模拟题祭orz
好久好久没有敲这种基本不用算法优化的大模拟题了 虽然这道题用了并查集
这道题2018年11月就决定敲。打来打去搞了好几次。这下总算是搞定这道题了。
L i n k : Link: Link:评测记录


这道题有SPJ,所以就顺序枚举黑棋白棋落子点。

先说一下大体思路吧
显然直接模拟是 O ( n 4 ) O(n^4) O(n4)的。所以考虑用并查集记录连通块和气,然后再记录下上一次黑棋白棋放置的位置,直接从那个位置往下枚举即可。
先处理好初始的连通块和气。
每次判断这个落子点是否可行。如果可以,那么就把棋子落在这里,并维护上下左右棋子的气,维护联通集合。

为了简便起见,我们修改一下气的定义。一个连通块的气为 ∑ s ( x ) ( x ∈ \sum s(x)(x\in s(x)(x该连通块 ) ) ),其中 s ( x ) s(x) s(x)表示棋子 x x x的上下左右有几个空格子。
这样定义的话,下图黑棋的气就是5气,而不是2气。
在这里插入图片描述

在这里插入图片描述

这样的好处是:如果我们用白棋填上最中间的位置,黑棋的气就只要减去3就可以了。否则的话还只能判断这个白棋所连接的黑棋是否是在同一个连通块内,会比较麻烦。


1.如何判断 ( x , y ) (x,y) (x,y)能否落子

一个点可以落子,只有满足以下任意条件才行:

  • 落子后,周围另一方的棋子的气变成0
  • 落子后,这个子及所在连通块的气为0

周围另一方棋子的气是比较好判断的。只要把周围所有的对方棋子所在连通块气减1,然后判断这些连通块内是否有块没气了。只要有1个块没气, ( x , y ) (x,y) (x,y)就是不可以落子的。

if (map[i-1][j]==oth) sum[find(C(i-1,j))]--;
if (map[i+1][j]==oth) sum[find(C(i+1,j))]--;
if (map[i][j-1]==oth) sum[find(C(i,j-1))]--;
if (map[i][j+1]==oth) sum[find(C(i,j+1))]--;
//。。。。。。
//判断这个子及所在连通块的气是否为0
bool ok=0;
if (sum[find(C(i-1,j))] && sum[find(C(i+1,j))] && sum[find(C(i,j-1))] && sum[find(C(i,j+1))])
	ok=1;
if (map[i-1][j]==oth) sum[find(C(i-1,j))]++;
if (map[i+1][j]==oth) sum[find(C(i+1,j))]++;
if (map[i][j-1]==oth) sum[find(C(i,j-1))]++;
if (map[i][j+1]==oth) sum[find(C(i,j+1))]++;

判断这个子及所在连通块的气是否为0的话,先假设这个点的气为4,如果上下左右有棋子和该棋子的颜色相同,那么就加上这个连通块的气,但是同时也要减去2。因为在没有落子之前,该连通块有1气是在这个点上的,但是现在这个点落子了,这个气就没有了。并且我们一开始假设 ( x , y ) (x,y) (x,y)的气为4,但是现在并不是上下左右都是空的,还要减去1。

然后,如果这个点的上下左右是边界或者对方棋子,气也要减少。

int s=4;
if (map[i-1][j]==ch)
{
	s-=2;
	if (!vis[find(C(i-1,j))])  //注意,每个连通块的气只能加一次,所以要判断这个连通块是否加过
	{		
		vis[find(C(i-1,j))]=1;
		s+=sum[find(C(i-1,j))];
	}
}
if (map[i+1][j]==ch)
{
	s-=2;
	if (!vis[find(C(i+1,j))])
	{
		vis[find(C(i+1,j))]=1;
		s+=sum[find(C(i+1,j))];
	}
}
if (map[i][j-1]==ch)
{
	s-=2;
	if (!vis[find(C(i,j-1))])
	{
		vis[find(C(i,j-1))]=1;
		s+=sum[find(C(i,j-1))];
	}
}
if (map[i][j+1]==ch)
{
	s-=2;
	if (!vis[find(C(i,j+1))])
	{
		vis[find(C(i,j+1))]=1;
	s+=sum[find(C(i,j+1))];
	}
}
vis[find(C(i-1,j))]=vis[find(C(i+1,j))]=vis[find(C(i,j-1))]=vis[find(C(i,j+1))]=0;  //还原
if (map[i-1][j]==oth||i==1) s--;
if (map[i+1][j]==oth||i==n) s--;
if (map[i][j-1]==oth||j==1) s--;
if (map[i][j+1]==oth||j==n) s--;  //判断边界和对方棋子

bool ok=0;
if (s) ok=1;

这样,我们的 c h e c k check check函数就写好了。


2.如何合并连通块

这个应该相对简单吧。
需要解决的问题有3个。

  • 新连通块的气
  • 如何合并连通块
  • 对手连通块的气

其实不用处理新连通块的气。因为我们在 c h e c k check check函数里已经判断了落子后这个连通块的气是否大于0,而用到的变量 s s s就是这个连通块的气。如果这个位置可以落子,那么直接把气赋值给 s s s就可以了。

合并连通块其实就是最基本的并查集操作,如果上下左右是我方棋子,那么就将这个连通块和 ( x , y ) (x,y) (x,y)合并。

处理对手的气也是非常简单的。由于我们把气的定义更改了,所以就不用判断“上和下的两个连通块是否是同一个连通块”之类的问题了。直接取上下左右的连通块的祖先,把它的气减1就可以了。

map[X][Y]=push;
if (map[X-1][Y]==map[X][Y]) father[find(C(X-1,Y))]=find(C(X,Y));
if (map[X+1][Y]==map[X][Y]) father[find(C(X+1,Y))]=find(C(X,Y));
if (map[X][Y-1]==map[X][Y]) father[find(C(X,Y-1))]=find(C(X,Y));
if (map[X][Y+1]==map[X][Y]) father[find(C(X,Y+1))]=find(C(X,Y));
		
char oth=(push=='X'?'O':'X');
if (map[X-1][Y]==oth) sum[find(C(X-1,Y))]--;
if (map[X+1][Y]==oth) sum[find(C(X+1,Y))]--;
if (map[X][Y-1]==oth) sum[find(C(X,Y-1))]--;
if (map[X][Y+1]==oth) sum[find(C(X,Y+1))]--;
		
printf("%d %d\n",X,Y);
if (push=='X') pushX=C(X,Y)+1;
	else pushO=C(X,Y)+1;
push=(push=='X'?'O':'X');

然后这道大模拟就这样切了。
AC的第一道大模拟题祭orz \color{blue}\texttt{AC的第一道大模拟题祭orz} AC的第一道大模拟题祭orz


代码:

#include <cstdio>
#include <cstring>
using namespace std;

const int N=610;
int n,X,Y,pushX,pushO,father[N*N],sum[N*N];
char map[N][N],push;
bool vis[N*N];

int find(int x)
{
	return x==father[x]?x:father[x]=find(father[x]);
}

int C(int x,int y)
{
	if (x>n||y>n||x<1||y<1) return 0;
	return (x-1)*n+y;
}

bool check_push(char ch)
{
	char oth=(ch=='X'?'O':'X');
	for (int k=(ch=='X'?pushX:pushO);k<=n*n;k++)
	{
		int i=(k-1)/n+1;
		int j=(k-1)%n+1;
		if (map[i][j]=='.')
		{
			if (map[i-1][j]==oth) sum[find(C(i-1,j))]--;
			if (map[i+1][j]==oth) sum[find(C(i+1,j))]--;
			if (map[i][j-1]==oth) sum[find(C(i,j-1))]--;
			if (map[i][j+1]==oth) sum[find(C(i,j+1))]--;
			  
			int s=4;
			if (map[i-1][j]==ch)
			{
				s-=2;
				if (!vis[find(C(i-1,j))])
				{
					vis[find(C(i-1,j))]=1;
					s+=sum[find(C(i-1,j))];
				}
			}
			if (map[i+1][j]==ch)
			{
				s-=2;
				if (!vis[find(C(i+1,j))])
				{
				  	vis[find(C(i+1,j))]=1;
					s+=sum[find(C(i+1,j))];
				}
			}
			if (map[i][j-1]==ch)
			{
				s-=2;
				if (!vis[find(C(i,j-1))])
				{
					vis[find(C(i,j-1))]=1;
					s+=sum[find(C(i,j-1))];
				}
			}
			if (map[i][j+1]==ch)
			{
				s-=2;
				if (!vis[find(C(i,j+1))])
				{
				  	vis[find(C(i,j+1))]=1;
					s+=sum[find(C(i,j+1))];
				}
			}
			vis[find(C(i-1,j))]=vis[find(C(i+1,j))]=vis[find(C(i,j-1))]=vis[find(C(i,j+1))]=0;
			if (map[i-1][j]==oth||i==1) s--;
			if (map[i+1][j]==oth||i==n) s--;
			if (map[i][j-1]==oth||j==1) s--;
			if (map[i][j+1]==oth||j==n) s--;
			
			bool ok=0;
			if (s && sum[find(C(i-1,j))] && sum[find(C(i+1,j))] && sum[find(C(i,j-1))] && sum[find(C(i,j+1))])
				ok=1;
			
			if (map[i-1][j]==oth) sum[find(C(i-1,j))]++;
			if (map[i+1][j]==oth) sum[find(C(i+1,j))]++;
			if (map[i][j-1]==oth) sum[find(C(i,j-1))]++;
			if (map[i][j+1]==oth) sum[find(C(i,j+1))]++;
			
			if (ok)
			{
				sum[C(i,j)]=s;
				X=i,Y=j;
				return 1;
			}
		}
	}
	return 0;
}

int main()
{
	memset(sum,0x3f3f3f3f,sizeof(sum));
	scanf("%d",&n);
	for (int i=1;i<=n;i++)
		for (int j=1;j<=n;j++)
		{
			map[i][j]=getchar();
			while (map[i][j]!='X'&&map[i][j]!='O'&&map[i][j]!='.')
				map[i][j]=getchar();
			father[C(i,j)]=C(i,j);
		}
	for (int i=1;i<=n;i++)
		for (int j=1;j<=n;j++)
		{
			sum[C(i,j)]=0;
			if (map[i-1][j]==map[i][j]&&map[i][j]!='.')
				father[find(C(i,j))]=find(C(i-1,j));
			if (map[i][j-1]==map[i][j]&&map[i][j]!='.')
				father[find(C(i,j))]=find(C(i,j-1));
			if (map[i-1][j]=='.') sum[find(C(i,j))]++; 
			if (map[i+1][j]=='.') sum[find(C(i,j))]++;
			if (map[i][j+1]=='.') sum[find(C(i,j))]++;
			if (map[i][j-1]=='.') sum[find(C(i,j))]++;
		}
	push='X';
	pushX=pushO=1;
	while (check_push(push))
	{
		map[X][Y]=push;
		if (map[X-1][Y]==map[X][Y]) father[find(C(X-1,Y))]=find(C(X,Y));
		if (map[X+1][Y]==map[X][Y]) father[find(C(X+1,Y))]=find(C(X,Y));
		if (map[X][Y-1]==map[X][Y]) father[find(C(X,Y-1))]=find(C(X,Y));
		if (map[X][Y+1]==map[X][Y]) father[find(C(X,Y+1))]=find(C(X,Y));
		
		char oth=(push=='X'?'O':'X');
		if (map[X-1][Y]==oth) sum[find(C(X-1,Y))]--;
		if (map[X+1][Y]==oth) sum[find(C(X+1,Y))]--;
		if (map[X][Y-1]==oth) sum[find(C(X,Y-1))]--;
		if (map[X][Y+1]==oth) sum[find(C(X,Y+1))]--;
		
		printf("%d %d\n",X,Y);
		if (push=='X') pushX=C(X,Y)+1;
			else pushO=C(X,Y)+1;
		push=(push=='X'?'O':'X');
	}
	printf("-1 -1\n");
	return 0;
}
  • 18
    点赞
  • 99
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
好的,下面是使用备忘录模式来模拟围棋的和悔棋的示例代码(使用 Python 实现): ```python # 备忘录类,用于保存围棋棋盘状态 class ChessboardMemento: def __init__(self, board): self.board = board def get_board(self): return self.board # 负责管理备忘录对象的存储和恢复 class MementoCaretaker: def __init__(self): self.mementos = [] def save_memento(self, memento): self.mementos.append(memento) def get_memento(self, index): return self.mementos[index] # 围棋游戏类 class GoGame: def __init__(self): self.board = [[0] * 19 for i in range(19)] # 初始化棋盘 self.caretaker = MementoCaretaker() def play(self, x, y, player): # 下棋操作,改变棋盘状态 self.board[x][y] = player # 保存当前状态到备忘录对象中 memento = ChessboardMemento([row[:] for row in self.board]) self.caretaker.save_memento(memento) def undo(self): # 回退操作,将棋盘状态恢复到上一次保存的状态 memento = self.caretaker.get_memento(-1) self.board = memento.get_board() self.caretaker.mementos.pop() def print_board(self): for i in range(19): for j in range(19): print(self.board[i][j], end=" ") print() # 测试代码 if __name__ == "__main__": game = GoGame() game.play(1, 1, 1) # 黑方下在 (1, 1) 处 game.play(2, 2, 2) # 白方下在 (2, 2) 处 game.print_board() # 打印当前棋盘状态 game.play(3, 3, 1) # 黑方下在 (3, 3) 处 game.undo() # 回退到上一步棋 game.print_board() # 打印回退后的棋盘状态 ``` 在上面的示例代码中,`ChessboardMemento` 类表示备忘录对象,保存了当前围棋棋盘的状态。`MementoCaretaker` 类负责管理备忘录对象的存储和恢复。`GoGame` 类实现了围棋游戏的逻辑,包括下棋和回退操作。在下棋时,将当前状态保存到备忘录对象中,并存储到 `MementoCaretaker` 对象中。在回退时,从 `MementoCaretaker` 对象中获取相应的备忘录对象,并将当前状态恢复为备忘录对象中保存的状态。
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值