趣味算法:国王和100个囚犯(据说是腾讯的面试题)
题目如下:
这所监狱有一个院子,每天只少随机(注意是完全随机)打开一间牢房的门,让一个囚犯到院子里来放风。院子里有一盏灯,放风的囚犯可以控制它的开关,将它打开或是关闭。除囚犯之外,其他人都不会去碰开关。这盏灯会永远有充足的能源供应,灯泡和电路不会出故障。
除了开关这盏灯,放风的囚犯放风时留下的任何其它痕迹都会在夜晚被清除干净(包括在灯上作的任何记号)。牢房是完全封闭的,院子里的灯光在牢房里看不到。只有放风到院子里的人才能看到。
好了现在我向你们提出一个要求,只要你们做到了,就可以全部获得释放:
给你们15分钟商量你们的方案。15分钟以后,你们将被关进我刚才说的那个监狱,永远无法再交流,被关若干天后,你们中间如果任何一个人能够向我证明你们每个人都至少放风了一次,我就把你们放了,不然永远别想再出来。
好吧!这样吧,如果你们有谁现在可以告诉我这个方法,也就是能够证明你们每人至少放风一次的方法,我马上放掉你们!
其中一个囚犯想了几分钟,回答了这个问题,国王听后,如自己所说的把他们全部给放了。请问那个囚犯是用什么方法证明的?
大致的解题思路(建议思考后再看):
囚犯们指定一人为计数人(A),A以外的囚犯每次出来放风时,如果看到灯是关闭的,则将灯打开,但如果已经开过一次灯,则不理会,当A出来放风时,如果灯是开着的,则将灯关掉,关一次则表示有一个人出来了一次,计数至100,完成。
分析一下,这里面参与求解的东西无非就是监狱、灯、囚犯(计数员)。
监狱:没有什么动作需要做的。
灯:有一个状态标识, 提供开灯和关灯的方法。
囚犯:有一个是否开过灯的标识,并带有一个开灯的方法
计数员:继承囚犯,且拥有关灯的方法。
实例代码:
监狱:
2 /// 监狱
3 /// </summary>
4 public class Prison
5 {
6 /// <summary>
7 /// 灯
8 /// </summary>
9 public Light Light { get; set; }
10
11 /// <summary>
12 /// 牢房
13 /// </summary>
14 public Prisoner[] Prisoners { get; set; }
15
16 /// <summary>
17 /// 随机数生成器
18 /// </summary>
19 Random random;
20
21 /// <summary>
22 /// 囚犯数量
23 /// </summary>
24 private int PrisonerCount { get; set; }
25
26 /// <summary>
27 /// 构造函数
28 /// </summary>
29 /// <param name="count">囚犯数量</param>
30 public Prison(int count)
31 {
32 PrisonerCount = count;
33 random = new Random(GetRandomSeed());
34
35 //初始化灯,并将状态设置为关
36 Light = new Light() { Status = LightStatus.OFF };
37
38 //初始化牢房数组
39 Prisoners = new Prisoner[PrisonerCount];
40 for (int x = 0; x < PrisonerCount; x++)
41 {
42 Prisoners[x] = new Prisoner() { NO = x };
43 }
44 }
45
46 public int Run()
47 {
48 SetCountPrisoner();
49 bool finished = false;
50 int count = 0;
51 while (!finished)
52 {
53 count++;
54 Helper.Print("{1}\t第 {0} 天", count, DateTime.Today.AddDays(count).ToString("yyyy-MM-dd"));
55 int x = GetRandomNumber();
56 var prisoner = Prisoners[x];
57 prisoner.Release();
58 prisoner.TrunLightOn(this.Light);
59 if (prisoner is CountPrisoner)
60 {
61 //ShowMsg("囚犯({0})是计数人", x);
62 var counter = prisoner as CountPrisoner;
63 counter.TrunLightOff(this.Light);
64 finished = counter.Finished;
65 }
66 }
67 return count;
68 }
69
70 /// <summary>
71 /// 随机设置一个囚犯作为计数人
72 /// </summary>
73 void SetCountPrisoner()
74 {
75 int num = GetRandomNumber();
76 var prisoner = Prisoners[num];
77 Prisoners[num] = new CountPrisoner(prisoner) { Total = PrisonerCount };
78 Helper.Print(true, "计数人是:囚犯({0})", num);
79 }
80
81 /// <summary>
82 /// 取得随机数
83 /// </summary>
84 /// <returns></returns>
85 int GetRandomNumber()
86 {
87 return random.Next(0, PrisonerCount);
88 }
89
90 public int GetRandomSeed()
91 {
92 byte[] bytes = new byte[4];
93 System.Security.Cryptography.RNGCryptoServiceProvider rng = new System.Security.Cryptography.RNGCryptoServiceProvider();
94 rng.GetBytes(bytes);
95 return BitConverter.ToInt32(bytes, 0);
96 }
97 }
再建立一个灯的类:
2 /// 灯
3 /// </summary>
4 public class Light
5 {
6 /// <summary>
7 /// 灯的状态
8 /// </summary>
9 public LightStatus Status { get; set; }
10
11 public void TurnOn()
12 {
13 if (this.Status == LightStatus.OFF)
14 {
15 this.Status = LightStatus.ON;
16 Helper.Print("灯被打开了");
17 }
18 }
19
20 public void TurnOff()
21 {
22 if (this.Status == LightStatus.ON)
23 {
24 this.Status = LightStatus.OFF;
25 Helper.Print("灯被关闭了");
26 }
27 }
28 }
29
30 /// <summary>
31 /// 灯的状态枚举
32 /// </summary>
33 public enum LightStatus { ON, OFF }
接下来是囚犯:
2 /// 囚犯
3 /// </summary>
4 public class Prisoner
5 {
6 public int NO { get; set; }
7
8 /// <summary>
9 /// 是否开过灯了
10 /// </summary>
11 public bool LightOn { get; set; }
12
13 public void Release()
14 {
15 Helper.Print("囚犯({0})出来放风", NO);
16 }
17
18 /// <summary>
19 /// 将监狱的灯打开
20 /// </summary>
21 /// <param name="light"></param>
22 public void TrunLightOn(Light light)
23 {
24 if (light.Status == LightStatus.ON)
25 {
26 Helper.Print("灯是开着的", this.NO);
27 return;
28 }
29 if (LightOn)
30 {
31 Helper.Print("囚犯({0})已经开过灯了", NO);
32 return;
33 }
34 LightOn = true;
35 Helper.Print("囚犯({0})开灯", NO);
36 light.TurnOn();
37 }
38 }
还要有人负责计数:
2 /// 负责计数的囚犯
3 /// </summary>
4 public class CountPrisoner : Prisoner
5 {
6 /// <summary>
7 /// 构造函数
8 /// </summary>
9 /// <param name="prisoner"></param>
10 public CountPrisoner(Prisoner prisoner)
11 {
12 this.NO = prisoner.NO;
13 this.LightOn = prisoner.LightOn;
14 }
15
16 /// <summary>
17 /// 关灯计数
18 /// </summary>
19 public int Count { get; set; }
20
21 public int Total { get; set; }
22
23 public bool Finished { get; set; }
24
25 /// <summary>
26 /// 将监狱的灯关闭
27 /// </summary>
28 /// <param name="light"></param>
29 public void TrunLightOff(Light light)
30 {
31 if (light.Status == LightStatus.ON)
32 {
33 Count++;
34 Helper.Print("囚犯({0})关灯", this.NO);
35 light.TurnOff();
36 if (Count < Total)
37 {
38 Helper.Print(true, "囚犯({0})已经统计到{1}个囚犯出来放风过了", this.NO, this.Count);
39 }
40 else
41 {
42 Finished = true;
43 Helper.Print(true, "囚犯({0})已经统计到所有囚犯全部出来放风过了,任务完成了", this.NO);
44 Console.ReadLine();
45 }
46 }
47 }
48 }
再定义一个Helper来显示文字记录:
2 {
3 static bool DisabledWait = true;
4
5 public static void Print(string msg, params object[] args)
6 {
7 Print(false, msg, args);
8 }
9
10 public static void Print(bool wait, string msg, params object[] args)
11 {
12 Console.WriteLine(msg, args);
13 if (wait && !DisabledWait)
14 {
15 Console.ReadLine();
16 }
17 }
18
19 }
测试代码:
2 var prison = new Prison(100);
3 prison.Run();
4 int len = prison.Prisoners.Length;
5 for (int x = 0; x < len; x++) {
6 if (!prison.Prisoners[x].LightOn) {
7 Console.WriteLine("Wrong!!!囚犯({0})还没有出来放风过", x);
8 Console.ReadLine();
9 }
10 }
11 }
至此,所有的类已经定义完毕。执行Main方法即可得到答案。 计算结果如下(片段):
1 2042-12-28 第 11828 天
3 灯是开着的
4 2042-12-29 第 11829 天
5 囚犯(75)出来放风
6 灯是开着的
7 2042-12-30 第 11830 天
8 囚犯(70)出来放风
9 灯是开着的
10 2042-12-31 第 11831 天
11 囚犯(66)出来放风
12 灯是开着的
13 2043-01-01 第 11832 天
14 囚犯(69)出来放风
15 灯是开着的
16 2043-01-02 第 11833 天
17 囚犯(51)出来放风
18 灯是开着的
19 2043-01-03 第 11834 天
20 囚犯(58)出来放风
21 灯是开着的
22 2043-01-04 第 11835 天
23 囚犯(92)出来放风
24 灯是开着的
25 2043-01-05 第 11836 天
26 囚犯(39)出来放风
27 灯是开着的
28 囚犯(39)关灯
29 灯被关闭了
30 囚犯(39)已经统计到所有囚犯全部出来放风过了,任务完成了
乐观的结果是27年左右,计数人就可以统计到所有囚犯全部出来放风一次,但前提条件是,这27年中不能有人挂掉,要不然,计数人就永远Count不到100个,也就永远出不来了。上面的代码,可以适应国王与N个囚犯,但是当囚犯达到150+的时候,基本上就都不用出来了,因为150人时已经差不多需要70年了。。。
附项目文件下载: KingAndPrisoner
其实,这里面有一个问题,在我的代码中,创建监狱实例时,我们就将灯的初始状态置为关了,就表示我们的代码只有在假定灯初始状态为关的情况下才有效,如果灯的初始状态不定,那该怎么解呢?我想了很久,没有找到答案,不知道有没有人能告诉我答案,谢谢。
最新更新解法:
由Curry同学提供的,每人开灯两次则可以解决灯初始状态不确定的问题.
解题思路:
N=囚犯数量
计数总数 = N * 2 - 2
计数人只关灯不开灯,如果N = 100,则需要关灯的次数为 100 * 2 - 2 = 198.
假设灯初始状态为关,则计数到198时,其它的99个囚犯都已经开过两次灯.
假设灯初始状态为开,则计数到198时, 198 = 98(囚犯) * 2(次) + 1(囚犯) * 1(次) + 1(灯初始状态开)
修改后的代码:KingAndPrisoner20100810.7z