《挑战程序设计竞赛》之“反转”问题总结
萌新又来写总结了
“反转”问题在《挑战程序设计竞赛》P150左右的位置~
拒!绝!搜!索!
这类问题有以下几个特征:
1.大多集中在一维/二维两种情形,二维数据范围一般很小(毕竟需要一部分的枚举);是对一个区间/相邻几个格子进行反转;
2.一般用搜索(dfs/bfs)是无法在规定时间内完成的(除非数据太小);
3.每一个格子有两个状态(正/反),区间的反转顺序对最终的结果毫无影响;
4.对某一个格子进行两次以上的反转是多于的,所以只需要%2便可知道反转的结果。
借用一句话:
“反转法跟尺取法有点像,都是限定一个区间,往前挪动,尾部挪动之后,需要减掉尾部的影响。”
这里列举几个经典题目:
一维反转:
Face The Right Way POJ - 3276
The Water Bowls POJ - 3185
二维反转:
Fliptile POJ - 3279
The Pilots Brothers’ refrigerator POJ - 2965
Flip Game POJ - 1753
EXTENDED LIGHTS OUT POJ - 1222
A. Face The Right Way POJ - 3276
tips:我们可以认为,这“反转连续的K头牛”只能按照序号递增的顺序(比如k = 3 ,反转 7 6 8三头连续的牛,可以认为是以6号为起点,连续反转6 7 8 三头牛,顺序毫无影响)。即:问题被转化为求需要反转的区间的集合。
对于最后不足k头牛,直接进行判断,无需再反转(已经不够k头牛了)。
#include<cstdio>
#include<cstring>
const int maxn = 5005;
int book[maxn],flip[maxn],n,k,m,ans,reck;
char c;
int solve(int k){
memset(flip,0,sizeof(flip));
int sum = 0, res = 0; //res用来统计答案(反转次数)
for(int i = 1; i <= n - k + 1; ++i){
if((book[i] + sum) % 2 == 0) flip[i] = 0;
else sum++, res++, flip[i] = 1;
if(i - k >= 0) sum -= flip[i - k + 1]; //为下一个位置做准备
}
for(int i = n - k + 2; i <= n; ++i){ //直接判断
if((book[i] + sum) % 2 != 0) return -1;
if(i - k >= 0) sum -= flip[i - k + 1];
}
return res;
}
int main(){
scanf("%d",&n); ans = 1e9;
for(int i = 1; i <= n; ++i){
getchar();
scanf("%c",&c);
book[i] = (c == 'B' ? 1 : 0);
}
for(k = 1; k <= n; ++k){
int tmp = solve(k);
if(tmp != -1 && ans > tmp){
ans = tmp;
reck = k;
}
}
printf("%d %d\n",reck,ans);
return 0;
}
B.The Water Bowls POJ - 3185
tips:和上一个题相似,只是限定了每次反转自己和相邻2个格子。可以直接运算,不用像上一题那样用sum记录。【唯一的区别在于,如果是两头的格子,只能反转2个;中间的格子可以反转3个,k不是固定值】
注意分类讨论:第一个格子不反转/第一个格子反转。
我们认为,从最左端开始,当某一个格子定下来之后,只有后一个格子能改变他(因为他前面的和他已经定下来了),因此根据当前格子的状态可以唯一确定下一个格子的反转次数。
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int book[25],flip[25],minn,tmp[25];
void solve1(){ //第一个格子不反转
int cnt = 0;
memcpy(tmp, book, sizeof(book));
memset(flip, 0,sizeof(flip));
for(int i = 2; i <= 20; ++i){
if((flip[i - 1] + tmp[i - 1]) % 2 == 0) continue; //当前格子反转与否,取决于前一个格子的状态
else flip[i]++, tmp[i + 1]++, cnt++; //当前格子反转,下一个格子的 状态也会受影响,用tmp++表示。之所以用memcpy,是不想改变原来的数据
}
if((flip[20] + tmp[20]) % 2 == 0)
minn = min(minn, cnt);
}
void solve2(){ //第一个格子反转
int cnt = 0;
memset(flip, 0,sizeof(flip));
memcpy(tmp, book, sizeof(book));
flip[1] = 1, tmp[2]++, cnt++;
for(int i = 2; i <= 20; ++i){
if((flip[i - 1] + tmp[i - 1]) % 2 == 0) continue;
else flip[i]++, tmp[i + 1]++, cnt++;
}
if((flip[20] + tmp[20]) % 2 == 0)
minn = min(minn, cnt);
}
int main(){
minn = 1e5;
for(int i = 1; i <= 20; ++i)
scanf("%d",&book[i]);
solve1(); solve2();
printf("%d",minn);
return 0;
}
/*
0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
2
*/
也可以借鉴A题的写法,两次处理(第一个元素改/不改)。注意修改最后一个数据时,只能修改2个,所以判断只是对最后一个进行判断,而不是从第n-k+2个。
另一种写法:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn = 25;
int book[maxn],filp[maxn],n,k,minn;
int solve(int k){
memset(filp,0,sizeof(filp));
int sum = 0, res = 0;
for(int i = 1; i <= n - 1; ++i){
if((book[i] + sum) % 2 == 0) filp[i] = 0;
else sum++, res++, filp[i] = 1;
if(i - k >= 0) sum -= filp[i - k + 1];
}
return (book[n] + sum) % 2 != 0 ? -1 : res;
}
int main(){
n = 20;
for(int i = 1; i <= n; ++i)
scanf("%d",&book[i]);
int tmp1 = solve(3); //k = 3, 第一个不反转
book[1]++, book[2]++; //第一个反转相当于次数+1,第1、2个数据+1
int tmp2 = solve(3) + 1;
printf("%d",min(tmp1,tmp2));
return 0;
}
C.Fliptile POJ - 3279
非常经典的二维反转。因为n,m最大可以到15,用dfs/bfs必会TLE。
每一个格子可以改变自己和上下左右,所以无法向一维反转那样做(因为能改变(1,1)的还可以有(1,2)和(2,1),而上一个题让最左端牛改变的方法只有一种,因为只有一个区间包含1)。
我们可以枚举第一行的所有反转情况,第一行的反转情况定下后,只有其下一行可以改变上一行的状态,以此类推,直到最后一行,然后检查最后一行是否都为0即可。
tips:状态压缩,用二进制表示集合的枚举。
#include<cstdio>
#include<cstring>
using namespace std;
const int maxn = 20;
int n,m,book[maxn][maxn],flip[maxn][maxn],ans[maxn][maxn];
int dir[5][2] = {{1,0},{-1,0},{0,0},{0,1},{0,-1}};
int getcolor(int x, int y){
int c = book[x][y];
for(int k = 0; k < 5; ++k){
int tx = x + dir[k][0], ty = y + dir[k][1];
if(tx < 1 || ty < 1 || tx > m || ty > n) continue;
c += flip[tx][ty];
}
return c % 2;
}
int calc(){
for(int i = 2; i <= m; ++i)
for(int j = 1; j <= n; ++j)
if(getcolor(i - 1, j)) flip[i][j] = 1;
for(int j = 1; j <= n; ++j)
if(getcolor(m, j)) return -1;
int cnt = 0;
for(int i = 1; i <= m; ++i)
for(int j = 1; j <= n; ++j)
cnt += flip[i][j];
return cnt;
}
void solve(){
int res = -1;
for(int s = 0; s < 1 << n; ++s){
memset(flip, 0, sizeof(flip));
for(int j = 1; j <= n; ++j)
flip[1][j] = s >> (j - 1) & 1;
int num = calc();
if(num >= 0 && (res < 0 || res > num)){
res = num;
memcpy(ans, flip, sizeof(flip));
}
}
if(res < 0) printf("IMPOSSIBLE\n");
else{
for(int i = 1; i <= m; ++i)
for(int j = 1; j <= n; ++j)
printf("%d%c",ans[i][j], j == n ? '\n' : ' ');
}
}
int main(){
scanf("%d%d",&m,&n);
for(int i = 1; i <= m; ++i)
for(int j = 1; j <= n; ++j)
scanf("%d",&book[i][j]);
solve();
return 0;
}
D.Flip Game POJ - 1753
tips:差不多的题,只是n = m = 4,数据范围大幅度缩小,且“翻成同一个面”可以都是正,也可以都是反,分两类讨论即可。
数据范围太小导致dfs也可以过,但是还是反转更优一些。
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int book[10][10],flip[10][10];
int dir[5][2] = {{1,0},{-1,0},{0,0},{0,1},{0,-1}};
char c;
int getcolor(int x ,int y){
int c = book[x][y];
for(int k = 0; k < 5; ++k){
int tx = x + dir[k][0], ty = y + dir[k][1];
if(tx < 1 || ty < 1 || tx > 4 || ty > 4) continue;
c += flip[tx][ty];
}
return c % 2;
}
int calc1(){
for(int i = 2; i <= 4; ++i)
for(int j = 1; j <= 4; ++j)
if(getcolor(i - 1, j)) flip[i][j] = 1;
for(int j = 1; j <= 4; ++j)
if(getcolor(4, j)) return -1;
int cnt = 0;
for(int i = 1; i <= 4; ++i)
for(int j = 1; j <= 4; ++j)
cnt += flip[i][j];
return cnt;
}
int calc2(){
for(int i = 2; i <= 4; ++i)
for(int j = 1; j <= 4; ++j)
if(getcolor(i - 1, j) == 0) flip[i][j] = 1;
for(int j = 1; j <= 4; ++j)
if(getcolor(4, j) == 0) return -1;
int cnt = 0;
for(int i = 1; i <= 4; ++i)
for(int j = 1; j <= 4; ++j)
cnt += flip[i][j];
return cnt;
}
void solve(){
int res1 = -1, res2 = -1;
for(int s = 0; s < 1 << 4; ++s){
memset(flip, 0, sizeof(flip));
for(int j = 1; j <= 4; ++j)
flip[1][j] = s >> (j - 1) & 1;
int num1 = calc1();
if(num1 >= 0 && (res1 == -1 || res1 > num1)) res1 = num1;
}
for(int s = 0; s < 1 << 4; ++s){
memset(flip, 0, sizeof(flip));
for(int j = 1; j <= 4; ++j)
flip[1][j] = s >> (j - 1) & 1;
int num2 = calc2();
if(num2 >= 0 && (res2 == -1 || res2 > num2)) res2 = num2;
}
if(res1 == -1 && res2 == -1) printf("Impossible\n");
else if(res1 == -1) printf("%d",res2);
else if(res2 == -1) printf("%d",res1);
else printf("%d",min(res2, res1));
}
int main(){
for(int i = 1; i <= 4; ++i){
for(int j = 1; j <= 4; ++j){
scanf("%c",&c);
book[i][j] = (c == 'b') ? 1 : 0; //b是1
}
getchar();
}
solve();
return 0;
}
E.EXTENDED LIGHTS OUT POJ - 1222
tips:同3279.多组输入,且固定m = 5, n = 6。
#include<cstdio>
#include<cstring>
using namespace std;
const int maxn = 10;
int t,n,m,book[maxn][maxn],flip[maxn][maxn],ans[maxn][maxn];
int dir[5][2] = {{1,0},{-1,0},{0,0},{0,1},{0,-1}};
int getcolor(int x, int y){
int c = book[x][y];
for(int k = 0; k < 5; ++k){
int tx = x + dir[k][0], ty = y + dir[k][1];
if(tx < 1 || ty < 1 || tx > m || ty > n) continue;
c += flip[tx][ty];
}
return c % 2;
}
int calc(){
for(int i = 2; i <= m; ++i)
for(int j = 1; j <= n; ++j)
if(getcolor(i - 1, j)) flip[i][j] = 1;
for(int j = 1; j <= n; ++j)
if(getcolor(m, j)) return -1;
int cnt = 0;
for(int i = 1; i <= m; ++i)
for(int j = 1; j <= n; ++j)
cnt += flip[i][j];
return cnt;
}
void solve(){
int res = -1;
for(int s = 0; s < 1 << n; ++s){
memset(flip, 0, sizeof(flip));
for(int j = 1; j <= n; ++j)
flip[1][j] = s >> (j - 1) & 1;
int num = calc();
if(num >= 0 && (res < 0 || res > num)){
res = num;
memcpy(ans, flip, sizeof(flip));
}
}
if(res < 0) printf("IMPOSSIBLE\n");
else{
for(int i = 1; i <= m; ++i)
for(int j = 1; j <= n; ++j)
printf("%d%c",ans[i][j], j == n ? '\n' : ' ');
}
}
int main(){
scanf("%d",&t);
for(int x = 1; x <= t; ++x){
m = 5, n = 6;
for(int i = 1; i <= m; ++i)
for(int j = 1; j <= n; ++j)
scanf("%d",&book[i][j]);
printf("PUZZLE #%d\n",x);
solve();
}
return 0;
}
F.The Pilots Brothers’ refrigerator POJ - 2965
tips:有点变化。以上的二维反转都是只改变自己和上下左右,这个题除了改变自己,还会改变所在行、所在列的所有格子。固定大小4*4.
一个重要的发现:如第一行第二个是+,那个只要他所在的行和列的格子全都翻转一次,就会让当前的 + 变成 - ,但是其他格子全部都不会发生变化。(证明:某一个格子所在行/列一共加起来的反转次数如果是偶数,该格子不变;奇数,该格子反转。上述操作后,整个棋盘除了该格子反转次数是奇数(4 + 3 == 7),其余位置均为偶数。可以动手算一下)
于是,发现+格子,就把他所在行列所有格子的flip++,最后答案就是所有格子的(flip%2)相加。
4*4用dfs也能过,但是用反转的思想显然比dfs快很多。
#include<cstdio>
#include<utility>
#include<vector>
using namespace std;
int book[5][5], flip[5][5],ans;
char c;
vector<pair<int,int> > v;
vector<pair<int,int> > :: iterator it;
void change(int x, int y){
for(int i = 1; i <= 4; ++i) flip[x][i]++;
for(int i = 1; i <= 4; ++i) flip[i][y]++;
flip[x][y]--;
}
int main(){
for(int i = 1; i <= 4; ++i){
for(int j = 1; j <= 4; ++j)
scanf("%c",&c), book[i][j] = (c == '+') ? 1 : 0;
getchar();
}
for(int i = 1; i <= 4; ++i){
for(int j = 1; j <= 4; ++j){
if(book[i][j]) change(i, j);
}
}
for(int i = 1; i <= 4; ++i)
for(int j = 1; j <= 4; ++j){
ans += flip[i][j] % 2;
if(flip[i][j] % 2) v.push_back(make_pair(i, j));
}
printf("%d\n",ans);
for(it = v.begin(); it != v.end(); ++it)
printf("%d %d\n",it->first, it->second);
return 0;
}