“翻转门游戏”的分析

问题说明:

  游戏《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

 

<!-- /*
 * 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!&nbsp;&nbsp;&nbsp;&nbsp;
                <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>&nbsp;&nbsp;</span>
                        </td>
                        <td>
                            <button id="add_diverse">Add Diverse</button>                            
                        </td>
                        <td>
                            <button id="reduce_diverse">Reduce Diverse</button>                            
                        </td>
                        <td>
                            <span>&nbsp;&nbsp;</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直接死机……

 

 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=4时所有通关组合

 


 

 

结语

   一个简单的游戏,如果扩展起来,可能会相当复杂。对于这个问题理解其中的原理才是关键,同时把这个游戏实现一下也是很有意思的问题……

其中的原理,个人认为有两点:

  • 每一个按钮对其它门的控制与“掩码”相对应,点击按钮操作与“掩码”的异或运算对应
  • 利用异或运算的性质简化求解,只需要遍历特定“掩码”组合的所有子集即可

  实现求出所有组合的过程中,由于对N增大时的计算量估计不足,直接导致了死机。当然,在实现过程中,也有一些其他收货:

  • 求一个子集的组合数序列,如6个不同数中所有4个不同数的序列,这个可以用递归实现,当N比较小时,可以直接使用位运算,参考http://blog.csdn.net/w57w57w57/article/details/6657547
  • 求集合的所有子集,可以一次求集合的所有1元子集一直到N元子集,当N比较小时,也可以使用位运算。同时,python直接提供了itertools.combinations,很强大也很方便
  • 位运算在许多场合都有出其不意的效果,有时候真是简单、暴力又有效.

 

posted @ 2013-11-09 21:06  风城  阅读(736)  评论(0编辑  收藏  举报