紫书第3章 数组和字符串

1  序


       系统的整理下第三章的学习笔记。

例题代码是在未看书本方法前自己尝试并AC的代码,不一定比书上的标程好。习题除了3-8百度了求解方法,其他均独立完毕后,会适当查阅网上资料进行整理总结。希望本博文方便自己日后复习的同一时候。也能给他人带来点故意的帮助(建议配合紫书——《算法竞赛入门经典(第2版)》阅读本博客)。有不足或错误之处。欢迎读者指出。

       转载请注明出处:code4101,谢谢。

2  例题

2.1  UVa272--Tex Quotes

#include <stdio.h>

int main() {
    bool logo = 1;
    char ch;

    while ((ch = getchar()) != EOF) {
        if (ch == '\"') {
            if (logo) printf("``");
            else printf("''");
            logo ^= 1;
        }
        else putchar(ch);
    }

    return 0;
}
       第9~10行:我本来是用puts来输出``和'',发现会自带换行符,所以用printf的%s输出。LRJ的代码是用了这技巧:q?"``":"''"。


       第11行:LRJ的标记变量是用q = !q来实现状态变换,我这里用异或位运算效率更高。


2.2  UVa10082--WERTYU

       第一次遇这样的题。以为是要用if来暴力做的变态题,然后才发现常量数组的妙用。假设不是小白书看过。真的写不出来。
       注意转义字符'\'要写成'\\'。以下是第一次AC的代码。
#include <stdio.h>

char str[4][50] = {"`1234567890-=", "QWERTYUIOP[]\\",
                "ASDFGHJKL;'", "ZXCVBNM,./"};
int main() {
    int a[200], i, j;

    for (i = 0; i < 4; i++)
        for (j = 1; a[str[i][j]] = str[i][j-1]; j++);
    a[' '] = ' '; a['\t'] = '\t'; a['\n'] = '\n';
    while ((i = getchar()) != EOF) putchar(a[i]);
    return 0;
}
       第9行:用相当于“哈希映射”的方式。把每一个输入字符(str),应该匹配的数值存到一个数组(a)。
       第11行:这样用getchar循环读入i,相应输出a[i]即可了。



       看了白书后,发现自己的办法有些复杂了(尽管效率上高一点)。为什么要分四个子串呢?全然能够合并成一行,然后用一个循环找,找到后输出前一个字符。找不到就原样输出。避免了原代码中第10行的操作。

只是我的方法紫书兴许也提到了,以下是我综合下优化后的代码:

#include <stdio.h>
char str[200] = {"`1234567890-=QWERTYUIOP[]\\ASDFGHJKL;'ZXCVBNM,./  \t\t\n\n"};
int main() {
    int a[200], i;
    for (i = 1; a[str[i]] = str[i-1]; i++);
    while ((i = getchar()) != EOF) putchar(a[i]);
    return 0;
}
       第2行:注意末尾的空格、制表符、回车三个字符各连写了两个。这样不用像上次那样多写了第10行。

2.3  UVa401--Palindromes

       这题曾经小白书做过,写了非常久。不够最后的代码还是挺精炼的。

#include <stdio.h>
#include <string.h>

char s[100], key1[] = "AEHIJLMOSTUVWXYZ12358", key2[] = "A3HILJMO2TUVMXY51SEZ8", *p;

int ispal()
{
	int i = 0, j = strlen(s) - 1;
	while (i < j) if (s[i++] != s[j--]) return 0;
	return 1;
}

int ismir()
{
	int i = -1, j = strlen(s);
	while (++i <= --j)
	{
		p = strchr(key1, s[i]);
		if (p) { if (s[j] != key2[p-key1]) return 0;}
		else return 0;
	}
	return 1;
}

int main()
{
	int t;
	while (scanf("%s", s) != EOF)
	{
		t = ismir();
		printf("%s -- is ", s);
		if (ispal()) printf("%s", t?

"a mirrored palindrome":"a regular palindrome"); else printf("%s", t?"a mirrored string":"not a palindrome"); printf(".\n\n"); } return 0; }

       关键是怎样推断镜像性质:
  1. 用两个字符数组存储镜像字符的映射关系。(思想同上题)
  2. 没考虑到最中间的字符也要推断是否水平翻转后为其本身,WA几次。(仅仅需略改动循环条件)
  3. 学习strchr函数。(该函数找不到字符时返回空指针NULL)
       LRJ的代码分析
       首先回文串部分LRJ直接在main里推断,我的话不是必需为了精简代码而复杂了思维,分成两个子函数不易出错。我的看似短,但整体思路更易于理解。


       我是用指针来相应到左右两端,往中间移。LRJ是用下标索引。左是i,右边自然是len-1-i,两种方法都行。
       至于他的r函数,正是利用了题目说没有空白输入的特性。写了那个rev字符串,然后将字符直接反转比較。由于字符串肯定没有空格,而反转后非法的字符。在LRJ代码里就变成空格了。肯定匹配不了,所以能巧妙的解决这个问题!


2.4  UVa340--Master-Mind Hints

       这题小白书也做过,由于数字仅仅有1~9,仅仅要统计每一个数字出现的次数,减去强匹配的数量即可。思路同书本但没有那么精简,就不贴代码了。


2.5  UVa1583--Digit Generator

       思路与书本一样。

#include <stdio.h>

int y[100100];

int main() {
    for (int i = 0; i < 100000; i++) {
        int s = i, t = i;
        while (t) { s+=t%10; t/=10;}
        if (y[s] == 0) y[s] = i;
    }

    int T, pos;
    scanf("%d", &T);
    while (T--) {
        scanf("%d", &pos);
        printf("%d\n", y[pos]);
    }
    return 0;
}

2.6  UVa1584--Circular Sequence

       题目:输入一个长度在2~100的字符串,要理解成环状。然后能从随意地方作为起点生成一个新的同长度字符串。问这些字符串中字典序最小的是哪个。

       分析与求解:字符串的比較能够使用strcmp函数。将要比較的两个字符串首地址,即指针传入就可以。那么,我们能够用一个min_p的值来存储当前字典序最小的字符串起始下标,用i来遍历1~len-1的起始下标,由于是环状,所以每轮i循环,都在字符串y末尾加入相应的字符,并加入'\0'方便strcmp的调用。
       方法跟紫书不一样。

详细思路和细节。与strcmp的灵活使用,读者能够自己分析思考。

#include <stdio.h>
#include <string.h>

int main() {
    int T;
    scanf("%d", &T);

    while (T--) {
        char y[300];
        scanf("%s", y);

        int len = strlen(y), i, min_p = 0;
        for (i = 0; i < len; i++) {
            y[i+len] = y[i];
            y[i+len+1] = 0;
            if (strcmp(y+min_p,y+i+1) > 0)
                min_p = i+1;
        }
        y[min_p+len] = 0;
        printf("%s\n", y+min_p);
    }
    return 0;
}

3  习题

3.1  UVa1585--Score

#include <stdio.h>

int main() {
    int T;
    scanf("%d", &T);

    while (T--) {
        char s[100];
        scanf("%s", s);

        int sum = 0, cnt = 0;
        for (int i = 0; s[i]; i++) {
            if (s[i]=='X') {
                sum += cnt*(cnt+1)/2;
                cnt = 0;
            }
            else cnt++;
        }
        printf("%d\n", sum+cnt*(cnt+1)/2);
    }
    return 0;
}
       每遇到X做一次更新,否则cnt++。注意第19行最后还要一次更新。

       看了sinat_17231979的代码后,发现还是想复杂了,不是必需用前n项和公式,直接用一个变量记录当前有多少个O即可了:
#include <stdio.h>

int main() {
    int T;
    scanf("%d", &T);

    while (T--) {
        char s[100];
        scanf("%s", s);

        int sum = 0, cnt = 0;
        for (int i = 0; s[i]; i++) {
            if (s[i]=='X') cnt = 0;
            else {
                cnt++;
                sum += cnt;
            }
        }
        printf("%d\n", sum);
    }
    return 0;
}

3.2  UVa1586--Molar Mass

        暴力每一位i,假设i+1位是数字。推断i+2位是不是数字,分情况计算数量;假设i+1位不是数字,那么数量就是默认的1。接着推断第i位是不是字母,不是则continue。否则累加对应的质量。
#include <stdio.h>
#include <ctype.h>

int main() {
    int T;
    scanf("%d", &T);

    while (T--) {
        char s[100];
        scanf("%s", s);

        double sum = 0, w;
        for (int i = 0; s[i]; i++) {
            // 算出后两位所代表的数值
            if (isdigit(s[i+1])) {
                if (isdigit(s[i+2])) w = 10*(s[i+1]-'0')+s[i+2]-'0';
                else w = s[i+1] - '0';
            }
            else w = 1;

            // 推断第一位
            if (!isalpha(s[i])) continue;
            else if (s[i] == 'C') sum += 12.01*w;
            else if (s[i] == 'H') sum += 1.008*w;
            else if (s[i] == 'O') sum += 16.00*w;
            else sum += 14.01*w;
        }
        printf("%.3lf\n", sum);
    }
    return 0;
}

3.3  UVa1225--Digit Counting

#include <stdio.h>

int main() {
    int T;
    scanf("%d", &T);

    while (T--) {
        int N, i, t, a[10] = {};
        scanf("%d", &N);

        for (i = 1; i <= N; i++) {
            t = i;
            while (t) a[t%10]++, t/=10;
        }
        for (i = 0; i < 9; i++) printf("%d ", a[i]);
        printf("%d\n", a[9]);
    }
    return 0;
}
       直接每组暴力都能过。假设会TLE。也能够建立二维数组a[10000][10],先暴力一轮求出数据。后面仅仅要打表即可了。

3.4  UVa455--Periodic Strings

       算一个字符串的周期,其周期一定是长度的约数,按这一点枚举,并写一个推断T是不是一个字符串的周期的函数(isThePeriod)就可以。



       输入格式略坑。每两组測试间会有一个空行,所以我用第19行那样的方式解决。



       最后注意输出格式,也要每两个隔一个空行~~~

#include <stdio.h>
#include <string.h>

int isTthePeriod(char s[], int T) {
    for (int i = 0; i < T; i++) {
        for (int j = i+T; j < strlen(s); j+=T)
            if (s[i] != s[j]) return 0;
    }
    return 1;
}

int main() {
    int T, kase, len, i;
    scanf("%d", &T);

    for (kase = 1; kase <= T; kase++)
    {
        char s[100];
        while (scanf("%s", s), len = strlen(s), !len);

        for (i = 1; i <= len; i++)
            if (len%i == 0 && isTthePeriod(s,i)) break;
        if (kase != 1) putchar('\n');
        printf("%d\n", i);
    }
    return 0;
}

3.5  UVa227--Puzzle

       这题略微有些麻烦了。模拟操作,我尽量进行了些优化,并且为了方便交换变量。用了C++里algorithm的swap。没有自己写交换函数。

【基本框架】
将5*5网格定义为全局变量s(从1開始编号)。当前空格所在行列为x,y
output函数用来输出s的矩阵形式
ok用来操作一个命令为ch的移动。返回值1代表操作成功。返回0代表失败。成功的话。要更新对应的x、y和进行交换操作swap
test用来完毕每组測试的操作
       
       总结:对于复杂的问题,要想清思路。然后把大问题划分成一个个小的子问题去求解。
#include <cstdio>
#include <algorithm>
using namespace std;

int x, y;
char s[6][6];

void output() {
    for (int i = 1; i <= 5; i++) {
        for (int j = 1; j < 5; j++)
            printf("%c ", s[i][j]);
        printf("%c\n", s[i][5]);
    }
}

int ok(char ch) {
    switch (ch) {
    case 'A':
        if (x == 1) return 0; else x--;
        swap(s[x][y], s[x+1][y]); break;
    case 'B':
        if (x == 5) return 0; else x++;
        swap(s[x][y], s[x-1][y]); break;
    case 'L':
        if (y == 1) return 0; else y--;
        swap(s[x][y], s[x][y+1]); break;
    case 'R':
        if (y == 5) return 0; else y++;
        swap(s[x][y], s[x][y-1]); break;
    }
    return 1;
}

int test() {
    char ch;
    int i, j;
    for (i = 1; i <= 5; i++) for (j = 1; j <= 5; j++)
        if (s[i][j] == ' ') x = i, y = j;

    while ((ch = getchar()) != '0') {
        if (ch == '\n') continue;
        if (!ok(ch)) {  // 非法操作,仍要清掉字符'0'前的值
            while (getchar() != '0');
            return 0;
        }
    }

    return 1;
}

int main() {
    int kase, i, j;

    for (kase = 1; ; kase++) {
        gets(&s[1][1]);
        if (s[1][1] == 0) {kase--; continue;}   // 注意此处读到空串,要忽略读入continue
        if (s[1][1] == 'Z' && s[1][2] == 0) break;

        if (kase != 1) putchar('\n');
        printf("Puzzle #%d:\n", kase);
        for (i = 2; i <= 5; i++) gets(&s[i][1]);
        if (test()) output();
        else printf("This puzzle has no final configuration.\n");
    }
    return 0;
}

3.6  UVa232--Crossword Answers

       这题就是像中学英语填单词的那种网格。须要对起始点编号,然后按行输出、再按列输出全部单词。
#include <stdio.h>
#include <string.h>
int main() {
    int n, m, a[15][15], k, i, j;
    char s[15][15];

    for (int kase = 1; ; kase++) {
        if (scanf("%d%d ", &n, &m) != 2) break;

        k = 0;
        memset(a, 0, sizeof(a));
        memset(s, 0, sizeof(s));

        for (i = 1; i <= n; i++) gets(&s[i][1]);
        for (i = 1; i <= n; i++) for (j = 1; j <= m; j++) {
            if (s[i][j] == '*') s[i][j] = 0;
            if (!(s[i-1][j]&&s[i][j-1]) && s[i][j]) a[i][j] = ++k;
        }

        if (kase != 1) putchar('\n');
        printf("puzzle #%d:\nAcross\n", kase);
        for (i = 1; i <= n; i++) for (j = 1; j <= m; j++)
            if (s[i][j] && !s[i][j-1]) printf("%3d.%s\n", a[i][j], s[i]+j);
        printf("Down\n");
        for (i = 1; i <= n; i++) for (j = 1; j <= m; j++) {
            if (s[i][j] && !s[i-1][j]) {
                printf("%3d.", a[i][j]);
                for (k = i; s[k][j]; k++) putchar(s[k][j]);
                putchar('\n');
            }
        }
    }
    return 0;
}

3.7  UVa1368--DNA Consensus

       思路:找出每列中ACGT出现次数最多的字符,就是最优解序列在这一列的字符值。
#include <stdio.h>
#include <string.h>

inline int c2d(char c) {
    if (c == 'A') return 0;
    else if (c == 'C') return 1;
    else if (c == 'G') return 2;
    else return 3;
}

inline int d2c(int d) {
    if (d == 0) return 'A';
    else if (d == 1) return 'C';
    else if (d == 2) return 'G';
    else return 'T';
}

int main() {
    int T, m, n, i, j, a[1010][4], sum;
    char s[50][1010];
    scanf("%d", &T);

    while (scanf("%d%d ", &m, &n) == 2) {
        for (i = 0; i < m; i++) gets(s[i]);
        memset(a, 0, sizeof(a));
        for (i = 0; i < m; i++) for (j = 0; j < n; j++)
            a[j][ c2d(s[i][j]) ]++;
        for (sum = j = 0; j < n; j++) {
            int the_max = a[j][0], id = 0;
            for (i = 1; i < 4; i++) if (a[j][i]>the_max)
                id = i, the_max = a[j][i];
            putchar(d2c(id));
            sum += m - the_max;
        }
        printf("\n%d\n", sum);
    }
    return 0;

}

3.8  UVa202--Repeating Decimals

       这题须要模拟除法。当出现同样的余数时,就代表循环点的出现。以下我以5/7为例具体的讲下思路。

       如上图。第0步,分子是5,分母是7(分母在整个除法中始终不变),5/7的商是0,代表这个除法的整数部分是0,,余数是5,乘以10,得50作为第1步的分子;50/7的商为7,代表这个除法的第一位小数是7,余数是1。乘以10,得10作为第2步的分子...能够发现每步的除法仅仅跟分子有关,也就是当分子出现反复值。如上图第1步和第7步的分子值都为50时,第7步后面的运算事实上是在反复第1步至第6步间的运算。所以第1步至第7步就是一个循环节。

       程序实现中,能够不用存储余数的值,仅仅需存储上图中的分子和商,我定义了A[2][3010](注意循环长度一定不会大于分母。也就是3000次内一定会出现循环)。用A[0][j]存储第j步的分子。A[1][j]存储第j步的商。注意最后的数值由商能够直接构成,如上图的红色字体,数值为0.7142857142...
#include <stdio.h>

int A[2][3010], p, q;

// 找i位之前是否有反复分子出现;有则返回周期,否则返回-1.
inline int myfind() {
    for (p = 1; p < q; p++) {
        if (A[0][p] == A[0][q])
            return q - p;
    }
    return -1;
}

int main() {
    int kase, a, b, i, len;
    for (kase = 1; scanf("%d%d", &a, &b) != EOF; kase++) {
        A[0][0] = a; A[1][0] = a/b;
        for (q = 1; ; q++) {
            A[0][q] = (A[0][q-1] - b*A[1][q-1])*10;
            A[1][q] = A[0][q]/b;
            len = myfind();
            if (len > 0) break;
        }
        printf("%d/%d = %d.", a, b, A[1][0]);
        for (i = 1; i < p; i++) printf("%d", A[1][i]);
        putchar('(');
        for (i = p; i < q; i++) {
            if (i > 50) {
                printf("...");
                break;
            }
            printf("%d", A[1][i]);
        }
        printf(")\n   %d = number of digits in repeating cycle\n\n", len);
    }

}

3.9  UVa10340--All in All

       遍历后者。找前者往后是否有相应的字符。

#include <string>
#include <iostream>
using namespace std;

int fun(string& a, string& b) {
    int i, j = 0;
    for (i = 0; i < a.size(); i++, j++) {
        while (j < b.size() && a[i]!=b[j]) j++;
        if (j == b.size()) return 0;
    }
    return 1;
}

int main() {
    string a, b;
    while (cin >> a >> b) {
        if (fun(a,b)) cout << "Yes\n";
        else cout << "No\n";
    }
}

3.10  UVa1587--Box

       仅仅要用一堆疯狂的条件分支就能解答此题了,如KinderRiven的代码。只是这里我更乐意把数据“标准化”,然后用C++构造“Face类”来求解。

       首先构造一个宽w一定不小于高h的“面类(Face)”。而且按h为第一级,w为第二级准则从小到大排序。假设是能组成长方体的。边长肯定仅仅有三种值。最好还是设从小到大依次为a,b,c。

则输入的6个面排序后,一定满足下面分布:


       首先每种面肯定都出现了两次,即编号0、1同样,编号2、3同样,编号4、5同样。

把这个准则,设计为測试函数test1。

       接下来。编号0的h一定与编号2的h相等。编号0的w一定与编号4的h相等,编号2的w一定与编号4的w相等。把这个准则。设计为測试函数test2。

       满足上述两个測试。是数据能组合成长方体的充要条件。
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;

struct Face{
    int h, w;
    void make(int x, int y) {
        h = min(x,y);
        w = max(x,y);
    }
    bool operator < (const Face& b) const {
        if (h == b.h) return w < b.w;
        else return h < b.h;
    }
    bool operator == (const Face& b) const {
        return (h == b.h) && (w == b.w);
    }
};
vector<Face> A(6);

int test1() {
    for (int i = 0; i < 6; i += 2) if (!(A[i]==A[i+1])) return 0;
    return 1;
}

int test2() {
    if (A[0].h==A[2].h && A[0].w==A[4].h && A[2].w==A[4].w) return 1;
    else return 0;
}

int main() {
    int x, y;
    while (cin >> x >> y) {
        A[0].make(x, y);
        for (int i = 1; i < 6; i++) {
            cin >> x >> y;
            A[i].make(x, y);
        }
        sort(A.begin(), A.end());
        if (test1() && test2()) cout << "POSSIBLE\n";
        else cout << "IMPOSSIBLE\n";
    }
    return 0;

}

3.11  UVa1588--Kickdown

       如果固定装置s1。那么装置s2仅仅要初始状态左端与s1左端对齐(该状态用偏移量k=0来标记)。然后測试k=0时。两个装置是不是”每一位的和都不会超过3“即可了,如果不行就k++,继续往右边偏移进行測试。能够知道肯定存在着一个k能使其成立的(也就是s2左端接在s1右端时)。

用这样的方式遍历,找到的第一个可行解就是最佳解。

       只是上述讨论还遗漏了一种组合方式,就是s2也能够往左偏移。

事实上将上述解法写成一个fun函数进行封装。外界(main函数)仅仅要巧妙的利用对称性,就能反复利用上一段的代码了。

#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;

int len1, len2;
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  UVa11809--Floating-Point Numbers

       首先要知道二进制小数的原理:

       本题的思路就是先把全部的M、E相应的值算出来,记为矩阵C,当中C[M,E]=尾数M、阶码E相应的最大值。

然后对于输入的AeB,找出C中与其相等的值所在的行M、列E即是答案。

       只是这里有两个要点。

一是这个数值太大,能够达到2^(2^30-1)。远超了double的存储范围。

所以我们最好还是对数值做一个变换,取以10为底的对数值来存储。二是计算过程的舍尾误差与double本身存在的误差会导致精度问题。所以能够在矩阵C中找与输入值最接近的作为匹配点,也就是差值的绝对值最小。
       最后还有M=0。E=1的特殊点,在程序中会出现0.0-0.0<0.0条件为否的错误,导致pi,pj无法更新,这个问题仅仅要把pi、pi分别初始化为0、1就能攻克了。

#include <math.h>
#include <stdio.h>
#include <string.h>

double C[10][31];

void init() {
    int i, j, v;
    double f[10] = {0.5}, g[31], t;

    for (i = 1, t = 0.5; i < 10; i++) {
        f[i] = f[i-1] + t/2;
        t /= 2;
    }
    for (i = 0; i < 10; i++) f[i] = log10(f[i]);
    for (i = 1, v = 2; i <= 30; i++) {
        g[i] = (v - 1.0)*log10(2.0);
        v <<= 1;
    }
    for (i = 0; i < 10; i++) for (j = 1; j <= 30; j++)
        C[i][j] = f[i] + g[j];
}

int main() {
    int i, j;
    char s[100], *p;
    double A, B;

    init();

    while (scanf("%s", s), strcmp(s, "0e0")) {
        p = strchr(s, 'e');
        *p = 0;                 // 将'e'所在位置变为'\0'
        sscanf(s, "%lf", &A);
        sscanf(p+1, "%lf", &B);

        int pi = 0, pj = 1;
        B = A = log10(A) + B;   // 接下来A存储表达式值,B记录差值
        for (i = 0; i < 10; i++) for (j = 1; j <= 30; j++)
        if (fabs(A-C[i][j]) < B) pi = i, pj = j, B = fabs(A-C[i][j]);
        printf("%d %d\n", pi, pj);
    }
    return 0;
}
posted on 2017-06-17 09:32  ljbguanli  阅读(193)  评论(0编辑  收藏  举报