add problem 84
This commit is contained in:
parent
32f026119b
commit
2a9272ab47
|
@ -0,0 +1,418 @@
|
|||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "8014918d",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# [Monopoly Odds](https://projecteuler.net/problem=84)\n",
|
||||
"\n",
|
||||
"We could solve this problem by actually simulating moving across a Monopoly board with dice rolls and card decks, but with a little creativity, we can model it more precisely with a [Markov chain](https://en.wikipedia.org/wiki/Markov_chain). But before we get to any of that, let's enumerate the labels of the squares so we don't have to count so much!"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "25da6c30",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"squares = {s: idx for (idx, s) in enumerate((\"GO\", \"A1\", \"CC1\", \"A2\", \"T1\", \"R1\", \"B1\", \"CH1\", \"B2\", \"B3\", \"JAIL\", \"C1\", \"U1\", \"C2\", \"C3\", \"R2\", \"D1\", \"CC2\", \"D2\", \"D3\", \"FP\", \"E1\", \"CH2\", \"E2\", \"E3\", \"R3\", \"F1\", \"F2\", \"U2\", \"F3\", \"G2J\", \"G1\", \"G2\", \"CC3\", \"G3\", \"R4\", \"CH3\", \"H1\", \"T2\", \"H2\"))}"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "9e02ed81",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"To construct the [stochastic matrix](https://en.wikipedia.org/wiki/Stochastic_matrix) of the Markov chain, we'll start simple by ignoring the special rules for:\n",
|
||||
"* rolling three doubles in a row\n",
|
||||
"* landing on Go To Jail\n",
|
||||
"* landing on community chest\n",
|
||||
"* landing on chance\n",
|
||||
"\n",
|
||||
"Then, we'll consider each of these special rules one at a time and adjust the transition probabilities accordingly. Each entry $(i,j)$ in the matrix will ultimately represent the probability of ending in square $j$ after starting a turn in square $i$."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"id": "0882d9de",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"P = matrix(QQ, 40, 40, sparse=True)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "3da7bd89",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Here, we'll construct a dictionary of every two-dice roll and their probabilities."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"id": "1781ea14",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"die_sides = 4\n",
|
||||
"dice_dist = {(p, q): 1/die_sides^2 for p in range(1, die_sides + 1) for q in range(1, die_sides + 1)}"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "14264cff",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Here's where we compute the basic probabilities for landing on each square, without the special rules:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"id": "120c1fd6",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"for start in range(0, 40):\n",
|
||||
" for ((p, q), prob) in dice_dist.items():\n",
|
||||
" roll = p + q\n",
|
||||
" P[start, (start + roll) % 40] += prob"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "c66fb983",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Rolling doubles\n",
|
||||
"\n",
|
||||
"The first special rule we'll consider is going to jail after three consecutive rolls of doubles. To model this, we'll make three states for each square to capture the number of consecutive doubles that have been rolled (0, 1, or 2). This will triple both the number of rows and number of columns in the transition matrix.\n",
|
||||
"\n",
|
||||
"If you don't roll a double, you'll transition to one of the states represented in the first 40 columns. These transitions have the same probabilities as the simple matrix $P$ we constructed above (at least for now)."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"id": "a4db2a46",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"Q = matrix(QQ, 120, 120, sparse=True)\n",
|
||||
"Q[:40, :40] = P\n",
|
||||
"Q[40:80, :40] = P\n",
|
||||
"Q[80:120, :40] = P"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "0a5aad11",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"When there's a double, you transition to the next double state in the sequence (in other words, you move 40 columns over). When the third double is rolled, [right to jail](https://www.youtube.com/watch?v=eiyfwZVAzGw). This also reduces the probabilities in the first 40 columns, so they don't stay exact duplicates of $P$."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"id": "d80047f4",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"for start in range(0, 40):\n",
|
||||
" for ((p, q), prob) in dice_dist.items():\n",
|
||||
" if p == q:\n",
|
||||
" roll = p + q\n",
|
||||
" end = (start + roll) % 40\n",
|
||||
" \n",
|
||||
" Q[start, end + 40] += prob\n",
|
||||
" Q[start + 40, end + 80] += prob\n",
|
||||
" Q[start + 80, squares['JAIL']] += prob\n",
|
||||
" \n",
|
||||
" Q[start, end] -= prob\n",
|
||||
" Q[start + 40, end] -= prob\n",
|
||||
" Q[start + 80, end] -= prob"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "cdb40e1c",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Go To Jail\n",
|
||||
"\n",
|
||||
"Next, if we land on Go To Jail, the end result is landing on Jail (duh). There's a nested loop here so that we adjust the probabilities for each possible transition between all our duplicated doubles states, as well."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"id": "cc220fd7",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"for roll in range(2, 2 * die_sides + 1):\n",
|
||||
" for (s, t) in ((0, 0), (0, 40), (40, 0), (40, 80), (80, 0)):\n",
|
||||
" init = (squares['G2J'] - roll) % 40 + s\n",
|
||||
"\n",
|
||||
" Q[init, squares['JAIL'] + t] += Q[init, squares['G2J'] + t]\n",
|
||||
" Q[init, squares['G2J'] + t] = 0"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "f330965a",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Community chest\n",
|
||||
"\n",
|
||||
"Next, we'll handle the probability that we move to either Go or Jail after landing on community chest. We make this adjustment by applying [conditional probability](https://en.wikipedia.org/wiki/Conditional_probability).\n",
|
||||
"\n",
|
||||
"(A technical note: since a Markov chain requires memorylessness, it would be impractical to model the true nature of the community chest and chance decks (shuffled at the beginning, then repeating in a loop). Instead, we'll assume that the decks are shuffled before each draw, effectively making them [uniform random variables](https://en.wikipedia.org/wiki/Discrete_uniform_distribution). This won't be an issue for this problem since we don't need the exact probabilities of the squares.)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"id": "b8288c38",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"for cc in (squares['CC1'], squares['CC2'], squares['CC3']):\n",
|
||||
" for roll in range(2, 2 * die_sides + 1):\n",
|
||||
" for (s, t) in ((0, 0), (0, 40), (40, 0), (40, 80), (80, 0)):\n",
|
||||
" init = (cc - roll) % 40 + s\n",
|
||||
" cc_prob = Q[init, cc + t]\n",
|
||||
"\n",
|
||||
" # handle moving straight to new square\n",
|
||||
" Q[init, squares['GO'] + t] += cc_prob * 1/16\n",
|
||||
" Q[init, squares['JAIL'] + t] += cc_prob * 1/16\n",
|
||||
"\n",
|
||||
" # reduce community chest space's probability\n",
|
||||
" Q[init, cc + t] *= 14/16"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "c9a55446",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Chance\n",
|
||||
"\n",
|
||||
"Similarly to above, we adjust for drawing from chance, which is a little more complicated, since there are also cases that depend on the specific chance space we landed on."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 9,
|
||||
"id": "1203bddf",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"for ch in (squares['CH1'], squares['CH2'], squares['CH3']):\n",
|
||||
" for roll in range(2, 2 * die_sides + 1):\n",
|
||||
" for (s, t) in ((0, 0), (0, 40), (40, 0), (40, 80), (80, 0)):\n",
|
||||
" init = (ch - roll) % 40 + s\n",
|
||||
" ch_prob = Q[init, ch + t]\n",
|
||||
"\n",
|
||||
" # handle moving straight to new square\n",
|
||||
" for sq in ('GO', 'R1', 'JAIL', 'C1', 'E3', 'H2'):\n",
|
||||
" Q[init, squares[sq] + t] += ch_prob * 1/16\n",
|
||||
"\n",
|
||||
" # handle moving to the *next* railroad/utility\n",
|
||||
" if ch == squares['CH1']:\n",
|
||||
" Q[init, squares['R2'] + t] += ch_prob * 2/16\n",
|
||||
" Q[init, squares['U1'] + t] += ch_prob * 1/16\n",
|
||||
" elif ch == squares['CH2']:\n",
|
||||
" Q[init, squares['R3'] + t] += ch_prob * 2/16\n",
|
||||
" Q[init, squares['U2'] + t] += ch_prob * 1/16\n",
|
||||
" elif ch == squares['CH3']:\n",
|
||||
" Q[init, squares['R1'] + t] += ch_prob * 2/16\n",
|
||||
" Q[init, squares['U1'] + t] += ch_prob * 1/16\n",
|
||||
"\n",
|
||||
" # handle moving three squares back\n",
|
||||
" Q[init, ch - 3 + t] += ch_prob * 1/16\n",
|
||||
"\n",
|
||||
" # reduce chance space's probability\n",
|
||||
" Q[init, ch + t] *= 6/16"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "42eb0bb0",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Finding the stationary distribution\n",
|
||||
"\n",
|
||||
"As a sanity check, we can make sure all of the entries of the matrix are nonnegative and that all the rows sum to 1."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 10,
|
||||
"id": "8d52eefe",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"assert all(x >= 0 for row in Q for x in row)\n",
|
||||
"assert all(x == 1 for x in sum(Q.T))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "96be7f65",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Now with all of the adjustments for special rules in place, we can solve the problem by finding the chain's stationary distribution, which is an [eigenvector](https://en.wikipedia.org/wiki/Eigenvalues_and_eigenvectors) of the stochastic matrix with eigenvalue 1. To find this eigenvector, we'll first find the basis vector of the [kernel](https://en.wikipedia.org/wiki/Kernel_(linear_algebra)) of $Q - I$ (where $I$ is the identity matrix), with the help of SageMath."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 11,
|
||||
"id": "95114e4f",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"b = (Q - identity_matrix(120)).kernel().basis()[0]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "093e67bc",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Since each square is represented by three states, we want to sum each triplet to get the total probability for each square."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 12,
|
||||
"id": "129f23f3",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"v = sum(matrix(QQ, 3, 40, b))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "08f154c3",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"With this vector found, we just need to normalize it so the entries sum to 1 - we can do this with the 1-norm, otherwise known as the [Manhattan norm](https://en.wikipedia.org/wiki/Taxicab_geometry)."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 13,
|
||||
"id": "10a04c94",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"pi = v.normalized(1)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "c5616c0a",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Then we'll sort by highest probability."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 14,
|
||||
"id": "de34afa9",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"[(10, 0.0701408235247207),\n",
|
||||
" (15, 0.0361487100050623),\n",
|
||||
" (24, 0.0328724475541427),\n",
|
||||
" (16, 0.0322584962091447),\n",
|
||||
" (25, 0.0310964093466631),\n",
|
||||
" (19, 0.0306062292541469),\n",
|
||||
" (21, 0.0304867096651446),\n",
|
||||
" (20, 0.0299664869236993),\n",
|
||||
" (23, 0.0299370269427328),\n",
|
||||
" (18, 0.0297290718671866),\n",
|
||||
" (5, 0.0290016101756835),\n",
|
||||
" (14, 0.0288235313851932),\n",
|
||||
" (28, 0.0285344599840671),\n",
|
||||
" (31, 0.0281227577909694),\n",
|
||||
" (0, 0.0278137848332617),\n",
|
||||
" (29, 0.0275205747552836),\n",
|
||||
" (17, 0.0271550379206804),\n",
|
||||
" (26, 0.0264655804498234),\n",
|
||||
" (32, 0.0258238774287566),\n",
|
||||
" (27, 0.0255174128929372),\n",
|
||||
" (13, 0.0251599001514243),\n",
|
||||
" (39, 0.0247949649359000),\n",
|
||||
" (11, 0.0244402810295771),\n",
|
||||
" (12, 0.0241521076809198),\n",
|
||||
" (4, 0.0228303231805554),\n",
|
||||
" (33, 0.0220537515544996),\n",
|
||||
" (34, 0.0219893556241630),\n",
|
||||
" (37, 0.0212205690768340),\n",
|
||||
" (8, 0.0211429239372672),\n",
|
||||
" (9, 0.0209948500526884),\n",
|
||||
" (6, 0.0209206861048968),\n",
|
||||
" (38, 0.0208293527278273),\n",
|
||||
" (3, 0.0203131015847667),\n",
|
||||
" (35, 0.0198361718493843),\n",
|
||||
" (1, 0.0176178373964372),\n",
|
||||
" (2, 0.0167938454532514),\n",
|
||||
" (22, 0.0112454467024504),\n",
|
||||
" (36, 0.00788815920381253),\n",
|
||||
" (7, 0.00775533284404496),\n",
|
||||
" (30, 0.000000000000000)]"
|
||||
]
|
||||
},
|
||||
"execution_count": 14,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"sorted(enumerate(pi.n()), key=lambda x: x[1], reverse=True)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "1c1cc8aa",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"This gives our top squares as 10 (JAIL), 15 (R2), and 24 (E3)."
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "SageMath 9.5",
|
||||
"language": "sage",
|
||||
"name": "sagemath"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.2"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
Loading…
Reference in New Issue