4.4 竞赛题目选讲

从技术实现来说,不用函数和递归也可以写出所有程序,也就是说函数和递归均可以用其他内容替代
但是从实用的情况下来讲,递归能够帮我们大忙。
比如说,函数递归可以增加程序的可读性,可维护性
更好的调试技巧,不如先学习如何更好的写程序,如果方法得当,不仅能够更快的写出更短的程序,而且调试起来也更轻松,隐含的错误也会更少
一般而言,程序是不会从第一行开始写到最后一行结束的,而是遵循两种承建的顺序之一;自顶向下和自底向上,自顶向下就是先写框架,再写细节,类似之前的先写伪代码,然后转化成实际的代码
有了函数这个工具之后,可以更好的贯彻这个方法:先写主程序,包括对函数的调用,再实现函数本身
自底向上和这个顺序相反,是先写函数,再写主程序。对于编写复杂软件来说,自底向上的构建方式由他独特的优势,但是算法竞赛中这样做的选手并不多,注意:在测试的时候,先测试工具函数的方式非常实用

关于刽子手游戏(my version)

点击查看代码
#include<stdio.h>
#include<string.h>
#define len 105
char s1[len];
char s2[len];
int cnt1[26];
int cnt2[26];

int main()
{
	int n;
	while(scanf("%d", &n) == 1 && n != -1)
	{
		memset(cnt1, 0, sizeof(cnt2));
		memset(cnt2, 0, sizeof(cnt1)); 
		int cnt = 0, flag = 0, num1 = 0, num2 = 0;
		scanf("%s%s", s1, s2);
		for(int i = 0; i < strlen(s1); i++)
		{
			if(!cnt1[s1[i] - 'a'])
			{
				cnt1[s1[i] - 'a']++;
				num1++;
			}	
		}
		for(int i = 0; i < strlen(s2); i++)
		{	
			if(cnt1[s2[i] - 'a'] && !cnt2[s2[i] - 'a'])
			{
				cnt2[s2[i] - 'a']++;
				num2++;
				if(num2 == num1)
				{
					flag = 3;
					printf("Round %d\nYou win.\n", n);
					break;
				}
			}	
			else
			{
				cnt++;
				if(cnt == 7)
				{
					printf("Round %d\nYou lose.\n", n);		
					flag = 1;
					break;
				}		
			}	
		}		
		if(!flag)	
		{
			for(int i = 0; i < 26; ++i)
			{
				if(cnt1[i])
					if(!cnt2[i])
					{
						flag = 2;
						break;
					}
			}
		}
		if(!flag)
		{
			printf("Round %d\nYou win.\n", n);
		}
		else if(flag == 2)
		{
			printf("Round %d\nYou chickened out.\n", n);
		}
	}
	return 0;
}

以下是作者给出的整个程序的一个大致框架

点击查看代码
#include<stdio.h>
#include<string.h>
#define maxn 100
int left, chance;
char s[maxn], s2[maxn];
int win, lose;

void guess(char ch)
{
	
}

int main()
{
	int rnd;
	while(scanf("%d%s%s", &rnd, s, s2) == 3 && rnd != -1)
	{
		printf("Round %d\n", rnd);
		win = lose = 0;
		left = strlen(s);
		chance = 7;
		for(int i = 0; i < strlen(s2); i++)
		{
			guess(s2[i]);
			if(win || lose) break;
		}
		if(win) printf("You win.\n");
		else if(lose) printf("You lose.\n");
		else printf("You chickened out.\n");
	}
	return 0;
}

细节说明:
1.变量名的选取,那个rnd本应叫round,但是有一个库函数也叫round,也就是这时候不可以定义一个变量的名字是关键字,所以只能改名字改为rnd,当然Rnd也是可以的了,因为C语言中区分大小写,学习过STL之后,这种被用过的常用名字还会增加,例如count,min,max等都是STL已经使用过的名字,程序中最好避开他们
2.变量的使用,全局变量本应该尽量少用,但是对于本体来说,需要维护的内容比较多,例如是否赢了,是否输了,以及剩余的机会。如果不用全局变量,则它们都需要传递给函数guess,有些被修改的变量就只能传递指针(可能会使代码变得比较丑),也就是全局变量有时候可以简化这种不同函数之间需要共同访问同一变量,并且需要改变这个变量的值的时候会起到比较好的效果。
注意:变量和函数的调用方式的设计是一个需要思考的问题,如果设计出的方案还未写出便觉得别扭,恐怕写出来的程序会既不优美,也不好调试,甚至容易隐藏bug。

当然这种自顶向下的程序设计在框架打出来的时候,剩下的就是对各自的函数的实现,guess函数的实现,注意题目中的说明是猜到过的字母再猜一次就是错误,一种解决方法就是增加一个字符数组来判断这个字母是否已经判断过,还有一种判断方法就是将才对的字符改成空格

实际的程序实现如下:

点击查看代码
#include<stdio.h>
#include<string.h>
#define maxn 100
int left, chance;
char s[maxn], s2[maxn];
int win, lose;

void guess(char ch)
{
	int bad = 1;
	for(int i = 0; i < strlen(s); i++)
		if(s[i] == ch)
		{
			left--;
			s[i] = ' ';
			bad = 0;
		}
	if(bad) --chance;
	if(!chance) lose = 1;
	if(!left) win = 1;
}

int main()
{
	int rnd;
	while(scanf("%d%s%s", &rnd, s, s2) == 3 && rnd != -1)
	{
		printf("Round %d\n", rnd);
		win = lose = 0;
		left = strlen(s);
		chance = 7;
		for(int i = 0; i < strlen(s2); i++)
		{
			guess(s2[i]);
			if(win || lose) break;
		}
		if(win) printf("You win.\n");
		else if(lose) printf("You lose.\n");
		else printf("You chickened out.\n");
	}
	return 0;
}

这样的程序的调试会比较简单,每猜完一个字母之后打印出s,left,chance等重要变量的值,很容易就发现程序出现的错误(尽可能用少的变量来体现程序中重要步骤的变量的值,也就是说这边的变量就是对于步骤的数值上的体现),如果此时我们前面采用了guessed[255]数组就会看傻了(笔者认为26应该会好一点,但是仍然不适合调试,前面的变量选择方式更适合调试)
一般来说,减少变量的个数,对于编程和调试都会有帮助

对于救济金发放(UVA133),笔者对于UVA的输出格式深感痛恶,注意本题如果出现presentation error,可能是因为没有实现左对齐,也就是说本题需要每个数据都是所占的字符是三个字符,也就是输出三位数,不足的用空格补齐,需要注意输出格式的重要性

以下是笔者自己的版本

点击查看代码
#include<stdio.h>
#define maxn 25
int q[maxn];
int n, k, m, p1, p2;

void Del()
{
	p1 = (p1 + k - 1) % n;
	p2 = ((p2 - n - (m-1)) % n + n) % n;//Perhaps the answer of (p2 - n - (m-1)) % n is not always be minus, sometimes is zero,in this case,if you add n to it, it will be wrong 
	if(p1 == p2)
	{
		n--;
		if(!n)
		{
			printf("% 3d\n", q[p1]);
			return;
		}
		else
			printf("% 3d,", q[p1]);
		for(int i = p1; i < n; i++)
			q[i] = q[i+1];
		p2 -= 1;//considerate why p2 should always add minus 1
	}
	else
	{
		n -= 2;
		if(!n)
		{
			printf("% 3d% 3d\n", q[p1], q[p2]);
			return;
		}
		else
			printf("% 3d% 3d,", q[p1], q[p2]);
		for(int i = (p1 > p2) ? p2 : p1; i < n + 1; i++)
			q[i] = q[i+1];
		for(int i = ((p1 > p2) ? p1 : p2) - 1 ; i < n; i++)
			q[i] = q[i+1];
		if(p1 < p2)
			p2 -= 1;
		else 
			p1 -= 1;
		p2 -= 1;
	}
	
}

int main()
{
//	printf("%d\n", -14%5+5);
	while(scanf("%d%d%d", &n, &k, &m) == 3 && (n || k || m))
	{
		for(int i = 0; i < n; i++)
			q[i] = i+1;
		p1 = 0;
		p2 = n - 1;
		while(n)
		{
			Del();
		}
	} 
	return 0;
}

程序的优化,或者是代码段的优化,越少的代码的出现,笔者认为这是从特殊到一般,从具体到抽象的一个过程。正如数学的研究一步步走向抽象化,抽象化换句话来说就是抽象的东西的兼容性是非常好的,类似于从方程组走向矩阵,行列式,如果我们想要掌握这一技巧,就必须在平时的训练中多进行总结,尝试发现各行代码中的共性
下面给出原文作者的代码完整版,其中的思路非常妙:

点击查看代码
#include<stdio.h>
#define maxn 25
int n, k, m, a[maxn];

int go(int p, int d, int t)
{
	while(t--)
	{
		do{
			p = (p + d + n - 1) % n + 1;
			//p = (((p -1 + d - n) % n) + n) % n + 1;
		}while(a[p] == 0);
	}
	
	return p;
}

int main()
{
	while(scanf("%d%d%d", &n, &k, &m) == 3 && n)
	{
		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;
}

仍然采用自顶向下的方法编写程序,用一个大小为0(?笔者认为应该是一处笔误,这边应该是n+1)的数组表示人站成的圈,为了避免人走之后移动数组元素,用0表示离开队伍的人,数数时跳过即可
(因为测试数据并没有专门对于时间的限制,因此此时采用暴力循环会有很好的效果)
注意其中的go函数,当然也可以写成两个函数,逆时针go和顺时针go,但是仔细思考后会发现这两个函数是可以合并的,逆时针函数和顺时针函数的区别在于只是下标是加1还是减1.把这个+1-1抽象为步长参数就可以将两个go函数统一了,这就是上面所说的抽象思维,或者说一种总结。

Message Decoding笔者的代码如下,写的比较零散,同时本题最关键的是出在数据存储的问题上,如何准确地读入一行带有空格的字符串

点击查看代码
#include<stdio.h>
#include<math.h> 
#include<string.h>
#include<ctype.h>
char s[8][70];
int cnt = 0;

int set()
{	
	cnt++;
	int ch;
	int flag = 0;
	for(int i = 1; i < 8; i++)
		for(int j = 0; j < 70; j++)
			s[i][j] = ' ';
	while((ch = getchar()) == '\n' || ch == -1 || (ch == ' ' && cnt != 1))
	{
		if(ch == -1)
			return 0;
		if(ch == '\n')
		{
			ch = getchar();
			if(ch == -1)
				return 0;
			break;
		}
	}
	s[1][0] = ch;
//	printf("1 0 %c\n", s[1][0]);
	for(int i = 2; ; i++)
	{
		for(int j = 0; j < floor(pow(2, i) + 0.5) - 1; ++j)
		{
			if((ch = getchar()) == '\n')
			{
				flag = 1;
				break; 
			}	
			s[i][j] = ch;
//			printf("%d %d %c\n", i, j, ch);
		}
		if(flag)
			break;
	}	
	return 1;
}

int get(int* p)
{
	char ch;
	while(scanf("%c", &ch) == 1 && !isdigit(ch))
	{
	}
	*p = ch - '0';
	return 1;
}


void deal(int l)
{
	while(true)
	{
		int p, tp;
		get(&p);
		for(int i = 1; i < l; i++)
		{
			get(&tp);
			p = p * 2 + tp;
		}
		if(pow(2, l) - 1 - p < 1e-6)
			return;	
//		printf("len : %d p : %d %c\n", l, p, s[l][p]);
		printf("%c", s[l][p]);
	}
}

int main()
{
//	freopen("data.in", "r", stdin);
//	freopen("data.out", "w", stdout);
	while(set())
	{
		int l1, l2, l3;
		while(get(&l1) && get(&l2) && get(&l3) && (l1 || l2 || l3))
		{
			int l;
			l = (l1 * 2 + l2) * 2 + l3;
//			printf("%d%d%d l : %d\n", l1, l2, l3, l<details>
			deal(l);
		}
		printf("\n");
	}
	return 0;
}

有了二进制就不必以字符串的形式保存这一大串编码了,只需要把编码理解成二进制,用(len,value)这个二元组来表示一个编码,其中len是编码的长度,value是编码对应的十进制值。如果用codes[len][value]保存这个编码所对应的字符,则主程序看上去应该是以下的样子

点击查看代码
#include<stdio.h>
#include<string.h>
int readchar()
{
	
}
int readint(int c)
{
	
}
int code[8][1<<8];
int readcodes()
{

}
int main()
{
	while(readcodes())
	{
//              printfcodes();
		for( ; ; )
		{
			int len = readint(3);
			if(len == 0) break;
//			printf("len = %d\n", len);
			for( ; ;) 
			{
				int v = readint(len);
//				printf("v = %d\n", v);
				if(v == (1 << len) - 1) break;
				putchar(code[len][v]);
			}
		}
		putchar('\n');
	}
	return 0;
}

主程序中还有两个还没有介绍的函数,readcodes和readint。前者用来读取编码后者用来读取c位二进制字符,并转化为十进制字符
本题的调试方法就是在之间被注释掉的语句中,如果程序的输出不是想要的结果,题目中的举例就可以派上用场,只需要把举例中的数据的中间结果与我们打印的中间结果进行一一比较就可以了
当然编写readint的时候会遇到一些问题编码文本可以由多行组成,这个问题的解决方法可以再编写一个跨行都字符的函数readchar

注意本题的空字符不需要自己一个个进行赋值因为空字符本来ascii码就是0,注意空格的ascii码是32,但是感觉两者的输出效果上是相同的,(因为scanf把空格和换行都当作结束标志了,像gets就是以EOF或者换行为结束标志,那么他就可以读取空格了)通过memset来赋值更加方便快速,这点需要注意。

实现的代码如下

点击查看代码
#include<stdio.h>
#include<string.h>
int readchar()
{
	for( ; ; ) 
	{
		int ch = getchar();
		if(ch != '\n' && ch != '\r')//注意这边的有时候会产生一点小错误,如果只是直接粘贴样例,样例中的空格会影响结果,因为这边并没有对于空格的判断,即会导致程序的错误
			return ch;//一直读到非换行符为止 
	}
}
int readint(int c)
{
	int v = 0;
	while(c--)
	{
		v = v * 2 + readchar() - '0';
	}
	return v;
}
int code[8][1<<8];
int readcodes()
{
	memset(code, 0, sizeof(code));
	code[1][0] = readchar();
//	printf("1 0 %c\n", code[1][0]);
	for(int len = 2; len <= 7; len++)
	{
		for(int i = 0; i < (1<<len)-1; i++)
		{
			int ch = getchar();
			if(ch == EOF) return 0;
			if(ch == '\n' || ch == '\r') return 1;
//			printf("%d %d %c\n", len, i, ch);
			code[len][i] = ch;
		}
	}
	return 1;
}
void printfcodes()
{
	for(int len = 1; len <= 7; len++)
	{
		for(int i = 0; i < (1<<len)-1; i++)
		{
			if(code[len][i] == 0)	return;
//		printf("code[%d][%d] = %c\n", len, i, code[len][i]);
		}
	}
}

int main()
{
	while(readcodes())
	{
		for( ; ; )
		{
			int len = readint(3);
			if(len == 0) break;
//			printf("len = %d\n", len);
			for( ; ;) 
			{
				int v = readint(len);
//				printf("v = %d\n", v);
				if(v == (1 << len) - 1) break;
				putchar(code[len][v]);
//				printf("\n");
			}
		}
		putchar('\n');
	}
	return 0;
}

也就是说对于会换行的数据我们可以通过跨行读字符来实现,注意在Windows下的换行符虽然是\n但是他总是要和\r搭配在一起使用\n下移\r左移至最左边
使用数组之前通过memset函数将数组清零这是一个比较好的习惯
当然对于上述例子的解决确实使用getchar函数会比较快速,而且方便
最后是printcodes函数,这个函数对于接替来说不是必须的,但是对于调试却是有用的
对于printfcodes函数,因为每次读取编码的时候都把code数组清空了额,所以只要遇到字符为0的情况,就表示编码头已经结束

笔者关于追踪电子表格中的单元格的代码(因为少考虑了一些特殊情况,导致代码段被补的非常散乱,qaq)

点击查看代码
#include<stdio.h>
#define maxn 55
struct p{
	int x, y;
};
p map[maxn][maxn];
int a, b;

void start(int a, int b)
{
	for(int i = 1; i <= a; i++)
	{
		for(int j = 1; j <= b; j++)
		{
			map[i][j].x = i;
			map[i][j].y = j;
		}
	}
}

p* findpoint(int x, int y)
{
	for(int i = 1; i <= a; i++)
		for(int j = 1; j <= b; j++)
		{
			if(map[i][j].x == x && map[i][j].y == y)
				return &map[i][j];
		}
	return NULL;
}

//void paint()
//{
//	printf("\n");
//	for(int i = 1; i <= a; i++)
//	{
//		for(int j = 1; j <= b; j++)
//		{
//			printf(".%d %d.", map[i][j].x, map[i][j].y);
//		}
//		printf("\n");
//	}
//	printf("\n");
//}

void ex()
{
	int x1, y1, x2, y2;
	scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
	p* p1 = findpoint(x1, y1); 
	p* p2 = findpoint(x2, y2);
	if(x1 > 0 && x2 > 0 && y1 > 0 && y2 > 0)
	{
		if(p1 != NULL && p2 != NULL)
		{
			int tpxy = p1->x;
			p1->x = p2->x;
			p2->x = tpxy;
			tpxy = p1->y;
			p1->y = p2->y;
			p2->y = tpxy;
		}
		else
		{
			if(p1 == NULL && p2 == NULL)
			{
				return;
			}	
			else if(p1 == NULL)
			{
				p2->x = x1;
				p2->y = y1;
			}
			else if(p2 == NULL)
			{
				p1->x = x2;
				p1->y = y2;
			}
		}
	}
	else
	{
		if(p1 != NULL)
		{
			p1->x = 0;
			p1->y = 0;
		}
		if(p2 != NULL)
		{
			p2->x = 0;
			p2->y = 0;
		}
	}
}

void rl(int x, int y, int id)
{
	int t;
	int tdrl[20];
	scanf("%d", &t);
	for(int i = 0; i < t; i++)
		scanf("%d", &tdrl[i]);
	for(int i = 0; i < t - 1; i++)
		for(int j = i + 1; j < t; j++)
		{
			if(tdrl[i] > tdrl[j])
			{
				int temp = tdrl[i];
				tdrl[i] = tdrl[j];
				tdrl[j] = temp;
			}
		}
	for(int i = 1; i <= a; i++)
	{
		for(int j = 1; j <= b; j++)
		{
			if(!map[i][j].x)
				continue;
			int tx = map[i][j].x, ty = map[i][j].y;
			for(int k = 0; k < t; k++)
			{
				if((tx == tdrl[k] && x) || (ty == tdrl[k] && y))
				{
					map[i][j].x = (!(id - 1)) * (map[i][j].x + x);
					map[i][j].y = (!(id - 1)) * (map[i][j].y + y);
				}
				else if((tx > tdrl[k] && x) || (ty > tdrl[k] && y))
				{
					map[i][j].x = map[i][j].x + x * id;
					map[i][j].y = map[i][j].y + y * id;
				}
			}
			
		}
	}
}

void getdone(int t)
{
	char sign[5];
	for(int i = 0; i < t; i++)
	{
		scanf("%s", sign);
		if(sign[0] == 'D')
		{
			if(sign[1] == 'R')
				rl(1, 0, -1);
			else
				rl(0 , 1, -1);
		}
		else if(sign[0] == 'I')
		{
			if(sign[1] == 'R')
				rl(1, 0, 1);
			else
				rl(0, 1, 1);
		}
		else
		{
			ex();
		}
	}
}
	
void quest()
{
	int x, y;
	scanf("%d%d", &x, &y);
	if(map[x][y].x)
		printf("Cell data in (%d,%d) moved to (%d,%d)\n", x, y, map[x][y].x, map[x][y].y);
	else
		printf("Cell data in (%d,%d) GONE\n", x, y);	
}


int main()
{
	int cnt = 0;
	while(scanf("%d%d", &a, &b) == 2 && (a || b))
	{
		if(cnt != 0)
			printf("\n");
		printf("Spreadsheet #%d\n", ++cnt);
		start(a, b);
		int t;
		scanf("%d", &t);
		getdone(t);
		scanf("%d", &t);
		for(int i = 0; i < t; i++)
		{
			quest();
		}
	}
	return 0;
}

笔者在这边犯了一个错误,两个数交换,在内存条件允许的情况下,推荐使用三元交换法,不推荐使用加减法或者只适合整数的异或运算,因为他们一旦遇到自己交换自己的时候,就会是自己的值变成0,会造成一些隐藏的错误,同时结构体的指针如果需要调用其中的成员的时候貌似只能使用语法p->成员,其他的编译器都会报错,等笔者以后摸索清楚了,再来描述清楚,同时结构体指针的赋值如果需要函数的返回值,好像不能同时进行赋值

最直接的思路就是首先模拟操作,算出最后的电子表格,然后在每次查询时直接在电子表格中找到所求的单元格
下面时作者给出的代码:

点击查看代码
#include<stdio.h>
#include<string.h>
#define maxd 100
#define BIG 10000
int r, c, n, d[maxd][maxd], d2[maxd][maxd], ans[maxd][maxd], cols[maxd];

void copy(char type, int p, int q)
{
	if(type == 'R')
	{
		for(int i =1; i <= c; i++)
			d[p][i] = d2[q][i];
	}
	else
	{
		for(int i = 1; i <=r; i++)
			d[i][p] = d2[i][q]; 
	}
}

void del(char type)
{
	memcpy(d2, d, sizeof(d));//为了同时删去的原因否则容易出错,注意这边之所以有d2的出现是因为作者通过保留原图像的方式避免了乱序输出带来的混乱,而可以很清楚的达到自己的目的 
	int cnt = type == 'R' ? r : c, cnt2 = 0;
	for(int i = 1; i <= cnt; i++)
	{
		if(!cols[i])
			copy(type, ++cnt2, i);//行列类型,需要复制的位置,被复制的位置 
	} 
	if(type == 'R')
		r = cnt2;
	else
		c = cnt2;
}

void ins(char type)
{
	memcpy(d2, d, sizeof(d));
	int cnt = type == 'R' ? r : c, cnt2 = 0;
	for(int i = 1; i <= cnt; i++)
	{
		if(cols[i])
			copy(type, ++cnt2, 0);//美妙的实现了单次有序插入 
		copy(type, ++cnt2, i);
	}
	if(type == 'R')
		r = cnt2;
	else
		c = cnt2;
}

int main()
{
	int r1, c1, r2, c2, q, kase = 0;
	char cmd[10];
	memset(d, 0, sizeof(d));//将数组初始化是一个很好的习惯 
	while(scanf("%d%d%d", &r, &c, &n) == 3 && r)
	{
		int r0 = r, c0 = c;//保留原有的行列,为后面的模拟不破坏做准备 
		for(int i = 1; i <= r; i++)
			for(int j = 1; j <= c; j++)
				d[i][j] = i * BIG + j;//本题的精华之一,节省空间的同时又能同时存放两个数据 
		while(n--)
		{
			scanf("%s", cmd);
			if(cmd[0] == 'E')
			{
				scanf("%d%d%d%d", &r1, &c1, &r2, &c2);
				int t = d[r1][c1];
				d[r1][c1] = d[r2][c2];//正确的三元交换,不推荐使用异或以及加减法换元,如果没有特殊的判断,很容易挂掉 
				d[r2][c2] = t;
			}
			else
			{
				int a, x;
				scanf("%d", &a);
				memset(cols, 0, sizeof(cols));//接受指令,同时实现 
				for(int i = 0; i < a; i++)
				{
					scanf("%d", &x);
					cols[x] = 1;
				}
				if(cmd[0] == 'D')
					del(cmd[1]);
				else
					ins(cmd[1]);//判断是对行还是列的操作 
			}
		}
		memset(ans, 0, sizeof(ans));
		for(int i= 1; i <= r; i++)
			for(int j = 1; j <= c; j++)
			{
				ans[d[i][j]/BIG][d[i][j]%BIG] = i * BIG + j;//通过ans来实现调用以及快速输出 
			}
		if(kase > 0) 
			printf("\n");
		printf("Spreadsheet #%d\n", ++kase);
		scanf("%d", &q);
		while(q--)
		{
			scanf("%d%d", &r1, &c1);
			printf("Cell data in (%d,%d) ", r1, c1);
			if(ans[r1][c1] == 0)
				printf("GONE\n");
			else
				printf("moved to (%d,%d)\n", ans[r1][c1]/BIG, ans[r1][c1]%BIG);
		}
	}
	return 0;
}

d[i][j] = i * BIG + j;//本题的精华之一,节省空间的同时又能同时存放两个数据
也就是说整数除法和取余运算有时候结合起来可以起到非常神奇的效果

另一个思路时将所有操作保存,然后对于每个查询重新执行每个操作,但不需要计算整个电子表格的变化,而只需关注所查询的单元格的位置变化,对于题目给定的规模来说,这个方法不仅更好写,而且效率更高:

点击查看代码
#include<stdio.h>
#include<string.h>
#define maxd 10000

struct Command {
	char c[5];
	int r1, c1, r2, c2;
	int a, x[20];
}; 
int r, c, n;
Command cmd[maxd];//构建了指令结构体,涵盖了所有的指令 

int simulate(int* r0, int* c0)//获取原先所在的位置 
{
	for(int i = 0; i < n; i++)//获取总共的操作数 
	{
		if(cmd[i].c[0] == 'E')
		{
			if(cmd[i].r1 == *r0 && cmd[i].c1 == *c0)
			{
				*r0 = cmd[i].r2;
				*c0 = cmd[i].c2;
			}
			else if(cmd[i].r2 == *r0 && cmd[i].c2 == *c0)//如果ex操作有涉及到这个元素,那么就需要将他操作一波,换一个位置 
			{
				*r0 = cmd[i].r1;
				*c0 = cmd[i].c1;
			}
		}	
		else
		{
			int dr = 0, dc = 0;
			for(int j = 0; j < cmd[i].a; j++)//所需要操作的具体行数或列数 
			{
				int x = cmd[i].x[j];
				if(cmd[i].c[0] == 'I')//判断这个命令是插入还是删除 
				{
					if(cmd[i].c[1] == 'R' && x <= *r0) dr++;//当插入的时候,对于行列来说,只有当原先的位置大于等于所需要的操作行列数才会被影响到,注意这边的dr,dc是必须的,不能直接对*r0,*c0进行操作,原因和前面的dd2数组的原因是一样的 
					if(cmd[i].c[1] == 'C' && x <= *c0) dc++;
				}
				else
				{
					if(cmd[i].c[1] == 'R' && x == *r0) return 0;//如果删除的时候恰好被删除了,那么无论后面进行什么操作,该元素都是消失了,不可能还能出现 
					if(cmd[i].c[1] == 'C' && x == *c0) return 0;
					if(cmd[i].c[1] == 'R' && x < *r0) dr--;
					if(cmd[i].c[1] == 'C' && x < *c0) dc--;//如果没有删除到,那么依然是只有比操作的行数或列数大的时候才会受到影响 
				}
			}
			*r0 += dr;//dr和dc中记载了行列的相对变化趋势,因此可以改变或者输出真正的位置 
			*c0 += dc;
		}	
	}
	return 1; 
}

int main()
{
	int r0, c0, q, kase = 0;
	while(scanf("%d%d%d", &r, &c, &n) == 3 && r)//读取行列以及判断程序是否可以继续执行 
	{
		for(int i = 0; i < n; i++)
		{
			scanf("%s", cmd[i].c);//开始读取所有的指令 
			if(cmd[i].c[0] == 'E')
			{
				scanf("%d%d%d%d", &cmd[i].r1, &cmd[i].c1, &cmd[i].r2, &cmd[i].c2);//r1,c1,r2,c2的存储是为了ex命令 
			}
			else
			{
				scanf("%d", &cmd[i].a);//该指令所需要的操作数 
				for(int j = 0; j < cmd[i].a; j++)
					scanf("%d", &cmd[i].x[j]);//该指令操作的具体位置 
			}
		}
		if(kase > 0)
			printf("\n");//UVA经典分行 
		printf("Spreadsheet #%d\n", ++kase);
		
		scanf("%d", &q);
		while(q--)//开始对答案进行输出 
		{
			scanf("%d%d", &r0, &c0);//找出目标位置 
			printf("Cell data in (%d%,d) ", r0, c0);
			if(!simulate(&r0, &c0))//到达关键函数处理,如果原先的元素已经被删除了,那么一定是不存在了 
				printf("GONE\n");
			else
				printf("moved to (%d,%d)\n", r0, c0);//否则必然会出现新的位置 
		}
	}
}

这种想法很独特,他抛弃了大局观,因为题目所关注的是一个点,因此模拟一个点的操作有时候会比将所有的数据都模拟出来要方便的多,以及减少了思维量和代码莫名其妙错误的可能性

可能会觉得simulate函数不是特别自然,因为所用到的r0,c0的地方都必须加上一个*,幸运的是,C++语言中有另外一个语法,可以更自然地表达这种需要被修改的参数,不过这是第五章的引用部分的内容了

师兄帮帮忙
阴间题目,格式问题会卡死很久
笔者的代码如下:

点击查看代码
#include<stdio.h>
#include<string.h>
#define maxn 105
#define INF 0xfffffff
struct stu{
	char SID[10];
	int CID, rank, sum;
	double ave;
	char name[10];
	int score[4];
};

stu st[maxn];
int cnt = 0;

void start()
{
	printf("Welcome to Student Performance Management System (SPMS).\n\n");
	printf("1 - Add\n");
	printf("2 - Remove\n");
	printf("3 - Query\n");
	printf("4 - Show ranking\n");
	printf("5 - Show Statistics\n");
	printf("0 - Exit\n\n");
}

int check()
{
	for(int i = 0; i < cnt; i++)
	{
		if(!strcmp(st[cnt].SID, st[i].SID))
			return 1;
	}
	return 0;
}

void Add()
{
	printf("Please enter the SID, CID, name and four scores. Enter 0 to finish.\n");
	while(scanf("%s", st[cnt].SID) == 1 && (strlen(st[cnt].SID) - 1 || st[cnt].SID[0] - '0'))
	{
		if(check())
		{
			scanf("%d%s%d%d%d%d", &st[cnt].CID, st[cnt].name, &st[cnt].score[0], &st[cnt].score[1], &st[cnt].score[2], &st[cnt].score[3]);
			printf("Duplicated SID.\n");
			printf("Please enter the SID, CID, name and four scores. Enter 0 to finish.\n");
			continue;
		}
		scanf("%d%s%d%d%d%d", &st[cnt].CID, st[cnt].name, &st[cnt].score[0], &st[cnt].score[1], &st[cnt].score[2], &st[cnt].score[3]);
		st[cnt].sum = 0;
		for(int i = 0; i < 4; i++)
		{
			st[cnt].sum += st[cnt].score[i];
		}
		st[cnt].ave = (double)st[cnt].sum / 4;
		cnt++;
		printf("Please enter the SID, CID, name and four scores. Enter 0 to finish.\n");
	}
}

void Rem()
{
	char t[15];
	printf("Please enter SID or name. Enter 0 to finish.\n");
	while(scanf("%s", t) == 1 && ((strlen(t) - 1) || t[0] - '0'))
	{
		int sta = 0, num = cnt;
		for(int i = 0; i < num; i++)
		{
			if(!strcmp(st[i].SID, t) || !strcmp(st[i].name, t))
			{
				st[i].CID = 0;
				cnt--;
			}	
		}
		for(int i = 0; i < num; i++)
		{
			if(st[i].CID)
			{
				if(sta != i)
					memcpy(&st[sta], &st[i], sizeof(st[i]));	
				sta++;
			}	//paint();
		}
		printf("%d student(s) removed.\n", num - cnt);
//		paint();
		printf("Please enter SID or name. Enter 0 to finish.\n");
	}
}

void ranked()
{
	int ranking = 0, num = 0, ans = -INF;
	for(int i = 0; i < cnt; i++)
		st[i].rank = 0;
	for(int i = 0; ranking < cnt; i++)
	{
		for(int j = 0; j < cnt; j++)
		{
			if(st[j].rank)
				continue;
			if(st[j].sum > ans)
			{
				ans = st[j].sum;
				num = 1;
			}
			else if(st[j].sum == ans)
			{
				num++;
//				printf("aaaaaaaaa%d %d %d\n", j, st[i].sum, ans);
			}
		}
		ranking++;
//		printf("ranking %d num %d\n", ranking, num);
		for(int j = 0; j < cnt; j++)
		{
			if(st[j].rank)
				continue;
			if(st[j].sum == ans)
			{
				st[j].rank = ranking; 
//					printf("%d %s %d %s %d %d %d %d %d %.2lf\n", st[i].rank, st[i].SID, st[i].CID, st[i].name, st[i].score[0], st[i].score[1], st[i].score[2], st[i].score[3], st[i].sum, st[i].ave);
			}	
		}
		ranking = ranking + num - 1;
		ans = -INF;
//		printf("ans %d ranking %d\n", ans, ranking);
	}
}

void Que()
{
	char t[15];
	printf("Please enter SID or name. Enter 0 to finish.\n");
	ranked();
	while(scanf("%s", t) == 1 && (strlen(t) - 1 || t[0] - '0'))
	{
		for(int i = 0; i < cnt; i++)
		{
			if(!strcmp(st[i].SID, t) || !strcmp(st[i].name, t))
			{
				printf("%d %s %d %s %d %d %d %d %d %.2lf\n", st[i].rank, st[i].SID, st[i].CID, st[i].name, st[i].score[0], st[i].score[1], st[i].score[2], st[i].score[3], st[i].sum, st[i].ave+1e-5);
			}
		}
		printf("Please enter SID or name. Enter 0 to finish.\n");
	}
}

void Ran()
{
	printf("Showing the ranklist hurts students' self-esteem. Don't do that.\n");
}

void output(int* tcla, int len)
{
	char seg[4][20] = {"Chinese", "Mathematics", "English", "Programming"};
	int phe[5];
	memset(phe, 0, sizeof(phe));
//	for(int i = 0; i < len; i++)
//		printf(".\n%d\n.\n", tcla[i]);
	for(int i = 0; i < 4; i++)
	{
		int sum = 0, pass = 0, loss = 0;
		printf("%s\n", seg[i]);
		for(int j = 0; j < len; j++)
		{
			sum += st[tcla[j]].score[i];
			if(st[tcla[j]].score[i] < 60)
				loss++;
			else
				pass++;
		}
		if(len)
			printf("Average Score: %.2lf\n", (double)sum / len + 1e-5);
		else
			printf("Average Score: -nan\n");
		printf("Number of passed students: %d\n", pass);
		printf("Number of failed students: %d\n\n", loss);
	}
	for(int i = 0; i < len; i++)
	{
		int loss = 0;
		for(int j = 0; j < 4; j++)
		{
			if(st[tcla[i]].score[j] < 60)
				loss++;
		}		
		phe[loss]++;
	}
	for(int i = 1; i < 4; i++)
		phe[i] += phe[i - 1];
	printf("Overall:\n");
	printf("Number of students who passed all subjects: %d\n", phe[0]);
	for(int i = 1; i < 4; i++)
	{
		printf("Number of students who passed %d or more subjects: %d\n", 4-i, phe[i]);
	}
	printf("Number of students who failed all subjects: %d\n\n", phe[4]);
}

void Sta()
{
	int tCID, len = 0;
	int clasmets[maxn];
	printf("Please enter class ID, 0 for the whole statistics.\n");
	scanf("%d", &tCID);
	if(!tCID)
	{
		for(int i = 0; i < cnt; i++)
			clasmets[i] = i;
		len = cnt;	
	}	
	else
	{
		for(int i = 0; i < cnt; i++)
		{
			if(st[i].CID == tCID)
			{
				clasmets[len] = i;
				len++;	
			}	
		}
	}
	output(clasmets, len);
}

int main()
{
//	freopen("data.in", "r", stdin);
//	freopen("data.out", "w", stdout);
	int t;
	memset(st, 0, sizeof(st));
	start();
	while(scanf("%d", &t) == 1 && t)
	{
		if(t == 1)
			Add();
		else if(t == 2)
			Rem();
		else if(t == 3)
			Que();
		else if(t == 4)
			Ran();
		else if(t == 5)
			Sta();
		start();
	}
	return 0;
}

对于可能仍然奋战在这道题目的兄台们,注意
1.welcome那一行需要两个换行符,0 - Exit以及后面查询各个科目的时候的最后一行都是要两个换行符
2.直接复制4的回复语句是不行的,因为那里的‘是中文,AC中的是半角'
3.题目中涉及输出%.2lf的最后都需要加上1e-5,来提高输出精度,否则会挂掉(是的没错就在pdf版本output下面的hint中)
4.除0虽然笔者进行了判断,但是实际数据中并不会出现这样的情况,所以可以根据自己的需求是否加上-nan代码,当然为了程序的鲁棒性越来越好自然是可以的

作者的大致框架如下

点击查看代码
#include<cstdio.h>

int main()
{
	for( ; ; )
	{
		int choice;
		printf_menu();
		scanf("%d", &choice);
		if(choice == 0) break;
		if(choice == 1) add();
		if(choice == 2) DQ(0);
		if(choice == 3) DQ(1);
		if(choice == 4) printf("Showing the ranklist hurts students' self-esteem. Don't do that.\n");
		if(choice == 5) stat();
	}
	return 0;
}

具体的某些DQ实现如下:

点击查看代码
void DQ(int isq)
{
	char s[max1];
	for( ; ; )
	{
		printf("Please enter SID or name. Enter 0 to finish.\n");
		scanf("%s", s);
		if(strcmp(s, "0") == 0) break;//直捣黄龙,不像笔者一样乱起八糟的强行判断 
		int r = 0;
		for(int i = 0; i < n; i++)
			if(!removed[i])
			{
				if(strcmp[sid[i], s] == 0 || strcmp(name[i], s) == 0)
				{
					if(isq) printf("%d %s %d %s %d %d %d %d %d %.2lf\n", rank(i), sid[i], cid[i], name[i], score[i][0], score[i][1], score[i][2], score[i][3], score[i][4], score[i][4]/4.0+EPS);
					else 
					{
						removed[i] = 1;
						r++; 
					}
				}
			} 
		if(!isq) printf("%d student(s) removed.\n, r");
	}
}

在编写上述函数的过程中,用到了尚未编写的rank函数,并且直接使用了还没有声明的removed,sid,cid,name和score,换句话说根据函数编写的需要定义了数据结构,而不是一开始就设计好数据结构,程序的其他部分比较麻烦,作者并未给出,作者建议完成作为c语言部分的结束,是的,你毕业了!
rank函数的实现并不一定需要对于数据排序?(比大小就可以了?)不知道,等笔者有空回来时候填坑。

posted @   banyanrong  阅读(43)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示