[置頂] 紫書第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;
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;
for(int i = 0; i < n; i++) 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雖然建議多想幾種方法,不過我想不到比這效率更優或代碼更簡單的思路。也就不亂湊方法了。
关注微信公众号

注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2020 ITdaan.com