线性代数在ACM中的应用
周鹏飞
(丽水学院 工学院 计112本,浙江 丽水 323000)
摘要:针对如何利用计算机来解决点灯游戏的问题,点灯游戏在ACM的题型中也是一类,可以采用搜索来解决,但是也可以采用线性代数中的高斯消元法来解决。先求出灯组矩阵的系数矩阵,然后列出增广矩阵。采用异或操作,求出方程的解。
关键字:高斯消元法;国际大学生程序设计竞赛(ACM);点灯游戏;灯光师小明;
0 引言
在南阳理工学院ACM系统上有着这么一道有趣的题目《灯光师小明》
灯光师小明
时间限制:1000 ms |内存限制:65535 KB
难度:1
描述
灯光师小明控制着各种晚会的各种大小灯,每次晚会小明都会对灯进行很多次操作。对每盏灯只能进行两种操作,开和关。现在小明希望自己随时都知道还有多少盏灯亮着。你需要编写一个程序当小明问你时你能快速的说出还有多少盏灯亮着,晚会开始时所有的灯都是灭的。
输入
只有一组数据第一行输入两个正整数N,T(0 输出 每次查询指令输出占一行,输出当前亮着灯的个数 样例输入 10 8 CHANGE 1 QUERY CHANGE 2 QUERY CHANGE 1 CHANGE 9 CHANGE 7 QUERY 样例输出 1 2 3 这一道题目非常的简单,之所以引出这么一道题目,是让大家先对点灯操作有个了解。下面是我对该题目的一个解法: http://acm.nyist.net/JudgeOnline/problem.php?pid=224 #include int main() { intN, T, num; intwork = 0; intlight[100000]; for(int i = 0; i < 100000; i++) light[i]= 0; charorder[10]; scanf("%d%d",&N, &T); while(T--){ scanf("%s",order); if(order[0] == 'C'){ scanf("%d",&num); if(light[num] == 1){ light[num]= 0; work--; } else{ light[num]= 1; work++; } } else{ printf("%d\n",work); } } } 对于真正的点灯游戏问题这个真的是太简单了 下面引出我从百度贴吧里找到的一个题目: http://tieba.baidu.com/f?kz=945725034 题目是这样的,给你一个4*4的灯组,灯组中每盏灯的初始状态不定(即开始时不一定是全关),当改变任意一盏灯时,该灯所在的行上和列上的所有灯状态一起改变,例如 然后再在excel上列出本题的一个基本框图,该矩阵的解即为,即为所需操作: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 1 1 1 1 1 1 0 0 0 1 0 0 0 1 0 0 0 0 2 1 1 1 1 0 1 0 0 0 1 0 0 0 1 0 0 1 3 1 1 1 1 0 0 1 0 0 0 1 0 0 0 1 0 0 4 1 1 1 1 0 0 0 1 0 0 0 1 0 0 0 1 0 5 1 0 0 0 1 1 1 1 1 0 0 0 1 0 0 0 0 6 0 1 0 0 1 1 1 1 0 1 0 0 0 1 0 0 0 7 0 0 1 0 1 1 1 1 0 0 1 0 0 0 1 0 0 8 0 0 0 1 1 1 1 1 0 0 0 1 0 0 0 1 0 9 1 0 0 0 1 0 0 0 1 1 1 1 1 0 0 0 0 10 0 1 0 0 0 1 0 0 1 1 1 1 0 1 0 0 0 11 0 0 1 0 0 0 1 0 1 1 1 1 0 0 1 0 0 12 0 0 0 1 0 0 0 1 1 1 1 1 0 0 0 1 0 13 1 0 0 0 1 0 0 0 1 0 0 0 1 1 1 1 0 14 0 1 0 0 0 1 0 0 0 1 0 0 1 1 1 1 1 15 0 0 1 0 0 0 1 0 0 0 1 0 1 1 1 1 0 16 0 0 0 1 0 0 0 1 0 0 0 1 1 1 1 1 0 下面贴出自己写的一个代码: #include int main() { intinput[10][10], j_get, Matrix[101][100]; printf("请输入灯组:\n"); for(int i = 0; i < 10; i++){ //输入 for(int j = 0; j < 10; j++){ scanf("%d",&input[i][j]); if(getchar() == '\n'){ j_get= j; break; } } if(i == j_get) break; } for(i = 0; i <= j_get; i++){ //异或 for(int j = 0; j <= j_get; j++){ input[i][j]= input[i][j] ^ 1; } } for(i = 0; i < (j_get + 1) * (j_get + 1); i++){ //置零 for(int j = 0; j < (j_get + 1) * (j_get + 1); j++){ Matrix[i][j]= 0; } } for(i = 0; i <= j_get; i++){ //赋1 for(int j = 0; j <= j_get; j++){ /* if(i - 1 >= 0) Matrix[i* (j_get + 1) + j][(i - 1) * (j_get + 1) + j] = 1; if(i + 1 <= j_get) Matrix[i* (j_get + 1) + j][(i + 1) * (j_get + 1) + j] = 1; Matrix[i* (j_get + 1) + j][i * (j_get + 1) + j] = 1; if(j - 1 >= 0) Matrix[i* (j_get + 1) + j][i * (j_get + 1) + j - 1] = 1; if(j + 1 <= j_get) Matrix[i* (j_get + 1) + j][i * (j_get + 1) + j + 1] = 1; */ for(int k = 0; k <= j_get; k++) Matrix[i* (j_get + 1) + j][k * (j_get + 1) + j] = 1; for(k = 0; k <= j_get; k++) Matrix[i* (j_get + 1) + j][i * (j_get + 1) + k] = 1; Matrix[i* (j_get + 1) + j][(j_get + 1) * (j_get + 1)] = input[i][j]; } } printf("灯组异或后:\n"); for(i = 0; i <= j_get; i++){ //输出 for(int j = 0; j <= j_get; j++){ printf("%d", input[i][j]); } printf("\n"); } printf("系数矩阵:\n"); for(i = 0; i < (j_get + 1) * (j_get + 1); i++){ for(int j = 0; j < (j_get + 1) * (j_get + 1); j++){ printf("%d", Matrix[i][j]); } printf("\n"); } printf("增广矩阵:\n"); for(i = 0; i < (j_get + 1) * (j_get + 1); i++){ for(int j = 0; j <= (j_get + 1) * (j_get + 1); j++){ printf("%d", Matrix[i][j]); } printf("\n"); } inta[20]; for(i = 0; i < 20; i++) a[i]= 0; intfind = 0; intj2_get = (j_get + 1) * (j_get + 1); for(int j = 0; j < j2_get; j++){ for(int i = 0; i < j2_get; i++){ if(Matrix[i][j] == 1){ if(a[i] == 1) continue; find= 1; a[i]= 1; break; } } if(find == 0) continue; for(int k = 0; k < j2_get; k++){ if(k == i) continue; if(Matrix[k][j] == 1){ for(int l = 0; l <= j2_get; l++){ Matrix[k][l]= Matrix[k][l] ^ Matrix[i][l]; } } } find= 0; } printf("矩阵:\n"); for(i = 0; i < j2_get; i++){ for(int j = 0; j <= j2_get; j++){ printf("%d", Matrix[i][j]); } printf("\n"); } printf("\n"); intnum = 0; for(i = 0; i < j2_get; i++){ if(Matrix[i][j2_get] == 1){ for(int j = 0; j < j2_get; j++){ if(Matrix[i][j] == 1) printf("(%d,%d) ", j/(j_get + 1) + 1, j % (j_get + 1) +1); } num++; } } printf("\n\n一共操作了%d次\n", num); return0; }
http://bbs.bccn.net/viewthread.php?tid=359142&extra=&page=2 先分析一下游戏规则,看看我们从中能得到些什么信息。 想要详细代码研究的同学可以 mailto:[email protected]
初始灯组状态是这样的:
1000
0111
0111
0111
改变了第一盏灯的状态以后,灯组变为了
0111
1111
1111
1111
(1代表开,0代表关)
现在给你一个灯组,求使这个灯组所有灯全部打开的最少步骤
例如input
1011
1111
1111
1011
OUPUT
6(改变了(1,1),(1,3),(1,4),(4,1),(4,3),(4,4))
N*N灯阵,初始状态任意,每盏灯开关可控制该灯及所有相邻灯,即本灯开关开或合一次使本灯及相邻灯的开关状态取反。
给出算法使所有灯全亮开关次数最少(或不可能)。
由上面的规则可以看出
1、灯阵中每盏灯的状态只有两种(亮或者灭),为便于接下来的讨论我们把灯的状态参数化,规定亮为1,灭为0。
2、一次可以操作一盏灯。
3、对某一盏灯的操作只有一种,即对该灯的状态取反。如果该灯原来为1,那么操作结果使它为0;如果该灯为0,那么操作结果使它为1。
4、由上一信息可以知道:对一盏灯操作奇数次结果均为使灯状态改变,操作偶数次结果均不改变灯的状态。
这些说明,要改变某盏灯的状态最多只需对它操作1次(1次以上的操作是多余的)。
5、设灯阵为LM(LM是“Lights Matrix”的缩写,这只是我个人的喜好,没什么特别的意思,大家可以随意取名:))LM具有N * N个元素或者叫灯,它的某一状态为LMA,另一状态为LMB(LMA不等于LMB,即是说LMA和LMB中至少存在1个对应位置的灯的状态是不同的)。从LMA变换到LMB需要操作K盏灯,设这K盏灯分别为L1、L2、...、LK(Li表示LM中的某一个元素或者说位置,即某个LM(a,b))。
那么,只要操作了这K盏灯就能从LMA变换到LMB,与这K盏灯的操作顺序无关(即假设需要操作L1和L2两盏灯,那么先操作L1再操作L2与先操作L2再操作L1的结果是一样的)。关于这一结论的证明并不难,我就不赘述了。由这一结论也可以知道K盏灯中不会有两盏相同的灯。
为了下面讨论方便,我们设从LMA到 LMB 的操作为T(取Turn的意思)。
上面的描述可以函数化为 T(LMA)= LMB。不同的操作将用T1、T2或TA、TB等表示。两个相同的操作是指它们操作的灯的数量及位置都是相同的。(例如:T1操作LM中L1、L2、L3这三盏灯,如果T2操作的也是这三盏灯那我们说T1=T2,如果T2中操作了其它灯或者只是T1中操作的部分灯,那T1 != T2)在下面的讨论中相同操作名的操作将表示相同的操作。
6、设LM中灯的状态全为0的灯阵状态为LM0,LM中灯的状态全为1的灯阵状态为LMZ。在本文中LM0和LMZ将作为特定灯阵,其它灯阵状态作为变量将用LM1、LM2、LMA、LMB等等泛指。
作这些定义是为了描述下面这样一个问题:
设T(LMA) = LMB,是否存在一个LMC,使得T(LM0) = LMC ?如果存在LMC又是什么样的?
答案是肯定的。如果存在T(LMA) =LMB,那么一定存在LMC,且LMC = LMA ^ LMB(“^”表示异或)
即是说T(LM0) = LMA ^ LMB。
这个证明也不难,留给大家自己思考了。
由此,原题要求由某一状态LMA变换到LMZ就映射成由LM0变换到LMA ^ LMZ。即由T(LMA) = LMZ => T(LM0) = LMA ^ LMZ。
分析到这里也差不多了,该着手由我们分析的结果来解决这个问题了。
首先来将上面提到的那些抽象概念具体化。
LM是一个矩阵,可以用一个二维数组表示。LM(i,j)表示LM中第i行第j列的灯。
T要操作的灯我们用L来表示。L也是一个矩阵。L(i,j)表示LM中第i行第j列的灯的操作。L(i,j)为1表示要操作LM中第i行第j列的灯。L(i,j)为0表示不操作。
设T(LM0) = LMA。那么根据题中点灯规则可以得出
LMA(i,j) = L(i-1,j) ^ L(i,j-1) ^ L(i,j) ^ L(i,j+1) ^ L(i+1,j)
其中的i-1,i+1,j-1,j+1可能超出数组的界限。对于超出界限的L(a,b)我们视做0。
这个运算过程也就是T操作的过程了。我们想要的结果也就是这个T的L了。
这么说还是太抽象。我举个2X2的例子说明一下。
设
LMA = [ a00, a01,
a10, a11]
设一个列向量Y,
Y = [ a00,
a01,
a10,
a11]
能实现将LM0转换成LMA的操作中的L为
L = [ b00, b01,
b10, b11]
设一个列向量X,
X = [ b00,
b01,
b10,
b11]
那么根据操作过程我们得到下面的方程
b00 ^ b01 ^ b10 = a00
b00 ^ b01 ^ b11 = a01
b00 ^ b10 ^ b11 = a10
b01 ^ b10 ^ b11 = a11
其中a00,a01,a10,a11是已知数,b00,b01,b10,b11是代求的未知数。
解这个方程组可以求得
b00 = a00 ^ a01 ^ a10
b01 = a00 ^ a01 ^ a11
b10 = a00 ^ a10 ^ a11
b11 = a01 ^ a10 ^ a11
这是手工解法,现在讨论怎么用计算机求解
由于逻辑运算中的异或在很多方面与代数运算中的加法很相似,所以这里我们可以借鉴代数中线性方程组的解法来解这个逻辑方程组。
我们重新规范一下上面的方程组
a00 = b00 ^ b01 ^ b10,这个方程里只出现了三个未知数,我们将b11也补进去。
a00 = 1*b00 ^ 1*b01 ^ 1*b10 ^ 0*b11。因为0*b11=0,而0异或其它值还是其它值,这样的改写我想大家没异议吧。
同理
a00 = 1*b00 ^ 1*b01 ^ 1*b10 ^ 0*b11
a01 = 1*b00 ^ 1*b01 ^ 0*b10 ^ 1*b11
a10 = 1*b00 ^ 0*b01 ^ 1*b10 ^ 1*b11
a11 = 0*b00 ^ 1*b01 ^ 1*b10 ^ 1*b11
这样我们得到方程的系数矩阵
1 1 1 0
1 1 0 1
1 0 1 1
0 1 1 1
设这个系数矩阵为A
那么上面的方程可以表示成矩阵形式
AX = Y
之后对于矩阵的运算完全可以用于上式的运算,只不过原来的加法现在要换成异或。比如,初等行变换中“某一行所有元素的K倍加到另一行的元素上去”应换成“某一行所有元素异或到另一行的元素上去”。而且异或的运算变量是布尔型的,所以那个K倍也就用不上了(K倍的真还是真,K倍的假还是假)。
用高斯消去法可以很方便的解这个方程组。更高阶的游戏也可以同理解出来。对于N*N的游戏,其中未知量将有N的平方个,游戏要的结果就是解中为1的量对应的灯。操作解中为1的灯就会得到结果。
当系数矩阵A为非奇异矩阵,或者说是满秩矩阵时,方程组有唯一解。
当A为降秩矩阵时,方程组有多组或没有解。
具体情况是,当方程的增广矩阵经过初等行变换后,A中元素全为0的某行对应的Y的元素为0时方程组有多组解,对应的Y的元素不为0时方程组无解。
这与代数线性方程组的情况是一样的。只不过因为代数中变量的定义域是实数范围,一但方程组的解不唯一它将有无穷多组解。但我们这样逻辑线性议程组的定义域只有0和1,解不唯一时其解的总数是有限的。当有n个自由未知量时,方程将有n的2次方组解。