说说算法题的那些事儿(2)

问题求解常见策略

偶数矩阵(Even Parity, UVa 11464)
给你一个n×n的01矩阵(每个元素非0即1),你的任务是把尽量少的0变成1,使得每个元素的上、下、左、右的元素(如果存在的话)之和均为偶数。比如,如图(a)所示的矩阵至少要把3个0变成1,最终如图(b)所示,才能保证其为偶数矩阵。
这里写图片描述
(a) (b)

【输入格式】
输入的第一行为数据组数T(T≤30)。每组数据的第一行为正整数n(1≤n≤15);接下来的n行每行包含n个非0即1的整数,相邻整数间用一个空格隔开。
【输出格式】
对于每组数据,输出被改变的元素的最小个数。如果无解,应输出-1。
【分析】
也许最容易想到的方法就是枚举每个数字“变”还是“不变”,最后判断整个矩阵是否满足条件。遗憾的是,这样做最多需要枚举2255≈5×1067种情况,实在难以承受。
注意到n只有15,第一行只有不超过215=32 768种可能,所以第一行的情况是可以枚举的。接下来根据第一行可以完全计算出第二行,根据第二行又能计算出第三行(想一想,如何计算),以此类推,这样,总时间复杂度即可降为O(2n×n2)。代码如下。

#include
#include
#include
using namespace std;

const int maxn = 20;
const int INF = 1000000000;
int n, A[maxn][maxn], B[maxn][maxn];

int check(int s) {
  memset(B, 0, sizeof(B));
  for(int c = 0; c < n; c++) {
    if(s & (1<0][c] = 1;
    else if(A[0][c] == 1) return INF; //1不能变成0
  }
  for(int r = 1; r < n; r++)
    for(int c = 0; c < n; c++) {
      int sum = 0; //元素B[r-1][c]的上、左、右3个元素之和
      if(r > 1) sum += B[r-2][c];
      if(c > 0) sum += B[r-1][c-1];
      if(c < n-1) sum += B[r-1][c+1];
      B[r][c] = sum % 2;
      if(A[r][c] == 1 && B[r][c] == 0) return INF; //1不能变成0
    }
  int cnt = 0;
  for(int r = 0; r < n; r++)
    for(int c = 0; c < n; c++) if(A[r][c] != B[r][c]) cnt++;
  return cnt;
}

int main() {
  int T;
  scanf("%d", &T);
  for(int kase = 1; kase <= T; kase++) {
    scanf("%d", &n);
    for(int r = 0; r < n; r++)
      for(int c = 0; c < n; c++) scanf("%d", &A[r][c]);

    int ans = INF;
    for(int s = 0; s < (1<if(ans == INF) ans = -1;
    printf("Case %d: %d\n", kase, ans);
  }
  return 0;
}

彩色立方体(Colored Cubes, Tokyo 2005, LA 3401)
有n个带颜色的立方体,每个面都涂有一种颜色。要求重新涂尽量少的面,使得所有立方体完全相同。两个立方体相同的含义是:存在一种旋转方式,使得两个立方体对应面的颜色相同。

【输入格式】
输入包含多组数据。每组数据的第一行为正整数n(1≤n≤4);以下n行每行6个字符串,分别为立方体编号为1~6的面的颜色(由小写字母和减号组成,不超过24个字符)。输入结束标志为n=0。立方体的6个面的编号如图所示。

说说算法题的那些事儿(2)_第1张图片

【输出格式】
对于每组数据,输出重新涂色的面数的最小值。
【分析】
立方体只有4个,暴力法应该可行。不过不管怎样“暴力”,首先得搞清楚一个立方体究竟有几种不同的旋转方式。
为了清晰起见,我们借用机器人学中的术语,用姿态(pose)来代替口语中的旋转方法。假设6个面的编号为1~6,从中选一个面作为“顶面”,然后在剩下的4个面中选一个作为“正面”,则其他面都可以唯一确定,因此有6×4=24种姿态。
在代码中,每种姿态对应一个全排列P。其中,P[i]表示编号i所在的位置(1表示正面,2表示右面,3表示顶面等,如图左所示)。如图右所示的姿态称为标准姿态,用排列{1,2,3,4,5,6}表示,因为1在正面,2在右面,3在顶面等。
说说算法题的那些事儿(2)_第2张图片
说说算法题的那些事儿(2)_第3张图片
图1-10是标准姿态向左旋转后得到的。对应的排列是{5,1,3,4,6,2}。
接下来有两种方法。一种方法是手工找出24种姿态对应的排列,编写到代码中。显然,这种方法比较耗时,且容易出错,不推荐使用。下面的方法可以用程序找出这24种排列,而且不容易出错。除了刚才写出的标准姿态向左翻之外,我们再写出标准姿态向上翻所对应的排列:{3,2,6,1,5,4},如图1-11所示。

图 1-10 图 1-11
注意到旋转是可以组合的,比如,图1-11标准姿态先向左转再向上翻就是55, 13, 36, 41, 64, 22,即{5, 3, 6, 1, 4, 2}。因此,有了这两种旋转方式,我们就可以构造出所有24种姿态了(均为从标准姿态开始旋转)。
1在顶面的姿态:向上翻1次(此时1在顶面),然后向左转0~3次。
2在顶面的姿态:向左转1次(此时2在顶面),向上翻1次,然后向左转0~3次。
3在顶面的姿态:(3本来就在顶面)向左转0~3次。
4在顶面的姿态:向上翻2次(此时4在顶面),然后向左转0~3次。
5在顶面的姿态:向左转2次,向上翻一次(此时5在顶面),然后向左转0~3次。
6在顶面的姿态:向左转3次,向上翻一次(此时6在顶面),然后向左转0~3次。
这段代码应该写在哪里呢?一种方法是直接手写在最终的程序中,但是一旦这部分代码出错,非常难调;另一种方法是写到一个独立程序中,用它生成24种姿态对应的排列,而在最终程序中直接使用常量表。生成排列表的程序如下(注意,在代码中编号为0~5,而非1~6)。

#include
#include

int left[] = {4, 0, 2, 3, 5, 1};
int up[] = {2, 1, 5, 0, 4, 3};

//按照排列T旋转姿态p
void rot(int* T, int* p) {
  int q[6];
  memcpy(q, p, sizeof(q));
  for(int i = 0; i < 6; i++) p[i] = T[q[i]];
}

void enumerate_permutations() {
  int p0[6] = {0, 1, 2, 3, 4, 5};
  printf("int dice24[24][6] = {\n");
  for(int i = 0; i < 6; i++) {
    int p[6];
    memcpy(p, p0, sizeof(p0));
    if(i == 0) rot(up, p);
    if(i == 1) { rot(left, p); rot(up, p); }
    if(i == 3) { rot(up, p); rot(up, p); }
    if(i == 4) { rot(left, p); rot(left, p); rot(up, p); }
    if(i == 5) { rot(left, p); rot(left, p); rot(left, p); rot(up, p); }
    for(int j = 0; j < 4; j++) {
      printf("{%d, %d, %d, %d, %d, %d},\n", p[0], p[1], p[2], p[3], p[4], p[5]);
      rot(left, p);
    }
  }
  printf("};\n");
}

int main() {
  enumerate_permutations();
  return 0;
}

下面让我们来看看如何“暴力”。一种方法是枚举最后那个“相同的立方体”的每个面是什么,然后对于每个立方体,看看哪种姿态需要重新涂色的面最少。但由于4个立方体最多可能会有24种不同的颜色,最多需要枚举246种“最后的立方体”,情况有些多。
另一种方法是先枚举每个立方体的姿态(第一个作为“参考系”,不用旋转),然后对于6个面,分别选一个出现次数最多的颜色作为“标准”,和它不同的颜色一律重涂。由于每个立方体的姿态有24种,3个立方体(别忘了第一个不用旋转)的姿态组合一共有243种,比第一种方法要好。程序如下(程序头部是生成的常量表,为了节省篇幅,合并了一些行)。

int dice24[24][6] = {
{2, 1, 5, 0, 4, 3},{2, 0, 1, 4, 5, 3},{2, 4, 0, 5, 1, 3},{2, 5, 4, 1, 0, 3},{4, 2, 5, 0, 3, 1},
{5, 2, 1, 4, 3, 0},{1, 2, 0, 5, 3, 4},{0, 2, 4, 1, 3, 5},{0, 1, 2, 3, 4, 5},{4, 0, 2, 3, 5, 1},
{5, 4, 2, 3, 1, 0},{1, 5, 2, 3, 0, 4},{5, 1, 3, 2, 4, 0},{1, 0, 3, 2, 5, 4},{0, 4, 3, 2, 1, 5},
{4, 5, 3, 2, 0, 1},{3, 4, 5, 0, 1, 2},{3, 5, 1, 4, 0, 2},{3, 1, 0, 5, 4, 2},{3, 0, 4, 1, 5, 2},
{1, 3, 5, 0, 2, 4},{0, 3, 1, 4, 2, 5},{4, 3, 0, 5, 2, 1},{5, 3, 4, 1, 2, 0},
};

#include
#include
#include
#include
#include
using namespace std;

const int maxn = 4;
int n, dice[maxn][6], ans;

vector<string> names;
int ID(const char* name) {
  string s(name);
  int n = names.size();
  for(int i = 0; i < n; i++)
    if(names[i] == s) return i;
  names.push_back(s);
  return n;
}

int r[maxn], color[maxn][6];    //每个立方体的旋转方式和旋转后各个面的颜色

void check() {
  for(int i = 0; i < n; i++)
    for(int j = 0; j < 6; j++) color[i][dice24[r[i]][j]] = dice[i][j];

  int tot = 0;                  //需要重新涂色的面数
  for(int j = 0; j < 6; j++) {  //考虑每个面
    int cnt[maxn*6];            //每种颜色出现的次数
    memset(cnt, 0, sizeof(cnt));
    int maxface = 0;
    for(int i = 0; i < n; i++)
      maxface = max(maxface, ++cnt[color[i][j]]);
    tot += n - maxface;
  }
  ans = min(ans, tot);
}

void dfs(int d) {
  if(d == n) check();
  else for(int i = 0; i < 24; i++) {
    r[d] = i;
    dfs(d+1);
  }
}

int main() {
  while(scanf("%d", &n) == 1 && n) {
    names.clear();
    for(int i = 0; i < n; i++)
      for(int j = 0; j < 6; j++) {
        char name[30];
        scanf("%s", name);
        dice[i][j] = ID(name);
      }
    ans = n*6;  //上界:所有面都重涂色
    r[0] = 0;   //第一个立方体不旋转
    dfs(1);
    printf("%d\n", ans);
  }
  return 0;
}

你可能感兴趣的:(数学问题,算法)