Data Structure第四次作业讲解

Data Structure第四次作业讲解

写给读者的话(务必阅读)

期中以来,有不少同学向我反应代码写的慢、正确率不高等问题。由于OS已经爆炸闲的没事干 因此我决定将自己原来写的代码重构重构,并整理成博客附上整个思路的讲解。首先,我必须申明,博客所写的东西,是供想提升自己代码水平,理清写码思路的同学用的。我希望同学们能够明白:作为一个考试分数占 80% 的学科,抄袭他人代码完成作业不是白赚了那 20% 的分数,而是失去了一次良好的练兵的机会。其次,个人所写代码只是提供一个写码、思考问题的思路,并不代表题目的唯一解法,更不代表最优解法没有提交到课程网站上测试,只在本地通过测试数据,甚至可能有bug噢。我希望我的代码能够起到抛砖引玉的作用,能够让同学对课上内容有更深刻的理解,写代码时能够有更多更好的想法。最后,我希望同学们在完成作业的同时,能够对自己的代码进行复杂度的分析。数据结构的使用,往往离不开对性能的约束,因此,掌握复杂度的分析也是这门课程重要的一环。

关于代码风格

本文中所有的代码风格皆采取 OO 的标准,同时作者也希望同学们能够以这种标准约束自己,这样也会方便助教 debug。简单来说,大致约束如下:

1、符号后带空格。

2、大括号不换行。

3、if、while、for 等括号两端应该带空格,并且一定要用大括号括起来。

4、一行不要写过多字符(不超过60),较长的判断可以换行处理。

5、缩进为 4 个空格,不同层次间要有合适的缩进。

6、一行只声明一个变量,只执行一个语句。

第一题:栈操作(基本题)

题目描述

【问题描述】

假设给定的整数栈初始状态为空,栈的最大容量为100。从标准输入中输入一组栈操作,按操作顺序输出出栈元素序列。栈操作:1表示入栈操作,后跟一个整数(不为1、0和-1)为入栈元素;0表示出栈操作;-1表示操作结束。

【输入形式】

从标准输入读取一组栈操作,入栈的整数和表示栈操作的整数之间都以一个空格分隔。

【输出形式】

在一行上按照操作的顺序输出出栈元素序列,以一个空格分隔各元素,最后一个元素后也要有一个空格。如果栈状态为空时进行出栈操作,或栈满时进行入栈操作,则输出字符串“error”,并且字符串后也要有一空格。所有操作都执行完后,栈也有可能不为空。

【样例输入】

1 3 1 5 1 7 0 0 1 8 0 1 12 1 13 0 0 0 0 1 90 1 89 0 -1

【样例输出】

7 5 8 13 12 3 error 89

【样例说明】

入栈元素依次为3、5、7,然后有两次出栈动作,所以先输出7和5,这时栈中只有元素3;之后元素8入栈,又出栈,输出8;随后元素12和13入栈,再进行4次出栈操作,输出13、12和3,这时栈为空,再进行出栈操作会输出error;最后90和89入栈,进行一次出栈操作,输出89,栈中剩余1个元素。

题目大意

一眼题,一看就是课上讲的栈的模拟。

题目思路

就实现个栈,简单点就直接用数组模拟,复杂点就链表维护。

但是,就算是这么简单的一个问题,我们也应该清晰的认识到,栈是我们使用的工具,而其入栈出栈是两个分离的操作,我们应该分别对其实现函数,而不是挤在 main 里。

代码实现

#include <stdio.h>
#include <ctype.h>

inline int read() { //快速读入,可以放在自己的缺省源里面 
	int x = 0; //数字位 
	int f = 1; //符号位 
	char ch = getchar(); //读入第一个字符 
	while (!isdigit(ch)) { //不是数字 
		if (ch == '-') { //特判负号 
			f = -1;
		}
		ch = getchar();
	}
	while (isdigit(ch)) { //读入连续数字 
		x = (x << 3) + (x << 1) + ch - '0'; // x * 10 == (x << 3) + (x << 1) 
		ch = getchar();
	}
	return x * f;
}

#define maxn 105
int sta[maxn]; //越简单越好,必然是用数组实现栈 
int top;

void push(int x) {//入栈 
	if (top == 100) { //栈满 
		printf("error ");
		return;
	}
	sta[++top] = x;
}

void pop() { //出栈并输出 
	if (top == 0) { //栈空 
		printf("error ");
		return;
	}
	printf("%d ", sta[top--]);
}

int main() {
	int ty;
	while (1) {
		ty = read();
		if (ty == -1) { //结束条件 
			break;
		}
		if (ty == 1) {
			int x = read();
			push(x);	
		}
		if (ty == 0) {
			pop();
		}
	}	
	return 0;
}

复杂度分析

典型的栈模拟题,每个元素只会入栈一次,出栈一次,因此,复杂度是 O(n) 的。

第二题:C程序括号匹配检查

题目描述

【问题描述】

编写一程序检查C源程序文件中{}、()等括号是否匹配,并输出第一个检测到的不匹配的括号及所对应括号所在的行号(程序中只有一个括号不匹配)。

注意:

1.除了括号可能不匹配外,输入的C源程序无其它语法错误。

2.字符常量、字符串常量及注释中括号不应被处理,注释包括单行注释//和多行/* */注释

3.字符常量和字符串常量中不包含转义字符'和";

4.程序中出现有意义括号的个数不超过200个;

不匹配判断规则:

1.当检测的程序括号为'{'时,若其前序尚未匹配的括号为'('时,输出该'('左括号及所在行号;

2.当遇到一个不匹配的右括号')'或'}'时,输出该右括号及所在行号;

3.当程序处理完毕时,还存在不匹配的左括号时,输出该左括号及所在行号。

【输入形式】

打开当前目录下文件example.c,查询其括号是否匹配。该文件中每行字符数不超过200。

【输出形式】

若存在括号不匹配时,应输出首先能判断出现不匹配的括号及其所在的行号。当出现括号不匹配时,按下面要求输出相关信息:

without maching at line

其中为‘{’, ‘}’, ‘(’, ‘)’等符号,为该符号所在的行号。

若整个程序括号匹配,则按下面所示顺序输出括号匹配情况,中间没有空格。

(){(()){}}

题目大意

让我们对一个 C 代码做括号匹配,匹配成功的话还要输出匹配的过程。

需要注意的是代码中存在注释,因此我们不能简单的处理所有的长得像注释的字符,注意的第三点也点明了引号的存在,不在字符串常量中出现转义的引号可以说是极大的简化了我们的问题。

题目思路

依然是十分典型的栈问题,我们需要对括号做匹配。

而注释的处理我们可以这样理解:我们当前读到的代码有两种状态:注释态和非注释态,我们可以通过一些巧妙的处理,合理地在这两种状态之间转换,而括号匹配只在注释态中进行就可以了。

代码实现

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

#define maxn 100005
char s[maxn]; //临时储存地字符串数组 
char ans[maxn]; //用于存储已经匹配的括号序列
int pnum; //已经匹配的数量
int lineNum[maxn]; //记录栈中括号所在的行数
int sta[maxn]; //括号栈
int top; //栈顶
int ban; //当前是否有效的标记,无效时同时记录无效的类型
char fType[6]; //括号类型到括号的反向映射 

int getType(char c) { // 返回括号的类型 
	if (c == '(') return 2;
	if (c == ')') return 3;
	if (c == '{') return 4;
	if (c == '}') return 5;
	return 0;
} 

void pre() { //初始化反向映射
	fType[2] = '(';
	fType[3] = ')';
	fType[4] = '{';
	fType[5] = '}';
}

int tryPush(int x, int line) {
	if (top && sta[top] < x) { //当前存在栈顶,并且栈顶的括号大小关系错误 
		printf("without maching \'%c\' at line %d\n", fType[sta[top]], lineNum[top]);
		return -1;
	}
	sta[++top] = x; //压入栈顶
	lineNum[top] = line; //记录行数
	return 0;
}

int tryPop(int x, int line) {
	if (!top || (sta[top] ^ 1) != x) { //检查括号是否匹配
		printf("without maching \'%c\' at line %d\n", fType[x], line);
		return -1;
	}
	--top;
	return 0;
}

void changeBan(int* pos) { //特判更改当前的注释状态
	if (!ban) {
		if (s[*pos] == '\"') {
			ban = 1;
			return;
		}
		if (s[*pos] == '\'') {
			ban = 2;
			return;
		}
		if (s[*pos] == '/' && s[(*pos) + 1] == '*') {
			ban = 3;
			++(*pos);
			return;
		}
		if (s[*pos] == '/' && s[(*pos) + 1] == '/') {
			ban = 4;
			++(*pos);
			return;
		}
	} else {
		if (ban == 1 && s[*pos] == '\"') {
			ban = 0;
			return;
		}
		if (ban == 2 && s[*pos] == '\'') {
			ban = 0;
			return;
		}
		if (ban == 3 && s[*pos] == '*' && s[(*pos) + 1] == '/') {
			ban = 0;
			++(*pos);
			return;
		}
	}
}

int main() {
	pre();
	FILE* IN = fopen("example.c", "r");
	int line = 0; //记录当前行数
	while (fgets(s, maxn - 5, IN) != NULL) { //每次处理一行 
		++line;
		int len = strlen(s);
		int i;
		int temp;
		for (i = 0; i < len; ++i) {
			if (!ban && (temp = getType(s[i]))) {
				if (temp & 1) {
					if (tryPop(temp, line) < 0) {
						return 0;
					}
				} else {
					if (tryPush(temp, line) < 0) {
						return 0;
					}
				}
				ans[++pnum] = s[i]; //直接记录有效括号
			} else {
				changeBan(&i);
			}
		}
		if (ban == 4) { //当前行结束,去除//所产生的注释状态
			ban = 0;
		}
	}
	int i;
	if (top) {
		printf("without maching \'%c\' at line %d\n", fType[sta[top]], lineNum[top]);
	} else {
		for (i = 1; i <= pnum; ++i) {
			printf("%c", ans[i]);
		}
	}
	return 0;
} 

相信不少同学就算看完注释还是一头雾水,下面我们就将这个代码分解一下,解释一下映射在简化代码之中的作用。

我们将 '(' 、')' 、'{'、'}' 分别映射为 2、3、4、5,通过数学的性质方便了我们的处理。

2 ^ 1 == 3、3 ^ 1 == 2,通过这种映射我们能够通过一个异或操作判断括号是否是正确匹配的。

同时,由于括号的大小关系我们用数字表示了出来,因此在压栈的时候,我们应该做的是一个单调不增的栈,否则说明括号的包含关系产生了错误。

之后,注释这种极其特殊的操作我们就只能通过繁杂的特判来进行了。

复杂度分析

每个字符显然只被处理了一次,复杂度是 O(|S|) 的。

第三题:计算器(表达式计算-后缀表达式实现)

题目描述

【问题描述】

从标准输入中读入一个整数算术运算表达式,如24 / ( 1 + 5%3 + 36 / 6 / 2 - 2) * ( 12 / 2 / 2 )= ,计算表达式结果,并输出。

要求:

1、表达式运算符只有+、-、*、/、%,表达式末尾的=字符表示表达式输入结束,表达式中可能会出现空格;
2、表达式中会出现圆括号,括号可能嵌套,不会出现错误的表达式;

3、出现除号/时,以整数相除进行运算,结果仍为整数,例如:5/3结果应为1。

4、要求采用逆波兰表达式来实现表达式计算。

【输入形式】

从键盘输入一个以=结尾的整数算术运算表达式。操作符和操作数之间可以有空格分隔。

【输出形式】

在屏幕上输出计算结果(为整数,即在计算过程中除法为整除)。

【样例输入】

24 / ( 1 + 5%3 + 36 / 6 / 2 - 2) * ( 12 / 2 / 2 ) =

【样例输出】

18
【样例说明】

按照运算符及括号优先级依次计算表达式的值。

【评分标准】

通过所有测试点得满分。

题目大意

表达式计算,要求用后缀表达式来实现。

题目思路

既然是要求转换为后缀表达式再计算,我们就按生成后缀表达式的步骤做就好了。

1、依次读取输入的表达式,如果是操作数,则把它放入到输出中。

2、如果是操作符,栈为空的话直接将该操作符入栈;如果栈非空,则比较栈顶操作符和该操作符优先级,如果栈顶操作符优先级小于该操作符,则该操作符入栈;否则弹出栈顶操作符并将其放入到输出中,直到栈为空或者发现优先级更低的操作符为止。

3、如果是括号,比如'('和')',则特殊处理。如果是'('的话,直接入栈;如果是')',那么就将栈顶操作符弹出写入到输出中,直到遇到一个对应的'(',但是这个'('只弹出不写入到输出中。注意:"("可以理解为优先级最高。

4、当表达式读取完毕后,如果栈中还有操作符,则依次弹出操作符并写入到输出中。

而计算后缀表达式的步骤则更加简单:

1、是数字,直接压入栈

2、是符号,取出栈顶的两个值计算后成为新栈顶。

代码实现

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

#define maxn 1000005
int len; //记录字符串长度
int top; //记录栈顶
char s[maxn]; //读入并转换的表达式
char sta[maxn]; //当前的符号栈
int pri[256]; //定义符号的优先级

void getSuf() { // 读入并将表达式转换为后缀形式 
	char c;
	pri['+'] = pri['-'] = 1;
	pri['*'] = pri['/'] = pri['%'] = 2;
	while (1) {
		c = getchar();
		if (isdigit(c)) {
			while(isdigit(c)) {
				s[len++] = c;
				c = getchar();
			}
			s[len++] = ' '; // 后缀表达式数字可能相连,因此添加空格避免数字连续。 
		}
		if (c == '=') {
			break;
		}
		if (pri[c]) { //说明是一个符号,需要与栈中符号优先级进行比较 
			while (top && pri[sta[top]] && pri[sta[top]] >= pri[c]) {//注意 () 优先级为 0 
				s[len++] = sta[top--];
			}
			sta[++top] = c;
		}
		if (c == '(') {
			sta[++top] = c;
		}
		if (c == ')') { //需要将 ( 之前所有符号弹出 
			while(sta[top] != '(') {
				s[len++] = sta[top--];
			}
			--top;
		}
	}
	while (top) {
		s[len++] = sta[top--];
	}
}

int numSta[maxn]; //数字栈 
int numTop; //数字栈栈顶 

int calc(int x, char c, int y) {
	if (c == '+') {
		return x + y;
	}
	if (c == '-') {
		return x - y;
	}
	if (c == '*') {
		return x * y;
	}
	if (c == '/') {
		return x / y;
	}
	if (c == '%') {
		return x % y;
	}
}

void calcSuf() {
	int i = 0;
	while (i < len) {
		if (isdigit(s[i])) { //是数字,准备压入数字栈 
			int x = s[i++] - '0';
			while(isdigit(s[i])) {
				x = (x << 3) + (x << 1) + s[i++] - '0';
			}
			numSta[++numTop] = x;
		}
		if (pri[s[i]]) { //是符号,对栈顶两个数字计算得到新栈顶 
			int a = numSta[numTop--];
			int b = numSta[numTop--];
			numSta[++numTop] = calc(b, s[i], a);
		}
		++i;
	}
	printf("%d\n", numSta[numTop]);
}

int main() {
	getSuf();
	calcSuf();
	return 0;
} 

这个题我们同样采取了类似上题映射的方法,将每个计算符号映射到自己的优先级,并通过这种方式来得到一种统一的处理方式。

复杂度分析

每个字符被处理了两次,复杂度依然是 O(|S|) 的。

第四题:文本编辑操作模拟(简)

题目描述

【问题描述】

编写一程序模拟文本编辑操作。首先从标准输入读取一行字符串(字符个数不超过512),该行字符串是已经过n(大于0,小于等于10)步编辑操作后的结果。然后从下一行读取n,以及已发生过的n步编辑操作,编辑操作分行输入,输入格式为:

op pos str

其中op为编辑操作命令编码(在此只有插入和删除操作,1表示插入或2表示删除操作);pos表示插入或删除的位置;str表示已经插入或删除的字符串(中间没有空格)。各数据间以一个空格分隔。

然后在空一行后,再分行输入当前将要进行的编辑操作,包括如下四种操作(操作编码分别为:1表示插入,2表示删除操作,3表示撤销(即undo操作),-1表示结束):

1 pos str

表示将在pos位置插入字符串str(中间没有空格),各数据间以一个空格分隔;

2 pos n

表示将从pos位置开始删除n个字符(各数据间以一个空格分隔),若要删除的字符个数多于已有字符个数(即在文本中从pos开始的字符个数小于n),则按实际字符数删除即可。(提示:为了能够撤销删除操作,应按“2 pos str”形式保存命令。)

3

表示撤销最近执行的插入或删除操作,可以进行多次撤销操作,注意:也可以撤销之前已经发生过的n步编辑操作中的操作。

-1

表示退出编辑操作,在屏幕上输出最终编辑后的文本。

要求:

1、上述所有输入的编辑操作中的字符串str都不包含空白字符(空格符、制表符或换行符);

2、插入操作中的位置pos大于等于0,并且小于等于当前文本的字符个数;0位置表示文本第一个字符的位置;若pos为当前文本的字符个数,则表示在文本最后插入字符串;

3、删除操作中的位置pos大于等于0,并且小于当前文字的字符个数;

4、若已无操作可撤销,则再进行撤销操作无效;

5、文本在编辑过程中,总字符个数不会超过512。

【输入形式】

先从键盘输入一行字符串,表示已经经过n步编辑操作后的文本串,然后在下一行输入一个正整数n,并分行输入n步插入或删除操作(表示按时间先后顺序已进行的操作),格式如上所述。随后空一行,再分行输入将要进行的编辑操作,格式如上所述。直到输入-1操作为止。

【输出形式】

在屏幕上输出最终编辑后的文本内容。

题目大意

对单行的文本进行一系列的文本操作,要支持操作的回退。

同时给你的单行文本是在已经给出的几个操作之后得出的结果。

题目思路

比较直接的思路是用栈来维护每一次操作的序列,需要完成直接的操作和对于操作的撤销(当然撤销显然可以复用正向的操作)。

稍微拓展一下思路甚至可以保存每次操作之后的字符串来做到返回任意一个时刻的状态?

代码实现

由于个人对 string 的各种库函数不算特别熟悉,因此选择了自己造轮子

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

#define maxn 605
inline int read() { //快速读入,可以放在自己的缺省源里面 
	int x = 0; //数字位 
	int f = 1; //符号位 
	char ch = getchar(); //读入第一个字符 
	while (!isdigit(ch)) { //不是数字 
		if (ch == '-') { //特判负号 
			f = -1;
		}
		ch = getchar();
	}
	while (isdigit(ch)) { //读入连续数字 
		x = (x << 3) + (x << 1) + ch - '0'; // x * 10 == (x << 3) + (x << 1) 
		ch = getchar();
	}
	return x * f;
}

struct change { // change结构体用于记录每次改变的信息 
	int type;
	int pos;
	int n;
	char s[maxn];
} c[105];

char s[maxn]; //记录字符串 
int n;  
int top; //记录栈顶 
int len; //记录字符串长度 

int max(int a, int b) {
	return a > b ? a : b;
} 

void del(int pos, int l) {
	int i;
	for (i = pos + l; i < len; ++i) {
		s[i - l] = s[i];
	}
	len = max(len - l, pos); //注意删除可能会超过原有的长度 
}

void add(int pos, char* c) { //加入新的字符串,题目约束加入的pos < len
	int l = strlen(c);
	int i;
	for (i = len - 1; i >= pos; --i) {
		s[i + l] = s[i];
	}
	len += l;
	for (i = 0; i < l; ++i) {
		s[pos + i] = c[i];
	}
} 

int main() {
	fgets(s, maxn - 5, stdin);
	len = strlen(s);
	n = read();
	int i;
	for (i = 1; i <= n; ++i) { //读入已经进行过的操作
		++top;
		scanf("%d", &c[top].type);
		if (c[top].type == 1) {
			scanf("%d%s", &c[top].pos, c[top].s);
		}
		if (c[top].type == 2) {
			scanf("%d%s", &c[top].pos, c[top].s);
		}
		if (c[top].type == 3) {
			top -= 2;
			top = max(top, 0);
		}
	}	
	for (i = top; i >= 1; --i) { //撤销已经进行过的操作
		if (c[i].type == 1) {
			del(c[i].pos, strlen(c[i].s));
		}
		if (c[i].type == 2) {
			c[i].n = strlen(c[i].s);
			add(c[i].pos, c[i].s);
		}
	}
	while (1) { //读入新的操作
		++top;
		scanf("%d", &c[top].type);
		if (c[top].type == -1) {
			break;
		}
		if (c[top].type == 1) {
			scanf("%d%s", &c[top].pos, c[top].s);
		}
		if (c[top].type == 2) {
			scanf("%d%d", &c[top].pos, &c[top].n);
		}
		if (c[top].type == 3) {
			top -= 2;
			top = max(top, 0);
		}
	}
	--top;
	for (i = 1; i <= top; ++i) { //执行所有有效的操作,得到最后的序列
		if (c[i].type == 1) {
			add(c[i].pos, c[i].s);
		}
		if (c[i].type == 2) {
			del(c[i].pos, c[i].n);
		}
	}
	s[len] = '\0';
	printf("%s", s);
	return 0;
} 

可能很多同学这里会好奇,为什么要把已经执行过的操作撤销?

其原因在于,对于已经执行过的操作,我们有着更多的信息(以删除为例,我们知道他删除的是哪些字符串),因此我们也更好对这些操作进行撤销。如果我们要动态支持撤销后面给出的新操作的话,我们势必需要保存更多的信息。

而在这个题中,最简单的撤销操作其实就是根本不执行。因此,我们撤销具有完整信息的已执行操作,再用栈维护有效操作的序列,最后按顺序执行一次,就能得到最终的结果,同时也能极大地简化我们的处理。

复杂度分析

删除和插入操作,涉及到了整个字符串的移动,基本可以认为复杂度是 O(q * |S|) 的 (q为操作数,|S|为字符串最大长度)。

第五题:银行排队模拟(生产者-消费者模拟)

题目描述

【问题描述】

一个系统模仿另一个系统行为的技术称为模拟,如飞行模拟器。模拟可以用来进行方案论证、人员培训和改进服务。计算机技术常用于模拟系统中。

生产者-消费者(Server-Custom)是常见的应用模式,见于银行、食堂、打印机、医院、超等提供服务和使用服务的应用中。这类应用的主要问题是消费者如果等待(排队)时间过长,会引发用户抱怨,影响服务质量;如果提供服务者(服务窗口)过多,将提高运管商成本。(经济学中排队论)

假设某银行网点有五个服务窗口,分别为三个对私、一个对公和一个外币窗口。银行服务的原则是先来先服务。通常对私业务人很多,其它窗口人则较少,可临时改为对私服务。假设当对私窗口等待服务的客户(按实际服务窗口)平均排队人数超过(大于或等于)7人时,等待客户将可能有抱怨,影响服务质量,此时银行可临时将其它窗口中一个或两个改为对私服务,当客户少于7人时,将立即恢复原有业务。设计一个程序用来模拟银行服务。

说明:

  1. 增加服务窗口将会增加成本或影响其它业务,因此,以成本增加或影响最小为原则来增加服务窗口,即如果增加一个窗口就能使得按窗口平均等待服务人数小于7人,则只增加一个窗口。一旦按窗口平均等待服务人数小于7人,就减少一个所增加的窗口。

  2. 为了简化问题,假设新到客户是在每个服务周期开始时到达。

  3. 当等待服务人数发生变化时(新客户到达或有客户已接受服务),则及时计算按实际服务窗口平均等待服务人数,并按相应策略调整服务窗口数(增加或减少额外的服务窗口,但对私窗口不能减少)。注意:只在获取新客户(不管到达新客户数是否为0)时或已有客户去接受服务时,才按策略调整服务窗口数。进一步讲,增加服务窗口只在有客户到达的周期内进行(也就是说增加窗口是基于客户的感受,银行对增加窗口是不情愿的,因为要增加成本,一旦不再有新客户来,银行是不会再增加服务窗口的);一旦有客户去接受服务(即等待客户减少),银行将根据策略及时减少服务窗口,因此,在每个周期内,有客户去接受服务后要马上判断是否减少服务窗口(因为能减少成本,银行是积极的)

本问题中假设对公和对外币服务窗口在改为对私服务时及服务期间没有相应因公或外币服务新客户到达(即正好空闲),同时要求以增加成本或影响最小为前提,来尽最大可能减少对私服务客户等待时间。

【输入形式】

首先输入一个整数表示时间周期数,然后再依次输入每个时间周期中因私业务的客户数。注:一个时间周期指的是银行处理一笔业务的平均处理时间,可以是一分钟、三分钟或其它。例如:

6

2 5 13 11 15 9

说明:表明在6个时间周期内,第1个周期来了2个(序号分别为1,2),第2个来了5人(序号分别为3,4,5,6,7),以此类推。

【输出形式】

每个客户等待服务的时间周期数。输出形式如下:

用户序号 : 等待周期数

说明:客户序号与等待周期数之间用符号:分隔,冒号(:)两边各有一个空格,等待周期数后直接为回车。

【样例输入】

4

2 5 13 11

【样例输出】

1 : 0

2 : 0

3 : 0

4 : 0

5 : 0

6 : 1

7 : 1

8 : 0

9 : 1

10 : 1

11 : 1

12 : 1

13 : 2

14 : 2

15 : 2

16 : 3

17 : 3

18 : 3

19 : 4

20 : 4

21 : 3

22 : 4

23 : 4

24 : 4

25 : 5

26 : 5

27 : 5

28 : 6

29 : 6

30 : 6

31 : 7

【样例说明】

样例输入表明有四个时间周期,第一个周期来了2人(序号1-2);第二个周期来了5人(序号3-7);第三个周期来了13人(序号8-20);第四个周期来了11人(序号21-31)。由于第一个时间周期内只来了2人,银行(有三个服务窗口)能及时提供服务,因此客户等待时间为0;第二个时间周期内来了5人,银行一个周期内一次只能服务3人,另有2个在下个周期内服务,因此等待时间为1,其它类推。

题目大意

根据题目的意思,实现一个动态增加窗口的银行排队模拟系统(如果觉得题目有问题的同学,请反复阅读加粗部分)

题目思路

根据题目的含义,我们需要模拟一个 FCFS (FIRST COME FIRST SERVE)的银行排队模型。因此十分显然,我们需要用到队列的数据结构来维护服务序列。

代码实现

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

#define maxn 1000005
inline int read() { //快速读入,可以放在自己的缺省源里面 
	int x = 0; //数字位 
	int f = 1; //符号位 
	char ch = getchar(); //读入第一个字符 
	while (!isdigit(ch)) { //不是数字 
		if (ch == '-') { //特判负号 
			f = -1;
		}
		ch = getchar();
	}
	while (isdigit(ch)) { //读入连续数字 
		x = (x << 3) + (x << 1) + ch - '0'; // x * 10 == (x << 3) + (x << 1) 
		ch = getchar();
	}
	return x * f;
}

struct people {
	int time;
	int bh;
} q[maxn];

int cnt;
int main() {
	int n = read();
	int T = 0; // 初始化时间
	int head = 1; //初始化队首 
	int tail = 0;  //初始化队尾
	int C = 3; //初始化窗口数
	int i;
	for (i = 1; i <= n; ++i) {
		++T; //时间增加 
		int k = read();
		while (k--) {
			q[++tail] = (struct people){T, ++cnt}; //新加入队列 
		}
		while (C < 5 && tail - head + 1 >= C * 7) { //根据题设条件增加窗口 
			++C;
		}
		int t = C;
		while (t-- && head <= tail) { //窗口有剩余并且队列中有人等待 
			printf("%d : %d\n", q[head].bh, T - q[head].time); //通过时间差计算出等待时间 
			++head;
		} 
		while (C > 3 && tail - head + 1 < C * 7) { //根据题设条件减少窗口 
			--C;
		}
	} 
	while (head <= tail) { //队列中仍然有人等待
		++T;
		int t = C;
		while (t-- && head <= tail) {
			printf("%d : %d\n", q[head].bh, T - q[head].time); //通过时间差计算出等待时间 
			++head;
		}
		while (C > 3 && tail - head + 1 < C * 7) { //根据题设条件减少窗口 
			--C;
		}
	}
	return 0;
}

个人认为,利用时间差来算等待时间对这题算是较为不错的解法。

需要认识到,增加窗口条件的补集,并不是减少窗口的条件。因此,需要严格按照题目描述模拟窗口增加和减少。

复杂度分析

每个顾客进队一次、出队一次。复杂度显然是 O(人数) 的。

posted @ 2021-05-21 23:51  L_RUA  阅读(761)  评论(0编辑  收藏  举报