例.给定一个01串,现有翻转规则:翻转某一个位置时其后面2个位置也会跟着翻转,也就是每次翻转都会翻转3个连续的位置。要将01串全部翻转为0,求最小的翻转次数
形似这类题的问题叫做翻转问题,也可以叫开关问题,对于这类题通常有以下的特点,思考一下就可以想到。
**1.交换区间得反转顺序队结果没有影响。
2.此外可以知道对于同一个区间进行两次以上得反转是多余的。
3.由此,反转问题(也叫开关问题)就转化成求反转区间的集合,常常和枚举一起服用。**
比如以下的题目:
poj3276
题意:
N个牛 每个都有一定的方向 B背对 F表示头对着你 给你一个装置 每次可以选择连续的K个牛反转方向 问你如何选择K 使得操作数最少 k也应尽量小.
思路:
定义 f[i]:区间[i,i+k-1]进行反转的话就为1,否则为0
区间反转部分很好优化:
在考虑第i头牛时候,如果 ∑i−1j=(i−K+1)f[j] ∑ j = ( i − K + 1 ) i − 1 f [ j ] 和为奇数,就说明此时这个牛方向与最初相反。
由于
∑ij=(i+1)−K+1f[j] ∑ j = ( i + 1 ) − K + 1 i f [ j ] = ∑i−1j=(i−K+1)f[j] ∑ j = ( i − K + 1 ) i − 1 f [ j ] +f[i]-f[i-K+1]
所以这个每一次都可以用常数算出来,时间复杂度O(n^2)
#include
#include
#include
using namespace std;
const int N=5000+10;
int f[N],dir[N],n;
int solve(int k){
int cnt=0,sum=0;//sum为f的和
memset(f,0,sizeof(f));
for(int i=1;i<=n-k+1;i++){
if((dir[i]+sum)%2){
cnt++;
f[i]=1;
}
sum+=f[i];
if(i-k+1>=1) sum-=f[i-k+1];
}
for(int i=n-k+2;i<=n;i++){//检查剩下的牛有没有朝后面的情况
if((dir[i]+sum)%2) return n+1;
if(i-k+1>=1) sum-=f[i-k+1];
}
return cnt;
}
int main(){
while(~scanf("%d",&n)){
for(int i=1;i<=n;i++){
char c;scanf(" %c",&c);
if(c=='B') dir[i]=1;
}
int ansk,ansm=n,t;
for(int i=1;i<=n;i++){
t=solve(i);
if(tprintf("%d %d\n",ansk,ansm);
}
}
poj3279
题意:
一个M*N的黑白棋棋盘摆满棋子,每次操作可以反转一个位置和其上下左右共五个位置的棋子的颜色,求要使用最少翻转次数将所有棋子反转为黑色所需翻转的是哪些棋子.
思路:
在上面的那道题,让最左端的奶牛反转的情况只有一种,于是直接判断的方法就可以确定,但是这里不一样,比如,看最左上面的角,除了反转(1,1),(1,2),(2,1)都可以导致他翻装。
于是不妨我们先确定最上面一行的反装方式,此时能反转(1,1)只有(2,1),
所以如果已知第一行就可以知道第二行那些点需要反转。这样反复下去,只要最后一行全部为白,就说明可行。
那么这个算法时间复杂度是(N*M*2^N).
#include
#include
using namespace std;
const int dx[5]={-1,0,0,0,1};
const int dy[5]={0,-1,0,1,0};
int m,n,M[20][20],tmp[20][20],ans[20][20],cnt;
int get(int x,int y){
int t=M[x][y];
for(int i=0;i<5;i++){
int tx=dx[i]+x,ty=dy[i]+y;
if(tx>=0&&tx=0&&tyreturn t%2;
}
int cal(){
for(int i=1;ifor(int j=0;jif(get(i-1,j)) tmp[i][j]=1;
}
}
for(int j=0;jif(get(m-1,j)!=0) return n*m+1;
}
int res=0;
for(int i=0;ifor(int j=0;jreturn res;
}
int main(){
while(~scanf("%d%d",&m,&n)){
cnt=n*m+1;
for(int i=0;ifor(int j=0;jscanf("%d",&M[i][j]);
}
}
for(int i=0;i<(1<memset(tmp,0,sizeof(tmp));
for(int j=0;j0][j]=i>>j&1;
}
int t=cal();
if(tmemcpy(ans,tmp,sizeof(tmp));
}
}
if(cnt==n*m+1){
printf("IMPOSSIBLE\n");
}
else{
for(int i=0;ifor(int j=0;jprintf("%d%c",ans[i][j],j+1==n ? '\n':' ');
}
}
}
}
}
poj3185
题意:
翻盖有奖:将一列碗翻成口朝上,一把下去可能同时反转3个或2个(首尾),求最小翻转次数。
思路:
这题分析一下,可以知道选择从第一个开始翻,还是聪第二个开始翻,会导致两种不同的状态和结果,但都是唯一的。所以就枚举第一个还是第二个开始翻,然后从左往右依次判断,接下来每个点需不需要翻转取决它左边是不是朝下。
#include
#include
#include
using namespace std;
int dir[25],f[25];
int main(){
while(~scanf("%d",&dir[0])){
int tmp=0,ans=20;
memset(f,0,sizeof(f));
for(int i=1;i<20;i++) scanf("%d",&dir[i]);
f[0]=1;tmp++;
for(int i=1;i<20;i++){
if(f[i]=(f[i-2]^f[i-1]^dir[i-1])) tmp++;
}
if((f[18]^f[19]^dir[19])==0&&tmp0;f[0]=0;
for(int i=1;i<20;i++){
if(f[i]=(f[i-2]^f[i-1]^dir[i-1])) tmp++;
}
if(f[18]^f[19]^dir[19]==0&&tmpprintf("%d\n",ans);
}
}
poj1222
题意:
有一个5 * 6的矩阵,每个位置表示灯,1表示灯亮,0表示灯灭。
然后如果选定位置i,j点击,则位置i,j和其上下左右的灯的状态都会反转。
现在要你求出一个5 * 6的矩阵,1表示这个灯被点击过,0表示没有。
要求这个矩阵能够使得原矩阵的灯全灭。
思路:
和poj3279一模一样
#include
#include
#include
using namespace std;
int m[8][8],f[8][8],as[8][8],dir[5][5]={-1,0,0,0,0,1,1,0,0,-1};
int get(int x,int y){
int c=m[x][y];
for(int i=0;i<5;i++){
int tx=x+dir[i][0];
int ty=y+dir[i][6];
c+=f[tx][ty];
}
return c%2;
}
int calc(){
for(int i=1;i<5;i++){
for(int j=0;j<6;j++){
if(get(i-1,j)==1){
f[i][j]=1;
}
}
}
for(int j=0;j<6;j++){
if(get(4,j)!=0) return 30;
}
int res=0;
for(int i=0;i<5;i++){
for(int j=0;j<6;j++){
res+=f[i][j];
}
}
return res;
}
int main(){
int T,cnt=1;scanf("%d",&T);
while(T--){
for(int i=0;i<5;i++){
for(int j=0;j<6;j++) scanf("%d",&m[i][j]);
}
int ans=31;
for(int i=0;i<1<<6;i++){
memset(f,0,sizeof(f));
for(int j=0;j<6;j++){
f[0][6-j-1]=i>>j&1;
}
int num=calc();
if(nummemcpy(as,f,sizeof(f));
}
}
printf("PUZZLE #%d\n",cnt++);
for(int i=0;i<5;i++){
for(int j=0;j<6;j++){
printf("%d%c",as[i][j],j==5? '\n':' ');
}
}
}
}
后记:往往开关问题可以转换成矩阵求解一组方程的解是否存在,用高斯消元求解,并且通过这些分析知道,当自由变员不超过N个时候,也可以用来求解最优解。