P2455 [SDOI2006]线性方程组 题解

· · 题解

题目传送门

本题的前置知识:P3389 【模板】高斯消元法。

关于如何进行常规的高斯消元或者高斯-约旦消元,许多题解已经讲得非常清楚了,本文不再过多赘述,

这道题真正的难点在于,如何判断无解和无穷解的情况,以及如何正确处理解的表达

本篇题解将以这几个问题为重点,通过几组整合自讨论版的 hack 数据来讲解。

先展示代码

#include<bits/stdc++.h>
#define db double
using namespace std;
db a[110][110],ans[110];
const db eps=1e-8;
int main()
{
    int n;
    cin>>n;
    for(int i=1;i<=n;++i)
        for(int j=1;j<=n+1;++j)cin>>a[i][j];
    int res=1;
    for(int i=1;i<=n;++i)
    {
        int r=res;
        for(int j=res+1;j<=n;++j)
            if(fabs(a[r][res])<fabs(a[j][res]))r=j;
        if(res!=r)swap(a[res],a[r]);
        db temp=a[res][i];
        if(fabs(temp)<eps)continue;
        for(int j=i;j<=n+1;++j)
            a[res][j]/=temp;
        for(int j=res+1;j<=n;++j)
        {
            temp=a[j][i];
            for(int k=res;k<=n+1;++k)
                a[j][k]-=a[res][k]*temp;
        }
        res++;
    }
    ans[n]=a[n][n+1];
    if(fabs(ans[n])<eps)ans[n]=0;
    for(int i=n-1;i>=1;--i)
    {
        ans[i]=a[i][n+1];
        for(int j=i+1;j<=n;++j)
            ans[i]-=a[i][j]*ans[j];
        if(fabs(ans[i])<eps)ans[i]=0;
    }
    bool mark=0;
    for(int i=1;i<=n;++i)
    {
        bool ok=0;
        for(int j=1;j<=n;++j)
        {
            if(fabs(a[i][j])>eps)
            {
                ok=1;
                break;
            }
        }
        if(!ok)
        {
            if(fabs(a[i][n+1])>eps)
            {
                cout<<-1<<endl;
                return 0;
            }
            else mark=1;
        }
    }
    if(mark)
    {
        cout<<0<<endl;
        return 0;
    }
    for(int i=1;i<=n;++i)printf("x%d=%.2lf\n",i,ans[i]);
    return 0;
}

细节讲解

以下内容以高斯消元的算法处理为主。

数据处理

先来看这样一组数据:

input:
3
5 1 5 3
5 4 1 3
5 5 2 3
output:
x1=0.60
x2=0.00
x3=0.00

假如我们不对计算出的答案做任何判断的处理,直接输出答案,那么我们可能会得到 x3=-0.00 这样的结果。

因为浮点数计算时一定存在精度误差,所以我们可能无法准确计算出刚好为 0 的答案,而是会计算出一个绝对值极小的小数,所以输出的真实值可能会带有负号。

但是由于答案保留两位小数,我们显然会认为这样的答案就是在数值上等同于 0,要把它当成 0 来存储,所以我们有这些判断比如:

if(fabs(ans[i])<eps)ans[i]=0;

需要注意的是,既然我们有了上述的认识,我们就要意识到,在本题中所有需要判断数据是否等于 0 时,我们都应改为判断其绝对值是否小于 eps,在判断无解和无穷解时都要注意这一点,否则也会出错。

解的判定

现在,我们来讲解判断无解和无穷解时需要注意的事项。

我们肯定是优先判断无解,再判断无穷解,因为如果满足一个无穷解的条件不代表最终这个方程一定不是无解,但是满足一个无解的条件,这个方程一定无解,也就是无解的条件具有更高优先级,这一点在其他题解中已经写得很清楚了。

显然我们是在最后回带结束,求出每个 ans[i] 之后再进行判断。

那么首先,在消元的过程当中,如果遇到除数为 0 的情况,不要着急判断解的情况,我们首先跳过这一轮,如代码中的:

if(fabs(temp)<eps) continue;

如果不记得跳过的话,可能在消元某个矩阵的过程中会出现这样的情况:

1.00 1.00 1.00 1.00 0.00
0.00 nan inf inf inf
0.00 nan nan nan nan
0.00 nan nan nan nan

很可能导致最终答案一律变为 0

然后,我们进入到具体的判定过程,

如果仅仅像这样:

if(fabs(a[i][i])<eps&&fabs(ans[i])>eps)
{
    cout<<-1<<endl;
    return 0;
}
if(fabs(a[i][i])<eps&&fabs(ans[i])<eps)......;

每行只判断两个数的值是不够的。

看下面这组数据:

intput:
4
0 0 2 4 6
0 0 1 1 2
0 0 4 8 12
1 1 1 1 0
output: 
0

如果类似上述代码那样的消元,最后一轮矩阵会消成这样:

1.00 1.00 1.00 1.00 0.00
0.00 0.00 1.00 1.00 2.00
0.00 0.00 1.00 2.00 3.00
0.00 0.00 0.00 0.00 0.00

那么如果只判断两个数,输出就会是 -1,第二行就满足了 -1 的输出条件,而这一行还有其他不为 0 的数,所以显然答案不是 -1

所以我们要仔细地检查每一个数,就像上面的代码那样:

    bool mark=0;
    for(int i=1;i<=n;++i)
    {
        bool ok=0;
        for(int j=1;j<=n;++j)
        {
            if(fabs(a[i][j])>eps)
            {
                ok=1;
                break;
            }
        }
        if(!ok)
        {
            if(fabs(a[i][n+1])>eps)
            {
                cout<<-1<<endl;
                return 0;
            }
            else mark=1;
        }
    }
    if(mark)
    {
        cout<<0<<endl;
        return 0;
    }

消元时的细节

最后需要注意的是,在消元的过程之中,我们还需要一个 res 指针来记录上一次用以消元的那行的行号。

这组数据就很好地阐明了它的作用:

input
2
0 2 4
0 2 3
output:
-1

假如不使用类似的一个指针的话,常规消元得到的结果可能是这样的:

0.00 2.00 4.00
0.00 1.00 1.50

正如我们所见,这相当于根本没消元。

没有消元的原因在于,第一行的主元是找不到的,所以处理第一行时会跳过这一轮,然后就会处理第二行,

然而第二行是最后一行了,不会去处理其他行,所以总体上我们根本没消元,

那么自然,这组数据最终会输出一组解,但是这是不对的。

因此我们才需要一个类似 res 指针的变量,

我们可以对比一下常规高斯消元的代码:

        int r=i;
        for(int j=i+1;j<=n;++j)
            if(fabs(a[r][i])<fabs(a[j][i]))r=j;
        if(fabs(a[r][i])<eps)continue;
        if(i!=r)swap(a[i],a[r]);
        db temp=a[i][i];
        for(int j=i;j<=n+1;++j)
            a[i][j]/=temp;
        for(int j=i+1;j<=n;++j)
        {
            temp=a[j][i];
            for(int k=i;k<=n+1;++k)
                a[j][k]-=a[i][k]*temp;
        }

可以看到,在消元时,res 与循环变量 i 的地位是等同的,

但是 i不断自增的,所以如果我们要单纯使用 i 作为下标,这就相当于我们默认了我们的每一轮的消元都是成功的

然而事实并非如此,像上面的那组数据的第一轮消元实际上就是不成功的。

因此我们需要一个根据我们实际消元情况进行来变化的指针,

如我们所见,在代码中,如果本轮触发了 if(fabs(temp)<eps)continue; 这就说明本轮消元不成功,res 不改变,仍指向这原来其所指向的那一行。

然而如果我们的消元正常进行,在每轮的末尾有 res++;res 指向原先指向的那行的下一行。

如果用上面那组数据来具体说明,那么我们第一行时没有正常消元,在到第二轮时,我们就不应该用第二行进行消元,而还是用第一行进行消元。

这是引入了 res 指针之后消元的结果:

0.00 1.00 2.00
0.00 0.00 -1.00

这样就成功进行了整体的消元,然后我们也能正确地判断答案了。