一点一点前进...

0%

欧拉项目 | 84题 | 大富翁

Problem 84

这个游戏就是小时候玩的大富翁,掷骰子来决定前进步数,到了某个方格可能会触发一些事件,转移到某个方格。
问题如果无限玩这个游戏,那么走到哪三个格子的概率最大呢?

一个比较正规的做法是建立一个四十乘四十的矩阵,是概率转移矩阵,记录每个格子转移到其他格子的概率,这个矩阵自乘成千上万次收敛之后,用初始状态,一个四十维的向量,和矩阵相乘,得到最终的四十维的向量,选择三个概率最高的格子。

但是我觉得一个一个算概率太麻烦,下面采用模拟游戏的方法,掷骰子一百万次,然后计算每个格子的次数,得到一个近似概率。
下面写贴出整个代码,然后通过注释的方式解释玩法、算法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
private static readonly int Rolls = 1_000_000;

// some special positions
private static readonly int Go = 0;
private static readonly int Jail = 10;
private static readonly int Go2Jail = 30;
private static readonly int C1 = 11;
private static readonly int E3 = 24;
private static readonly int H2 = 39;
private static readonly int R1 = 5;
private static readonly int[] Community = new int[] { 2, 17, 33 };

// 有三个 Chance 点,后面两个下一站坐标和 Chance 是对应的
private static readonly int[] Chance = new int[] { 7, 22, 36 };
private static readonly int[] NextRailway = new int[] { 15, 25, 5 };
private static readonly int[] NextUtility = new int[] { 12, 28, 12 };

public string GetAnswer()
{
// 记录每个到每个格子的次数
int[] board = new int[40];
Random random = new Random();
int doubles = 0;
int current = 0;

// Index,指代特殊牌的位置,也是特殊牌的值,详见下面的注释
int chanceIndex = 0;
int communityIndex = 0;

for (int i = 0; i < Rolls; i++)
{
// 这个题目和原始玩法不一样的地方,使用两个点数为 1-4 的骰子
int dice1 = random.Next(1, 5);
int dice2 = random.Next(1, 5);

// 如果两个骰子点数一样,记作 double
doubles = (dice1 == dice2) ? doubles + 1 : 0;
if (doubles > 2)
{
// 如果连续三次 double,直接进监狱
current = Jail;
doubles = 0;
}
else
{
// 没有连续三次 double,前进若干步
current = (current + dice1 + dice2) % 40;

// 现在考虑到了各种特殊的格子
//
// 处理 Chance
if (current == Chance[0] || current == Chance[1] || current == Chance[2])
{
// 使用一张特殊牌,切换位置
// 按理说,这两个应该在这个 if 的最后
// 但其实没有关系,先处理相当于整体牌的位置变化了一下而已
chanceIndex++;
chanceIndex %= 16;

// 这里记录 index 是为了跳转到对应的 Railway 或者 Utility
int index = 0;
if (current == Chance[1])
{
index = 1;
}
else if (current == Chance[2])
{
index = 2;
}

// 十六张特殊的牌,关于牌的堆叠,比较好的做法是初始化 1-16,
// 然后洗牌,在循环中摸牌,最后对各个点数进行处理
// 我这里取巧了一下,假定牌就是按照 1-16 码放的,理论上随机很多次之后
// 牌是如何码放的对最后结果没有影响
// 为了简化 chanceIndex 的操作,1-16 映射到了 0-15
switch (chanceIndex)
{
case 0:
current = Go;
break;
case 1:
current = Jail;
break;
case 2:
current = C1;
break;
case 3:
current = E3;
break;
case 4:
current = H2;
break;
case 5:
current = R1;
break;
case 6:
case 7:
current = NextRailway[index];
break;
case 8:
current = NextUtility[index];
break;
case 9:
current -= 3;
break;
}
}

// 处理 Community Chest
// 这里的思路和处理 Chance 一致
if (current == Community[0] || current == Community[1] || current == Community[2])
{
switch (communityIndex)
{
case 0:
current = Go;
break;
case 10:
current = Jail;
break;
}

communityIndex++;
communityIndex %= 16;
}

if (current == Go2Jail)
{
current = Jail;
}
}

board[current]++;
}

// 玩了一百万个回合了,排序
// 按照 Item 排序,Indx 记录原始的位置
int[] order = board.Select((item, indx) => new { Item = item, Index = indx })
.OrderByDescending(x => x.Item)
.Select(x => x.Index)
.ToArray();

// 选择前三个,拼接最后题目要求的字符串
string modalString = "";
for (int i = 0; i < 3; i++)
{
if (order[i] < 10)
{
modalString += "0";
}

modalString += order[i].ToString();
}

return modalString;
}