演算法競賽入門經典(第二版)-劉汝佳-第三章 陣列與字串 例題+習題(17/18)
說明
本文是我對第三章題目的練習總結,建議配合紫書——《演算法競賽入門經典(第2版)》閱讀本文。
另外為了方便做題,我在VOJ上開了一個contest,歡迎一起在上面做:第三章contest
如果想直接看某道題,請點開目錄後點開相應的題目!!!
例題
例3-1 UVA 272 TeX 中的引號
思路
這個題主要講帶空格的輸入輸出處理。我總結了一下,主要有三種方案:
1、用getchar()一個一個字元處理
2、用fgets讀入(gets已經過時)
3、用getline讀入
程式碼
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
int main(void)
{
char c;
bool flag = true;
while ((c = getchar()) != EOF) {
if (c == '\"') {
printf("%s", flag ? "``" : "''");
flag = !flag;
} else
printf("%c", c);
}
return 0;
}
例3-2 UVA 10082 WERTYU
思路
常量陣列的妙用,可以使程式簡潔很多。
程式碼
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
char s[] = "`1234567890-=QWERTYUIOP[]\\ASDFGHJKL;'ZXCVBNM,./";
int main(void)
{
char c;
while ((c = getchar()) != EOF) {
char *p = strchr(s, c);
if (!p) putchar(c);
else putchar(s[p-s-1]);
}
return 0;
}
例3-3 UVA 401 迴文詞
思路
常量字串和字串陣列的妙用,使程式更簡潔。
另外,學習了strchr函式,主要功能是在字串中查詢字元,返回字元指標。
程式碼
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const char mirror[] = "A 3 HIL JM O 2TUVWXY51SE Z 8 ";
const char *msg[4] = {" -- is not a palindrome.",
" -- is a regular palindrome.",
" -- is a mirrored string.",
" -- is a mirrored palindrome."};
char trans(char c)
{
if (c <= '9') return mirror[c - '0' + 25];
return mirror[c - 'A'];
}
int main(void)
{
char s[30];
while (cin >> s) {
int p = 1, m = 1;
int n = strlen(s);
for (int i = 0; i <= n/2; i ++) {
if (s[i] != s[n-1-i]) p = 0;
if (trans(s[i]) != s[n-1-i]) m = 0;
}
printf("%s%s\n\n", s, msg[m*2+p]);
}
return 0;
}
例3-4 UVA 340 猜數字遊戲的提示
思路
當數值範圍較小時,可以用統計陣列。我這裡判斷正確值和錯誤值的方式與例題稍有不同,思路大同小異。
程式碼
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1000;
const int M = 10;
int n, a0[N], c0[M], c2[M];
int a1[N], c1[M];
int main(void)
{
int t = 0;
while (cin >> n && n) {
memset(c0, 0, sizeof(c0));
for (int i = 0; i < n; i ++) {
scanf("%d", &a0[i]);
c0[a0[i]]++;
}
printf("Game %d:\n", ++t);
while (true) {
int flag = false;
int cntA = 0, cntB = 0;
memcpy(c2, c0, sizeof(c0));
memset(c1, 0, sizeof(c1));
for (int i = 0; i < n; i ++) {
scanf("%d", &a1[i]);
if (a1[i]) flag = true;
c1[a1[i]]++;
if (a1[i] == a0[i]) {
cntA ++;
c2[a0[i]] --;
c1[a0[i]] --;
}
}
if (flag == false)
break;
for (int i = 1; i < M; i ++)
if (c2[i]) cntB += min(c1[i], c2[i]);
printf(" (%d,%d)\n", cntA, cntB);
}
}
return 0;
}
例3-5 UVA 1583 生成元
思路
當計算過程複雜而且對結果有多次查詢時,就應當考慮將計算結果儲存成表,從而大大提高查詢效率。
這是本題的主要思想。
程式碼
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100000;
int main(void)
{
int n, a[N+1];
memset(a, 0, sizeof(a));
for (int i = 1; i < N; i ++) {
int m = i, sum = 0;
while (m) { sum += m%10; m /= 10;}
n = sum + i;
if (n <= N && a[n] == 0) a[n] = i;
}
int t;
cin >> t;
while (t--) {
cin >> n;
printf("%d\n", a[n]);
}
return 0;
}
例3-6 UVA 1584 環狀序列
思路
此題考查字串排序。我的做法與書中不同,我是將長度為n字串s複製一份連線到它的後面成為s2,這樣環狀序列的所有表示就是s2中所有長度為n的子字串,用strncmp比較即可。
書中做法和我的方法都避免了n次字串複製操作。
程式碼
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100;
int main(void)
{
int t, n;
char s0[2*N+1], s1[2*N+1];
cin >> t;
while (cin >> s0) {
n = strlen(s0);
strcpy(s1, s0);
strcat(s0, s1);
strcpy(s1, s0);
int mi = 0;
for (int i = 0; i < n; i ++) {
if (strncmp(s0+mi, s1+i, n) > 0)
mi = i;
}
strncpy(s1, s0+mi, n);
s1[n] = '\0';
printf("%s\n", s1);
}
return 0;
}
習題
習3-1 UVA 1585 得分
思路
用add變數記錄當前的O字元連續出現的個數,遇到X清零。
程式碼
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
int main(void)
{
int t;
char s[81];
cin >> t;
while (t--) {
scanf("%s", s);
int add = 0, sum = 0;
for (int i = 0; s[i]; i ++) {
if (s[i] == 'O') {
add ++;
sum += add;
} else
add = 0;
}
printf("%d\n", sum);
}
return 0;
}
習3-2 UVA 1586 分子量
思路
考察基本的輸入分析,可以先讀入整個字串然後分析。
程式碼
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const char name[] = "CHON";
double weight[] = {12.01, 1.008, 16.00, 14.01};
int main(void)
{
int t;
char s[81];
cin >> t;
while (t--) {
scanf("%s", s);
int num;
double sum = 0;
int i = 0;
while (s[i]) {
int j;
for (j = 0; j < 4; j ++) {
if (s[i] == name[j]) break;
}
i ++;
num = 1;
if (isdigit(s[i])) num = (s[i++]-'0');
if (isdigit(s[i])) num = num*10 + (s[i++]-'0');
sum += num * weight[j];
}
printf("%.3lf\n", sum);
}
return 0;
}
習3-3 UVA 1225 數數字
思路
這個題暴力搜尋也能過,因為資料範圍太小。但這樣就失去了意義。
我用函式寫的,具有較強的普適性。主要思想是對每一位分別分析——當前位、高位、低位分別為指定數字——情況下的數的個數。
程式碼
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
int cnt(int n, int x)
{
int res = 0;
int fact = 1, high = n/10, crt = n%10, low = 0;
while (high || (x && crt >= x)) {
//printf("%d %d %d %d\n", fact, high, crt, low);
res += high*fact;
if (x == 0) res -= fact;
if (crt > x) res += fact;
if (crt == x) res += (low+1);
low += fact*crt;
crt = high%10;
high /= 10;
fact *= 10;
}
return res;
}
int main(void)
{
int t, n;
cin >> t;
while (t --) {
cin >> n;
for (int i = 0; i < 10; i ++) {
printf("%d%c", cnt(n, i), i == 9 ? '\n' : ' ');
}
}
return 0;
}
習3-4 UVA 455 週期串
思路
字串的週期一定是長度的約數,根據這個進行列舉就可以了。
程式碼
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
using namespace std;
int main(void)
{
int t;
char s[81];
cin >> t;
while (t --) {
scanf("%s", s);
int i;
int n = strlen(s);
for (i = 1; i <= n; i ++) {
if (n % i) continue;
bool flag = true;
for (int j = 1; j < n/i; j ++) {
for (int k = 0; k < i; k ++) {
if (s[k] != s[k+j*i]) {
flag = false; break;
}
}
if (flag == false) break;
}
if (flag == true) break;
}
printf("%d\n", i);
if (t) printf("\n");
}
return 0;
}
習3-5 UVA 227 謎題
思路
這個題我用了兩個常量陣列,inst陣列的作用是將字元翻譯成方向陣列對應的下標(0-3),方向陣列dir的作用是表示四個方向x和y座標的變化。這樣一個迴圈就ok了,不需要4個方向重複寫4次程式碼。
另外注意最後一組資料後面沒有空行,UVA很多題目都要求這樣輸出。
程式碼
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const char inst[] = "ABLR";
const int dir[4][2] = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
int main(void)
{
int t = 0;
char s[5][6];
char c;
while ((s[0][0] = getchar()) != 'Z') {
int bi = 0, bj = 0;
for (int i = 0; i < 5; i ++) {
for (int j = 0; j < 5; j ++) {
if (!i && !j) continue;
s[i][j] = getchar();
if (s[i][j] == ' ') {bi = i, bj = j;}
}
getchar();
}
bool legal = true;
while ((c = getchar()) != '0') {
if (legal == false || c == '\n') continue;
int k;
for (k = 0; k < 4; k ++) {
if (c == inst[k]) break;
}
if (k == 4)
legal = false;
else {
int ni = bi+dir[k][0], nj = bj+dir[k][1];
if (0 <= ni && ni < 5 && 0 <= nj && nj < 5) {
swap(s[bi][bj], s[ni][nj]);
bi = ni, bj = nj;
} else
legal = false;
}
}
if (++t > 1) printf("\n");
printf("Puzzle #%d:\n", t);
if (legal == false)
printf("This puzzle has no final configuration.\n");
else {
for (int i = 0; i < 5; i ++) {
for (int j = 0; j < 5; j ++) {
printf("%c%c", s[i][j], j == 4 ? '\n' : ' ');
}
}
}
getchar();
}
return 0;
}
習3-6 UVA 232 縱橫字謎的答案
思路
因為需要編號,應當先掃描並儲存起始格的位置,然後分別輸出橫向和縱向的單詞。
程式碼
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 10;
int main(void)
{
int t = 0;
int n, m;
char s[N][N+1];
int cnt = 0;
int pos[N*N+1][2];
while (scanf("%d", &n) != EOF && n) {
scanf("%d", &m);
getchar();
cnt = 0;
memset(pos, 0, sizeof(pos));
for (int i = 0; i < n; i ++) {
for (int j = 0; j < m; j ++) {
s[i][j] = getchar();
if (s[i][j] == '*') continue;
if (i == 0 || j == 0 || s[i-1][j] == '*' || s[i][j-1] == '*') {
pos[cnt][0] = i, pos[cnt][1] = j;
cnt ++;
}
}
getchar();
}
if (t > 0) printf("\n");
printf("puzzle #%d:\n", ++t);
printf("Across\n");
for (int k = 0; k < cnt; k ++) {
int i = pos[k][0], j = pos[k][1];
if (j > 0 && s[i][j-1] != '*') continue;
printf("%3d.", k+1);
do {
printf("%c", s[i][j]);
j ++;
} while (j < m && s[i][j] != '*');
printf("\n");
}
printf("Down\n");
for (int k = 0; k < cnt; k ++) {
int i = pos[k][0], j = pos[k][1];
if (i > 0 && s[i-1][j] != '*') continue;
printf("%3d.", k+1);
do {
printf("%c", s[i][j]);
i ++;
} while (i < n && s[i][j] != '*');
printf("\n");
}
}
return 0;
}
習3-7 UVA 1368 DNA 序列
思路
找出每列中ACGT出現次數最多的字元,就是最優解序列在這一列的字元值。另外注意要求的是字典序最小的解。
程式碼
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1000;
const int M = 50;
char *DNA = "ACGT";
int main(void)
{
int m, n;
char s[M][N+1];
int cnt[N][4];
int ans[N];
int d;
int t;
cin >> t;
while (t --) {
cin >> m >> n;
for (int i = 0; i < m; i ++)
scanf("%s", s[i]);
memset(cnt, 0, sizeof(cnt));
for (int i = 0; i < m; i ++) {
for (int j = 0; j < n; j ++) {
cnt[j][strchr(DNA, s[i][j]) - DNA] ++;
}
}
memset(ans, 0, sizeof(ans));
d = 0;
for (int j = 0; j < n; j ++) {
for (int k = 0; k < 4; k ++) {
if (cnt[j][k] > cnt[j][ans[j]])
ans[j] = k;
}
for (int k = 0; k < 4; k ++)
if (k != ans[j]) d += cnt[j][k];
}
for (int j = 0; j < n; j ++)
putchar(DNA[ans[j]]);
printf("\n%d\n", d);
}
return 0;
}
習3-8 UVA 202 迴圈小數
思路
求迴圈節需要模擬迴圈小數的求解過程。那麼什麼時候會出現迴圈呢?在除的過程中,除數b是不變的,而被除數a一直在變化,那麼當a變換為之前出現過的某個值時,就出現了迴圈。
程式碼
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 3000;
int main(void)
{
int a, b;
while (scanf("%d%d", &a, &b) != EOF) {
printf("%d/%d = %d.", a, b, a/b);
a %= b;
int n = 0;;
int dec[N+1];
int arr[N+1];
bool used[N+1] = {0};
while (!used[a]) {
arr[n] = a;
used[a] = 1;
a *= 10;
dec[n] = a/b;
n++;
a %= b;
}
int m = 0;
for (m = 0; m < n; m++) {
if (arr[m] == a) break;
}
for (int i = 0; i < m; i++)
printf("%d", dec[i]);
printf("(");
for (int i = m; i < n && i < m+50; i++)
printf("%d", dec[i]);
int len = n - m;
if (len > 50) printf("...");
printf(")\n %d = number of digits in repeating cycle\n\n", len);
}
return 0;
}
習3-9 UVA 10340 子序列
思路
順序掃描t中字元,遇到與s首字元相同情況即刪除s首字元,同時繼續往前掃描。當s中字元空時,說明t刪除字元可以得到s。
程式碼
#include <iostream>
#include <cstdio>
#include <string>
#include <algorithm>
using namespace std;
int main(void)
{
string s1, s2;
while (cin >> s1 >> s2) {
int i = 0;
for (int j = 0; j < s2.size(); j ++) {
if (s1[i] == s2[j]) i++;
if (i == s1.size()) break;
}
printf("%s\n", i == s1.size() ? "Yes" : "No");
}
return 0;
}
習3-10 UVA 1587 盒子
思路
這種題目看似簡單,但不好寫標準統一的程式碼,而且容易漏掉一些細節而出錯。我建議儘量將程式碼標準化,減少失誤的可能。有同學用類的思想處理,有值得借鑑之處。
程式碼
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
int main(void)
{
int a[12];
while (scanf("%d%d", &a[0], &a[1]) != EOF) {
if (a[0] > a[1]) swap(a[0], a[1]);
for (int i = 2; i < 12; i += 2) {
scanf("%d%d", &a[i], &a[i+1]);
if (a[i] > a[i+1]) swap(a[i], a[i+1]);
}
int b[12];
memcpy(b, a, sizeof(a));
sort(a, a+12);
bool flag = true;
int n[3];
for (int i = 0; i < 3; i ++) {
n[i] = a[i*4];
for (int j = 1; j < 4; j ++) {
if (n[i] != a[i*4+j])
flag = false;
}
}
int m[3] = {0};
for (int i = 0; i < 12; i += 2) {
if (m[0] < 2 && b[i] == n[0] && b[i+1] == n[1]) m[0] ++;
else if (m[1] < 2 && b[i] == n[0] && b[i+1] == n[2]) m[1] ++;
else if (m[2] < 2 && b[i] == n[1] && b[i+1] == n[2]) m[2] ++;
else flag = false;
}
if (! (m[0] == 2 && m[1] == 2) )
flag = false;
//printf("%d %d %d\n", m[0], m[1], m[2]);
if (flag)
printf("POSSIBLE\n");
else
printf("IMPOSSIBLE\n");
}
return 0;
}
習3-11 UVA 1588 換低檔裝置
思路
此題同上題一樣,在標準化方面有一定困難。我一開始寫的程式能通過用例,但死活就一直WA。
這段程式碼是參考別人的寫的,其程式碼比較規範,值得推薦。
程式碼
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
char s1[110], s2[110];
int test(int k, char s1[], char s2[]) {
for (int i = 0; s1[k+i] && s2[i]; i++)
if (s1[k+i]+s2[i]-2*'0' > 3) return 0;
return 1;
}
int fun(char s1[], char s2[]) {
int k = 0;
while (!test(k, s1, s2)) k++;
return max(strlen(s1), strlen(s2)+k);
}
int main() {
while (scanf("%s%s", s1, s2) != EOF) {
printf("%d\n", min(fun(s1, s2), fun(s2, s1)));
}
return 0;
}
習3-12 UVA 11809 浮點數
思路
我的做法是根據最大十進位制數反推二進位制表示中的M和E,例子都過了,但是提交後TLE。搜了一下其他人的解法,清一色的打表。這個題的M和E範圍確實有限,打表只需要預先計算300個並儲存,然後查表即可。
估計這個題的查詢量比較大吧,否則我直接求應該也可以的。
有時間重新寫一個打表的程式把這題AC掉,先貼上我的TLE程式碼。看到本文的大神如有指導也請不吝賜教。
程式碼
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<cmath>
using namespace std;
const double EPS = 1e-6;
int main()
{
double a;
while (scanf("%lf", &a) != EOF) {
if (abs(a) < EPS) break;
int y = 0;
while (a >= 1) {
a /= 2;
y++;
}
int m = 0;
a = 1-a;
while (abs(a-1) > EPS) {
a = a*2;
m ++;
}
int e = 0;
while (y) {
e++;
y /= 2;
}
printf("%d %d\n", m-1, e);
}
return 0;
}