1.引言
上个月简直闲到离谱,于是乎打开王者,追了一个活动,如图。puzzle 本体是一个4*4的棋盘。
通关条件:翻出4个相连通的正确答案,成一条水平线、垂直线或者主副对角线即可通关。
我们将水平线的解称为横向通路,垂直线的解称为纵向通路,对角线的解则称作主副对角线通路。
最后puzzle做出来的样子如下图(抱歉忘记截图了,凑合看一看!)
可以看到,正确答案一共10个,错误答案一共6个,且puzzle 的解只有一条(第一横向通路)。
我是MATLAB 的初学者 ,觉得MATLAB 很适合处理这种矩阵数据,矩阵元素调用以及行项列项求和等操作很是方便。于是尝试用MATLAB 进行该puzzle 的制作。
2.建模分析
制作该puzzle 最重要的部分是生成一个随机的、10个正确答案、6个错误答案、且只有一条通路的4*4 棋盘。
因为在Matlab 中,可以对方阵进行上下翻转或左右旋转的操作(后面会提到),所以我们只需要模拟出:Z1唯一解为第一列通路、Z2唯一解为第二列通路以及Z3唯一解为主对角线通路这三种情况的puzzle,即可完成puzzle 建模。
判断一条道路的‘‘通断’’,我们可以在该方向求和,当哪一条道路上的4个元素的和为4时,这条路即是通路。(这个求和很重要,其思路取自16宫格,即通路和相等。共有48解。在验证puzzle是否正确生成,以及编写app designer ,定位答案的时候会用)
结果输出:符合条件的4*4 方阵,正确答案标记为1(数值),错误答案标记为0(数值)。
3.1.Matlab 建模——Z1
这是我建立Z1模型的思路及过程:(Z1.mlx)Z
通过a=ones(4) 建立一个4*4 的全一矩阵,此后只需将6个错误答案填入a 中即可。
将非第一列的主、副对角线断路,即在(2,2)、(3,3)、(4,4)和(3,2)、(2,3)、(1,4)中各挑选一个位置填入0。代码如下;
p=0;
a=ones(4);
b1=randperm(3,1)+1;b2=randperm(3,1)+1; %随机主副对角线列数
s1=5*b1-4;s2=3*b2+1;
a(s1)=0;a(s2)=0;
%a % 2 off 6
通过求和判断列向通路并定位。保留第一列a(:,1),便于定位,使sum(a(:,1))==4;
sum1=sum(a);sum1(1)=0; %列向求和
%disp('cols sum') %列项和向量
%disp(sum1)
no1=find(sum1==4); %断路列的定位
len1=size(no1);len1=len1(2); %确定定位向量的长度
纵向断路,为a补齐纵向的0;
for n=1:len1
ch=randperm(4,1); %行定位
a(ch,no1(n))=0; %0的位置
end
%a % 2+len1 off 6
通过求和判断行向通路并定位。同样保留a(:,1);
sum2=sum(a,2)'; %行向求和
%disp('rows sum') %行项和向量
%disp(sum2)
no2=find(sum2==4); %断路行的定位
len2=size(no2);len2=len2(2); %确定定位向量的长度
横向断路,为a补齐横向的0;
if sum(no2)~=0 %判断是否需要进行断路
for n=1:len2
ch=randperm(3,1)+1; %列定位
a(no2(n),ch)=0; %0的位置
end
end
%a % 2+len1+len2 off 6
至此,通过对行向和列向元素的求和,我们分3步,分别完成了对【主副对角线通路】、【2、3、4】路纵向通路以及【1、2、3、4】路横向通路的断开。可以思考出,此时0的个数在闭区间[4,6]之内,此时只需要判断0的个数,将剩余的0(如果存在的话)补齐至方阵元素为1的位置中即可(当然第一列除外)。
count【0】的个数,并补齐至0*6;
num0=size(find(a==0)');num0=num0(2);%提取a中0的个数
zeroin=a(5:16); %需要在a中插入0的位置
st=find(zeroin==1)+4; %定位1
lenst=size(st);lenst=(lenst(2)); %定位向量的长度
if num0~=6 %判断已存在0的个数
rand=randperm(lenst,6-num0); %随机位置
k=st(rand); %定位至a中的元素位置
a(k)=0; %补齐
end
%a % 6 off 6
最后再小小的验证一下,验证方式和上面提到的求和一致,直接上代码:
cross=[sum(diag(a)),sum(diag(flipud(a)))];
col=sum(a); % only col(1)==4
row=sum(a,2)';
mix=[cross,col,row];inmix=find(mix==4);
%disp('unique ans test')
if inmix==3 %答案唯一性验证
%disp('True')
else
%disp('False')
end
其中的cross 的两个元素为【主对角线和;副对角线和】
col 与 row 即为方阵a的列求和与行求和,各四个元素。
揉合到一起的mix 就是上文提到的10条通路所组成的向量。
因为是第一列向通路,4的定位位置就是col 的第1元素,在mix 里为第3元素。
判断成功,得到Z1型puzzle 。
3.2.Matlab 建模——Z2
第二模型选第二列向通路为唯一答案。
在建立Z2 模型的时候,一直在思考能不能用上Z1 的大部分代码。于是乎我想在补齐6个0之前,将生成的a方阵第一列与第二列位置互换:(下图第38行起)
经过实践及思考,发现不可行。因为Z1 的第一部分代码就断开了主副对角线通路,如果盲目互换1、2列元素,可能会导致主副对角线复通。(上图即为主对角线复通的情况)。
于是乎偷懒失败了,只能重新按照Z1 模型重写,不在话下。
重写之后
同样进行通路验证,理想的mix 验证位置为:7;验证部分代码同Z1.
3.3.Matlab 建模——Z3
建立Z3 的时候,相较Z1、Z2 更为复杂的就是元素位置的确定。
于是我思考:能不能经过Z1 的一系列操作,最后再补齐对角线上出现的0不就可以了吗!!?
(如下图组)
我们观看上两图,图一出现了(主对角线通路和第一列向通路);图二出现了(主对角线通路和第四列向通路)。实践发现,错误通路出现在纵向,但是此时已经补齐0了,那这个思路还能够继续进行吗。可以讨论。(不知是样本数量不足还是我的右手食指被施加以魔法...)。确实会出现这种错解情况,于是乎放弃,寻找新的思路。
本人还尝试过将全一矩阵的主对角线变为0,(因为觉得如此操作会便于定位1(要补齐0的位置),且将求和检测定位sum()==3即可。但想法最终未能完善。可以讨论。下图是一个(百年难遇——估计概率为:1/81)的崩溃情况,以此纪念,还是蛮快乐的。
于是只能按照老套路:第一部分只断开副对角线,然后横向纵向断开,最后补齐0...(因为不能投机取巧而无奈打出的省略)
p=0;
a=ones(4);
b1=randperm(4,1); %随机主副对角线列数
a(b1,5-b1)=0;
a; % 2 off 6
分别对列向、行向补充非主对角线上的0;
sum1=sum(a); %列向求和
%disp('cols sum') %列项和向量
%disp(sum1)
no1=find(sum1==4); %断路列的定位
len1=size(no1);len1=len1(2); %确定定位向量的长度sum2=sum(a,2)';
if len1~=0
for n=1:len1
add=randperm(3,1)+no1(n); % 0的横向定位
if add>4
add=add-4;
end
a(add,no1(n))=0;
end
end
a; % num0=1+len1
sum2=sum(a,2)'; %行向求和
%disp('rows sum') %行项和向量
%disp(sum2)
no2=find(sum2==4); %断路行的定位
len2=size(no2);len2=len2(2); %确定定位向量的长度
if len2~=0
for n=1:len2
add=randperm(3,1)+no2(n); % 0的纵向定位
if add>4
add=add-4;
end
a(no2(n),add)=0;
end
end
a; % num0=1+len1+len2
计算0的个数,并补全;
num0=1+len1+len2; %补齐前0的个数
if num0~=6
b=a-eye(4);
fin=find(b==1)';
leth=size(fin);
leth=leth(2);
op=randperm(leth,6-num0);
a(fin(op))=0;
end
a; % num0=6; 10 off 10
同样进行通路验证,理想的mix 验证位置为:1;验证部分代码同Z1.
4.为生成的3型puzzle 添加随机性
该部分对3型进行统一,并将3型puzzle 进行旋转以及翻转。此部分代码写入(F.mlx) 中。
因为3个类型的puzzle,最终获得的通路比例为4:4:2,即221。所以并没有按照1:1:1均等随机该3型模型,而是2:2:1。这个决定我也不知是错是对。可以讨论。
随机挑选3型puzzle;(下方代码很傻,有没有更好的方法?可以讨论。)
p=randperm(5,1);
if p==1
Z1;
end
if p==2
Z1;
end
if p==3
Z2
end
if p==4
Z2
end
if p==5
Z3
end
a;
再对a进行随机打乱:
spin=randperm(4,1); %随机确定次数
plr=randperm(2,1);
pud=randperm(2,1);
a=rot90(a,spin); %逆时针旋转
if plr==1
a=fliplr(a); %左右翻转
end
if plr==1
a=flipud(a); %上下翻转
end
a;
运存清理大师:只保留了生成的矩阵a。
clearvars -except a
至此,puzzle 的棋盘a 建模已经完成了。
5.写在第一部分的最后
蓝字为讨论部分啦,虽然不知有多少人可以看到。但是写这篇blog 的时候还是激情澎湃的。仅作纪念
作者纯属萌新,(特别萌??),触及到的Matlab 部分简直是皮毛中的皮毛,且我自己深知这一点。所以写这种简单的东西让我感到自己在亵渎这个软件。
我知道自己的代码有很多很多可以优化的部分,希望向大佬虚心求教,和萌新一起学习。