[ACM训练] 算法初级 之 基本算法 之 枚举(POJ 1753+2965)

时间:2022-01-15 16:47:58

先列出题目:

1、POJ 1753

POJ 1753  Flip Game:http://poj.org/problem?id=1753

Sample Input

bwwb
bbwb
bwwb
bwww

Sample Output

4

入手竟然没有思路,感觉有很多很多种情况需要考虑,也只能使用枚举方法才能解决了吧~

4x4的数组来进行数据存储的话操作起来肯定非常不方便,这里借用位压缩的方法来存储状态,使用移位来标识每一个位置的的上下左右的位置操作。 详细看这里

1、当棋盘状态id为0(全白)或65535(全黑)时,游戏结束,0^1=1,1^1=0,所以翻转的操作可以通过异或操作来完成,而翻转的位置可以通过移位来确定。

2、结束标识!!!!!

分步骤:

1、从输入到位标识状态:

     int state = ;
char ch[]; while(cin>>ch)
{
for(int j = ; j < ; j++)
{
state = state<<;
if(ch[j] == 'b')
state += ;
}
}

2、从一个状态到下一个状态的的转换,比如对应位置i处进行改变后得到的状态:

这里的16个数据使用代码生成可能是想不到的,但是可以通过手动变换一次获得~~~

其实相当于

0 0 0 0

0 0 0 0

0 0 0 0

0 0 0 0

在每一个位置进行变换后的状态记录,正好对应疑惑的16个数据

 int change[] ={
,,,,
,,,,
,,,,
,,,
}; state = state^change[i];//对应第i个位置的改变 //上面的16个数据是由下面的代码得来的 int dir[][]={{,},{-,},{,},{,-}};
void init()
{
int i,j,x,y,t,temp;
for(i=;i<;++i)
{
for(j=;j<;++j)
{
temp = ;
temp ^= (<<((-i)*+-j)); //第一行代表16位的高4位,同理第一列也代表高位,所以棋盘(i,j)处在16位中的位置是((3-i)*4+3-j) for(t=;t<;++t)
{
x = i + dir[t][];
y = j + dir[t][];
if(x< || y< || x> || y>)
continue;
temp ^= (<<((-x)*+-y));
}
cout<<temp<<" ";
}
cout<<endl;
}
}

3、判断是否满足情况只需要判断state == 0 || state == 65535 即可。

4、解决了小问题,再思考一下大逻辑:

初始状态即为纯色则直接输出0,初始不是纯色,翻转一个的状态是纯色,则输出1,翻转一个会产生n种状态,0<=n<=16,这些状态要放入到队列中进行保存,再从中出队列再进行下一次的翻转。关键问题是什么情况下判定翻转结束,仍然没有纯色出现,则输出impossible,大于2次的翻转的初态都是从队列中取出来的,必须提前设计一个状态标识,表明同一种状态不再次进行入队列,那么当队列为空时才可以下结论。

 //需要设置一个是否处理过的标识,共有状态有65536个,即0-65535,
//如果对应位置被标记为1了,则说明已经处理过,直接抛弃进行下一次 //这样的遍历过程被称为BFS的过程 bool visit[]; int bfs(int state)//返回值是进行的步数,impossible为-1
{
queue<Node> q;
Node current,next; current.state = state;
current.step = ;
q.push(current);
visited[state] = true; while(!q.empty())
{
current = q.front();
q.pop(); if(current.state == || current.state == )
return current.step; for(int i = ;i<;i++)//每一种状态都要进行16次操作
{
next.state = current.state^change[i];
next.step = current.step+; if(next.state == || next.state == )
return next.step;
else
{
if(visited[next.state])
continue;
else
{
visited[next.state] = true;
q.push(next);
}
}
}
}
return -;
}

总结:枚举,此题就是使用枚举的思想来找到需要的解,尤其是使用队列来进行一个的while循环来进行的。

这里记录一下全部代码:

 #include <iostream>
#include <stdio.h>
#include<queue> using namespace std; int change[] ={
,,,,
,,,,
,,,,
,,,
}; struct Node
{
int state;
int step;
};
int state = ;
bool visited[]; int bfs(int state)//返回值是进行的步数,impossible为-1
{
queue<Node> q;
Node current,next; current.state = state;
current.step = ;
q.push(current);
visited[state] = true; while(!q.empty())
{
current = q.front();
q.pop(); if(current.state == || current.state == )
return current.step; for(int i = ;i<;i++)//每一种状态都要进行16次操作的
{
next.state = current.state^change[i];
next.step = current.step+; if(next.state == || next.state == )
return next.step;
else
{
if(visited[next.state])
continue;
else
{
visited[next.state] = true;
q.push(next);
}
}
}
}
return -;
} int main()
{
char ch[];
while(cin>>ch)
{
for(int j = ; j < ; j++)
{
state = state<<;
if(ch[j] == 'b')
state += ;
}
} memset(visited,false,sizeof(visited));
int count = bfs(state);
if(count == -)
cout<<"Impossible";
else
cout<<count;
return ;
}

下一个类似的题目是POJ的2965题目:The Pilots Brothers' refrigerator http://poj.org/problem?id=2965

Sample Input

-+--
----
----
-+--

Sample Output

6
1 1
1 3
1 4
4 1
4 3
4 4

分析:与上面的类似,需要解决的分步骤有如下几个:

1、一次状态改变的对应异或状态,共有16个

2、每一个Node包括状态值、进行的步骤以及达到此状态之前进行的步骤序列

3、成功的标识是全部都变成减号,即冰箱门打开。

分步骤:

1、生成16个异或数据+对应0,-对应1,每次进行同行同列的反转

相当于

0 0 0 0

0 0 0 0

0 0 0 0

0 0 0 0

在每一个位置进行变换后的状态记录,正好对应异或的16个数据

 int change[] ={
, , , ,
, , , ,
, , , ,
, , ,
};

2、给出Node的定义,包括三个数据

 struct Node
{
int state;
int step;
string seq;//使用string可以append操作步骤,每次添加2个,最后当成数组读出来即可
};

3、这里先使用与上面的方法一致的策略得出结果,可以得出正确结果,但是超时,超时,超时,Time Limit Exceeded!!!

代码在这里,思路与上面的题目一致!

 #include <iostream>
#include <stdio.h>
#include<queue>
using namespace std; int change[] ={
, , , ,
, , , ,
, , , ,
, , ,
}; struct Node
{
int state;
int step;
string seq;
};
int state = ;
bool visited[]; string int2str(int num)
{
if(num == )
return "";
string str = "";
int num_ = num > ? num : - * num;
while(num_)
{
str = (char)(num_ % + ) + str;
num_ /= ;
}
if(num < )
str = "-" + str;
return str;
} Node bfs(int state)//返回值是进行的步数,impossible为-1
{
queue<Node> q;
Node current,next; current.state = state;
current.step = ;
current.seq ="Z"; q.push(current);
visited[state] = true; while(!q.empty())
{
current = q.front();
q.pop(); if(current.state == )
return current; for(int i = ;i<;i++)//每一种状态都要进行16次操作的!!!!!
{
next.state = current.state^change[i];
next.step = current.step+;
//这里添加上操作的步骤序列
next.seq = "Z";
string tmpi = int2str(i/ + );
string tmpj = int2str(i% + ); next.seq = current.seq;
next.seq.append(tmpi);
next.seq.append(tmpj); if(next.state == )
return next;
else
{
if(visited[next.state])
continue;
else
{
visited[next.state] = true;
q.push(next);
}
}
}
}
current.step = -;
return current;
} int main()
{
freopen("data.in", "r", stdin); char ch[];
while(cin>>ch)
{
for(int j = ; j < ; j++)
{
state = state<<;
if(ch[j] == '-')
state += ;
}
} Node count = bfs(state);
if(count.step != -)
{
cout<<count.step<<endl;
for(int tt = ;tt<count.seq.length();tt+=)
{
cout<<count.seq[tt]<<" "<<count.seq[tt+]<<endl;
}
} fclose(stdin);
return ;
}

4、需要重新分析题目逻辑,是不是哪里的分析啰嗦了,导致占用了过多的耗时!!!

题目的操作是整行整列翻转,所以按照每一个位置进行操作会产生很多的冗余操作,看这里的分析:(感谢原作者hackbuteer1)

  1. 先看一个简单的问题,如何把'+'变成'-'而不改变其他位置上的状态?答案是将该位置(i,j)及位置所在的行(i)和列(j)上所有的handle更新一次,
  2. 结果该位置被更新了7次,相应行(i)和列(j)的handle被更新了6次,剩下的被更新了4次.
  3. 被更新偶数次的handle不会造成最终状态的改变.因此得出高效解法,在每次输入碰到'+'的时候自增该位置与相应的行和列,当输入结束后,遍历数组,所有为T的位置则是操作的位置, 而T位置的个数之和则是最终的操作次数.

这里做一个理解:就是最终单纯的将一个位置由+变成-,只需要将它本身改变一次即可,其它相当于状态不变。

上面的方法可能不好理解,可以考虑另外一种方法:即使用深度搜索的方法。

代码没能够完全调试通过,先放一下,后面再回来学习。

 #include <iostream>
#include <stdio.h>
#include<queue>
using namespace std; int change[] ={
, , , ,
, , , ,
, , , ,
, , ,
}; int state = ;
bool flag=false;
bool visited[]; int step;
int ri[],cj[]; void dfs(int bit, int deep)//深度遍历
{
if(deep==step)
{
flag = (state == );
return;
} if(flag || bit>)
return; ri[deep]=bit/;
cj[deep]=bit%; state = state ^ change[bit];
dfs(bit+,deep+); state = state ^ change[bit];
dfs(bit+,deep); return;
} int main()
{
freopen("data.in", "r", stdin); char ch[];
while(cin>>ch)
{
for(int j = ; j < ; j++)
{
state = state<<;
if(ch[j] == '-')
state += ;
}
} for(step = ;step<=;step++)//共16个操作点,按照每一个操作点进行深度遍历
{
dfs(,);
if(flag)//满足条件直接跳出
break;
} //输出
cout<<step<<endl;
for(int tt = ;tt<step;tt++)
{
cout<<ri[tt]+<<" "<<cj[tt]+<<endl;
} fclose(stdin);
return ;
}

学习总结:针对枚举法的总结

枚举法就是从可能的解中一一进行枚举,并使用给定的条件进行判定,找到满足条件的一个或者全部解即结束。枚举法本质上属于搜索的算法。枚举法的特点是算法简单,对于可确定的解的值域但是又没有很好地算法可以解决时就可以考虑使用枚举法,比较原始,运算量大,算法的时间复杂度是指数级别的,一定注意考虑在小范围内局部中使用枚举法效率会高一些!!!

另外针对广度优先BFS和深度优先DFS两个算法进行学习,后面的搜索算法,图搜索等二叉树搜索等会经常用到,需要深刻学习!!!