点灯游戏算法

题目:
  有十个开关等间距排成一线,每个开关对应其上方的一盏灯(十盏灯也排成一线)。每按动一下开关,可以使对应的灯改变状态(原来亮着的将熄灭,原来熄灭的将被点亮)。

  但是,由于开关之间的距离很小,每次按动开关时,相邻的一个开关也将被按动。例如:按动第5个开关,则实际上第4、5、6个开关都被按动。而按动靠边的第1个开关时,第1、2个开关都被按动。并且,无法只按动最靠边的一个开关。

  现在给出十盏灯的初始的状态和目标状态,要求计算:从初始状态改变到目标状态所需要的最少操作次数。

  函数接口:

int  MinChange( const   int  Start[], const   int  End[]);


  其中:Start表示了初始状态,End表示了目标状态。表示状态的数组(Start和End)中,若某元素为0表示对应的灯亮着,否则表示对应的灯没有亮。调用函数时保证Start和End数组长度均为10,并保证有解。

  看了很多人的解法都是用循环遍历来判断是否达到最后要求,但是如果和线形代数结合的话,就有一种很快速的解法。

  约定:以下所用的‘+’号都是‘异或’的运算。

  先简化一下,假设有四个灯,初始状态s0~s3,目标状态是e0~e3,转换一次状态就是和1进行异或运算一次,所以状态转移矩阵为:

  (s0,s1,s2,s3)+k0*(1,1,0,0)+k1*(1,1,1,0)+k2*(0,1,1,1)+k3*(0,0,1,1)=(e0,e1,e2,e3);

  其中k(n)表示第n个开关所翻动的次数。并且,注意异或运算中a+b+b=a,所以,某个开关翻动偶数次的效果相当于没有翻动,翻动奇数次的效果相当于翻动一次;又由于异或运算满足交换律,所以翻动的顺序没有影响。综上每个开关翻动的次数只有1次或0次就足够了。

  设m(n)=s(n)+e(n),注意异或运算中的'-'也就是'+',所以解线性方程组:

  k0+k1 =m1;

  k0+k1+k2 =m2;

  k1+k2+k3=m3;

  k2+k3=m4;

  假设解存在,就可以算出通解(k0,k1,k2,k3),再统计出通解中1的个数,就是所需要翻动的次数了。并且还可以知道哪些开关需要拨动,比如算出解是(1,0,1,0)就是第0和2个开关需要拨动一次。

  因此针对本题目的10个灯泡,本人已算出这10元线性方程组的通解:

  k0=m0+m2+m3+m5+m6+m8+m9;

  k1=m2+m3+m5+m6+m8+m9;

  k2=m0+m1;

  k3=m3+m0+m1+m5+m6+m8+m9;

  k4=m5+m6+m8+m9;

  k5=m4+m3+m0+m1;

  k6=m6+m4+m3+m0+m1+m8+m9;

  k7=m8+m9;

  k8=m7+m6+m4+m3+m0+m1;

  k9=m9+m7+m6+m4+m3+m0+m1;

  和上面一样,m(n)为开始状态与目标状态的每位异或。至于是否存在解,本人已将次系数矩阵化简为对角矩阵,可以看到系数矩阵的秩(Rank)与未知数的个数相等,所以无论是任何的输入和输出变换都能找到唯一解。

  本人程序如下:

   int  MinChange( const   int  Start[], const   int  End[]){

  
int  m[ 10 ];

  
int  k[ 10 ];

  
int  res = 0 ;

  
int  i,j,n;

  
for (i = 0 ;i < 10 ;i ++ ){

  m
= Start ^ End;  /*  m[]=Start[] XOR End[]  */

  }

  
/*  calculate roots  */

  k[
0 ] = m[ 0 ] ^ m[ 2 ] ^ m[ 3 ] ^ m[ 5 ] ^ m[ 6 ] ^ m[ 8 ] ^ m[ 9 ];

  k[
1 ] = m[ 2 ] ^ m[ 3 ] ^ m[ 5 ] ^ m[ 6 ] ^ m[ 8 ] ^ m[ 9 ];

  k[
2 ] = m[ 0 ] ^ m[ 1 ];

  k[
3 ] = m[ 3 ] ^ m[ 0 ] ^ m[ 1 ] ^ m[ 5 ] ^ m[ 6 ] ^ m[ 8 ] ^ m[ 9 ];

  k[
4 ] = m[ 5 ] ^ m[ 6 ] ^ m[ 8 ] ^ m[ 9 ];

  k[
5 ] = m[ 4 ] ^ m[ 3 ] ^ m[ 0 ] ^ m[ 1 ];

  k[
6 ] = m[ 6 ] ^ m[ 4 ] ^ m[ 3 ] ^ m[ 0 ] ^ m[ 1 ] ^ m[ 8 ] ^ m[ 9 ];

  k[
7 ] = m[ 8 ] ^ m[ 9 ];

  k[
8 ] = m[ 7 ] ^ m[ 6 ] ^ m[ 4 ] ^ m[ 3 ] ^ m[ 0 ] ^ m[ 1 ];

  k[
9 ] = m[ 9 ] ^ m[ 7 ] ^ m[ 6 ] ^ m[ 4 ] ^ m[ 3 ] ^ m[ 0 ] ^ m[ 1 ];

  
/*  count for switch times  */

  
for (j = 0 ;j < 10 ;j ++ ){

  
if (k[j]) res ++ ;

  }

  
/*  display k(n);  */

  
for (n = 0 ;n < 10 ;n ++ ) printf( " %d, " ,k[n]);

  
return  res;

  }


  测试主程序:

  main(){

  
int  A[ 10 ] = { 1 , 1 , 1 , 0 , 0 , 1 , 0 , 1 , 1 , 0 };

  
int  B[ 10 ] = { 0 , 0 , 1 , 1 , 0 , 0 , 1 , 1 , 1 , 1 };

  
int  C;

  C
= MinChange(A,B);

  printf(
" **%d** " ,C);

  }


  显示结果为:

  1,0,0,0,1,1,1,1,0,1,**6**

  就是如果要把状态A转为状态B,需要把第0,4,5,6,7,9号开关翻动一次,共6次。

  测试验证结果正确:)

  当然,此做法也有一个缺点,就是当灯的个数改变时,就要重新设定线形方程组的特解形式。

转载于:https://www.cnblogs.com/kyovcs/archive/2007/09/04/881147.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值