高斯消元法

高斯消元转自:《算法设计与实现》主编 陈宇 吴昊

例题1题解转自:https://www.cnblogs.com/baiyi-destroyer/p/9473064.html 

例题2题解转自: https://blog.csdn.net/lianai911/article/details/48464007?biz_id=102&utm_term=Flip%20Game%20%E9%AB%98%E6%96%AF%E6%B6%88%E5%85%83%E6%B3%95&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduweb~default-0-48464007&spm=1018.2118.3001. 4187  

例题3题解转自:https://blog.csdn.net/bless924295/article/details/53575836 

例题4题解转自: https://www.cnblogs.com/ACRykl/p/8711227.html

 

高斯消元法

一 理论基础 

① 高斯消元:解 一个线性方程组,对方程组的增广矩阵施行初等行变换,变换后的矩阵所对应的方程组与原方程组同解。 

② 需要用到的理论知识有:增广矩阵,初等行变换,行阶梯形矩阵,矩阵的秩(这些先弄懂再说代码吧)

 

二,代码实现

1,一些名词的解释:

主元: 每个非零行第一个非零元素
不确定的变元:未知数(想要求解,但不知道能否解出来)
确定的变元:可求解的未知数
自由变元:无法求解的未知数 

 

2, 根据题意列出方程组,然后根据方程组构造增广矩阵

		for (int i = 0; i < ROW; i++)          // 增广矩阵
			for (int j = 0; j < COL + 1; j++)  // 要加上常数项向量
				scanf("%d", &a[i][j]);

 

3,求出每一列的主元,再通过初等行变换将主元以下的元素清零

①,循环:设变量 row,col  指向主元的行与列,将 col 做循环变量,循环系数矩阵的每一列;row 指向当前循环列的主元的行数,用来试探主元的位置,一直循环到主元的位置要越出系数矩阵的界。

②,确定主元:找到当前循环列的最大绝对值,将该元素作为主元,并将该元素所在行移至主元所在行。

③,清零:利用主元所在行,通过初等行变换将主元以下的元素清零。

 

4,根据矩阵的秩,判断解的情况

①,row 的含义

因为下标从 0 开始,所以化行阶梯型矩阵后,row  

代表主元的个数,

代表确定变元的个数,

代表系数矩阵的秩 (注意是循环结束条件,这个是系数矩阵的秩,而不是增广矩阵)

代表阶梯线下方的第一行。(阶梯线之下,系数矩阵全为 0)只看系数矩阵的话:以第 row 行 为分界线,< row 的 全是非零行,>= row 的全是零行(即该行全是零)

②,根据 row ,判断解的情况

无解:当方程中出现(0, 0, …, 0, a)的形式,且a != 0时,说明是无解的;row == COL  :代表方程组的每一个未知数 刚好可解,即该方程组唯一可解;row < COL:代表方程组的未知数 有些是解不出来的,即该方程组有无穷解(注:row 不可能大于 COL)

③,当方程组为唯一解时,可以通过回带,逐一求出解集

④,当方程组为无穷解时,如何判断哪些变元是自由还是确定的呢?

通过 数组 bool free_x[N];  判断是否是不确定的变元。free_x[1] == 1 表示 第i个未知数为自由变元,free_x[i] == 0 表示第i个未知数可解首先,自由变元有 COL - row 个。然后我们先把所有的变元视为不确定变元,即 free_x[i] = 1。通过回带,在每一行里尝试求解不确定变元。再判断每一行中不确定变元的个数,如果大于1个,则该行无法求解。如果只有1个变元,那么该变元即可求出,可解的不确定变元 即为确定变元,所以修改为 free_x[i] = 0,最后每一行都循环结束的话,剩下的不确定变元自然都是 自由变元。

 

5,模板

整数线性方程组的模板

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<math.h>
#define N 105
/*
主元: 每个非零行第一个非零元素
不确定的变元:未知数(想要求解,但不知道能否解出来)
确定的变元:可求解的未知数
自由变元:无法求解的未知数   */
int a[N][N];    // 增广矩阵
int x[N];        // 解集
bool free_x[N]; // 判断是否是不确定的变元,free_x[1] == 1 表示 第i个未知数为自由变元,free_x[i] == 0 表示第i个未知数可解
int ROW, COL;   // 系数矩阵的 行数,列数
inline int gcd(int a, int b)
{
    return b ? gcd(b, a%b) : a;
}
inline int lcm(int a, int b)
{
    return a*b / gcd(a, b);
}
// 高斯消元法解方程组(Gauss-Jordan elimination).
// 求解整数线性方程组的模板,浮点数把那个 return -2 去掉,改下数据类型就可
// (-2表示有浮点数解,-1表示无解,0表示唯一解,大于0表示无穷解,并返回自由变元的个数)
int Gauss(void)
{
    int row = 0, col = 0;
    /* 
    循环目的:求行阶梯形矩阵
    col:循环变量,循环系数矩阵的每一列
    row 指向当前循环列的主元的行数,它是用来试探主元的位置
    循环结束条件是:主元的位置要越系数矩阵的界了

    总结起来就是:通过循环每一列,将主元以下的元素清零     */
    for (; row < ROW&&col < COL; row++, col++)
    {
        // 找到当前循环列的最大绝对值,将该元素所在行移至主元所在行    
        int k = row; 
        for (int i = row + 1; i < ROW; i++)   // 阶梯线上的元素不算
            if (abs(a[i][col]) > abs(a[k][col]))
                k = i;
        if (k != row)   // 交换最大值所在的行到主元行
            for (int i = col; i < COL + 1; i++)
            {
                int t = a[row][i];
                a[row][i] = a[k][i];
                a[k][i] = t;
            }
        /*
        该列(阶梯下方的元素,包括阶梯线上的元素)的最大值为 0,说明阶梯线下方 全为 0
        说明该列无主元,则该列不用处理,进入下一列。k-- 是要消去 for循环的++
        */
        if (a[row][col] == 0)
        {
            row--;
            continue;
        }
        // 将当前循环列中,主元以下的元素清零
        for (int i = row + 1; i < ROW; i++)
        {
            if (a[i][col] != 0)
            {
                int LCM = lcm(abs(a[i][col]), abs(a[row][col]));
                int ta = LCM / abs(a[i][col]);         // 被清零行需要乘的倍数
                int tb = LCM / abs(a[row][col]);     // 主元所在的行需要乘的倍数
                if (a[i][col] * a[row][col] > 0)     // 同号的话,要弄成异号
                    tb = -tb;
                for (int j = col; j < COL + 1; j++)
                    a[i][j] = a[i][j] * ta + a[row][j] * tb;
            }
        }
    }
    /*
    因为下标从 0 开始,所以化行阶梯型矩阵后,
    row 代表主元的个数,代表确定变元的个数,同时也代表系数矩阵的秩 (注意是循环结束条件,这个是系数矩阵的秩,而不是增广矩阵)
    同时,row 也指向阶梯线下方的第一行。(阶梯线之下,系数矩阵全为 0)
    只看系数矩阵的话:以第 row 行 为分界线,< row 的 全是非零行,>= row 的全是零行(即该行全是零)
    */

    /* ① 无解:当方程中出现(0, 0, …, 0, a)的形式,且a != 0时,说明是无解的。
    系数矩阵出现零行的话,只可能从第 row 行开始,
    所以循环零行,再判断一下该行对应的常数项向量的元素是否为 0,即可 */
    for (int i = row; i < ROW; i++) 
    {
        if (a[i][col] != 0)     // 判断 当前行对应的常数项向量是否为 0
            return -1;
    }

    /*
    如何判断哪些变元是自由变元呢?
    首先,自由变元有 COL - row 个。
    我们先把所有的变元视为不确定变元,即 free_x[i] = 1,通过回带,在每一行里尝试求解不确定变元。
    首先判断该行中不确定变元的个数,如果大于1个,则该行无法求解。
    如果只有1个变元,那么该变元即可求出,可解的不确定变元 即为确定变元,所以修改为 free_x[i] = 0,
    最后每一行都循环结束的话,剩下的不确定变元自然都是 自由变元  
    */
    /*
    ② 无穷解:矩阵的秩 < 未知数的个数
    row 代表矩阵的秩:COL 代表未知数的个数
    */
    if (row < COL)
    {
        for (int i = row - 1; i >= 0; i--)  // 从下往上循环 行
        {
            int free_x_num = 0;        // 用于 记录该行中的不确定的变元的个数
            int free_x_c = 0;        // 记录 不确定变元的列数
            for (int j = 0; j < COL; j++) // 循环 该行中系数矩阵的元素
            {
                if (a[i][j] && free_x[j])
                    free_x_num++, free_x_c = j;
            }
            if (free_x_num > 1)  // 该行中的不确定的变元的个数如果超过1个,则无法求解,进入到下一次循环
                continue;
            // 回带
            int t = a[i][COL];  // 该行末位的常数项向量中的元素
            for (int j = 0; j < COL; j++)
            {
                if (a[i][j] && j != free_x_c)  // 减去确定变元
                    t -= a[i][j] * x[j];
            }
            x[free_x_c] = t / a[i][free_x_c]; // 解出这一行中唯一的一个 不确定变元
            free_x[free_x_c] = 0;  // 不确定变元 被解开 变成确定变元
        }
        return COL - row;
    }
    /*
    ③ 唯一解:矩阵的秩 == 未知数的个数 
    row 代表矩阵的秩:COL 代表未知数的个数
    此时行阶梯阵形成了严格的上三角。(注意这个三角形不一定是沿着对角线的,但它一定是等腰直角三角形,因为系数矩阵不一定是正方形)
    */
    for (int i = row - 1; i >= 0; i--)
    {
        int t = a[i][COL];
        for (int j = i + 1; j < COL; j++)
        {
            if (a[i][j])    // 减去已知变元
                t -= -a[i][j] * x[j];
        }
        if (t%a[i][i])         // 说明有浮点数解
            return  -2;
        x[i] = t / a[i][i];
    }
    return 0;
}
int main(void)
{
    while (scanf("%d%d", &ROW, &COL) != EOF)
    {
        memset(a, 0, sizeof(a));
        memset(x, 0, sizeof(x));
        memset(free_x, 1, sizeof(free_x)); // 初始化为 不确定的变元.
        for (int i = 0; i < ROW; i++)          // 增广矩阵
            for (int j = 0; j < COL + 1; j++)  // 要加上常数项向量
                scanf("%d", &a[i][j]);
        int free_x_num = Gauss();

        if (free_x_num == -1)
            printf("无解!\n");
        else if (free_x_num == -2)
            printf("有浮点数解,无整数解!\n");
        else if (free_x_num > 0)
        {
            printf("无穷多解!自由变元个数为%d\n", free_x_num);
            for (int i = 0; i < COL; i++)
            {
                if (free_x[i])
                    printf("x%d 是不确定的\n", i + 1);
                else
                    printf("x%d:%d\n", i + 1, x[i]);
            }
        }
        else
        {
            printf("有唯一解:\n");
            for (int i = 0; i < COL; i++)
                printf("x%d:%d\n", i + 1, x[i]);
        }
    }
    system("pause");
    return 0;
}
/*
无解
3 3
0 0 0 1
0 1 0 1
0 0 1 1
无穷解
3 3
1 0 0 1
0 1 0 1
0 0 0 0
有浮点数解
3 3
2 0 0 1
0 1 0 1
0 0 1 1
唯一解
3 3
1 0 0 1
0 1 0 1
0 0 1 1
*/
View Code

 

 

 

例题1:

EXTENDED LIGHTS OUT(POJ 1222)

题解:
因为按钮的顺序可以随便按,所以任何一个按钮都最多需要按下1次。因为按下第二次刚好抵消第一次,等于没有按。 

以3x3为例:我们记 L 为待求解的原始布局矩阵,A(i , j) 表示按下第 i 行第 j 列的按钮时的作用范围矩阵,x(i , j) 表示:想要使得 L 回到全灭状态,第 i 行第 j 列的按钮是否需要按下。0表示不按,1表示按下。

 L=  0 1 0 

  1 1 0 
  0 1 1 

A(1 , 1)=     A(2 , 2)= 
1 1 0       0 1 0 
1 0 0       1 1 1 
0 0 0       0 1 0 

由于灯只有两种状态,所以按下一个按钮,就可以转化成 原始矩阵异或上作用矩阵,或者转化成 原始矩阵加上作用矩阵模2。那么,这个游戏就转化为如下方程的求解: 

( L + x(1 , 1)*A(1 , 1) + x(1 , 2)*A(1 , 2) + x(1 , 3)*A(1 , 3) + x(2 , 1)*A(2 , 1) + ... + x(3 , 3)*A(3 , 3) ) %2 = 0

或者是: L ^ x(1 , 1)*A(1 , 1) ^ x(1 , 2)*A(1 , 2) ^ x(1 , 3)*A(1 , 3)  ^ x(2 , 1)*A(2 , 1) ^ ... ^ x(3 , 3)*A(3 , 3)  = 0

其中 x( i, j ) 是未知数。方程右边的 0 表示零矩阵,表示全灭的状态。直观的理解就是:原来的 L 状态,经过了若干个 A( i, j ) 的变换,最终变成 0,即全灭状态。 


两个矩阵异或等于 0  ,说明两个矩阵相等,所以上式可写为:
x(1 , 1)*A(1 , 1) ^ x(1 , 2)*A(1 , 2) ^ x(1 , 3)*A(1 , 3) ^ x(2 , 1)*A(2 , 1) ^ ... ^ x(3 , 3)*A(3 , 3) = L ,(懒的加括号了,异或是最晚算的)

两个矩阵相等,充要条件是矩阵中每个元素都相等,所以只要列出等式,将左右两边的矩阵上的每一个元素对应相等,就可以将上述矩阵转化成了一个9元1次方程组。最后解的:

x(1 , 1) x(1 , 2) x(1 , 3)     1 1 1 
x(2 , 1) x(2 , 2) x(2 , 3)  =      0 0 0 
x(3 , 1) x(3 , 2) x(3 , 3)     0 0 1 

也就是说,按下第一行的3个按钮,和右下角的按钮,就全灭了。

 

回到 题目的 5*6 矩阵,为了 表示方便,我们用 Xi 表示第 i 个按钮是否要按,Li 表示 L 矩阵的第 i 个元素,A(i , j)x: 第 i 行 第 j 列的按钮的作用矩阵 的 第 x 个元素。于是有:(注意 + 和 mod2 的组合  等效于 ^ )

X1*A(1,1)1 + X2*A(1,2)1 + X3*A(1,3)1 +…………+ X30*A(5,6)1 = L1;    mod 2

X1*A(1,1)2 + X2*A(1,2)2 + X3*A(1,3)2 +…………+ X30*A(5,6)2 =  L2;    mod 2

X1*A(1,1)3 + X2*A(1,2)3 + X3*A(1,3)3 +…………+ X30*A(5,6)3 = L3:    mod 2

.......

X1*A(1,1)30 + X2*A(1,2)30 + X3*A(1,3)30 +…………X30*A(5,6)30 = L30:    mod 2

容易发现,第一行的 A(1,1)1, A(1,2)1,A(1,3)……A(5,6)1,合起来就是 A(1,1) 的全部元素。这是因为 能够影响到当前开关的,当前开关也能影响到它。这里 "影响到当前开关” 指的是:在所有作用矩阵中,在当前开关位置为 1 的作用矩阵对应的开关,“当前开关也能影响到它” 指的是:当前开关对应的作用矩阵能作用于 影响到它的开关。

所以,又有:

X1*A(1,1)1 ^ X2*A(1,1)2 ^ X3*A(1,1)3  ^………^ X30*A(1,1)30 = L1;    

X1*A(1,2)1 ^ X2*A(1,2)2 ^ X3*A(1,2)3 ^………^ X30*A(1,2)30 =  L2;    

X1*A(1,3)1 ^ X2*A(1,3)2 ^ X3*A(1,3)3 ^………^ X30*A(1,3)30 = L3:    

........

X1*A(5,6)1 ^ X2*A(5,6)2 ^ X3*A(5,6)3 ^………^ X30*A(5,6)30 = L30:    

这就是我们要的增广矩阵。

 

Plus: 另外一种比较容易记住的看待这个问题的看法:

对于每一个开关,有可能影响到它的因素是:每一个开关是否按下。所以 影响到一个开关的变量有30个,即有:

start ^ ( X1*a1 ^ X2*a2 ^ X3*a3 ^…^ X30*a30 ) =  end ;

其中,start  表示 当前开关的初始状态,end  表示当前开关的结束状态,Xi 表示:第 i 个开关是否按下,ai 表示 第 i 个开关按下后是否能影响 当前开关。能够影响到当前开关的,当前开关也能影响到它(上面有解释)。所以 ai,其实就是当前开关的作用矩阵。

所以上式可进一步化为:

 X1*a1 + X2*a2 + X3*a3 +…………X30*a30 =  end ^ start

所以:30 个开关的系数矩阵是 30*30 的矩阵,第 i 行,第 j 列的元素表示:第 j 个开关是否能够影响到 第 i 个开关,1 表示 可,0 表示 非;每一行的常数项向量为 第 i 个开关的初始状态。

 

代码:

因为系数矩阵是固定的,所以可以求出秩为30,所以该异或方程组一定有唯一解。

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<math.h>
#define N 35
int a[N][N];  // 增广矩阵
int x[N];   // 未知数矩阵
void show(void)
{
    puts("");
    for (int i = 0; i < 30; i++)
    {
        for (int j = 0; j < 30 + 1; j++)
        {
            printf("%d ", a[i][j]);
        }puts("");
    }
}
// 知道刚好唯一解才能这样写
void Gauss(void)  
{
    for (int i = 0; i < 30; i++)  // 循环系数矩阵的 列
    {
        if (a[i][i] == 0) // 如果该列的主元为 0 
        {
            for (int j = i + 1; j < 30; j++) // 循环主元下面的行,找到随便一个不为零的行,与现在主元位置换行
            {
                if (a[j][i] != 0) 
                {
                    for (int k = i; k < 31; k++)
                    {
                        int t = a[j][k];
                        a[j][k] = a[i][k];
                        a[i][k] = t;
                    }
                    break;
                }
            }
        }
        // 化阶梯
        for (int j = 0; j < 30; j++) // 循环当前列的每一个元素
        {
            if (i != j&&a[j][i]) // 如果当前列,除了主元位置,还有的位置 不为 0
            {
                for (int k = i; k < 31; k++) // 循环列,初等行变换
                {
                    // 三者等价
                    //a[j][k] = a[i][k] ^ a[j][k];
                    a[j][k] = (abs(a[j][k] + a[i][k])) % 2;
                    /*
                    if(a[i][k] == 1)
                        a[j][k] 变号
                    else if(a[i][k] == 0)
                        a[j][k] 不变
                    */
                }
            }
        }
    }
    show();
    for (int i = 0; i < 30; i++)
        x[i] = a[i][30];
    /*
    化成行阶梯形矩阵后,每一行只有一个非零元素,a[i][i],且 a[i][i] == 1,
    所以有:1 ^ x[i] = a[i][30],
    所以, x[i] = a[i][30]
    */
}
int main(void)
{
    int t; scanf("%d", &t);
    for (int ii = 0; ii < t; ii++)
    {
        // 常数项向量
        for (int i = 0; i < 30; i++)
            scanf("%d", &a[i][30]);

        // 系数矩阵
        for (int i = 0; i < 30; i++)
        {
            // 系数矩阵的第i行,是代表灯光的初始状态矩阵L的第i个元素:L[i/6][i%6] 的作用矩阵 A(i/6, i%6)
            int x0 = i / 6, y0 = i % 6;
            for (int j = 0; j < 30; j++)
            {
                // 作用是相互的,算 当前开关的作用矩阵,相当于算其他能 影响当前开关的开关
                int x = j / 6, y = j % 6;
                if (abs(x - x0) + abs(y - y0) <= 1)  // 在作用范围之内的话
                    a[i][j] = 1;
                else
                    a[i][j] = 0;
            }
        }
        //    show();
        Gauss();
        printf("PUZZLE #%d\n", ii + 1);
        for (int i = 0; i < 30; i++)
            if ((i + 1) % 6 == 0)
                printf("%d\n", x[i]);
            else
                printf("%d ", x[i]);
    }
    system("pause");
    return 0;
}
View Code

 

 

例题2:

Flip Game(poj 1753)

题解:

1,解的情况

因为系数矩阵是固定的,随所以可求出秩为14,所以可以判断出方程组的解有两种情况: 无穷解和无解。当为无穷解时,自由变元有4个。

(  注意: 0*x1 ^ 0*x2 ^  …… ^ 0*xn = 1 是无解的 )

2,枚举自由变元

该题的思路与上面那题类似,不过多了一步,要枚举自由变元,判断自由变元赋值为多少翻转最少。

怎么枚举呢?

一个自由变元有 0,1 两种情况,该题有 4个自由变元,所以有 16 种情况。我们用 i 从0 循环到  15 , 每一个 i 的二进制对应 对应 1  种情况。如:i = 1 , num = 4,则 i 的二进制为 0001,  则对应的四个自由变元的值 可分别设为 0,0,0,1,此时,方程唯一可解,然后记录此时需要翻动的次数,比较得到最小值即为答案。

3,数组 f[] 

int f[N]:用来记录自由变元的行数。f[i]:第 i 个自由变元的列数

记录的两种方法:
① 行阶梯矩阵的同一列主元位置及其下方元素全为 0

		if (a[row][col] == 0)
		{
			row--;
			f[free_x_num++] = col;
			continue;
		}

② 无穷解,无法求解的不确定变元

    int cnt = 0;
            for (int i = 0; i < COL; i++)
                if (free_x[i])
                    f[cnt++] = i;

代码:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<math.h>
#define inf 0x3f3f3f3f
#define N 35
char str[N][N];
int a[N][N];
int free_x[N];
int x[N];
int f[N]; // 记录自由变元的列数,f[i] 表示第 i 个自由变元的列数
int ROW, COL;
void show()
{
    puts("");
    for (int i = 0; i < ROW; i++)
    {
        for (int j = 0; j < COL + 1; j++)
            printf("%d ", a[i][j]);
        puts("");
    }
}
void init(char key) // 初始化增广矩阵
{
    memset(a, 0, sizeof(a));
    // 初始化系数矩阵
    for (int i = 0; i < ROW; i++) // 循环每个开关
    {
        int x0 = i / 4, y0 = i % 4;
        for (int j = 0; j < COL; j++) // 循环当前开关的作用矩阵
        {
            int x = j / 4, y = j % 4;
            if (abs(x0 - x) + abs(y0 - y) <= 1)
                a[i][j] = 1;
        }
    }
    for (int i = 0; i < ROW; i++) // 初始化 常数项向量
    {
        if (str[i / 4][i % 4] == key)
            a[i][COL] = 1;
    }
}
// 只有 无解 和 无穷解 两种情况
int Gauss(void)
{
    memset(free_x, 1, sizeof(free_x));
    memset(x, 0, sizeof(x));
    int free_x_num = 0; // 自由变元的个数
    int col = 0, row = 0;
    for (; row < ROW&&col < COL; col++, row++)
    {
        int mr = row;
        for (int i = row + 1; i < ROW; i++)
            if (abs(a[i][col]) > abs(a[mr][col]))
                mr = i;
        if (mr != row)
            for (int i = 0; i < COL + 1; i++)
            {
                int t = a[row][i];
                a[row][i] = a[mr][i];
                a[mr][i] = t;
            }
        if (a[row][col] == 0)
        {
            row--;
            f[free_x_num++] = col;
            continue;
        }
        for (int i = 0; i < ROW; i++) //  将该行的其它元素清零                            
        {
            if (a[i][col] != 0 && i != row)
                for (int j = col; j < COL + 1; j++)
                    a[i][j] = a[i][j] ^ a[row][j];
            //  a[i][j] = (a[i][j] - a[k][j] + 2) % 2;
        }
    }
    // printf("%d\n", free_x_num);
    //show();
    for (int i = row; i < ROW; i++)
        if (a[i][COL] != 0)
            return -1;
    return COL - row; // 无穷解
}
int fun()
{
    int num = Gauss();
    if (num == -1) // 无解
        return inf;

    /* 无穷解
    枚举自由变元的可能取值,进而求解方程组
    一个自由变元有 0,1 两种情况,所以 num 个自由变元有 2^num 种情况,
    每一个 i 的二进制对应 对应 1  种情况
    如:i = 1 , num = 4,则 i 的二进制为 0001,
    则对应的四个自由变元的值 可分别设为 0,0,0,1
    */
    int ans = inf; // 在所有情况下,翻转次数最少的
    for (int i = 0; i < (1 << num); i++) // 枚举自由变元的所有可能情况
    {
        int cnt = 0; // ① 记录自由变元种中,被赋值为 1 的个数,② 记录解集中为 1 的个数。 两个加起来就是翻转次数了
        for (int j = 0; j < num; j++) // 枚举当前情况下,哪几个自由变元被赋值为 1
        {
            if (i&(1 << j))  // 如果 i 的第 j 位为 1
            {
                x[f[j]] = 1;
                cnt++;
            }
            else
                x[f[j]] = 0;
        }
        // 给自由变元赋值后,方程组即唯一可解
        for (int j = COL - num - 1; j >= 0; j--) // 枚举阶梯线上的每一行
        {
            // 将该行的系数矩阵的元素乘以他们的解,并将积全都异或起来,就等于该行的常数项,即方程的解
            // 反之,将该行的常数项,异或上该行除了主元的元素,就是主元所在列的解
            x[j] = a[j][COL]; // 该行的常数项
            for (int k = j + 1; k < COL; k++)
            {
                if (a[j][k])  //  如果 该元素非零的话
                    x[j] = x[j] ^ x[k];
            }
            cnt += x[j]; // 解为1,就加1;解为0,就加0
        }
        ans = ans > cnt ? cnt : ans;
    }
    return ans;
}
int main(void)
{
    ROW = COL = 16;
    for (int i = 0; i < 4; i++)
        scanf("%s", str[i]);
    init('b');
    int a1 = fun();
    init('w');
    int a2 = fun();

    if (a1 == inf && a2 == inf)
        printf("Impossible\n");
    else
        printf("%d\n", a1 > a2 ? a2 : a1);

    system("pause");
    return 0;
}
View Code

 

 

例题3:

SETI (POJ 2065)

题解:

思路:若字符串长度为 n,那么这是一个含有n个方程n个未知数的线性方程组,所以只需把相应的系数转为成矩阵解方程组。高斯消元+同余方程+乘法逆元

题目大意:

输入一个素数p和一个字符串s(只包含小写字母和‘*’),字符串中每个字符对应一个数字,'*'对应0,‘a’对应1,‘b’对应2....  例如str[] = "abc", 那么说明 n=3, 字符串所对应的数列为1, 2, 3。

题目中定义了一个函数:

a0*1^0 + a1*1^1+a2*1^2+........+an-1*1^(n-1) = f(1)(mod p), f(1) = str[0] = a = 1;
a0*2^0 + a1*2^1+a2*2^2+........+an-1*2^(n-1) = f(2)(mod p), f(2) = str[1] = b = 2;
..........
a0*n^0 + a1*n^1+a2*n^2+........+an-1*n^(n-1) = f(n)(mod p),f(n) = str[n-1] = ````

求出 a0,a1,a2....an-1.

 

在求解解集时会出现:x[ i ] *  a[ i ][ i ] ≡ a[ i ][ COL ] (mod p),想要求 x[ i ] 就需要用到乘法逆元了。

另外该系数矩阵,可由范德蒙德行列式证明行列式大于 0,所以该 方程组必有唯一解。

代码:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<math.h>
#include<string.h>
#define N 100
int p, COL, ROW;
char s[N];
int a[N][N], x[N];
void show(void)
{
    puts("");
    for (int i = 0; i < ROW; i++)
    {
        for (int j = 0; j < COL + 1; j++)
        {
            printf("%d ", a[i][j]);
        }puts("");
    }
}
inline int gcd(int  a, int b)
{
    return b ? gcd(b, a%b) : a;
}
inline int lcm(int a, int b)
{
    return a*b / gcd(a, b);
}
int qmul(int a, int b)
{
    int res = 1;
    while (b)
    {
        if (b & 1)
            res = res*a%p;
        b >>= 1;
        a = (a%p)*(a%p) % p;
    }
    return res;
}
// 只有 唯一解
int  Gauss()
{
    int row, col;
    for (row = 0, col = 0; row < ROW&&col < COL; col++, row++)
    {
        int mr = row;
        for (int i = row + 1; i < ROW; i++)
            if (abs(a[i][col]) > abs(a[mr][col]))
                mr = i;
        if (mr != row)
            for (int i = col; i < COL + 1; i++)
            {
                int t = a[mr][i];
                a[mr][i] = a[row][i];
                a[row][i] = t;
            }
        for (int i = 0; i < ROW; i++)
            if (a[i][col] && i != row)
            {
                int LCM = lcm(abs(a[i][col]), abs(a[row][col]));
                int ta = LCM / a[i][col];
                int tb = LCM / a[row][col];
                if (a[i][col] * a[row][col] > 0)
                    tb = -tb;
                for (int j = 0; j < COL + 1; j++)
                    a[i][j] = ((a[i][j] * ta + a[row][j] * tb) % p + p) % p; // 加 p 是为了不变成负数
            }
    }
    // show();
    for (int i = row - 1; i >= 0; i--)
    {
        int t = a[i][COL];
        x[i] = (t*qmul(a[i][i], p - 2)) % p;
    }
    return 0;
}
int main(void)
{
    int t; scanf("%d", &t);
    while (t--)
    {
        scanf("%d %s", &p, s);
        COL = ROW = strlen(s);
        for (int i = 0; i < ROW; i++)
        {
            if (s[i] == '*')
                a[i][COL] = 0;
            else
                a[i][COL] = s[i] - 'a' + 1;
            for (int j = 0; j < COL; j++)
                a[i][j] = qmul(i + 1, j);
        }
        Gauss();
        for (int i = 0; i < COL; i++)
            printf("%d ", x[i]);
        puts("");
    }
    system("pause");
    return 0;
}
View Code

 

 

 例题4:

Electric resistance(hdu 3976)

总体思路:利用 节点电压法 列线性方程,然后高斯消元法求解。

1,先普及一下知识:

   节点:电路中三条或三条以上支路的交点。

   参考点:任选的一个点,把该点作为电路的的电位参考点,通常选择为节点,因为会多出一个没用的节点。

   节点电压:节点与参考点之间的电压

   独立源:包含电压源和电流源。

   电流源:电流不受外电路的影响而独立存在的电源。

   电导:电阻的倒数。

   自导:该节点所连的支路电导之和,如 G(11) 就是 1 号节点所连的各支路电导之和。

   互导:相邻节点间公共电导之负值,如 G(1,2) 就是1 号节点和 2 号节点之间的电导之和的负值。

 一般的,对于有 n 个节点的电路应用 KCL 列方程式时,只能写出 n-1 个独立方程,且为任意 n-1 个,这 n-1 个称为 独立节点,节点分析法可以从 KCL 推导证明(这里就不讲了)

所以我们用 节点分析法时,只需要用到 n-1 个节点

 

2,公式:

 (注意这里的 n 为 结点数 - 1  )

对于第一个矩阵,主对角线上的元素为:自导;其余元素为:互导

对于第二个矩阵,其值为对应节点的节点电压。如  U1:1 号节点的节点电压

对于第三个矩阵,Ii 代表: 连接到第 i 个节点的各支路中独立源所引起的电流代数和

        对于电流源产生的电流,当流入节点时为正,反之为负;

        对于电压源产生的电流,当电压源正极靠近该节点时为正,反之为负。

 

3,具体思路

节点电压法就是通过电流和电阻求解电压,而这道题只给我们电阻,那么就需要我们自己外加电压源。为了计算方便,我们就在 节点1 和 节点 n 之间外加电流源,为什么是电流源呢?当然是为了容易求电流了。而且既然追求简单,我们自然要贯彻到底,就可以设电流源流出的电流为  1 。又因为只需要用到 n-1 个节点,所以可设 节点1 电压为 0。

展开得到(结点数-1)个方程 :

于是有增广矩阵:

求解出 n 节点电压后,有:

    (节点 n 的电压 - 0 (节点 1的电压)) / 1(节点1和节点n 之间的 总电流)== 等效电阻

化简一下就是:节点 n 的电压 == 等效电阻

注意点①:节点1 是不用求的,所以我们只用到 节点2 到 节点n。 而我的代码里的增广矩阵是从下标  0 开始的,所以各位要看清楚,a[0][0] 对应的是 G21,即 节点1 和 节点 2 之间电阻的倒数。

注意点②:各节点间只有  节点1 和 节点n 之间有独立电流源引起的电流,所以 U1 和 Un 为 1(当然,U1 没用到),其他皆为 0 

 

4,该方程组唯一可解

① 目前不会证明。那你问我为什么知道唯一可解?我只能说因为代码用唯一可解的办法去写,去提交,过了,所以可以判断唯一可解。

② 值得一提的是,我在尝试证明并构造数据的过程中,发现

double b = 0;
printf("%.2lf\n", 1.0 / b);

 这个的输出是:inf

③ 目前系数矩阵有三个特点,我列出来,可惜不会证明,也不知道这条件够不够证明。

1,对称矩阵

2,没有零元素(因为题目说: You may assume that any two nodes are connected!)

3,只有对角线是正数,其余都是负数,且对角线的绝对值是其所在行,也是其所在列的最大值。

 

 5,代码

View Code

 

 

 

============ =========== ========= ======== ======== ====== ====== ==== == =  

宿新市徐公店   宋代: 杨万里

篱落疏疏一径深,树头新绿未成阴。(新绿 一作:花落)
儿童急走追黄蝶,飞入菜花无处寻。

 

posted @ 2020-10-18 11:47  叫我妖道  阅读(615)  评论(0编辑  收藏  举报
~~加载中~~