「浙江理工大学ACM入队200题系列」问题 F: 零基础学C/C++39——求方程的解

本题是浙江理工大学ACM入队200题第四套中的F题

我们先来看一下这题的题面.


由于是比较靠前的题目,这里插一句.各位新ACMer朋友们,请一定要养成仔细耐心看题的习惯,尤其是要利用好输入和输出样例.

  • 样例相当于给你举了个具体的例子,可以帮助你更好的理解题目
  • 样例会告诉你输入和输出的格式,你必须要在程序里以这样的格式输入和输出,否则会出问题
  • 样例可以在你本地写完代码之后用作测试,来检查你的代码能否正常地运行(不过样例运行正确并不代表完全对了,可能输入其他的数据会出现别的问题)

题面

题目描述

求ax2+bx+c=0方程的实根。a,b,c由键盘输入. 解方程要考虑系数a等于零的情况,且解x1、x2必须是float型。a等于零有两种情况(b == 0,b!=0),a不等于零有三种情况(delta>0、==0、<0),先计算得到x1、x2,再printf输出

输入

输入三个数a,b,c

输出

输出方程的实根,如果方程有实根,则输出根;如果方程有2个不等实根,则分2行输出,第一行输出较大根,第二行输出较小根。 其余情况(如无实根等)则输出No

样例输入

1 -3 2

样例输出

2.000000
1.000000

代码风格问题

这题在ACM成员群里询问的频率的非常高,但其实并不是一道hard题,只是判断条件比较多容易乱而已,并不涉及什么高级的数据结构和算法.

在讲思路之前,首先来说说问这道题的朋友大多出现的两个坏习惯,不缩进和代码块不喜欢加大括号.

如果你想直接看与这题有关的解释,可以跳过这部分.

缩进问题

首先,一定要养成好好缩进的习惯.不缩进的代码在C中并不会报错,但是会极大降低自己代码的可读性.

可读性是很重要的,不管是实际开发还是ACM竞赛,都不是一个人的战场,你的代码是需要给你的同伴和队员看的,同时也是需要给未来的自己看的.你也不希望别人看不懂你的代码而导致时间成本的增加吧?

缩进的本质目的是为了展现出代码的层次,一份良好缩进的代码可以很清楚地展示出代码的层次结构,帮助阅读者更好地理解代码.

如果你很想养成好好缩进的习惯但是苦于经常忘记缩进或者不知道具体应该怎么缩进的话,强烈建议有空去学习一下Python语言,在Python中,不缩进会抛出异常,你必须要进行缩进.同时Python在ACM涉及到高精度的算法中,也有出场的一席之地.

大括号问题

其次就是不写大括号的问题了,这边我强烈建议所有新ACMer朋友们不要去管什么可以不写大括号的规则,一律打上大括号!在你不完全熟悉什么地方可以省略大括号的情况下随意省略大括号都可能会导致极其严重的问题!打一下大括号也不是什么特别麻烦的事情,所以请一律打上大括号!

我们来看看一些真实的不打大括号导致出问题的案例,均来自ACM成员群,进行过简化和改编.

错误样例1 不打大括号的嵌套if-else语句

首先,下面如下不打大括号的嵌套if-else语句:

	int a = 10;
	if(a > 5)
		if(a > 10)
			printf("a > 10");
	else
		printf("a < 5,I think");

从缩进上看,写出这样代码的人似乎想实现a > 10时输出"a > 10"a <= 5时输出"a < 5,I think".但是,实际上根据else的匹配规则,此处的else是属于if(a > 10)的,所以此处的代码在运行之后会很滑稽地输出"a < 5,I think".但是打上大括号就可以直接解决并永远规避这个问题:

	int a = 10;
	if(a > 5)
	{
		if(a > 10)
		{
			printf("a > 10");
		}
	}
	else
	{
		printf("a < 5,I think");
	}

错误样例2 不打大括号的冒泡排序

接着,我们来看下面这一段不打大括号的冒泡排序代码(你不知道冒泡排序也没关系,只要看不打大括号的问题就好了,感兴趣可以去看我写的这篇博客,有详细的推出过程):

	for (int i = 0; i < len; i++)
		for(int j = len - 1;j > i;j--)
			if(a[j - 1] < a[j])
				int t = a[j];a[j] = a[j - 1];a[j - 1] = t;

根据冒泡排序的实现方式以及这位同学代码的写法,可以明白他是希望在a[j - 1] < a[j]满足时执行数据交换需要进行的这三句代码.尽管他很呆地把这三句代码写在了一行上,但依旧改变不了if语句在无大括号的时候只管一行语句的现实,要实现正常的功能,这里if语句应当加上大括号.

	for (int i = 0; i < len; i++)
	{
		for(int j = len - 1;j > i;j--)
		{
			if(a[j - 1] < a[j])
			{
				int t = a[j];a[j] = a[j - 1];a[j - 1] = t;
			}
		}
	}

限于篇幅原因这里就不再列举其他的自以为是不写大括号导致错误的错误样例了.总之,在没有完全熟悉if语句大括号省略规则的情况下,请完整地打出所有的大括号.另外即便你已经非常熟悉大括号省略的规则,也强烈建议不要省略任何大括号.仅仅打一对大括号并不是什么难事.

题目分析

我们完全可以模拟我们自己用纸笔解形二次方程的过程,慢慢写出各个分支语句.

首先,相信经历过高中毒打的各位朋友们在解一个形二次方程,第一反应都是讨论二次项系数是否为零.当二次项系数为零时,这便是一个形一次方程,同样要再讨论一下一次项系数是否为零,由数学知识可以得出这部分的代码:

	// 定义3个double型变量a,b,c,注意这里使用直接定义为double型来解决整数除以整数还是整数的问题,下同.这题其实存在一个潜在的问题,即double中存在负0,这里我们直接无视这种情况(实际上此题特判-0反而会导致答案错误).
	if (a == 0)
	{
		// 当前是形一次方程
		if (b == 0)
		{
			// 一次项系数也为0,无穷解或无解
			printf("No\n");
		}
		else
		{
			// 一次项系数不为0,为一次方程,直接解就好了(不知道为啥这里有朋友没有负号,或者是写成b / c,这边建议口算不行还是打个草稿好)
			printf("%lf\n", -c / b);
		}
	}

如果掉进了此处对两个整数使用除法并企图得到一个小数的结果的老坑,或者对此还不熟悉的朋友可以去看看我写的这篇博客

之后就是解二次方程的问题了,为了后续编写方便,我们先计算delta:

	double delta = b * b - 4 * a * c;

当delta小于零时,方程无解,我们直接输出No:

	if(delta < 0)
	{
		// 方程无解
		printf("No\n");
	}

接下来,欢迎来到无数朋友的掉坑点.


听取WA声一片

常见错误思路及解决方案

优先级问题

当delta等于0时,二次方程仅有一解,相信经历过初中和高中朋友们都能很熟练地写出求这个唯一解的公式吧,于是就有朋友给出了如下代码(局部且无视if-else结构):

	if(delta == 0)
	{
		// 方程仅有一解
		printf("%lf\n", -b / 2 * a);
	}

这里掉进的便是运算符优先级的坑.在数学中,同级运算符是从左往右运算的,在C中大体也是这样(不考虑结合性).因而在C语言中和数学一样,我们把一个分式改为除法运算式时,需要对分子分母添加括号(如果只有一项那当然不用加),这样才能保证优先级不出问题.局部参考代码如下(无视if-else结构):

	if(delta == 0)
	{
		// 方程仅有一解
		printf("%lf\n", -b / (2 * a)); // 时刻关注运算符优先级
	}

想当然未实现由大到小输出

接下来当delta大于0时,依据题目要求我们需要将两根从大到小输出,于是就有很多朋友们给出如下代码(写成这样来问的也是最多的):

	if(delta > 0)
	{
		// 方程有两解
		double x1 = (-b + sqrt(delta)) / (2 * a); // 记得导入<math.h>头文件鸭!
		double x2 = (-b - sqrt(delta)) / (2 * a);
		printf("%lf\n%lf\n", x1, x2);
	}

这里问题出在哪了呢?出在了没有将两根从大到小输出.可是每次当我指出这个问题时,都会有朋友们反驳道:”我考虑了啊,我就是从大到小输出的啊!”之所以出现这种情况,是因为这些朋友们想当然地以为二次方程求根公式中+的那个大而-的那个小.
实际上很明显并不是这样的,比如我们使用-3 4 5输入时,得到的输出便是-0.786300 2.119633(见下图)

image

这明显不是从大到小输出,因为决定这两个数大小的不仅是+还是-根号delta,还有其他的因素.当然,其他的因素同样不仅限于a的正负,有些朋友很呆地通过判断a的正负来当做判断两根的大小,这也是想当然了.

那怎么办呢,很容易啊,这不就是两个数降序输出的问题吗?直接比较交换就可以了(当然也有其他处理方法),局部参考代码如下(无视if-else结构):

	if (delta > 0)
	{
		// 方程有两解
		double x1 = (-b + sqrt(delta)) / (2 * a); // 记得导入<math.h>头文件鸭!
		double x2 = (-b - sqrt(delta)) / (2 * a);
		// 如果不满足降序,交换x1和x2
		if (x1 < x2)
		{
			double t = x1;
			x1 = x2;
			x2 = t;
		}
		printf("%lf\n%lf\n", x1, x2);
	}

参考代码

下面给出了我自己做这道题时候的完整代码:

(仅作为参考,一定要自己写一下奥,作弊没意思,害人又害己)

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

int main()
{
	double a, b, c; // 这里使用直接定义为double型来解决整数除以整数还是整数的问题.这题其实存在一个潜在的问题,即double中存在负0,这里我们直接无视这种情况(实际上此题特判-0反而会导致答案错误).
	scanf("%lf%lf%lf", &a, &b, &c);
	if (a == 0)
	{
		// 当前是形一次方程
		if (b == 0)
		{
			// 一次项系数也为0,无穷解或无解
			printf("No\n");
		}
		else
		{
			// 一次项系数不为0,为一次方程,直接解就好了(不知道为啥这里有朋友没有负号,或者是写成b / c,这边建议口算不行还是打个草稿好)
			printf("%lf\n", -c / b);
		}
	}
	else
	{
		double delta = b * b - 4 * a * c;
		if (delta < 0)
		{
			// 方程无解
			printf("No\n");
		}
		else if(delta == 0)
		{
			// 方程仅有一解
			printf("%lf\n", -b / (2 * a)); // 时刻关注运算符优先级
		}
		else
		{
			// 方程有两解
			double x1 = (-b + sqrt(delta)) / (2 * a); // 记得导入<math.h>头文件鸭!
			double x2 = (-b - sqrt(delta)) / (2 * a);
			// 如果不满足降序,交换x1和x2
			if (x1 < x2)
			{
				double t = x1;
				x1 = x2;
				x2 = t;
			}
			printf("%lf\n%lf\n", x1, x2);
		}
	}
	
	return 0;
}

“正是我们每天反复做的事情,最终造就了我们,优秀不是一种行为,而是一种习惯” —亚里士多德

这篇题解就到这里了,各位朋友如果有问题欢迎到acm成员群中提问哦!

原文链接:https://www.cnblogs.com/geministar/p/zstuACM200_4F.html