算法竞赛入门 (一)语言篇 数组和字符串 1

学习目标

  1. 一维数组的声明和使用
  2. 二维数组的声明和使用
  3. 字符串的声明、赋值、比较和连接
  4. ASCII码和ctype.h中的字符函数
  5. 正确认识“++”、“+=”等能修改变量的运算符
  6. fgetc和getchar的使用方法
  7. 了解不同操作系统中换行符的表示
  8. 掌握fgets的使用方法并了解gets的“缓冲区溢出”漏洞
  9. 学会用常量表简化代码

 

一、数组

程序1: 输入5个数,逆序输出

#include<stdio.h>
#define maxn 8
int a[maxn];
int main()
{
    int x, n = 0;
    while(scanf("%d", &x) == 1){
        a[n++] = x;
    }
    for(int j = n-1 ; j >= 0; j--)
        printf("%d ",a[j]);
    return 0;
}

 

 

 最后的^D 用来结束输入【即Ctrl +D】

# 在算法竞赛中,常常难以精确计算出需要的数组大小,数组一般会声明得稍 大一些。在空间够用的前提下,浪费一点不会有太大影响,所以即使是 输入5个数,保险起见完全可以定义数组大小为8

语句:a[n++]=x —— 做了两件事:首先赋值a[n]=x,然后执行n=n+1

#  对于变量n,n++和++n都会给n加1,但当它们用在一个表达式中时,行为有所差别:n++会使用加1前的值计算表达式【即n的原值】,而++n会使用加1后的值计算表达式【即n+1后的值】 ———— 谁在前面,就先用谁 【n++,n在前mian所以用n的原值,++n,++在前面,就将n先+1】

 

为什么要把a的定义放在main函 数的外面?

只有放在外面时,数组a的大小才可以开得很大;放在main函数内时,数组稍大就会异常退出

#  所以,比较大的数组应尽量声明在main函数外,否则程序可能无法运行

 

 

但数组不能进行赋值操作:

在程序3-1中,如果声明的是“int a[maxn],b[maxn]”,是不能赋值b=a的

如果要从数组a复制k个元素到数组b,可以这样做 —— memcpy(b,a,sizeof(int)*k)

当然,如果数组a和b 都是浮点型的,复制时要写成 —— memcpy(b,a,sizeof(double)*k)。另外需要注意的是, 使用memcpy函数要包含头文件string.h

如果需要把数组a全部复制到数组b中,可以写得简单 一些:memcpy(b,a,sizeof(a))

 

 

 

程序2:开灯问题

有n盏灯,编号为1~n。第1个人把所有灯打开,第2个人按下所有编号为2 的倍数的开关(这些灯将被关掉),第3个人按下所有编号为3的倍数的开关(其中关掉的灯 将被打开,开着的灯将被关闭),依此类推。一共有k个人,问最后有哪些灯开着?输 入n和k,输出开着的灯的编号。k≤n≤1000

样例输入:
7 3
样例输出:
1 5 6 7

 

我的思路: n个灯 k个人,如果 灯的编号 % 人的编号 == 0,就将灯的状态取反 ————>  可以利用 数组 + 双层for循环 + 状态值 实现

#include<stdio.h>
#include<string.h>
#define maxn 1010
int a[maxn];
int main()
{
    int n,k,first =1;
    memset(a,0,sizeof(a));
    scanf("%d %d",&n,&k);
    for(int i = 1;i <= k;i++){
        for(int j = 1;j <= n;j++){
            if(j % i == 0)
                a[j] = !a[j];
        }
    }
    for(int i = 1;i <= n;i++){
        if(a[i]){     //如果第i个灯是亮的,即a[i] = 1
            if(first)
                first = 0;
            else
                printf(" ");
            printf("%d",i);
        }
    }
    printf("\n");
    return 0;
}

技巧:

  1. memset(a,0,sizeof(a))—— 作用是把数组a清零,在string.h中定义
  2. 为了避免输出在最开始和最后输出多余空格,设置了一个标志变量first,可以表示当前要输出的变量是否为第一个。第一个变 量前不应有空格,但其他变量都有

 

采用样例数据测试时【即第1 5 6 7个灯是亮的】,可以在 程序 最后加入  printf("%d",a[1]); 输出结果是1,即 

a[j] = !a[j];

取反后,由0变1,或由1变0

 

 

程序3:蛇形填数

在n×n方阵里填入1,2,…,n×n,要求填成蛇形。例如,n=4时方阵为:

上面的方阵中,多余的空格只是为了便于观察规律,不必严格输出。n≤8。

 

我的思路:。。。我不会  =_=......  =__= .....=___=....

用二维数组解决 —— int a[n][n] ——> 然后从a[0][n] --> a[n][n]初始化 ---> a[n][n]--->a[n][0] ---->a[0][0]--->a[0][n-1]。。。。for循环 【但含有多次不定长的转向,短时间内无法做出来】

 

书的思路:

也是二维数组,从1开始依次填写。设“笔”的坐标为(x,y),则一开始x=0,y=n-1,即第0行,第n-1列 (行列的范围是0~n-1,没有第n列)。“笔”的移动轨迹是:下,下,下,左,左,左, 上,上,上,右,右,下,下,左,上。总之,先是下,到不能填为止,然后是左,接着是 上,最后是右。“不能填”是指再走就出界(例如4→5),或者再走就要走到以前填过的格子 (例如12→13)。如果把所有格子初始化为0,就能很方便地加以判断

#include<stdio.h>
#include<string.h>
#define maxn 20
int a[maxn][maxn];
int main()
{
    int n, x, y, tot = 0;
    scanf("%d", &n);
    memset(a, 0, sizeof(a));
    tot = a[x=0][y=n-1] = 1;//从1开始写
    while(tot < n*n)
    {
            while(x+1<n && !a[x+1][y])
                a[++x][y] = ++tot;    //【1】
            while(y-1>=0 && !a[x][y-1])
                a[x][--y] = ++tot;    //【2】
            while(x-1>=0 && !a[x-1][y])
                a[--x][y] = ++tot;    //【3】
            while(y+1<n && !a[x][y+1])
                a[x][++y] = ++tot;    //【4】
    }
    for(x = 0; x < n; x++)
        {
            for(y = 0; y < n; y++)
                printf("%3d", a[x][y]);
            printf("\n");
        }
    return 0;
}

 

【1】:这一行中,保持y不变,x每次移动加一,从上往下“填”数字,同时笔在往下移动的时候要满足 x不能超过n,因为是 n* n的数字阵列,且为了循环填完外层数后逐步循环填内层数,要保证下一次将要“填数”的位置   是0,即还没有填入数字【填上数后 !a[x][y] = 0】

 

 

 此时x = 0,y =2 在a[x][++y]打印16,即y = 3,此时开始循环判定,y + 1 <n  true,但  a[x][y+1] 已被填入 “1”,无法继续往右填,开始往下写(下一轮循环)

如果去除所有的预判是否过界:【去掉  &&  !a[x][y]  】

 

 其余过程同理

 

# 在很多情况下,最好是在做一件事之前检查是不是可以做,而不要做完再后 悔。因为“悔棋”往往比较麻烦

# 如果x+1<n为假,将不会计算“!a[x+1][y]”,也就不会越界 ———— &&是短路运算符 【这里提过】

 

为什么是++tot而不是tot++?

#include<stdio.h>

int main(){
    int i = 1;
    printf("%d\n",i);
    i++;
    printf("%d\n",i);
    ++i;
    printf("%d\n",i);
    return 0;
}

 

 回顾刚才提到的:

#  对于变量n,n++和++n都会给n加1,但当它们用在一个表达式中时,行为有所差别:n++会使用加1前的值计算表达式【即n的原值】,而++n会使用加1后的值计算表达式【即n+1后的值】 ———— 谁在前面,就先用谁 【n++,n在前mian所以用n的原值,++n,++在前面,就将n先+1】

 

 

 

二、字符数组

在C语言中,字符串其实就是字符数组——可以像处理普通数组一样,处理字符串,只需要注意输入输出和字符串函数的使用

程序4:竖式问题

找出所有形如abc*de(三位数乘以两位数)的算式,使得在完整的竖式中,所有数字都属于一个特定的数字集合。输入数字集合(相邻数字之间没有空格),输出所有竖式。每个竖式前应有编号,之后应有一个空行。最后输出解的总数。具体格式见样例输出(为了便于观察,竖式中的空格改用小数点显示,但所写程序中应该输出空格,而非小数点)。

样例输入:
2357
样例输出:
<1>
..775
X..33
-----
.2325
2325.
-----
25575
The number of solutions = 1

 

 我的思路:通过for循环遍历尝试所有的abc和de,判断是否满足题目要求,如果满足,打印出来

#include<stdio.h>
#include<string.h>
int main()
{
    int count = 0;
    char s[20], buf[99];
    scanf("%s", s);
    for(int abc = 111; abc <= 999; abc++)
        for(int de = 11; de <= 99; de++)
            {
                int x = abc*(de%10), y = abc*(de/10), z = abc*de;
                sprintf(buf, "%d%d%d%d%d", abc, de, x, y, z);
                int ok = 1;
                for(int i = 0; i < strlen(buf); i++)
                    if(strchr(s, buf[i]) == NULL)
                        ok = 0;
                if(ok)
                    {
                        printf("<%d>\n", ++count);
                        printf("%5d\nX%4d\n-----\n%5d\n%4d\n-----\n%5d\n\n", abc, de, x, y, z);
                    }
            }
    printf("The number of solutions = %d\n", count);
    return 0;
}

 

解释:

【1】

printf("%5d\nX%4d\n-----\n%5d\n%4d\n-----\n%5d\n\n", abc, de, x, y, z);

因为最大不过999*99 = 98901,5位数字,所以最后结果z 用%5d 输出即可,满足当结果5位时,显示5位,结果4位时,前留空格,满足格式要求

【2】

int x = abc*(de%10), y = abc*(de/10), z = abc*de;  

在样例输入中,分别是: abc=775,ad=33,x=775*3[de的个位数],y=775*3,z=755*33

【3】

char s[20], buf[99];  

buf 是缓冲字符串,用来验证“在完整的竖式中,所有数字都属于一个特定的数字集合”

【4】

sprintf(buf, "%d%d%d%d%d", abc, de, x, y, z);   

将完整的竖式所有数字 输出到 缓冲字符串,遍历每一项查询是否 在输入的数字集合中存在

 

 

 

 

char是“字符型”的意思,而字符是一种特殊的整数

ASCII码地址

每一个字符都有一个整数编码,称为ASCII码。

为了方便书写,C语言允许用直接的方法表示字符,例如,“a”代表的就是a的ASCII码。不过,有一些字符直接表示出来并不方便,例如,回车符是“\n”,而空字符是“\0”,它也是C语言中字符串的结束标志。其他例子包括“\\”(注意必须有两个反斜线)、“\'”(这个是单引号),甚至还有的字符有两种写法:“\"”和“"”都表示双引号。像这种以反斜线开头的字符称为转义序列

# C语言中的字符型用关键字char表示,它实际存储的是字符的ASCII码。字符常量可以用单引号法表示。在语法上可以把字符当作int型使用。

# scanf("%s", s)。和 scanf("%d", &n) 类似,它会读入一个不含空格、TAB和回车符的字符串,存入字符数组s。注意,不是“scanf("%s", &s)”,s前面没有“&”符号。

# 在“scanf("%s", s)”中,不要在s前面加上“&”符号。如果是字符串数组chars[maxn] [maxl],可以用“scanf("%s", s[i])”读取第i个字符串。注意,“scanf("%s", s)”遇到空白字符会停下来。

判断和输出:

先考虑输出,因为它比较简单。每个竖式需要打印7行,但不一定要用7条printf语句,1条足矣。首先计算第一行乘积x=abc*e,然后是第二行y=abc*d,最后是总乘积z=abc*de,然后一次性打印出来:

printf("%5d\nX%4d\n-----\n%5d\n%4d\n-----\n%5d\n\n", abc, de, x, y, z);

 

注意这里的%5d,它表示按照5位数打印,不足5位在前面补空格

 

sprintf和strchr:

strchr的作用是在一个字符串中查找单个字符,而这个sprintf似曾相识:之前用过printf和fprintf。printf输出到屏幕,fprintf输出到文件,而sprintf输出到字符串。多数情况下,屏幕总是可以输出的,文件一般也能写(除非磁盘满或者硬件损坏),但字符串就不一定了:应该保证写入的字符串有足够的空间

 

字符空间多大才足够?

答案是字符个数加1,因为C语言的字符串是以空字符“\0”结尾的。

空间够用的情况下,浪费一点没关系

  

函数strlen(s)

作用是获取字符串s的实际长度。什么叫实际长度呢?字符数组s的大小是20,但并不是所有空间都用上了。如果输入是“2357”,那么实际上s只保存了5个字符(不要忘记了还有一个结束标记“\0”),后面15个字符是不确定的(变量在赋值之前是不确定的)。strlen(s)返回的就是结束标记之的字符个数。因此这个字符串中的各个字符依次是s[0], s[1],…, s[strlen(s)-1],而s[strlen(s)]正是结束标记“\0”

 

 

# 由于字符串的本质是数组,只能用strcpy(a, b),strcmp(a, b), strcat(a, b)来执行“赋值”、“比较”和“连接”操作,而不能用“=”、“==”、“<=”、“+”等运算符。上述函数都在string.h中声明

 

实验:

 

 

 

 

 

 三、竞赛题目选讲

例题:3-1 TeX中的引号

在TeX中,左双引号是“``”,右双引号是“''”。输入一篇包含双引号的文章,你的任务是把它转换成TeX的格式。

样例输入:
"To be or not to be," quoth the Bard, "that
is the question".
样例输出:
``To be or not to be,'' quoth the Bard, ``that
is the question''.

 

本题有两个关键:

  1. 如何判断一个双引号是左双引号还是右双引号
  2. 输入字符串

输入字符串:

scanf("%s",xxx);  本题中无法使用 ---->  因为它碰到空格或者TAB 就会停下来

 

方法1: ———— fgetc(fin)

读取一个打开的文件fin,读取一个字符,然后返回一个int值。为什么返回的是int而不是char呢?因为如果文件结束,fgetc将返回一个特殊标记EOF,它并不是一个char。如果把fgetc(fin)的返回值强制转换为char,将无法把特殊的EOF和普通字符区分开。如果要从标准输入读取一个字符,可以用getchar,它等价于fgetc(stdin)

 

# 一般情况下应当在检查它不是EOF后再将其转换成char值。从标准输入读取一个字符可以用getchar,它等价于fgetc(stdin)

 

陷阱!!!   ———— 在使用fgetc和getchar时,应该避免写出和操作系统相关的程序

不同操作系统的回车换行符是不一致的。Windows是“\r”和“\n”两个字符,Linux是“\n”,而MacOS是“\r”。如果在Windows下读取Windows文件,fgetc和getchar会把“\r""吃掉”,只剩下“\n”;但如果要在Linux下读取同样一个文件,它们会忠实地先读取“\r”,然后才是“\n”。如果编程时不注意,所写程序可能会在某个操作系统上是完美的,但在另一个操作系统上就错得一塌糊涂。当然,比赛的组织方应该避免在Linux下使用Windows格式的文件,但正如前面所强调过的:选手也应该把自己的程序写得更鲁棒,即容错性更好。

 

 

方法2 ———— fgets(buf, maxn, fin)


读取完整的一行,其中buf的声明为char buf[maxn]。这个函数读取不超过maxn-1个字符,然后在末尾添上结束符“\0”,因此不会出现越界的情况。之所以说可以用这个函数读取完整的一行,是因为一旦读到回车符“\n”,读取工作将会停止,而这个“\n”也会是buf字符串中最后一个有效字符(再往后就是字符串结束符“\0”了)。只有在一种情况下,buf不会以“\n”结尾:读到文件结束符,并且文件的最后一个不是以“\n”结尾。尽管比赛的组织方应避免这样的情况(和输出文件一样,保证输入文件的每行均以回车符结尾),但正如刚才所说,选手应该把自己的程序写得更鲁棒

 

# fgets(buf, maxn, fin) 将读取完整的一行放在字符数组buf中。应当保证buf足够存放下文件的一行内容。除了在文件结束前没有遇到“\n”这种特殊情况外,buf总是以“\n”结尾。当一个字符都没有读到时,fgets返回NULL

# fgets也有一个"标准输入版"gets。遗憾的是,gets和它的"兄弟"fgets差别比较大:其用法是gets(s),没有指明读取的最大字符数。这里就出现了一个潜在的问题:gets将不停地往s中存储内容,而不管是否存储得下!

c语言并不禁止程序读写“非法内存”,例如char s[100],完全可以赋值 s[10000] = 'a'。甚至-Wall 也不警告 ,但,后果自负

因此gets 函数在c11 标准里已被正式删除

#include<stdio.h>
int main() {
    int c, q = 1;
    while((c = getchar()) != EOF) {
    if(c == '"') {
         printf("%s", q ? "``" : "''");
         q = !q;
    }
    else
        printf("%c", c);
    }
    return 0;
}

本题细节: 

1.可以边读边处理,不需要吧输入字符串完整地存下来 ———— 可以选择 getchar()

2.表达式"a?b:c"的含义是:当a为真时值为b,否则为c

3.用到了赋值语句"c = getchar()"的返回值,把它和EOF进行比较,让代码更简洁

 

posted @ 2019-09-07 20:23  远征i  阅读(491)  评论(0编辑  收藏  举报