算法概览:
例.给定一个01串,现有翻转规则:翻转某一个位置时其后面2个位置也会跟着翻转,也就是每次翻转都会翻转3个连续的位置。要将01串全部翻转为0,求最小的翻转次数
形似这类题的问题叫做翻转问题,也可以叫开关问题,对于这类题通常有以下的特点,思考一下就可以想到。
1.交换区间得反转顺序队结果没有影响。
2.此外可以知道对于同一个区间进行两次以上得反转是多余的。
3.由此,反转问题(也叫开关问题)就转化成求反转区间的集合,常常和枚举一起服用。
例子:
1. poj 3276
题意:
N个牛 每个都有一定的方向 B背对 F表示头对着你 给你一个装置 每次可以选择连续的K个牛反转方向 问你如何选择K 使得操作数最少 k也应尽量小.
思路:
定义 f[i]:区间[i,i+k-1]进行反转的话就为1,否则为0
分析:
即先反转1到3号牛, 然后在反转3到5号, 最后反转5号到7号。 步骤如下:
BBF BFBB ----------> FFB BFBB ---------> FF FFB BB -----> FFFF FFF
我们看出, 对一个区间(大小为K, 当然连续)反转两次以上是多余的, 也就是说需要反转, 一次就够, 在反, 就等于没反了。
于是问题就变成了求解需要被反转的区间的集合。 我们首先考虑最左端的牛, 包含这头牛的区间只有一个, 即[0, K - 1], 如果这头牛面超前,
就不需要反转, 否则, 这头牛面朝后, 当然这个区间(即[0, K - 1])需要被反转了。这样问题的规模就变为n - 1, 不断的重复下去, 即可以求
出反转的次数了。
忽略掉每个区间重复反转的多与操作,只要存在所有牛面朝前的办法, 那么操作就和顺序无关了, 因为每个区间只反转一次或者不反转。
算法复杂度如何呢?
对所有的K(K 从1 到N)遍历搜索一次, 最坏情况下, 需要进行 N - K + 1次的反转(最后K- 1个无法反转, 要考虑可行性, 因为每次
必须要转K头牛, 而不能少于K头牛), 每次反转需要操作K头牛, 所以总的复杂度为O(N^3)。
但是其实我们并不需要每次非要操作K头牛真的反转, 我们可以通过记录下每个区间是否反转了这一信息, 避免去做每一次的K头牛反转这一工作。 这样我们可以把时间复杂度降为O(N^2)。
具体的, 记录 f[i] = 区间[i, i + K - 1]是否进行了反转, 若反转了 f[i] = 1, 没有反转则记录 f[i] = 0, 这样我们考虑第i 头牛时(方向dir[i], 记为0的时候, 表示朝前, 为1 表示朝后), 如果 ∑f[j], 从 i - K + 1 累加到 i- 1, 若这个sum 为奇数, 表明i头牛已经进行了奇数次的反转, 也就是说, 此时这头牛的方向和其最开始的方向相反, 同理, 若这个sum为偶数, 表明这头牛已经进行了偶数次的反转, 也就是此时这头牛的方向dir[i]和其最开始的方向相同。
我们有如下的观察:
∑ f[j] = ∑f[j] + f[i] - f[i - K + 1]
注意, 第一个的求和 从 (i + 1) - K + 1 到 i
第二个求和从 i - K + 1 到 i - 1
有个上述公式, 我们就可以每一次在常数时计算上述的sum了。
另外我们有如下的观察:
dir[i] + sum <---------> 0 + 奇数, 为奇数, 表示还需要反转一次(原方向就是朝前的了, 已经进行奇数次反转, 变成了朝后)
dir[i] + sum <---------> 0 + 偶数, 为偶数, 表示不用反转了
dir[i] + sum <---------> 1 + 奇数, 为偶数, 表示不需要反转了, 原方向为1, 奇数次后为超前(即0)
dir[i] + sum <---------> 0 + 奇数, 为奇数, 表示还需要反转一次, 原方向为0, 已经朝前了。
综上, 我们总结, 当dir[i] + sum 为奇数的时候, 就需要反转, 为偶数时, 就不需要反转了。
总的程序如下:
#include <iostream> #include <cstring> #include <cstdio> using namespace std; const int MAX_N = 5000 + 10; int N; int dir[MAX_N]; int f[MAX_N]; int calc(int K) { // 反转区间大小 memset(f, 0, sizeof(f)); int res = 0; // 记录反转次数 int sum = 0; // f[i]记录区间大小为K, 从i开始的和 for(int i = 0; i + K <= N; i++) { // i = 0, 表示最左边的那头牛 // 计算区间[i, i + K - 1] if((dir[i] + sum) % 2 != 0) { res++; f[i] = 1; } sum += f[i]; if(i - K + 1 >= 0) { sum -= f[i - K + 1]; } } // 检查最后面的情况(K- 1)头牛 for(int i = N - K + 1; i < N; i++) { if((dir[i] + sum) % 2 != 0) { // 无解 return -1; } if(i - K + 1 >= 0) { sum -= f[i - K + 1]; } } return res; } int main() { while(scanf("%d", &N) != EOF) { for(int i = 0; i < N; i++) { scanf("%d", &dir[i]); } int K = 1; int M = N; for(int k = 1; k <= N; k++) { int m = calc(k); if(m >= 0 && M > m) { M = m; K = k; } } printf("%d %d ", K, M); } }
2. poj 3279
题意:
一个M*N的黑白棋棋盘摆满棋子,每次操作可以反转一个位置和其上下左右共五个位置的棋子的颜色,求要使用最少翻转次数将所有棋子反转为黑色所需翻转的是哪些棋子.
思路:
和 1 题一样,同一个格子(位置)翻转两次的话就会恢复原状,所以多次翻转是多余的。此外,翻转的格子的集合相同的话,其次序是无关紧要的。因此总共有 2NM 种方法,显然效率太低。
联想到上一道题,让最左端的牛反转的方法只有一种,不妨先看一下最左上角的格子。在这里,除了翻转 (1, 1)之外,翻转(1, 2)和(2, 1)也可以把这个格子翻转,所以像之前那样直接确定的办法行不通。
于是不妨先指定好最上面一行的翻转方法。此时能够翻转(1, 1) 的只剩下(2, 1)了,所以可以直接判断(2, 1)是否需要翻转。类似地(2, 1)~(2, N)都能这样判断,如此反复下去就可以确定所有的格子的翻转方法。最后(M, 1)~(M, N)如果并非全为白色,就意味着不存在可行的操作方法。
像这样,先确定第一行的翻转方式,然后可以很容易判断这样是否存在解以及解的最小步数是多少,这样将第一行的所有的翻转方式都尝试一次之后就能求出整个问题的最小步数。这个算法中最上面一行的翻转方式共有 2N 种,复杂度为O(MN 2N)
为什么要用搜索?因为n和m很小,最大是15
为什么要用二进制搜索?因为是枚举状态,而且是每行每列的状态
怎么样枚举最好?枚举第一行,然后依次遍历其他行,枚举第一行的复杂度是2的n次方,然后遍历全矩阵式n*m,整个复杂度相乘没有问题
怎么搜索呢?用一个整数表示1行的状态,比如255=11111111(2),那么循环就是从0到255表示256种情况,255的第0位至第7位均是1,所以呢,表示全部需要翻,然后从第二行到第n-1行依次推理得需要或者不需要翻,再用最后一行来检验方案是不是可行
#include<cstdio> #include<cstring> 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; int ty=dy[i]+y; if(tx>=0&&tx<m&&ty>=0&&ty<n) t+=tmp[tx][ty]; } return t%2; } // 求出第一行确定情况下的最小操作次数 int cal(){ // 求出从第二行开始的翻转方法 for(int i=1;i<m;i++){ for(int j=0;j<n;j++){ // (i-1,j)为黑色时,则必须翻转这个格子 if(get(i-1,j)) tmp[i][j]=1; } } // 判断最后一行是否为全白 for(int j=0;j<n;j++){ // 无解 if(get(m-1,j)!=0) return n*m+1; } // 求翻转次数 int res=0; for(int i=0;i<m;i++){ for(int j=0;j<n;j++){ res+=tmp[i][j]; } } return res; } int main(void){ while(~scanf("%d%d",&m,&n)){ cnt=n*m+1; for(int i=0;i<m;i++){ for(int j=0;j<n;j++){ scanf("%d",&M[i][j]); } } // 按照字典序尝试第一行的所有可能 for(int i=0;i < (1<<n);i++){ memset(tmp,0,sizeof(tmp)); for(int j=0;j<n;j++){ tmp[0][j]=i >> j & 1; } int t=cal(); if(t<cnt){ cnt=t; memcpy(ans,tmp,sizeof(tmp)); } } if(cnt==n*m+1){ printf("IMPOSSIBLE "); } else{ for(int i=0;i<m;i++){ for(int j=0;j<n;j++){ printf("%d%c",ans[i][j],j+1==n ? ' ':' '); } } } } }
4. poj 3185
题意:
将一列碗翻成口朝上,翻转一次可能同时反转3个或2个(首尾),求最小翻转次数。
思路:
这题分析一下,可以知道选择从第一个开始翻,还是从第二个开始翻,会导致两种不同的状态和结果,但都是唯一的。所以就枚举第一个还是第二个开始翻,然后从左往右依次判断,接下来每个点需不需要翻转取决它左边是不是朝下。
// #include<bits/stdc++.h> #include <cstdio> #include <iostream> #include <algorithm> #include <cstring> // for memset #include <queue> // priority_queue #include <map> #include <set> // multiset set<int,greater<int>>大到小 #include <vector> // vector<int>().swap(v);清空释放内存 #include <stack> #include <cmath> // auto &Name : STLName Name. #include <utility> #include <sstream> #include <string> //__builtin_popcount(ans);//获取某个数二进制位1的个数 using namespace std; #define rep(i,a,n) for(int i=a;i<=n;i++) #define per(i,a,n) for(int i=n;i>=a;i--) #define read_a_int(x) scanf("%d",&x) #define Read(x,y) scanf("%d%d",&x,&y) typedef long long ll; const int INF = 0x3f3f3f3f; const int mod1e9 = 1000000007; const int mod998 = 998244353; const int mod = mod1e9; const int MAX_N = 5000 + 10; int bows[25],flip[25]; int main(void) { while(~scanf("%d",&bows[0])){ int ans=20,tmp=0; memset(flip,0,sizeof(flip)); for(int i=1;i<20;i++) scanf("%d",&bows[i]); flip[0]=1; tmp++; for(int i=1;i<20;i++) if(flip[i]=(flip[i-1]^bows[i-1]^flip[i-2])) tmp++; if((flip[18]^flip[19]^bows[19])==0 && tmp<ans) ans=tmp; flip[0]=0; tmp=0; for(int i=1;i<20;i++) if(flip[i]=(flip[i-1]^bows[i-1]^flip[i-2])) tmp++; if((flip[18]^flip[19]^bows[19])==0 && tmp<ans) ans=tmp; printf("%d ",ans); } return 0; }
4. poj 1222
题意:
有一个5 * 6的矩阵,每个位置表示灯,1表示灯亮,0表示灯灭。然后如果选定位置 i,j 点击,则位置 i,j 和其上下左右的灯的状态都会反转。现在要你求出一个5 * 6的矩阵,1表示这个灯被点击过,0表示没有。要求这个矩阵能够使得原矩阵的灯全灭。
思路:
和poj3279一模一样
#include<cstdio> #include<cstring> #include<algorithm> 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(num<ans){ ans=num; memcpy(as,f,sizeof(f)); } } printf("PUZZLE #%d ",cnt++); for(int i=0;i<5;i++){ for(int j=0;j<6;j++){ printf("%d%c",as[i][j],j==5? ' ':' '); } } } }