“翻转门游戏”的分析
问题说明:
游戏《Doors and Rooms》中第二章第一关是这样的,如下图,点击每一个按钮,不同的门会翻转(每一个按钮对应的翻转的门是固定的,例如第一个按钮对应翻转的门是2、3、4,即每按一次1号按钮时,2、3、4号门的状态会翻转)。这关游戏的通关状态是打开所有4个门。这个问题开始没有多想,反正通过这一关也很简单,一通瞎按就可以……前几天突然想起了这个问题,进行了一番分析,权且把这个问题定义为“翻转门游戏”。
问题抽象
对这个问题进行如下抽象,假设共有N扇门和N个按钮,其中N最小为3,最大为10:
- 对于每一个按钮,点击时候所有翻转的其它门抽象为一个“掩码”。如存在4扇门时候,1号按钮对应“掩码”为0111,即表示点击此按钮时,对应的2、3、4号门会发生翻转(注意此掩码左边为最低位!)。
- 设定“掩码”不能为全0,也不能为全1。即不能一个按钮点下之后,没有门翻转;也不能一个按钮点下之后,所有门发生翻转从而通关。这样的话,对于N扇门,所有掩码个数为2N-2。
- 设定“掩码”不能重复,即任意两个按钮所对应的翻转的门不同。
- 对于N扇门,所有N个掩码组合,最多有C(2N-2,N)组,其中C表示组合数CNK。
- 初始的“掩码”为0,点击一个按钮之后的门的状态可以用目前的掩码MASK表示,MASK = MASK^F(i)。即当前掩码和第i个按钮的掩码异或运算,得到当前的掩码。当前的掩码和当前的门的状态是对应的。(如初始MASK=0,F(1)=0111,则点击一次1号按钮之后,MASK=0^0111=0111。此时表示2、3、4号门是打开的)
可以看出,游戏通关条件变成了,初始“掩码”为全0,通过每个按钮的不同“掩码”F(i)的异或运算组合,把当前掩码设置为全1。而异或运算有如下性质:
- 顺序可交换,即(A^B)^C)=(A^C)^B
- 偶数次异或归零,即A^A=0
由这连个性质可以得到,1、通关策略或者说通关步骤是无序的。2、如果一个通关策略中偶数次点击一个按钮,则改通关策略中去除所有改按钮,仍是一个通关策略。3、如果一个通关策略中基数次点击一个按钮,则可以将改通关策略简化为只点击改按钮一次。
因此,对于特定的N以及一组特定的“掩码”组合F(1),F(2),...,F(N)。判定该组合能不能通关,只需要依次得到这个集合的所有非空子集,如果一个子集所有元素经过异或运算后结果为全1,则次集合构成一个通关策略。
轻松一下,进行游戏
The Simple Rotating Door Game!
“翻转门游戏”说明
- 规则
- 每一扇门对应一个“掩码”,表示点击此门之后,会发生翻转的门。例如有三扇门(A、B、C)时,如果门A的掩码为011,表示当点击门A时候,门B和门C会发生翻转
- 胜利条件
- 游戏胜利条件为:翻转所有门
- 注意
- 最多有10扇门,最少有3扇门
- 点击“Random Mask”可以随机生成“掩码”,但随机生成的掩码不一定满足通关条件,可以点击“Check Mask”查看是否可行
- 点击“Help”可以获得所有通关组合
Current Mask: Current Step:
说明,如果页面上不能正常显示,下面是另一个源代码,直接保存为HTML,打开即可。需要注意的是:1、为了操作方便,代码中把CSS和JS全部写到单个文件。2、代码中使用了jquery,并且直接使用外部JS,故下载后正常运行,需要联网得到jquery
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<!-- /* * lycc's html file for the rotating doors game * Author: lycc316 * * */ --> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>New Web Project</title> <!-- <script type=text/javascript src="jquery.min.js"></script> --> <script type=text/javascript src="http://code.jquery.com/jquery-1.10.2.min.js"></script> <style type="text/css"> div.text-picture { height:200px; width:120px } .head_span{ float:left; /*font-weight: bold;*/ } .mask_span{ background-color: black; color: white; float: left; display:block; width:50px; } .step_span{ background-color: black; color: white; float: left; display:block; width:180px; } button span{ background-color: white; } .text-picture span{ font-family:"Times New Roman"; font-size: 120px; width:100px; display:block; color:blue; } .init_no_hide{ display:block; } .init_hide{ display:none; } table{ margin-top:20px; } </style> <script type="text/javascript"> Array.prototype.indexOf = function(val) { for (var i = 0; i < this.length; i++) { if (this[i] == val) return i; } return -1; }; Array.prototype.remove = function(val) { var index = this.indexOf(val); if (index > -1) { this.splice(index, 1); } }; var all_text = "ABCDEFGHIJ"; var current_diverse; var init_diverse=3; var MIN = 3; var MAX = 10; var MASK = 0; var current_step = []; var all_valid_step =[]; var valid_count = 0; var current_mask = []; var character_table={'A':0, 'B':1, 'C':2, 'D':3 ,'E':4 ,'F':5 ,'G':6 ,'H':7 ,'I':8 ,'J':9} //init mask table var mask_table = {}; var temp_array=[]; for(var i=1;i<=10;i++){ temp_array=[]; for(var j=0;j<i;j++){ temp_array.push(1<<j); } mask_table[i]=temp_array; } var button_begin='<td><div class="text-picture"><button id="button_'; var button_middle='"><span>'; var button_end = '</span></button></div></td>'; function clear_value(){ MASK = 0; current_step = []; all_valid_step =[]; valid_count = 0; } function init(number){ clear_value(); //init button var table_content = ''; for(var i=0;i<number;i++){ table_content+=button_begin; table_content+=all_text[i]; table_content+=button_middle; table_content+=all_text[i]; table_content+=button_end; } $("#picture-area").html(table_content); $("button[id^=button_]").click(button_calback); current_diverse = number; //init mask and current_mask=[]; for(var i=0;i<number;i++){ current_mask.push(mask_table[number][i]); } //init mask content draw_mask(); set_mask(); //init help info $("#help_text").html(get_help_info()); }; //Reset the mask function reset(){ clear_value(); set_mask(); redraw(); }; function get_random_mask(){ var min=1; var max=(1<<current_diverse)-2; var count=max-min+1; var matrix = new Array(count); var temp; var random; //init the matrix for(var i=0;i<count;i++){ matrix[i]=i+1; } //get the random mask for(var i=0;i<current_diverse;i++){ random=parseInt(Math.random()*(max-min+1)+min); temp = matrix[i]; matrix[i]=matrix[random-1]; matrix[random-1]=temp; } //init mask and current_mask=[]; for(var i=0;i<current_diverse;i++){ current_mask.push(matrix[i]); } draw_mask(); //clear clear_value(); $("#help_text").html(''); } function set_mask(){ $("#mask_span").html(':'+MASK); } function set_step(){ $("#step_span").html(':'+current_step.toString()); } function draw_mask(){ //init mask content var content = ''; var temp; for(var i=0;i<current_diverse;i++){ content +='<p>'+all_text[i]+':'; temp = current_mask[i].toString(2) temp = new Array(current_diverse + 1 - temp.length).join('0') + temp; temp = temp.split("").reverse().join(""); content +=temp+'</p>'; } $("#show_mask_text").html(content); } function redraw(){ var temp=MASK; var index=0; for(var i = 0;i<current_diverse;i++){ if(temp&1){ $("#button_"+all_text[i]+" span").css('background-color','blue'); } else{ $("#button_"+all_text[i]+" span").css('background-color','white'); } temp = temp>>1; } if(MASK == (1<<current_diverse)-1){ alert("Congratulate!\nYou open all the doors!"); } } function valid_mask_combination(index){ var temp_mask=0; for(var i=0;i<index.length;i++){ temp_mask ^= current_mask[index[i]]; } if(temp_mask == (1<<current_diverse)-1){ return true; } return false; } function get_next_combination(diverse,combination){ var result = combination; result = (((result+ (result&-result) )^result )>>2)/(result&-result) | (result + (result&-result)); if(result < (1<<diverse)){ return result; } return 0; } function get_combination_index(combination){ var temp = combination; var result = []; var posion = 0; while(temp){ if(temp&1){ result.push(posion) } posion +=1; temp = temp>>1 } return result; } function get_valid_step(){ var step=1; var combination; var index=[]; var test_mask=0; var result =[]; for(;step<=current_diverse;step++){ combination = (1<<step)-1; index=get_combination_index(combination); if(valid_mask_combination(index)){ result.push(index); } while(combination=get_next_combination(current_diverse,combination)){ index=get_combination_index(combination); if(valid_mask_combination(index)){ result.push(index); } } } return result; } function get_help_info(){ all_valid_step = get_valid_step(); var result = all_valid_step; var content=''; if(!result.length){ content+="<p>Oops!This can't be done!</p>"; return content; } for(var i=0;i<result.length;i++){ content+='<p>Valid step '+(i+1)+':'; for(var j=0;j<result[i].length;j++){ content+='<strong>'+all_text[result[i][j]]+'</strong>'; } content+='</p>'; } return content; } function button_calback(){ var content = $(this).attr('id'); var button = content[content.length-1]; if(current_step.indexOf(button) == -1){ current_step.push(button); } else{ current_step.remove(button); } var index=character_table[button]; MASK = MASK^current_mask[index]; set_mask(); set_step(); redraw(); } $(document).ready(function(){ init(init_diverse); $("#show_mask").click(function(){ $("#show_mask_text").show(); $("#hide_mask").show(); $(this).hide(); }); $("#hide_mask").click(function(){ $("#show_mask_text").hide(); $("#show_mask").show(); $(this).hide(); }); //functions of the menu $("button#reset").click(function(){ reset(); $("#help_text").html(get_help_info()); }); $("button#randommask").click(function(){ reset(); get_random_mask(); $("#help_text").html(get_help_info()); }); $("button#setmask").click(function(){ alert("To be done"); }); $("button#add_diverse").click(function(){ if(current_diverse + 1 > MAX){ alert("MAX diverse is "+MAX+"!"); return; } current_diverse++; init(current_diverse); }); $("button#reduce_diverse").click(function(){ if(current_diverse -1 < MIN){ alert("MIN diverse is "+MIN+"!"); return; } current_diverse--; init(current_diverse); }); $("button#checkmask").click(function(){ var count = all_valid_step.length; var content = ''; if(count){ content +='Believe youself!\nThere ' if(count == 1){ content +='is 1 method '; } else{ content +='are '+count+' methods '; } content +='to achieve this!' } else{ content = "<p>Oops!This can't be done!</p>"; } alert(content); }) $("button#help").click(function(){ $("#help_text").show(); $(this).hide(); $("button#hide_help").show(); }) $("button#hide_help").click(function(){ $("#help_text").hide(); $(this).hide(); $("button#help").show(); }) $("button#show_introduction").click(function(){ $("#introduction_area").show(); $(this).hide(); $("button#hide_introduction").show(); }) $("button#hide_introduction").click(function(){ $("#introduction_area").hide(); $(this).hide(); $("button#show_introduction").show(); }) }); </script> </head> <body> <div> <h1> The Simple Rotating Door Game! <button id="show_introduction" style="background: green;color: yellow;font-size: 20px;">Show Introduction</button> <button id="hide_introduction" style="background: green;color: yellow;font-size: 20px;display: none">Hide Introduction</button> </h1> </div> <div id='introduction_area' class="init_hide"> <h2>“翻转门游戏”说明</h2> <ul> <li>规则 <ul> <li>每一扇门对应一个“掩码”,表示点击此门之后,会发生翻转的门。例如有三扇门(A、B、C)时,如果门A的掩码为011,表示当点击门A时候,门B和门C会发生翻转</li> </ul> </li> <li>胜利条件 <ul> <li>游戏胜利条件为:翻转<span style="color: red;">所有门</span></li> </ul> </li> <li>注意 <ul> <li>最多有10扇门,最少有3扇门</li> <li>点击“Random Mask”可以随机生成“掩码”,但随机生成的掩码<span style="color: red;">不一定</span>满足通关条件,可以点击“Check Mask”查看是否可行</li> <li>点击“Help”可以获得所有通关组合</li> </ul> </li> </ul> </div> <div> <table> <div> <tr> <td> <button id="reset">Reset</button> </td> <td> <button id="randommask">Random Mask</button> </td> <td> <span> </span> </td> <td> <button id="add_diverse">Add Diverse</button> </td> <td> <button id="reduce_diverse">Reduce Diverse</button> </td> <td> <span> </span> </td> <!-- <td> <button id="setmask">Set Mask</button> </td> --> <td> <button id="checkmask">Check Mask</button> </td> </tr> </div> </table> </div> <div> <span class="head_span">Current Mask</span><span class="mask_span" id="mask_span">:</span> <span class="head_span">Current Step</span><span class="step_span" id="step_span">:</span> </div> <div> <table> <tr id='picture-area'> <td> <div class='text-picture'> <button id="button_A"><span>A</span></button> </div> </td> <td> <div class='text-picture'> <button id="button_B"><span>B</span></button> </div> </td> <td> <div class='text-picture'> <button id="button_C"><span>C</span></button> </div> </td> </tr> </table> </div> <div><button id="show_mask" class="init_hide">Show all mask</button><button id="hide_mask" >Hide all mask</button></div> <div id="show_mask_text" class="init_no_hide"></div> <div><button id="help">Help</button><button id="hide_help" class="init_hide">Hide help info</button></div> <div id="help_text" class="init_hide"></div> </body> </html>
求出特定N的所有通关组合
对于指定的N,其“掩码”组合最多有C(2N-2,N),计算每一个“掩码”组合的通关策略需要遍历其所有子集。如果遍历子集操作可以在线性时间完成,则求出所有组合的复杂度至少是C(2N-2,N)*2N。下面使用python得出当N=4时候,所有通关策略。
说明;N=4时,“掩码”可以取到1000~0111(限定不能去全1组合;且注意最左边为最低位,故掩码0111对应十进制数为14),4个“掩码”组合共有C144=1001个,其中有效组合为71组。
特别提醒:代码中RotatingDoor(4),即表示N=4,当N>5时候C(2N-2,N)*2N增长非常快,且由于代码中将输出结果定向到文件中(方便查看),当N>5时,对写文件和巨大的计算量要有准备。试了一下,当n=10时,Windows7直接死机……
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 #!/usr/bin/python 2 3 import os 4 from itertools import combinations 5 6 class RotatingDoor: 7 def __init__(self,number): 8 self.number = number 9 self.max = (1<<number)-1 10 self.min = 1 11 self.matrix = range(self.max-1,self.min-1,-1) 12 self.resultfile = "result.txt" 13 self.count = 0 14 self.good = 0 15 16 def calculate(self): 17 with open(self.resultfile,'w') as fp: 18 self.print_begin(fp) 19 for one_list in list(combinations(self.matrix, self.number)): 20 self.count += 1 21 self.testandprint(fp, one_list) 22 self.print_end(fp) 23 24 def testandprint(self,fp,one_list): 25 try: 26 find_list=[] 27 for step in range(self.number,0,-1): 28 for com_list in list(combinations(one_list,step)): 29 result = 0 30 for temp_number in com_list: 31 result ^= temp_number 32 if result == self.max: 33 find_list.append(com_list) 34 if find_list: 35 self.good +=1 36 self.print_head(fp,one_list) 37 for find_item in find_list: 38 self.print_item(fp,find_item) 39 except IOError as err: 40 print('File Error:%s' % str(err)) 41 42 def tobin(self,int_list): 43 return [ bin(one)[2:].rjust(self.number,'0') for one in int_list ] 44 45 def print_begin(self,fp): 46 try: 47 fp.write("Start!\tNumber:%d\n" % self.number) 48 except IOError as err: 49 print('File Error:%s' % str(err)) 50 51 def print_end(self,fp): 52 try: 53 fp.write("\nEnd!\t Total count:%d\tTotal good count:%d\n" % (self.count, self.good)) 54 except IOError as err: 55 print('File Error:%s' % str(err)) 56 57 def print_head(self,fp,test_list): 58 try: 59 fp.write("\n\nNumber:%s\tBinary:%s\n" % ( str(test_list), str(self.tobin(test_list)) )) 60 except IOError as err: 61 print('File Error:%s' % str(err)) 62 63 def print_item(self,fp,int_list): 64 try: 65 fp.write("\tFind one combinations:%s\t Binary:%s\n" % ( str(int_list), str(self.tobin(int_list)) )) 66 except IOError as err: 67 print('File Error:%s' % str(err)) 68 69 if __name__ == '__main__': 70 rotating = RotatingDoor(4); 71 rotating.calculate()
结语
一个简单的游戏,如果扩展起来,可能会相当复杂。对于这个问题理解其中的原理才是关键,同时把这个游戏实现一下也是很有意思的问题……
其中的原理,个人认为有两点:
- 每一个按钮对其它门的控制与“掩码”相对应,点击按钮操作与“掩码”的异或运算对应
- 利用异或运算的性质简化求解,只需要遍历特定“掩码”组合的所有子集即可
实现求出所有组合的过程中,由于对N增大时的计算量估计不足,直接导致了死机。当然,在实现过程中,也有一些其他收货:
- 求一个子集的组合数序列,如6个不同数中所有4个不同数的序列,这个可以用递归实现,当N比较小时,可以直接使用位运算,参考http://blog.csdn.net/w57w57w57/article/details/6657547。
- 求集合的所有子集,可以一次求集合的所有1元子集一直到N元子集,当N比较小时,也可以使用位运算。同时,python直接提供了itertools.combinations,很强大也很方便
- 位运算在许多场合都有出其不意的效果,有时候真是简单、暴力又有效.