P2346 四子连棋 题解
2020.4.1
在这里立个 \(\text{flag}\):一周内 \(\texttt{AC}\) 不了这道题目,我就 倒 ! 立 ! 洗 ! 头 !
本人还没 \(\texttt{AC}\),不过呢,谔谔,先整理下思路吧。
简要题意:
给定一个棋局,双方轮流将自己的棋子走到空位(任意方先走),问使得“四子连棋”(即四个子在一条直线上,斜线也可以)的最小步数。
显然,\(4 \times 4\) 的棋盘只要不出问题就能 \(\text{AC}\).(但我。。。)
本题需要优化搜索!(其实不然)
假设你用纯属的 \(\texttt{bfs}\),旁边开着哈希,你完全 有可能将所有状态都遍历一遍 。
而所有状态的种数是(根据组合计算):
然后,你映射到哈希还需要 \(4^2 = 16\) 的时间,然后还要每次判断答案再用 \(4^2 = 16\) 的时间,其它的不说,时间复杂度达到了:
再快的评测机,你广搜的常数也不会小的,然后妥妥的 \(\text{T}\) 掉了。。
你说:好,我用双向 \(\text{bfs}\).
没错确实可以,但这边不讲 究竟是懒,还是博主不会,大家细细品!
那么 \(\text{A*}\) 是什么呢?(这么神秘)
其实,\(\text{A*}\) 的搜索可以保证每次的状态越来越优(至少也不会变劣)。
假设当前状态为 \(x\),转移状态为 \(y\),对状态的咕值函数为 \(f\)(没错就是这个咕,咕咕咕的咕),则必须满足:
此时才可以进行转移,否则 \(\text{A*}\) 就直接把它结束。
为什么呢?
你想,如果你转移一步状态反而变劣了,那肯定还不如不转,对不对?
那么,这个 咕值函数 在本题中是什么呢?
就是 最大的连续棋子。
也就是说,如果你走了一步,原来连续的几个棋子被你破坏导致答案变劣,那这步就可以去世了。
那你说,如果我在破坏一个的同时又创造了新的一个呢?那不会影响,因为你新的一个如果还是较劣也会被剔除,否则这步就是可以的。
至少,\(\text{A*}\) 满足状态越来越优(至少不会变劣),那么就非常的好。(好!好!好!)
这样可以认为是一个 可行性剪枝 但是创造者为了提高咕值就把它列成算法怎么地
但是,在本题中,这个优化没有什么用处。
因为一个状态至少有 \(2\) 个连续的棋子(反证可知),所以如果原棋盘的咕值是 \(2\),那么还是会遍历的。
不过如果是 \(3\) 的话那瞬间时间复杂度就降到地下了。。(不过随机数据还没这个概率)
时间复杂度:\(O(\texttt{wys})\).(玄学,因为完全不会遍历所有状态)
实际得分:\(80pts\).(本人不知道哪里写炸了,如果有误可以在讨论区提出!)
#pragma GCC optimize(2)
#include<bits/stdc++.h>
using namespace std;
inline int read(){char ch=getchar();int f=1;while(ch<'0' || ch>'9') {if(ch=='-') f=-f; ch=getchar();}
int x=0;while(ch>='0' && ch<='9') x=(x<<3)+(x<<1)+ch-'0',ch=getchar();return x*f;}
struct node{
char p[5][5];
int step; char tt; //tt 表示上一局的走棋方
};
char a[5][5];
map<string,int> h;
queue<node> q;
const int dx[4]={1,-1,0,0};
const int dy[4]={0,0,1,-1};
inline node mp(char a[5][5],int x,char tt) {
node t; t.tt=tt; for(int i=1;i<=4;i++)
for(int j=1;j<=4;j++) t.p[i][j]=a[i][j];
t.step=x; return t;
} //将数据整装成结构体
inline string turn(char a[5][5]) {
string st="";
for(int i=1;i<=4;i++)
for(int j=1;j<=4;j++) st+=a[i][j];
return st;
} //压缩成字符串(其实3进制状压也行)
inline pair<int,int> get(char a[5][5],int x,int y) {
for(int i=1;i<=4;i++) for(int j=1;j<=4;j++)
if(a[i][j]=='O' && (i!=x || j!=y))
return make_pair(i,j);
} //寻找不在 (x,y) 位置的 O ,第一次找到后可以顺便找第二次
inline int f(char a[5][5]) {
int maxi=1; for(int i=1;i<=4;i++) {
if(a[i][1]==a[i][2] && a[i][2]==a[i][3] && a[i][3]==a[i][4]) return 4;
if(a[i][2]==a[i][3] && (a[i][1]==a[i][2] || a[i][3]==a[i][4])) maxi=max(maxi,3);
else if(a[i][1]==a[i][2] || a[i][2]==a[i][3] || a[i][3]==a[i][4]) maxi=max(maxi,2); //横着
} for(int i=1;i<=4;i++) {
if(a[1][i]==a[2][i] && a[2][i]==a[3][i] && a[3][i]==a[4][i]) return 4;
if(a[2][i]==a[3][i] && (a[1][i]==a[2][i] || a[3][i]==a[4][i])) maxi=max(maxi,3);
else if(a[1][i]==a[2][i] || a[2][i]==a[3][i] || a[3][i]==a[4][i]) maxi=max(maxi,2); //竖着
} bool l[5][5]; memset(l,0,sizeof(l));
for(int i=1;i<=3;i++) for(int j=1;j<=3;j++) {
if(l[i][j]) continue;
int x=i,y=j;
while(x<=3 && y<=3 && a[x+1][y+1]==a[x][y]) l[++x][++y]=1;
maxi=max(maxi,x-i+1); //往右下角走
} memset(l,0,sizeof(l));
for(int i=1;i<=3;i++) for(int j=2;j<=4;j++) {
if(l[i][j]) continue;
int x=i,y=j;
while(x<=3 && y>=2 && a[x+1][y-1]==a[x][y]) l[++x][--y]=1;
maxi=max(maxi,x-i+1); //往左下角走
} return maxi;
} //咕值函数
/*inline void print(char a[5][5],int step) {
putchar('\n');
for(int i=1;i<=4;i++) {
for(int j=1;j<=4;j++) putchar(a[i][j]);
putchar('\n');
} printf("%d\n",step);
}*/ //调试的代码
inline void bfs() {
q.push(mp(a,0,'O')); h[turn(a)]=1;
while(!q.empty()) {
node t=q.front(); char now[5][5]; int step;
for(int i=1;i<=4;i++) for(int j=1;j<=4;j++)
now[i][j]=t.p[i][j]; step=t.step; //取出队首
q.pop(); //print(now,step); //出队
pair<int,int> pr=get(now,0,0);
pair<int,int> pr2=get(now,pr.first,pr.second); //得到两个位置
// printf("%d %d %d %d\n",pr.first,pr.second,pr2.first,pr2.second);
for(int i=0;i<4;i++) {
int x=dx[i]+pr.first,y=dy[i]+pr.second;
if(x<1 || y<1 || x>4 || y>4 /*|| now[x][y]==t.tt*/ || now[x][y]=='O' || now[x][y]==t.tt) continue;
//不是上一局的那一方,并且不是两个空位转化
swap(now[x][y],now[pr.first][pr.second]);
int u=f(now),v=f(t.p);
if(u<v || h[turn(now)]) { //变劣 或 已出现
swap(now[x][y],now[pr.first][pr.second]);
continue; }
h[turn(now)]=1; q.push(mp(now,step+1,a[x][y])); //入队,标记
if(u==4) { //达到目标状态
// print(now,step+1);
printf("%d\n",step+1);
exit(0);
} swap(now[x][y],now[pr.first][pr.second]); //记得再换回去
} pr=pr2; //对第二个位置同样做一遍
for(int i=0;i<4;i++) {
int x=dx[i]+pr.first,y=dy[i]+pr.second;
if(x<1 || y<1 || x>4 || y>4 /*|| now[x][y]==t.tt*/ || now[x][y]=='O' || now[x][y]==t.tt) continue;
swap(now[x][y],now[pr.first][pr.second]);
int u=f(now),v=f(t.p);
if(u<v || h[turn(now)]) {
swap(now[x][y],now[pr.first][pr.second]);
continue; }
h[turn(now)]=1; q.push(mp(now,step+1,a[x][y]));
if(u==4) {
// print(now,step+1);
printf("%d\n",step+1);
exit(0);
} swap(now[x][y],now[pr.first][pr.second]);
}
}
}
int main(){
for(int i=1;i<=4;i++)
for(int j=1;j<=4;j++) cin>>a[i][j];
if(f(a)==4) puts("0"); //特判一开始就是目标状态
else bfs();
return 0;
}