首页 > 代码库 > 紫书第4章 函数和递归

紫书第4章 函数和递归

1  序

       系统的整理下第四章的学习笔记。同上次一样,尽量在不依赖书本的情况下自己先把例题做出来。这次有许多道题代码量都比较大,在例题中我都用纯C语言编写,但由于习题的挑战性和复杂度,我最终还是决定在第五章开始前,就用C++来完成习题。不过所有的代码都是能在C++提交下AC的。
       在习题中,我都习惯性的构造一个类来求解问题,从我个人角度讲,这会让我的思路清晰不少,希望自己的一些代码风格不会影响读者对解题思路的理解。
       其实在第四章前,我就顾虑着是不是真的打算把题目全做了,这些题目代码量这么大,要耗费很长一段时间。事实证明我多虑了,一分付出一分收获,经过两周的练习,我的编码能力进步很大。以前比赛一直很怕复杂麻烦的模拟题,现在遇到模拟题反而很开心,因为我算法题会的很少啊~~~
       我是从前往后花了两周把题目做一遍的,然后又从后往前整理出每道题的题解,再整理出博客。一些东西弄着弄着可能会没耐心了或心烦,忽悠了事,所以写的不好、有错的地方读者尽管指出,我会修改完善。
       建议配合紫书——《算法竞赛入门经典(第2版)》阅读本博客,转载请注明出处:code4101,谢谢。

2  例题

2.1  UVa1339--Ancient Cipher

       思路同书本。只要将两个字符串s1、s2的26个字母分别出现的次数统计出来,存在int a[26]与b[26],然后a、b数组按数值从小到大排序后,看a、b是否相等就能知道能否一一映射。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int cmp(const void* a,const void* b) {
    return *(int *)a - *(int *)b;
}

int main() {
    char s1[110], s2[110];

    while (scanf("%s%s", s1, s2) != EOF) {
        int a[26] = {}, b[26] = {}, i;
        for (i = 0; s1[i]; i++) a[s1[i]-'A']++;
        for (i = 0; s2[i]; i++) b[s2[i]-'A']++;
        qsort(a, 26, sizeof(int), cmp);
        qsort(b, 26, sizeof(int), cmp);
        printf("%s\n", memcmp(a, b, sizeof(a))?"NO":"YES");
    }
    return 0;
}

2.2  UVa489--Hangman Judge

       这题小白书做过,我是用标记数组,来判断哪些字母是否有访问过来求解的。
#include <stdio.h>

int main()
{
	int t, i, c;
	char s[1000];
	bool vis[26], has[26];
	
	while (scanf("%d ", &t), t != -1)
	{
		for (i = 0; i < 26; i++) vis[i] = has[i] = 0;
		scanf("%s", s);
		
		for (i = 0; s[i]; i++) has[s[i]-'a'] = 1;
		for (i = 0; i < 26; i++) vis[i] = has[i];
		
		scanf("%s", s);
		for (c = i = 0; s[i]; i++)
		{
			if (has[s[i]-'a']) vis[s[i]-'a'] = 0;
			else c++;
			
			if (c == 7) break;
		}
		for (i = 0; i < 26; i++) if (vis[i]) break;
		
		printf("Round %d\n", t);
		if (i == 26 && c <= 7) printf("You win.\n");
		else if (i < 26 && c < 7) printf("You chickened out.\n");
		else printf("You lose.\n");
	}
	return 0;
}

2.3  UVa133--The Dole Queue

       一开始的思路不太好,我想用一个数组,每有1个或2个离队,就把其值对应删掉(用数组平移,n递减来实现),导致条件分支太多。
       其实这题数据非常小,离队的用0值来标记,就不会变的这么麻烦了。
       代码是模仿书上的。
#include <stdio.h>
int n, k, m, a[25];

int go(int p, int d, int t) {
    while (t--) {
        do {
            p = (p+d+n-1) % n + 1;
        } while (a[p] == 0);
    }
    return p;
}

int main() {
    while (scanf("%d%d%d", &n, &k, &m), n&&k&&m) {
        for (int i = 1; i <= n; i++) a[i] = i;
        int left = n;
        int p1 = n, p2 =1;
        while (left) {
            p1 = go(p1, 1, k);
            p2 = go(p2, -1, m);
            printf("%3d", p1); left--;
            if (p2 != p1) { printf("%3d", p2); left--;}
            a[p1] = a[p2] = 0;
            if (left) printf(",");
        }
        printf("\n");
    }
    return 0;
}

2.4  UVa213--Message Decoding

       我先从LRJ讲的思路和框架入手,然后在根据自己的习惯优化代码。
       题目说了编码头(header)只占一行,所以没必要像LRJ的readcodes那么复杂,我直接用my_gets读入header字符串s,然后用init函数与s来初始化code。
#include <stdio.h>
#include <string.h>
char code[8][1<<8], s[1<<8];

// 过滤空行,返回字符串长度
int my_gets(char* s) {
    int k;
    while ((k = getchar()) == '\n');  // 空行重读
    if (k == -1) k = 0; else gets(s+1);
    s[0] = k; return strlen(s);
}

void init() {
    int i, j, k = 0;
    for (i = 1; ; i++) {
        for (j = 0; j < (1<<i)-1; j++) {
            code[i][j] = s[k++];
            if (!s[k]) return;
        }
    }
}

// 读取01字符,返回0、1整型值
int read() {
    char ch;
    while ((ch = getchar()) == '\n');
    return ch - '0';
}

// 读取c个01字符
int readint(int c) {
    int v = 0;
    while (c--) v = (v<<1)+read();
    return v;
}

int main() {
    int len, v;
    while (my_gets(s)) {
        init();
        while (len = readint(3))
            while ((v = readint(len)) != ((1<<len)-1))
                putchar(code[len][v]);
        putchar('\n');
    }
    return 0;
}

2.5  UVa512--Spreadsheet Tracking

       定义D[51][51][2],D[i][j][0]代表初始位置在第i行第j的元素当前在的行数,D[i][j][1]代表所在列。
       其他操作都好写,就是EX的交换操作挂了。WA 5次全是错在交换这里。其实这题自己想出的解题方法还好,主要不是思路不合适的原因。
       主要问题就是自己没有处理好查找问题,没想过:可不可能找不到?边查边修改值也是兵家之大忌,迫不得已这样操作时要万万小心。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define FOR() for (int i = 1; i <= R; i++) for (int j = 1; j <= C; j++)
int R, C, D[51][51][2];

int cmp(const void* a, const void* b) {return *(int *)a-*(int *)b;}
void init() { FOR() D[i][j][0] = i, D[i][j][1] = j; }

void EX() {
    int r1, c1, r2, c2;
    scanf("%d%d%d%d", &r1, &c1, &r2, &c2);
    FOR() if (D[i][j][0]==r1&&D[i][j][1]==c1) {D[i][j][0] = r2; D[i][j][1] = c2;}
     else if (D[i][j][0]==r2&&D[i][j][1]==c2) {D[i][j][0] = r1; D[i][j][1] = c1;}
}

void DR(int r) {
    FOR() {
        if (D[i][j][0] == r) D[i][j][0] = 0;
        else if (D[i][j][0] > r) D[i][j][0]--;
    }
}
void DC(int c) {
    FOR() {
        if (D[i][j][1] == c) D[i][j][0] = 0;
        else if (D[i][j][1] > c) D[i][j][1]--;
    }
}
void IR(int r) { FOR() if (D[i][j][0] >= r) D[i][j][0]++; }
void IC(int c) { FOR() if (D[i][j][1] >= c) D[i][j][1]++; }

int main() {
    int kase, cnt, i, j, r, c, a[15], k;
    char cmd[5];

    for (kase = 1; scanf("%d%d",&R,&C), R; kase++) {
        init(); scanf("%d", &cnt);
        for (i = 0; i < cnt; i++) {
            scanf("%s", cmd);
            if (!strcmp(cmd,"EX")) EX();
            else {
                scanf("%d", &k);
                for (j = 0; j < k; j++) scanf("%d", a+j);
                qsort(a, k, sizeof(int), cmp);

                int dt = 1;
                void (*pf)(int);    // 函数指针
                if (!strcmp(cmd,"DR")) pf = DR, dt = -1;
                else if (!strcmp(cmd,"DC")) pf = DC, dt = -1;
                else if (!strcmp(cmd,"IR")) pf = IR;
                else pf = IC;
                for (j = 0; j < k; j++) pf(a[j]+dt*j);
            }
        }

        scanf("%d", &cnt);
        if (kase != 1) putchar('\n');
        printf("Spreadsheet #%d\n", kase);
        for (i = 0; i < cnt; i++) {
            scanf("%d%d", &r, &c);
            printf("Cell data in (%d,%d) ", r, c);
            if (D[r][c][0]) printf("moved to (%d,%d)\n", D[r][c][0], D[r][c][1]);
            else puts("GONE");
        }
    }
    return 0;
}

2.6  UVa12412--A Typical Homework (a.k.a Shi Xiong Bang Bang Mang)

       Show_ranking()
       排名计算可以把总分存在另一个数组s,按从大到小排序,然后用二分查找找lower_bound。
       当然,第四章还没讲到二分法,而且这里数据很小,也没必要二分,直接遍历搜索就行了。

       Show_Statistics():需要计算每人通过多少科目,每门课有多少人通过。
       可以用一个二维表来存储通过信息。中间用0、1变量表示是否合格,每行、每列均求和。利用这个二维表的信息就能完成最后一个函数的编写了(实际只要存储“行和”和“列和”就行了)。

       还有就是精度问题,浮点数输出都要加eps,出题人显然故意设了特殊数据,不加eps绝对WA。

       最后output这么长,调试起来不是肉眼能解决的,所以推荐用个文件比较程序:文件比较函数diff。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define maxn 105
const double eps = 1e-5;    // 比保留2位有效数字多3个数量级

struct Student{
    char SID[50], name[50];
    int CID, sub[4], sco;// sub(subject),下标0123分别代表语数英编程成绩
}stu[maxn], *p;
int sum = 0, sco[maxn];

void menu() {
    char s[6][50] = {"Add", "Remove", "Query", "Show ranking", "Show Statistics", "Exit"};
    printf("Welcome to Student Performance Management System (SPMS).\n\n");
    for (int i = 1; i <= 6; i++) printf("%d - %s\n", i%6, s[i-1]);
    putchar('\n');
}

Student* find_SID(char* SID) {
    for (int i = 0; i < sum; i++) if (!strcmp(SID, stu[i].SID)) return stu+i;
    return NULL;
}

void Add() {
    while (puts("Please enter the SID, CID, name and four scores. Enter 0 to finish."),
           p = stu+sum, scanf("%s", p->SID), strcmp(p->SID, "0")) {
        scanf("%d%s", &p->CID, p->name);
        for (int i = 0; i < 4; i++) scanf("%d", &p->sub[i]);
        if (find_SID(p->SID) != NULL) puts("Duplicated SID.");
        else sum++;
    }
}

void Remove() {
    char s[50];
    while (puts("Please enter SID or name. Enter 0 to finish."),
           scanf("%s", s), strcmp(s, "0")) {
        int xx = 0;
        for (int i = 0; i < sum; i++) if (!strcmp(s,stu[i].SID) || !strcmp(s,stu[i].name)) {
            sum--; xx++;
            for (int j = i; j < sum; j++) stu[j] = stu[j+1];
            i--;
        }
        printf("%d student(s) removed.\n", xx);
    }
}

int cmp(const void* a, const void* b) { return *(int*)b-*(int*)a;}

int ranklist(int t) {
    int i = -1;
    while (sco[++i]>t);
    return i+1;
}

void Query() {
    int i, j;
    char s[50];

    for (i = 0; i < sum; i++) {
        stu[i].sco = stu[i].sub[0];
        for (j = 1; j < 4; j++) stu[i].sco += stu[i].sub[j];
        sco[i] = stu[i].sco;
    }
    qsort(sco, sum, sizeof(int), cmp);
    while (puts("Please enter SID or name. Enter 0 to finish."),
           scanf("%s", s), strcmp(s, "0")) {
        for (i = 0; i < sum; i++) if (!strcmp(s,stu[i].SID) || !strcmp(s,stu[i].name)) {
            printf("%d %s %d %s", ranklist(stu[i].sco), stu[i].SID, stu[i].CID, stu[i].name);
            for (j = 0; j < 4; j++) printf(" %d", stu[i].sub[j]);
            printf(" %d %.2lf\n", stu[i].sco, stu[i].sco/4.0 + eps);
        }
    }
}

void Show_ranking() {
    puts("Showing the ranklist hurts students' self-esteem. Don't do that.");
}

void Show_Statistics() {
    puts("Please enter class ID, 0 for the whole statistics.");

    // c[i]:第i门课有多少人合格; cnt[i]:过i门课恰好有多少人
    int r, c[4] = {}, cnt[5] = {};
    int i, j, n = 0, s[4] = {}, cid; // n人数,s[i]:第i门课的总分,cid班级编号
    scanf("%d", &cid);

    for (i = 0; i < sum; i++) if (!cid || stu[i].CID == cid) {
        n++;
        for (r = j = 0; j < 4; j++) {
            s[j] += stu[i].sub[j];
            if (stu[i].sub[j] >= 60) {
                r++;
                c[j]++;
            }
        }
        cnt[r]++;
    }

    char str[4][50] = {"Chinese", "Mathematics", "English", "Programming"};
    for (i = 0; i < 4; i++) {
        printf("%s\nAverage Score: %.2lf\n", str[i], 1.0*s[i]/n + eps);
        printf("Number of passed students: %d\nNumber of failed students: %d\n\n", c[i], n-c[i]);
    }
    printf("Overall:\nNumber of students who passed all subjects: %d\n", cnt[4]);
    for (i = 3; i > 0; i--)
        printf("Number of students who passed %d or more subjects: %d\n", i, cnt[i]+=cnt[i+1]);
    printf("Number of students who failed all subjects: %d\n\n", n - cnt[1]);
}

int main() {
#ifdef DEBUG
    freopen("12412.in", "r", stdin);
    freopen("12412.ans", "w", stdout);
#endif

    int c;
    while (menu(), scanf("%d", &c), c) {
        if (c == 1) Add();
        else if (c == 2) Remove();
        else if (c == 3) Query();
        else if (c == 4) Show_ranking();
        else if (c == 5) Show_Statistics();
    }
    return 0;
}
       最后看LRJ的代码,竟然没有用结构体解!

3  习题

3.1  UVa1589--Xiangqi

       习题第一道就极具挑战性,为了思路清晰,我还是决定用C++,构造一个Xiangqi类来求解。
       然后要好好分析好思路再动手敲代码。首先黑子会动很麻烦,能不能把动态问题改成静态?如果题目只是给你一个局面,问你当前红方动,能不能把黑方吃掉是不是简单多了?所以要生成一个Xiangqi对象A,按input数据初始化。然后只要黑将不跟红帅面对面直接吃掉,他至多有四种移动方案,用A来生成黑将移动后的Xiangqi对象B。如上,B是一个静态问题。
       那么现在关键是Xiangqi类的书写,棋盘存到二维数组board中不做修改。红帅和黑将是棋盘的核心,所以要定义ax,ay,bx,by存储它们所在的坐标,坐标也从1开始编号,不容易混乱思路,而且留有边界board[0],一些操作也会很方便。

       车、将、炮吃子的本质是相同的。都是要在同一横线或竖线,但中间隔的棋子一种是0个,一种是1个。这种操作可以定义一个基础函数Cnt,可以计算(x,y)与黑将是否在同一横线或竖线,不满足则返回-1。否则返回两坐标间隔的棋子数。注意这里的技巧,不是计算任意两点,而是任意一点到黑将,因为所有的子都是围绕黑将展开的,都是要去吃它。这样就能简化函数节省代码量。
       马的吃子看似麻烦,其实符合很特别的数量规律。记其行列偏移量分别为dx和dy,有没注意到dx*dy的绝对值一定是2?“蹩马腿”的技巧也是一样,详见函数H的代码。

       剩下的代码应该不难吧?再解释汉字比代码都长了~~~
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;

struct Xiangqi {
    int board[11][10];
    int ax, ay, bx, by; // a是红方的帅,b是黑方的将; x,y是坐标

    // 构造函数遵循题目的输入格式
    Xiangqi(int n, int _bx, int _by): bx(_bx), by(_by) {
        memset(board, 0, sizeof(board));
        board[bx][by] = 'Y';    // 用字母Y代表黑将
        char type[5];
        int x, y;
        for (int i = 0; i < n; i++) {
            scanf("%s%d%d", type, &x, &y);
            board[x][y] = type[0];
        }
    }

    // 黑将做(dx,dy)的移动,移动成功则返回1,否则0
    int go(int dx, int dy) {
        if (bx+dx>0 && bx+dx<4 && by+dy>3 && by+dy<7) {
            board[bx][by] = 0;
            bx += dx; by += dy;
            board[bx][by] = 'Y';
            return 1;
        }
        return 0;
    }

    // 判断(x,y)到(bx,by)中间一共有多少棋子,重叠与不在同一竖直、水平线返回-1
    int Cnt(int x, int y) {
        int cnt = -1, i, j;
        if (x == bx && y != by)
            for (cnt = 0, j = min(y,by)+1; j < max(y,by); j++) if (board[x][j]) cnt++;
        if (x != bx && y == by)
            for (cnt = 0, i = min(x,bx)+1; i < max(x,bx); i++) if (board[i][y]) cnt++;
        return cnt;
    }

    // 判断在(x,y)的马能不能吃掉黑将
    int H(int x, int y) {
        int dx = bx - x, dy = by - y;
        if (abs(dx*dy) != 2) return 0;
        if (abs(dx)==2) if (board[x+dx/2][y]) return 0;
        if (abs(dy)==2) if (board[x][y+dy/2]) return 0;
        return 1;
    }

    // 判断如果当前棋盘红方移动,黑方是否被将死
    int isBlackLost() {
        for (int i = 1; i <= 10; i++) for (int j = 1; j <= 9; j++) {
            switch (board[i][j]) {
            case 'G':
            case 'R': if (Cnt(i,j)==0) return 1; break;
            case 'H': if (H(i,j)) return 1; break;
            case 'C': if (Cnt(i,j)==1) return 1;
            }
        }
        return 0;
    }

    void out() {    // 用于输出棋盘进行调试的函数
        printf("**123456789\n");
        for (int i = 1; i <= 10; i++) {
            printf("%2d", i);
            for (int j = 1; j <= 9; j++) {
                putchar(board[i][j]?board[i][j]:' ');
            }
            putchar('\n');
        }
        putchar('\n');
    }
};

int isCheckmate(Xiangqi& A) {
    int dx[4] = {-1,1,0,0}, dy[4] = {0,0,1,-1};
    for (int i = 0; i < 4; i++) {
        Xiangqi B(A);  // 用复制构造函数拷贝副本
        if (B.go(dx[i],dy[i]) && !B.isBlackLost()) return 0;
    }
    return 1;
}

int main() {
#ifdef DEBUG
    freopen("1589.in", "r", stdin);
#endif // DEBUG
    int n, bx, by;
    while (scanf("%d%d%d", &n, &bx, &by), n) {
        Xiangqi A(n, bx, by);
        //A.out();
        //cout << A.Cnt(6,4) << endl;
        //cout << A.isBlackLost() << endl;
        //A.out();
        //printf("答案: %d\n", isCheckmate(A));
        printf("%s\n", isCheckmate(A)?"YES":"NO");
    }
    return 0;
}

3.2  UVa201--Squares

       这题紫书翻译有点小瑕疵,不过问题不大能理解题目啦...V给的第1个数表示列,第2个数表示行。
       我构造一个Squares类,用h和v直接标记[i][j]是否存在向前或向下的横线竖线。本题关键是写一个isSqu函数,判断以两点为对角线是否能形成闭环,这个函数很有技巧,类似经典的蛇形填数题,读者慢慢领悟。(整理解题报告的过程中,我发现在类里写成员函数,先后顺序竟然没有影响!也就是该段代码构造函数写在isSqu等前面也可以编译通过)。
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;

struct Squares {
    int h[10][10], v[10][10];
    int n, cnt[10];

    // 接下来判断(x1,y1)-(x2,y2)能否连环
    int isSqu(int x1, int y1, int x2, int y2) {
        int px = x1, py = y1;
        while (py < y2) { if (!h[px][py]) return 0; py++; }
        while (px < x2) { if (!v[px][py]) return 0; px++; }
        while (py > y1) { if (!h[px][py-1]) return 0; py--; }
        while (px > x1) { if (!v[px-1][py]) return 0; px--; }
        return 1;
    }

    void Count() {
        int i, j, k;
        memset(cnt, 0, sizeof(cnt));
        for (i = 1; i < n; i++)
          for (j = 1; j < n; j++)
            for (k = 1; i+k<=n && j+k<=n; k++)
              if (isSqu(i,j,i+k,j+k)) cnt[k]++;
    }

    Squares(int _n, int m):n(_n) {
        memset(h, 0, sizeof(h));
        memset(v, 0, sizeof(v));
        char str[10];
        int x, y, i;
        for (i = 0; i < m; i++) {
            scanf("%s%d%d", str, &x, &y);
            if (!strcmp(str, "H")) h[x][y] = 1;
            else v[y][x] = 1;
        }
        Count();
    }

    void Msg() {
        int logo = 1;
        for (int i = 1; i < n; i++) if (cnt[i]) {
            logo = 0;
            printf("%d square (s) of size %d\n", cnt[i], i);
        }
        if (logo) puts("No completed squares can be found.");
    }
};

int main() {
    int n, m, kase = 0;
    while (cin >> n >> m) {
        Squares A(n, m);

        if (kase) puts("\n**********************************\n");
        printf("Problem #%d\n\n", ++kase);
        A.Msg();
    }
    return 0;
}

3.3  UVa220--Othello

       首先构造一个Othello类。有一个board二维棋盘,还有一个cur存储当前行动的是‘W‘还是‘B‘。首先肯定有对应的构造函数Othello;然后为了方便行动对象转换,写了一个NextCur函数;最后黑白棋子的统计由一个Count函数完成。接着写L命令的List函数,Mrc命令的MakeMove是题目核心。列好框架后,思路就清晰了,慢慢完成相应的函数模块即可。
       为了完成L和Mrc命令,有个函数是必须的,就是在某一点,向外8个方向遍历操作。这里用walk函数来完成,并通过一个change参数标记操作是仅查找判断,还是修改棋盘状态。这里8方向的遍历是模拟题很常见的内容,我通常的实现方法是对8个方向按顺时针编号0~7:
       用两个数组dirx[i]和diry[i]分别表示i方向的行偏移量和列偏移量。
       有了walk函数后,就能写出,在(x,y)放置棋子是否合法的操作isMakeOk。那么List函数就是遍历棋盘,把合法且board[i][j]为空的坐标输出即可。MakeMove也只是调用改变棋盘状态的walk操作。
       最后注意输出格式,第39行要用%3d输出,题目根本没描述,要自己看样例输出才知道。当然这个坑算好的了,等做到数据挖掘那题读者就知道什么才是真正的坑。
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;

struct Othello {
    char board[10][10], cur;

    Othello() {
        memset(board, 0, sizeof(board));
        for (int i = 1; i < 9; i++) scanf("%s", &board[i][1]);
        char str[5]; scanf("%s", str); cur = str[0];
    }

    char NextCur() { return cur=='B'?'W':'B'; }

    void OutBoard() {
        for (int i = 1; i < 9; i++) puts(&board[i][1]);
    }

    void List() {
        int cnt = 0, i, j;
        for (i = 1; i < 9; i++)
            for (j = 1; j < 9; j++)
                if (board[i][j] == '-' && isMakeOk(i,j)) {
                    if (cnt) putchar(' '); cnt++;
                        printf("(%d,%d)", i, j);
                }
        if (cnt) putchar('\n');
        else puts("No legal move.");
    }

    void Count() {
        int b = 0, w = 0;
        for (int i = 1; i < 9; i++)
          for (int j = 1; j < 9; j++)
            if (board[i][j]=='B') b++;
            else if (board[i][j]=='W') w++;
        printf("Black -%3d White -%3d\n", b, w);
    }

    // 从(x,y)出发,沿dir所指方向查找. change确定要不要进行修改操作
    //返回值表示该条路径是否可以进行删除
    int walk(int x, int y, int dir, int change = 0) {
        static int dirx[8] = {-1,-1,0,1,1,1,0,-1};
        static int diry[8] = {0,1,1,1,0,-1,-1,-1};

        int dx = dirx[dir], dy = diry[dir], k = 0;
        char ch1 = NextCur(), ch; //ch1存与cur对应的另一种字符
        do {
            k++;
            ch = board[x+k*dx][y+k*dy];
            if (ch == '_' || !ch) return 0;
        } while (ch == ch1);
        if (k == 1 || ch != cur) return 0;
        if (change)
            for (int i = 1; i < k; i++)
                board[x+i*dx][y+i*dy] = cur;
        return 1;
    }

    // 当前棋子能否放在(x,y)
    int isMakeOk(int x, int y) {
        for (int i = 0; i < 8; i++)
            if (walk(x,y,i)) return 1;
        return 0;
    }

    void MakeMove(int x, int y) {
        if (!isMakeOk(x,y)) cur = NextCur();
        board[x][y] = cur;
        for (int i = 0; i < 8; i++) walk(x,y,i,1);
        cur = NextCur();
        Count();
    }
};

int main() {
    int T, kase;
    char c[10]; // choose
    cin >> T;
    for (kase = 1; kase <= T; kase++) {
        if (kase != 1) putchar('\n');
        Othello A;
        while (scanf("%s", c), strcmp(c, "Q")) {
            if (c[0] == 'L') A.List();
            else A.MakeMove(c[1]-'0', c[2]-'0');
        }
        A.OutBoard();
    }
    return 0;
}

3.4  UVa253--Cube painting

       这题一开始把自己转晕了,WA了几发,然后看了shiqi_614的博客,思路清晰多了。

       这题要怎么“暴力”呢?首先自己要定义一个规范。比如我的是把B的六个面依次朝上,然后有四种左右水平旋转操作,只要6*4=24中有一种B能跟A完全相同,那他们就是等价的。
       Cube类中,先定义了基本的旋转操作函数tran,然后只定义了从2往1的向上翻和从2往4的向右翻操作,为了增强函数通用性,up、right都一个带一个旋转次数的参数,负数代表反向旋转,因为本质上只有四种变换,注意这两个函数代码的实现原理,a[i]=j(程序里编号从0开始),表示原来编号为i的面,翻转到第j面。
       有了这两种操作,就不难写出剩下的比较操作了。为了方便,把四种旋转操作的判断,封装到函数Equal,两个Cube类是否等价通过重载==运算符来判断。
#include <iostream>
#include <string>
using namespace std;

struct Cube {
    string s;

    Cube(string _s): s(_s) { }

    void tran(int* a, int k) {
        for (int i = 0; i < k; i++) {
            string t(6,0);
            for (int j = 0; j < 6; j++) t[a[j]] = s[j];
            s = t;
        }
    }
    void up(int k = 1)    { int a[6] = {4,0,2,3,5,1}; tran(a, ((k%4)+4)%4); }
    void right(int k = 1) { int a[6] = {0,3,1,4,2,5}; tran(a, ((k%4)+4)%4); }

    bool Equal(Cube A, Cube B) {
        for (int i = 0; i < 4; i++) {
            B.right();
            if (A.s == B.s) return 1;
        }
        return 0;
    }
    bool operator == (Cube B) {
        string s0 = B.s;
        int r[6] = {0,0,1,1,0,0}, u[6] = {0,1,1,-1,-1,2};
        for (int i = 0; i < 6; i++) {
            B.s = s0;
            B.right(r[i]); B.up(u[i]);
            if (Equal(*this, B)) return 1;
        }
        return 0;
    }
};

int main() {
    string s;
    while (cin >> s) {
        Cube A(s.substr(0,6)), B(s.substr(6,6));
        cout << (A==B?"TRUE":"FALSE") << "\n";
    }
    return 0;
}

3.5  UVa1590--IP Networks

       题意其实就是计算二进制的最大公共前缀。
       这里刚好能用无符号32位整型来存储IP,然后就是一些位运算的技巧。子网掩码mask初始化为0xffffffff。然后一直左移遍历,直到所有IP和当前mask对应的IP地址都相等时,退出循环。
#include <cstdio>
#include <iostream>
using namespace std;

struct Network {
    unsigned int ip[1005], addr, mask;
    int n;

    void init() {
        int logo;
        mask = 0xffffffff;
        do {
            logo = 0;
            addr = ip[0]&mask;
            for (int i = 1; i < n; i++)
                if ((ip[i]&mask) != addr) logo = 1;
            if (logo) mask <<= 1;
        } while (logo);
    }

    Network(int _n): n(_n) {
        unsigned int a[4];
        for (int i = 0; i < n; i++) {
            scanf("%u.%u.%u.%u", &a[0], &a[1], &a[2], &a[3]);
            ip[i] = (a[0]<<24) + (a[1]<<16) + (a[2]<<8) + a[3];
        }
        init();
    }
};

void OutIp(unsigned a) {
    for (int i = 0; i < 3; i++)
        printf("%d.", (a>>(3-i)*8)&0xff);
    printf("%d\n", a&0xff);
}

int main() {
    int n;
    while (cin >> n) {
        Network A(n);
        OutIp(A.addr);
        OutIp(A.mask);
    }
    return 0;
}

3.6  UVa508--Morse Mismatches

       首先,这道题目紫书翻译错了。精确匹配的话应该输出第一个,而不是任意一个,如果有多个精确匹配,要加后缀“!”。未精确匹配的,不管是否有多个,都应该要加“?”。有关紫书勘误的信息,见官方链接(要是卡了就多F5一下)。

       回到该题,这题的正确率确实很吓人,把我也雷住了,第一发WA后就信心大失。然后在想是不是题意理解错了,试了各种各样的姿势,还是WA。最后找到AC该题12位中的CKSteven(他还未写该题博客),看了他的代码后才发现自己最开始的题意理解没错,是有一个bug(见代码第14行),不然能1A的。
       这道题题意原文说的很清楚。我这里主要是定义了一个dist函数,来判断两个字符串的“距离”。相同距离就是0。然后如果短的字符串(设为a)不是长的字符串(设为b)的子串,距离就是inf,否则距离就是b的长度减a的长度,这个操作就是增加或删除字符尽量少的等价含义。
#include <algorithm>
#include <iostream>
#include <map>
#include <string>
using namespace std;
const int inf = 10000;

map<char, string> unit_code;
map<string, string> dict;

int dist(string a, string b) {
    if (a == b) return 0;
    if (b.size() < a.size()) swap(a, b);
    // a必须是b的子串
    if (a != b.substr(0,a.size())) return inf;
    else return b.size()-a.size();
}

string encode(string& s) {
    string ans;
    for (int i = 0; i < s.size(); i++)
        ans = ans + unit_code[s[i]];
    return ans;
}

string decode(string s) {
    map<string, string>::iterator it = dict.begin();
    string ans = it->first;
    int dis = dist(it->second, s);
    while (++it != dict.end()) {
        int d = dist(it->second, s);
        if (d < dis) ans = it->first, dis = d;
        else if (d == dis && d==0 && ans[ans.size()-1] != '!')
            ans = ans + "!";
    }
    if (dis) ans = ans + "?";
    return ans;
}

int main() {
    string s1, s2;
    while (cin >> s1, s1 != "*") {
        cin >> s2;
        unit_code[s1[0]] = s2;
    }
    while (cin >> s1, s1 != "*") dict[s1] = encode(s1);
    while (cin >> s1 && s1 != "*") cout << decode(s1) << "\n";
    return 0;
}

3.7  UVa509--RAID!

       这题注意input给的数据是题目用的讲解图的转置,即每行代表一个磁盘disk。下面用的行列均指input标准输入的数组。

       我构造了一个RAID类,用一个bool类型的大数组data存储0、1正文信息,整数n代表存了多少位。然后关键是Set成员函数的实现。
       Set函数尝试将数据读入与恢复,并存储在data中,如果失败则返回0,否则返回1。Set函数的算法和步骤是本题的核心:
  1. 它先统计每列的‘x‘个数,已有的01也先做异或运算。
  2. 然后做列检验,如果该列没有‘x‘,则异或结果错误的return 0;如果该列有一个‘x‘,直接对其进行恢复;如果有多个‘x‘的,肯定有正文信息损坏且无法恢复,故也return 0。
  3. step 2若能顺利执行,进入该步,则此时str中一定不存在‘x‘。用技巧来依次读取正文即可。
#include <cstdio>
#include <iostream>
using namespace std;

struct RAID {
    int n;
    bool data[64*100*6+10];

    int Set(int d, int s, int b) {
        bool parity = 0;
        char str[7][6410], str1[10];
        // 读入数据
        scanf("%s", str1);
        if (str1[0]=='O') parity = 1;
        for (int i = 0; i < d; i++) scanf("%s", str[i]);
        // 统计每列有多少个x, 并统计已有01的异或值
        int cnt[6410] = {}, i, j;
        bool check[6410] = {};
        for (i = 0; i < d; i++)
            for (j = 0; j < s*b; j++) {
                if (str[i][j]=='x') cnt[j]++;
                else check[j] ^= (str[i][j]-'0');
            }
        for (j = 0; j < s*b; j++) { // 每列校验
            if (cnt[j] > 1) return 0;
            if (cnt[j] == 0 && check[j] != parity) return 0;
            // 否则只有一个x,直接恢复
            for (i = 0; i < d; i++) if (str[i][j]=='x') {
                str[i][j] = (parity^check[j]) + '0';
                break;
            }
        }
        // 生成数据data,遇到'x'时return 0
        for (n = j = 0; j < s*b; j += s) for (i = 0; i < d; i++) {
            if (i == (j/s)%d) continue; // 注意此处技巧,巧妙的跳过校验码
            for (int k = 0; k < s; k++)
                data[n++] = str[i][j+k]-'0';
        }
        while (n%4 != 0) data[n++] = 0;
        return 1;
    }

    void out() {
        for (int i = 0; i < n; i += 4)
            printf("%X", data[i]*8 + data[i+1]*4 + data[i+2]*2 + data[i+3]);
        putchar('\n');
    }
};

int main() {
    int d, s, b, kase = 0;
    RAID A;
    while (cin >> d >> s >> b) {
        if (A.Set(d, s, b)) {
            printf("Disk set %d is valid, contents are: ", ++kase);
            A.out();
        }
        else printf("Disk set %d is invalid.\n", ++kase);
    }
    return 0;
}

3.8  UVa12108--Extraordinarily Tired Students

       首先不要把这题想复杂了,题目给的数量都非常小,就是一道暴力模拟题。如果仍然不放心,比赛的时候根据现场AC情况也能猜出迭代上限不会太大。
       保险的话,上限可以设100万(笔者实验过,5000就能AC该题了)。
#include <algorithm>
#include <cstdio>
using namespace std;

struct Stu {
    int n, cnt; // n:总人数, cnt:当前睡觉人数
    int a[10], b[10], c[10];

    Stu(int _n): n(_n) {
        for (int i = cnt = 0; i < n; i++) {
            scanf("%d%d%d", a+i, b+i, c+i);
            if (c[i]>a[i]) cnt++;
        }
    }

    int Lim() { // 返回迭代上限
        long long lim = 1;
        for (int i = 0; i < n; i++)
            lim *= a[i]+b[i];
        return min(lim+5, 10000LL);
    }

    int Next() { // 返回当前睡觉总人数
        int CanSleep = 0;
        if (cnt > n - cnt) CanSleep = 1;
        for (int i = 0; i < n; i++) {
            if (c[i]==a[i]+b[i]) cnt--, c[i] = 0;
            else if (c[i]==a[i]) {
                if (CanSleep) cnt++;
                else c[i] = 0;
            }
            c[i]++;
        }
        return cnt;
    }
};

int main() {
    int n, kase = 0;
    while (scanf("%d", &n), n) {
        Stu A(n);
        int lim = A.Lim(), t = 2;
        while (A.Next() && t < lim) t++;
        printf("Case %d: %d\n", ++kase, t==lim?-1:t);
    }
    return 0;
}
       我这里把迭代次数优化了下,有些多此一举把代码写的复杂了些,与时俱进yhj这份写的比较简洁。

3.9  UVa1591--Data Mining

       花絮
       首先,要耐心读懂题意,题目并不是特别复杂,不过是有一些专业背景而已。
       然后想到,从实际角度考虑,A和B的值都应该不会太大才对。再看看数据规模,Pofs的值能达到2^20*2^10=2^30。故用64位整型存储,我们最多也只能左移30多位。
       不过从这里就感觉题目不太对劲,出题人应该说明A、B的范围才对,难道是故意挖坑,让ACMer自己猜?这种坑也不是没见过,今年上海邀请赛,输出方程表达式的那题,题目就没有说系数为1的要忽略这一点(如1x+3y-1z-4应该输出x+3y-z-4),而是让选手自己从常识角度理解,把我们队可坑惨了。
       与其乱猜,不如把测试数据翻出来:NEERC2003数据, 找到测试数据的输入输出,发现果然所有的A、B值都在0~31以内。

       求解
       然后这题就逗比了,只要暴力所有A、B的组合,看解是不是可行的并找出最优解。

       (设有n个元素,每个P占x个字节,每个Q占y个字节。)
       怎么判断一个解是否可行呢?想想如果Q连续存储,至少也得消耗n*y个字节,一个AB方案,如果算出的字节小于该值就是不可行的。否则这个内存越小,解就越优,依据这个来更新最优解ansA,ansB,ansN。
#include <iostream>
using namespace std;

int main() {
    long long n, x, y, N, A, B, ansN, ansA, ansB;
    while (cin >> n >> x >> y) {
        ansN = n*y<<10;
        for (A = 0; A < 32; A++) {
            for (B = 0; B < 32; B++) {
                N = (((n-1)*x+((n-1)*x<<A))>>B) + y;
                if (N>=n*y && N<ansN) {
                    ansA = A;
                    ansB = B;
                    ansN = N;
                }
            }
        }
        cout << ansN << " " << ansA << " " << ansB << "\n";
    }
    return 0;
}

3.10  UVa815--Flooded!

       以样例数据来解释解题思路。如下图所示:

  1. 可以把n*m块地的海拔高度a[i],按从小到大排成一列。
  2. 然后对输入的洪水体积,先除以100,这样一来,题目中的所有数据就统一了数量单位。如样例的洪水体积为10000,转化为高度就是sum=100。
  3. 接着设一个偏移量k,代表洪水从左往右淹到第k块地时停止。算法为k从1开始递增遍历,洪水每路过一块地,就把地的高度加入sum,退出条件就是洪水的平均高度不大于下一块地:sum/k < a[k+1]。
       为了方便,可以在高地末尾假想一块无穷高的地,这样能统一边界情况的处理。
#include <algorithm>
#include <cstdio>
using namespace std;
#define rep(i,a,b) for(int i=a; i<b; i++)
const int maxn = 35;

int a[maxn*maxn], n, m;

int main() {
    for (int kase = 1; scanf("%d%d", &n, &m), n&&m; kase++) {
        n *= m;
        rep(i, 0, n) scanf("%d", a+i);
        sort(a, a+n);
        a[n] = 0x7fffffff;

        int k;
        double sum;
        scanf("%lf", &sum); sum /= 100.0;
        for (k = 1; k <= n; k++) {
            sum += a[k-1];
            if (sum/k <= a[k])
                break;
        }
        printf("Region %d\n", kase);
        printf("Water level is %.2lf meters.\n", sum/k);
        printf("%.2lf percent of the region is under water.\n\n", k*100.0/n);
    }
    return 0;
}
       LRJ虽然建议多想几种方法,不过我想不到比这效率更优或代码更简单的思路。也就不乱凑方法了。