做题感悟:这题只能说很“ 经典 ” 。
解题思路:题意就是给你一个二行N例的一面墙,在墙上涂颜色,问你最少粉刷多少次,能达到给你的目标墙的状态,(每次粉刷只能粉刷出一个矩形,矩形可以是一行N例,也可以是二行N例);
思想:广度优先搜索(因为找最短路)
状态表示:
状态用二制位表示。例如当N=4时,就用8位二进制数表示状态如:
1100
1010
‘‘0’’表示与目标状态颜色不一致,‘1’表示一致。
初始状态当然为:
0000
0000
因为和目标状态没有相同颜色。
目标状态自然为:
1111
1111
状态转移:
状态转移就是粉刷墙的过程,按传统套路很容易把粉刷的颜色也考虑进去。这样就走弯路了。
仔细想想会发现 每次粉刷,都会使一状态中的一些0变成1。从而越来越接近目标状态。
问题来了,每次粉刷多大?粉刷的大小受什么影响?
先分两种情况:
第一种是刷一行,在图上选一个点的颜色粉刷,分别向该点的左右粉刷,如果遇到和选取点颜色相同,则将当前状态该位的0变成1,表示与目标状态相同
。
第二种是刷两行,刷两行只考虑一行就可以,在其中一行寻解时,同时考虑同列的另一行,也就是在列相同的情况下判断两行的颜色,以及粉刷。
这样就可以从一个状态转移到另一个状态。
注:上述方法可以得到一个极大粉刷区域,
每次状态转移全是转移最大粉刷区域,显然是不够准确的,算出最大粉刷区域后应该也将该区域的子集加到扩展队列中。求一个状态的子集的方法:整数temp的位数为当前状态 ,那么for(int j=temp;j;j=temp&(j-1));j就为子状态。可以自己模拟体会一下。
判重:
根据状态抽象,最多有2<16种状态,可以开一个bool型数组flag[1<<16];如flag[i]=1,则表示i二进制位表示的这个状态以经出现过。
代码:
#include<stdio.h>
#include<iostream>
#include<map>
#include<stack>
#include<string>
#include<string.h>
#include<stdlib.h>
#include<math.h>
#include<vector>
#include<queue>
#include<algorithm>
using namespace std ;
#define LEN sizeof(struct node)
#define pret(a,b) memset(a,b,sizeof(a))
#define lld __int64
const double PI = 3.1415926 ;
const double INF = 999999999 ;
const double esp = 1e-4 ;
const lld md= 2810778 ;
const int MX = 20 ;
int n ;
char s[MX] ;
bool vis[1<<16] ;
struct node
{
int key,step ; // key 表状态 step 记录步数
} ;
int bfs()
{
queue<node>q ;
node curt,next ;
while(!q.empty()) q.pop() ;
memset(vis,false,sizeof(vis)) ;
curt.key=curt.step=0 ;
vis[0]=true ;
q.push(curt) ;
while(!q.empty())
{
curt=q.front() ;
q.pop() ;
for(int i=0 ;i<(n<<1) ;i++) // 遍历每一个点
{
int key=curt.key,ky=0 ;
next.step=curt.step+1 ; // 步数加一
if(key&(1<<i)) continue ;
// 先单行左右扩展
for(int j=i ;j<(i/n+1)*n ;j++)// 从当前节点向右扩展
{
if(key&(1<<j)) break ;// 遇到已经染色的就结束
if(s[i]==s[j]) ky=ky|(1<<j) ; // 与起始颜色一样 ~> 染色
}
for(int j=i-1 ;j>=(i/n)*n ;j--) // 从当前节点向左扩展
{
if(key&(1<<j)) break ;
if(s[i]==s[j]) ky=ky|(1<<j) ;
}
if((key|ky)==(1<<(n*2))-1) return next.step ; // 放在这可以减少时间
for(int j=ky ;j ;j=key&(j-1))
{
if(!vis[key|j])
{
vis[key|j]=true ;
next.key=key|j ;
q.push(next) ;
}
}
// 以上为单行双向扩展
if(i/n) continue ;
key=curt.key ; ky=0 ;
if(key&(1<<(i+n))) continue ;
for(int j=i ;j<n ;j++)
{
if((key&(1<<j))||(key&(1<<(j+n)))) break ;
if(s[i]==s[j]) ky=ky|(1<<j) ;
if(s[i]==s[j+n])ky=ky|(1<<(j+n)) ;
}
for(int j=i-1 ;j>=0 ;j--)
{
if((key&(1<<j))||(key&(1<<(j+n)))) break ;
if(s[i]==s[j]) ky=ky|(1<<j) ;
if(s[i]==s[j+n]) ky=ky|(1<<(j+n)) ;
}
if((key|ky)==(1<<(n*2))-1) return next.step ;
for(int j=ky ;j ;j=ky&(j-1)) // 切记要枚举子集:因为并不一定需要涂最大区域,如果涂了最大区域
{ //别的地方就没法连续涂了。
if(!vis[key|j])
{
vis[key|j]=true ;
next.key=key|j ;
q.push(next) ;
}
}
}
}
return 0 ;
}
int main()
{
int Tx,q=1 ;
scanf("%d",&Tx) ;
while(Tx--)
{
scanf("%d",&n) ;
scanf("%s",s) ; // 用一维存储
scanf("%s",s+n) ;
printf("Case #%d: %d\n",q++,bfs()) ;
}
return 0 ;
}