目录
一,问题
二,背景
三,术语定义
标准递推式
递推所得矩阵
递推拓展行
等价矩阵
0等价矩阵
四,问题重述
2,理论分析
一,问题
在点亮所有的灯问题中,有多少全局操作和0矩阵是等价的?
即,有多少个n阶矩阵,使得每个元素的上、下、左、右的位置上的元素(如果存在)加上这个元素本身为偶数?
二,背景
引1:点亮所有的灯
参考:javascript:void(0)
在这一文的第七章中,我提到,通过映射的方法,可以证明,可以把所有的全局操作分成m个等价类,每个等价类的元素数目都是一样的。
所以,m是2^n的约数。
假设m=2^k,0<=k<=n,那么每个等价类都有2^(n-k)个元素。
也就是说只需要求k即可。
引2:奇怪的矩阵(类似点亮所有的灯)
参考:javascript:void(0)
两个问题的思路其实是一样的,都是枚举第一行,根据递推式依次计算每一行即可,最后判断最后一行是否满足。
但是这里的递推式是x[i][j] = (x[i - 2][j] + x[i - 1][j - 1] + x[i - 1][j] + x[i - 1][j + 1]) % 2;
三,术语定义
标准递推式
x[i][j] = (x[i - 2][j] + x[i - 1][j - 1] + x[i - 1][j] + x[i - 1][j + 1]) % 2
递推所得矩阵
对于任意给定的第一行(元素为0,1),通过标准递推式唯一确定的n行n列矩阵。
拓展递推所得矩阵
对于任意给定的第一行(元素为0,1),通过标准递推式唯一确定的n行n+1列矩阵。
递推拓展行
拓展递推所得矩阵的最后一行。
等价矩阵
如果2个递推所得矩阵x,y,满足对于任意i,j,都有x[i][j] + x[i - 2][j] + x[i - 1][j - 1] + x[i - 1][j] + x[i - 1][j + 1] + y[i][j] + y[i - 2][j] + y[i - 1][j - 1] + y[i - 1][j] + y[i - 1][j + 1]为偶数,即x和y对应的全局操作是等价的,那么x和y即为等价矩阵。
0等价矩阵
与0矩阵互为等价矩阵的矩阵
注1:
标准递推式中,数组越界的项直接忽略
注2:
任意第一行、递推所得矩阵、拓展递推所得矩阵都是一一对应的。
所以递推所得矩阵一共有2^n个,引1是基于递推所得矩阵的范围进行讨论的,不是基于整个2^(n*n)的范围讨论的。
注3:
“递推拓展行全为0” 等价于 “递推所得矩阵是0等价矩阵”
四,问题重述
求0等价矩阵的数量,即,2^n个递推所得矩阵可以分为2^k个等价类,每一类中的2^(n-k)个矩阵两两互为等价矩阵,不同类中的矩阵都不是等价矩阵,求k。
根据注3,其实就是要求递推拓展行全为0的条件
五,求解递推拓展行为0的条件
1,编程枚举,找规律
(1)思路:
枚举第一行,判断递推拓展行是否全为0
(2)代码:
#include<iostream>
using namespace std;
int main()
{
int n, ans, x[30][30], num, k, i, j, flag;
cout << "输入维度" << endl;
while (cin >> n)
{
ans = 1;
for (int number = 1; number < (1 << n); number++)
{
num = number, flag = 1;
for (k = 0; k < n; k++)x[1][k + 1] = num % 2, num /= 2;
for (i = 0; i < n + 2; i++)for (j = 0; j < n + 2; j++)
{
if (i == 0 || j == 0 || j == n + 1)x[i][j] = 0;
else if (i > 1)x[i][j] = (x[i - 2][j] + x[i - 1][j - 1] + x[i - 1][j] + x[i - 1][j + 1]) % 2;
}
for (i = 0; i < n; i++)if (x[n + 1][i + 1] != 0)flag = 0;
if (flag)ans++;
}
cout << ans << " ";
}
return 0;
}
(3)输入:
1 2 3 …… 27 28
(4)输出:
(1-10)1 1 1 16 4 1 1 1 256 1
(11-20)64 1 1 16 1 256 4 1 65536 1
(21-28)1 1 16384 16 1 1 1 1
(5)分析规律:
n从1到28,n-k的值依次是
(1-10)0 0 0 4 2 0 0 0 8 0
(11-20)6 0 0 4 0 8 2 0 16 0
(21-28)0 0 14 4 0 0 0 0
可以看到如下几条规律,
规律一:所有的n-k都是偶数
规律二:只有当n=4时k=0,其他情况下,k>0
规律三:n-k大部分是0,少部分是2的幂,极少数不是2的幂
2,理论分析
(1)思路:
列出递推所得矩阵的通式,判断成为0等价矩阵的条件。
用n个变量表示第一行的n个数,根据递推式求出拓展递推所得矩阵的最后一行,判断每个数都为0的条件。
(2)举例:
(2.1)n=3
递推所得矩阵:(省略了所有%2,本文下同)
递推拓展行为b+c, a+b+c, a+b
全为0的条件是a=b=c=0,只有一种情况,所以2^(n-k)=1,n-k=0
(2.2)n=4
递推所得矩阵:
递推拓展行为0,0,0,0,所以n-k=4,即k=0
(2.3)n=5
拓展递推所得矩阵: (省略了所有加号,本文下同)
递推拓展行为bce,abc,abde,cde,acd
全为0的条件即bce=abc=abde=cde=acd=0,化简得a=e=b+c,b=d
所以2^(n-k)=4,n-k=2
(3)统一表示法——系数矩阵
以(2.3)为例,式子bce=abc=abde=cde=acd=0可以表示为方程组AX=0,其中A是方程组的系数矩阵,X是向量(a b c d e)的转置,即竖向量。
满足条件的X向量的个数是2^(n-R(A)),其中R(n)是A矩阵的秩
同理可得出:
对于任意n和任意拓展递推所得矩阵,其递推拓展行中的每个数可以表述为拓展递推所得矩阵的第一行的若干个数的和(最少0个,最多n个)的形式,所以递推拓展行为0等价于对应的方程组AX=0
所以,我们得到,0等价矩阵的个数是2^(n-R(A)),即k=R(A),这个推理过程是不依赖引1中说的基于映射方法证明每个等价类的元素数目相同。
六,系数矩阵的规律
1,枚举系数矩阵,观察规律
当n=2,3,4,5时,系数矩阵分别是
规律四:系数矩阵都是对称矩阵
规律五:系数矩阵都是中心对称矩阵
规律六:系数矩阵都满足递推式x[i][j] = (x[i - 2][j] + x[i - 1][j - 1] + x[i - 1][j + 1]) % 2
要想证明规律五,首先要看系数矩阵的每个数代表什么。
2,系数矩阵的单项定义
对于n阶系数矩阵A,用Aij表示A的第i行第j列,取值范围为0或1
Aij=1表示方程组AX=0的第i个方程中有X的第j个分量,即递推拓展行第i个数的表达式中有第j个变量
Aij=0表示递推拓展行第i个数的表达式中没有第j个变量
3,系数矩阵的按行释义和按列释义
举例来说,
5阶系数矩阵的第1行是0 1 1 0 1,表明递推拓展行的第1个数的表达式中没有ad,有bce,即它的值为bce
5阶系数矩阵的第1列是0 1 1 0 1,表明递推拓展行的第1,4个数的表达式中没有a,第2,3,5个数的表达式中没有a
递推拓展行为bce,abc,abde,cde,acd
4,系数矩阵的按列构造方法
这里首先要引入一个概念:
4.1 单变量矩阵
如果一个拓展递推所得矩阵的第一行只有1个1,其他n-1个都是0,那么这个拓展递推所得矩阵我们称之为单变量矩阵。
如果单变量矩阵的第一行第i个数为1,其他为0,那么我们称之为第i个变量的单变量矩阵。
举例来说,n=5时,第2个变量的单变量矩阵是(空格子表示0,本文下同)
4.2 单变量矩阵与系数矩阵的映射关系
不难发现,第i个变量的单变量矩阵的递推拓展行,转置变成列之后,与系数矩阵的第i列是一样的。
举例来说,上述n=5时,第2个变量的单变量矩阵表明,5阶系数矩阵的第2列是1 1 1 0 0
PS:在证明系数矩阵都是对称矩阵之前,这里还是不要搞错了,不是系数矩阵的第i行,而是系数矩阵的第i列。
4.3 系数矩阵的按列构造方法
枚举n个单变量矩阵,依次得到系数矩阵的每一列,即可得到系数矩阵
5,证明规律五:系数矩阵都是中心对称矩阵
也就是要证明,Aij=An-i n-j 对于任意i,j 成立。
也就是要证明,如果递推拓展行第i个数的表达式中有第j个变量,那么递推拓展行第n-i个数的表达式中有第n-j个变量
也就是要证明,第i个变量的单变量矩阵和第n-i个矩阵的单变量矩阵是互相左右对称的,而这很明显是成立的。
6,用规律六证明规律四:系数矩阵都是对称矩阵
如果要证明系数矩阵都是对称矩阵,也就是要证明,Aij=Aji 对于任意i,j 成立。
也就是要证明,如果递推拓展行第i个数的表达式中有第j个变量,那么递推拓展行第j个数的表达式中有第i个变量。
我尝试了证明,没找到有效方法,很多看似有道理的证明,仔细写出来,发现最后会有一点点对不上,这就很令人苦恼了。
当然,本文也不会基于规律四进行推理。
然而,我在刷OJ的时候碰到了一个题目,在那个题目的附加思考中,我发现了规律六可以推出规律四,这才在时隔多年之后,重新开始研究点亮所有的灯问题。
参见:奇怪的矩阵(类似点亮所有的灯)javascript:void(0)
PS:规律五也是一样,可以用规律六推出来
7,遗留问题和思路梳理
n从1到28,n-k的值依次是
(1-10)0 0 0 4 2 0 0 0 8 0
(11-20)6 0 0 4 0 8 2 0 16 0
(21-28)0 0 14 4 0 0 0 0
截止目前的遗留问题一共有4个:
规律一:所有的n-k都是偶数
规律二:只有当n=4时k=0,其他27种情况下,k>0
规律三:n-k大部分是0,少部分是2的幂,极少数不是2的幂
规律六:系数矩阵都满足递推式x[i][j] = (x[i - 2][j] + x[i - 1][j - 1] + x[i - 1][j + 1]) % 2
规律五已经证明,规律四可以用规律六推出来。
现在,直接分析为什么n-k都是偶数,没什么思路,而且规律很可能是和2的幂有关,所以除了n=2,3,4,5的情况,我决定再看看n=9的情况。
七,9阶系数矩阵分析
9阶系数矩阵的计算量比较大,这里我们利用上述系数矩阵的按列构造方法来进行构造
n=9,第1,2,3,4,5个变量的单变量矩阵依次是
这样就得到了系数矩阵的前5列
因为系数矩阵是中心对称矩阵,所以只需要这5列就够了。
得到的系数矩阵:
所以k=1,n-k=8,对应的方程组是a+c+e+g+i=0
八,编程求系数矩阵
按照上述方法,可以在O(n^3)的时间内求出系数矩阵,时间复杂度大大降低。
代码:
#include<iostream>
using namespace std;
int n,x[303][303];
int main()
{
for (n = 1; n < 300;n++)
{
cout << n << endl;
for (int i = 1; i <= n;i++)
{
for (int k = 1; k <= n; k++)x[1][k] = (k==i);
for (int i = 0; i < n + 2; i++)for (int j = 0; j < n + 2; j++)
{
if (i == 0 || j == 0 || j == n + 1)x[i][j] = 0;
else if (i > 1)x[i][j] = (x[i - 2][j] + x[i - 1][j - 1] + x[i - 1][j] + x[i - 1][j + 1]) % 2;
}
for (int k = 1; k <= n; k++)cout<<x[n + 1][k];
cout << " ";
for (int k = 1; k <= n; k++)cout << (x[n+1][k] ? '1' : ' ');//输出系数矩阵的转置矩阵
cout << endl;
}
}
return 0;
}
说明:
对于每个n,我输出了2个矩阵,左边的是系数矩阵的转置矩阵(因为我没有证明系数矩阵是对称矩阵,所以还是说清楚点比较好),右边的是只把1输出,0用空格代替。
输出:
看左边的可能还有点眼花缭乱,但是右边的矩阵就很明显,非常有规律。
关于这个规律有太多的东西要写,而且图片多篇幅长,所以我另写了一篇博客。
参见:《点亮所有的灯》进阶分析——系数矩阵的规律
规律是有一些模糊的规律的,但是并没有发现统一的标准规律,也没有任何推理产物。
九,高斯消元解方程组
1,方程组AX=0
这里的方程组,其实就是异或方程组,可以用高斯消元快速计算A的秩,即k的值,
高斯消元的时间复杂度为O(n^3),可以迅速求出对于不同的n时,k的值
代码:
#include<iostream>
using namespace std;
int n, x[303][303], y[303][303];//y存系数矩阵
bool flag[303];
int main()
{
for (n = 1; n <= 300; n++)
{
//cout << n << endl;
for (int i = 1; i <= n; i++)
{
for (int k = 1; k <= n; k++)x[1][k] = (k == i);
for (int i = 0; i < n + 2; i++)for (int j = 0; j < n + 2; j++)
{
if (i == 0 || j == 0 || j == n + 1)x[i][j] = 0;
else if (i > 1)x[i][j] = (x[i - 2][j] + x[i - 1][j - 1] + x[i - 1][j] + x[i - 1][j + 1]) % 2;
}
for (int k = 1; k <= n; k++)y[k][i] = x[n + 1][k];
}
int ans = 0;//y矩阵的秩
for (int i = 1; i <= n; i++)flag[i] = true;
for (int i = 1; i <= n; i++)for (int j = 1; j <= n; j++)
{
if (flag[j] && y[j][i])
{
for (int k = j + 1; k <= n; k++)if (flag[k] && y[k][i])
for (int xi = 1; xi <= n; xi++)y[k][xi] ^= y[j][xi];
flag[j] = false, ans++;
break;
}
}
cout << n - ans << " ";
if (n % 10 == 0)cout << endl;
if (n % 50 == 0)cout << endl;
}
return 0;
}
结果:
n从1到300,n-k分别是:
0 0 0 4 2 0 0 0 8 0
6 0 0 4 0 8 2 0 16 0
0 0 14 4 0 0 0 0 10 20
0 20 16 4 6 0 0 0 32 0
2 0 0 4 0 0 30 0 8 8
0 0 2 4 0 0 0 0 22 0
40 24 0 28 42 0 32 0 8 0
14 0 0 4 0 0 2 0 64 0
0 0 6 12 0 0 0 0 10 0
0 20 0 4 62 0 0 20 16 0
18 0 0 4 0 0 6 0 8 0
0 0 2 4 0 0 0 8 46 0
0 0 80 4 50 56 0 56 56 0
86 0 0 4 64 0 2 0 16 0
0 0 30 4 0 0 0 0 10 0
0 8 0 24 6 0 0 0 128 0
2 0 0 24 0 0 14 0 24 36
0 0 2 4 0 0 0 0 22 0
0 0 0 4 42 8 0 24 8 0
126 0 0 28 0 0 42 0 32 0
0 0 38 24 0 0 0 0 10 0
0 0 0 4 14 20 0 0 16 8
2 0 0 4 0 0 6 0 8 20
0 0 2 4 0 0 16 0 94 0
0 0 0 4 2 0 160 0 8 0
102 0 112 140 0 144 114 0 112 0
0 0 174 4 0 0 0 0 10 0
128 0 0 4 6 0 0 20 32 0
2 0 0 4 0 0 62 8 8 0
0 0 2 4 0 20 0 0 22 0
可以看出,确实全部是偶数,也确实大部分为0
2,方程AX=b
这其实就是,把点亮所有的灯
里面的代码,用枚举法解方程组改成用高斯消元法,得到的就是游戏本身的解。
至于代码,只要把上面的代码改一改就行了。