Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ android {

defaultConfig {
applicationId "atorch.statspuzzles"
minSdkVersion 19
minSdkVersion 21
targetSdkVersion 35
versionCode 32
versionName "4.0"
Expand All @@ -51,18 +51,18 @@ android {
}

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'org.mariuszgromada.math:MathParser.org-mXparser:6.1.0'

implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.9.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.navigation:navigation-fragment:2.5.3'
implementation 'androidx.navigation:navigation-ui:2.5.3'
implementation 'androidx.appcompat:appcompat:1.7.1'
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
implementation 'androidx.navigation:navigation-fragment:2.9.5'
implementation 'androidx.navigation:navigation-ui:2.9.5'
implementation 'androidx.viewpager2:viewpager2:1.1.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.6.1'
implementation fileTree(dir: 'libs', include: ['*.aar', '*.jar'], exclude: [])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.2.20"
}
Binary file removed app/libs/mXparser-v.4.2.0-jdk.1.7.jar
Binary file not shown.
10 changes: 8 additions & 2 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<string name="app_name">Probability Puzzles</string>
<string name="new_in_this_update">This is version 4.0, released in October 2025 &#8212; now with over ninety puzzles, zero ads, and translations in Spanish, German, and Arabic. Enjoy!
\n\nSome of the puzzles in this app will be difficult if you haven\'t studied probability. Need to brush up? Have a look at this\u00A0<a href="https://chance.dartmouth.edu/teaching_aids/books_articles/probabilityccc_book/book-5-17-03.pdf">Dartmouth book</a> and these\u00A0<a href="https://ocw.mit.edu/courses/mathematics/18-440-probability-and-random-variables-spring-2014/lecture-notes/">MIT notes</a>. You could also read through some probability problems at\u00A0<a href="https://math.stackexchange.com/questions/tagged/probability">math.stackexchange.com</a>. If you prefer to watch lectures on youtube, try\u00A0<a href="https://www.youtube.com/playlist?list=PLUl4u3cNGP60hI9ATjSFgLZpbNJ7myAg6">these</a> or\u00A0<a href="https://www.youtube.com/playlist?list=PLUl4u3cNGP60A3XMwZ5sep719_nh95qOe">these</a>.
\n\nThis app is full of math puzzles, and uses Mariusz Gromada\'s mXparser version 4.2.0 to translate mathematical expressions to numbers: it\'ll understand .2, 0.2, 20\%, or 1/5. The parser also understands factorials and binomial coefficients: try typing 5!/(3!*2!) or\u00A0<a href="https://en.wikipedia.org/wiki/Binomial_coefficient">C(5,3)</a> with a capital C for \"5 choose 3.\" You can also type lowercase e for the base of the natural logarithm. Visit\u00A0<a href="http://mathparser.org">mathparser.org</a> for more information.
\n\nThis app is full of math puzzles, and uses Mariusz Gromada\'s mXparser version 6.1.0 to translate mathematical expressions to numbers: it\'ll understand .2, 0.2, 20\%, or 1/5. The parser also understands factorials and binomial coefficients: try typing 5!/(3!*2!) or\u00A0<a href="https://en.wikipedia.org/wiki/Binomial_coefficient">C(5,3)</a> with a capital C for \"5 choose 3.\" You can also type lowercase e for the base of the natural logarithm. Visit\u00A0<a href="http://mathparser.org">mathparser.org</a> for more information.
\n\nIf you\'d like to contribute to the app (or peek at solutions), have a look at its\u00A0<a href="https://github.com/atorch/probability_puzzles">github repo</a>!</string>

<string name="app_link">Challenge yourself with probability puzzles &#8212; install the app at https://play.google.com/store/apps/details?id=atorch.statspuzzles</string>
Expand Down Expand Up @@ -244,7 +244,7 @@
<item>For any event A, Pr[A] = 1 - Pr[!A] where !A means \"not A\", i.e. the complement of A. Sometimes it\'s easier to calculate Pr[!A], which in this case is the probability that all 50 families have at least one child of each gender. Start by figuring out the probability that an 8-person family has at least one child of each gender &#8212; call that p. Using the \"not A\" trick will also help you figure out p (look at the bar graph: you need to subtract off the leftmost and rightmost probabilities). Your final answer will be 1 - p^50.</item>
<item>Before Monty does anything, the probability that your initial choice is correct is 1/10. Does that change after he shows you 8 goats?</item>
<item>When tossing two fair dice there are 6*6 = 36 possible outcomes, all equally likely. The image is intended to hint at the solution without giving it away immediately: the circles are brighter when the sum of the two rolls is closer to seven.</item>
<item>If everyone were gay the answer would be 0.9, while if everyone were straight the answer would be 0.1. In the situation we\'re thinking about, the answer is a weighted average of those two numbers. Do you understand why?</item>
<item>If everyone were gay the answer would be 0.9, while if everyone were straight the answer would be 0.1. In the situation we\'re thinking about, the answer is a weighted average of those two numbers. Do you understand why? The reason Susie overestimates by so much is that the gay population is rare: even though she mislabels only 10\% of the (very common) straight people, that small slice is huge compared to the (few) gay people, so it badly inflates her count. What\'s fun is that the very same phenomenon turns up in a setting that looks completely unrelated &#8212; see https://research.fs.usda.gov/treesearch/31722 for the classic paper. It\'s about estimating land cover from satellite imagery, which seems a world away from a puzzle about gay and straight people, and yet the same concept explains both: if you misclassify even a tiny fraction of a common category (say, forest), and mistake it for a rare category, that will cause you to over-estimate the prevalence of the rare group.</item>
<item>A straightforward way to calculate the probability that one die beats another is to condition on the outcome of one of their rolls. For example, if the red die comes up 2, there\'s a 0/3 chance that it will beat blue; but if it comes up 4, it has a 1/3 chance. What if red comes up 9? Do you see how taking an average of those conditional probabilities will give you the correct answer? See https://en.wikipedia.org/wiki/Intransitive_dice and http://singingbanana.com/dice/article.htm for a detailed explanation.</item>
<item>A first hint is to show how life expectancy at birth was calculated: 0.2*(1 + 6 + 20 + 50 + 80) = 31.4. How can you modify that to get the answer we\'re looking for? Conditional on surviving to age 30, the probability of dying before age 30 must be zero. What about the (conditional) probability of dying between age 70 and 90? The numbers in this puzzle are made up, but they are intended to be semi-realistic -- see https://www.bbc.com/future/article/20181002-how-long-did-ancient-people-live-life-span-versus-longevity for a more detailed discussion of life spans in ancient Rome.</item>

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note to self: dog years as another easy or moderate puzzle

<item>An expectation is essentially an average. If you guess $1000, for example, you have a 0.01 chance of being correct, so you\'ll win 0.01 * 1000 + 0.99 * 0 = 10 dollars on average. You\'ll find that the answer is neither the most likely dollar amount (0 dollars), nor the largest (1,000 dollars)!</item>
Expand Down Expand Up @@ -614,6 +614,7 @@
<item></item>
<item>gambler_broke_11_bets_small</item>
<item>vote_counting_small</item>
<item></item>
</string-array>
<string-array name="hints_2">
<item>Linearity of expectation will help: name the foxes 1, 2, &#8230;, 5, and let X_i be an indicator for whether fox i is followed by a hound. The answer is E[X_1 + &#8230; + X_5] = E[X_1] + &#8230; + E[X_5]. Since the probability of being followed by a hound is the same for each of the foxes, the answer simplifies to 5*E[X_1], i.e. all you have to do is calculate the probability that a specific fox is followed by a hound and multiply it by five.</item>
Expand All @@ -634,6 +635,7 @@
<item>This puzzle is straightforward if you know the power series for the exponential function, i.e. that e^x equals the sum from N=0 to infinity of (x^N)/(N!). Try playing around with (e^x + e^(-x)) and (e^x - e^(-x)).</item>
<item>For any even number N, let F(N) denote the number of paths of length N (i.e. wealth trajectories over the course of N bets) that start at a wealth of one, end at a wealth of one, and never hit zero. For example, F(0)=1, F(2)=1 and F(4)=2; the two paths for F(4) are up-up-down-down and up-down-up-down. The solution is related to F(10). Can you express F recursively? Look up the Catalan numbers if you are stuck.</item>
<item>The graph illustrates several sample paths for C\'s net vote count. We\'re looking for the probability that the path never hits zero after the first vote. Several facts will be helpful: first, the probability that the first vote out of the ballot box is for C is 70/100. Second, suppose the path hits zero (e.g. at the orange circle in the illustration): at that point, when the path first hits zero, we\'ve counted an equal number of votes for T and C. We can therefore \"reflect\" the left side of the path &#8212; the portion from the origin to the orange circle &#8212; around the horizontal axis, giving us an alternate path which necessarily has the same probability as the original path. Therefore, conditional on the path hitting zero (e.g. at the orange circle), the first vote removed from the ballot box is equally likely to have been for C as for T. Combine those facts to get the answer.</item>
<item>This is Bayes\' rule with a continuous unknown. The posterior density of X given that you observe 19 people is proportional to the prior density times the likelihood, i.e. proportional to x^19 * e^(-x) for x in [0, 20] &#8212; a Gamma density truncated to the interval [0, 20]. The answer is the integral of x^19 * e^(-x) from 17 to 20, divided by the same integral from 0 to 20. Repeated integration by parts gives the antiderivative you need: the integral of x^n * e^(-x) from 0 to t equals n!*(1 - e^(-t)*(1 + t + t^2/2! + &#8230; + t^n/n!)). You\'ll find that the answer is much larger than the prior probability of 3/20: a long line is strong evidence that the bus is due soon. See https://chalkdustmagazine.com/blog/empty-bus-stop/ for a wonderful discussion of this phenomenon (and of why an empty bus stop is bad news), and see the Regular Bus City problems in https://math.mit.edu/~sheffield/2022600spring/spring2022-600ProblemSet5.pdf for a closely related setup.</item>
</string-array>
<string-array name="puzzles_2">
<item>Five foxes and seven hounds run into a foxhole. While they\'re inside they get all jumbled up, so that all orderings are equally likely.
Expand Down Expand Up @@ -682,6 +684,9 @@
\n\nThe voters\' ballots are placed in a large urn. Before it is opened, the urn is shaken in such a way as to randomly shuffle the ballots, with all orderings equally likely. The urn is then opened and the ballots taken out one by one and tallied.
\n\nGiven that 70 people voted for C (and 30 for T), we know she\'ll end up with a net advantage of 40 votes at the end of the count. There\'s randomness in how we get there, however.
\n\nWhat\'s the probability that C is strictly ahead of T at every point in the counting process?</item>
<item>You want to catch a bus that arrives every 20 minutes, like clockwork &#8212; but you don\'t know the schedule. Model your arrival at the stop as uniformly random: X ~ Uniform([0, 20]), where X=x means the previous bus came x minutes ago, so that the next one arrives in 20-x minutes.
\n\nOther riders show up at the stop according to a Poisson process, at a rate of one person per minute, and the line drops to zero every time a bus arrives (assume the bus has infinite capacity, and that everyone waiting hops on).
\n\nBefore you can see the bus stop, your probability that the bus arrives within the next three minutes is 3/20. When you reach the stop, however, you count 19 people waiting in line. Given the length of the line, what\'s the probability that the bus arrives within the next three minutes?</item>
</string-array>
<string-array name="answers_2">
<item>5*7/12</item>
Expand All @@ -702,6 +707,7 @@
<item>0.5*(1 + e^(-2))</item>
<item>1/42</item>
<item>40/100</item>
<item>(e^(-17)*sum(k,0,19,17^k/k!) - e^(-20)*sum(k,0,19,20^k/k!))/(1 - e^(-20)*sum(k,0,19,20^k/k!))</item>
</string-array>


Expand Down
97 changes: 95 additions & 2 deletions app/src/test/java/atorch/statspuzzles/AnswerCheckerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,107 @@ public class AnswerCheckerTest {

@Test
public void testCorrectAnswer() {
assertEquals(AnswerChecker.Result.CORRECT, AnswerChecker.checkAnswer("2+2", "4"));
assertEquals(AnswerChecker.Result.CORRECT, AnswerChecker.checkAnswer("1/2", "0.5"));
assertEquals(AnswerChecker.Result.CORRECT, AnswerChecker.checkAnswer("2 + 2", "4"));
assertEquals(AnswerChecker.Result.CORRECT, AnswerChecker.checkAnswer("1/ 2", "0.5"));
assertEquals(AnswerChecker.Result.CORRECT, AnswerChecker.checkAnswer("3!", "6.0"));
assertEquals(AnswerChecker.Result.CORRECT, AnswerChecker.checkAnswer("1/(3!)", "0.16666666"));
// Note that we have a Result.INACCURATE version of this test as well
assertEquals(AnswerChecker.Result.CORRECT, AnswerChecker.checkAnswer("1/3", "0.33333333333333"));
assertEquals(AnswerChecker.Result.CORRECT, AnswerChecker.checkAnswer("C(5, 3)", "10"));
}

@Test
public void testCombinations() {
// Test various combination expressions that users might enter
assertEquals(AnswerChecker.Result.CORRECT, AnswerChecker.checkAnswer("C(8, 6)", "28"));
assertEquals(AnswerChecker.Result.CORRECT, AnswerChecker.checkAnswer("C(10, 2)", "45"));
assertEquals(AnswerChecker.Result.CORRECT, AnswerChecker.checkAnswer("C(52, 5)", "2598960"));
}

@Test
public void testEulersNumber() {
// Test expressions involving e
assertEquals(AnswerChecker.Result.CORRECT, AnswerChecker.checkAnswer("1/e", "0.36787944"));
assertEquals(AnswerChecker.Result.CORRECT, AnswerChecker.checkAnswer("e^2", "7.389056"));
assertEquals(AnswerChecker.Result.CORRECT, AnswerChecker.checkAnswer("1-1/e", "0.63212056"));
}

@Test
public void testNegativeExponents() {
// Test negative exponents that users might enter
assertEquals(AnswerChecker.Result.CORRECT, AnswerChecker.checkAnswer("4^-1", "0.25"));
assertEquals(AnswerChecker.Result.CORRECT, AnswerChecker.checkAnswer("2^-3", "0.125"));
assertEquals(AnswerChecker.Result.CORRECT, AnswerChecker.checkAnswer("10 ^ -2", "0.01"));
}

@Test
public void testPercentages() {
// Test percentage expressions
assertEquals(AnswerChecker.Result.CORRECT, AnswerChecker.checkAnswer("50%", "0.5"));
assertEquals(AnswerChecker.Result.CORRECT, AnswerChecker.checkAnswer("25%", "0.25"));
assertEquals(AnswerChecker.Result.CORRECT, AnswerChecker.checkAnswer("100%", "1"));
assertEquals(AnswerChecker.Result.CORRECT, AnswerChecker.checkAnswer("1%", "0.01"));
}

@Test
public void testSpacingAndEquivalentExpressions() {
// Test that spacing variations and mathematically equivalent expressions are accepted
assertEquals(AnswerChecker.Result.CORRECT, AnswerChecker.checkAnswer("C(7,3)", "C(7, 3)"));
assertEquals(AnswerChecker.Result.CORRECT, AnswerChecker.checkAnswer("C(7, 3)", "C(7,4)"));
assertEquals(AnswerChecker.Result.CORRECT, AnswerChecker.checkAnswer("4!", "4 * 3!"));
assertEquals(AnswerChecker.Result.CORRECT, AnswerChecker.checkAnswer("12!/(4!*4!*4!)", "12! / ((4!)^3)"));
assertEquals(AnswerChecker.Result.CORRECT, AnswerChecker.checkAnswer("1/2 + (1/2)*49/99", "1/2+(49/2) * 1/99"));
assertEquals(AnswerChecker.Result.CORRECT, AnswerChecker.checkAnswer("(4/52)*(3/51)", "( 4 * 3 ) / (51 *52)"));
}

@Test
public void testEquivalentFormats() {
// Test that the app accepts equivalent formats as documented in the intro
// .2, 0.2, 20%, and 1/5 should all be equivalent
assertEquals(AnswerChecker.Result.CORRECT, AnswerChecker.checkAnswer("0.2", ".2"));
assertEquals(AnswerChecker.Result.CORRECT, AnswerChecker.checkAnswer("0.2", "20%"));
assertEquals(AnswerChecker.Result.CORRECT, AnswerChecker.checkAnswer("0.2", "1/5"));
assertEquals(AnswerChecker.Result.CORRECT, AnswerChecker.checkAnswer("1/5", "20%"));

// .4, 0.4, 40%, 4/10, and 2*1/5 should all be equivalent
assertEquals(AnswerChecker.Result.CORRECT, AnswerChecker.checkAnswer("0.4", ".4"));
assertEquals(AnswerChecker.Result.CORRECT, AnswerChecker.checkAnswer("0.4", "40%"));
assertEquals(AnswerChecker.Result.CORRECT, AnswerChecker.checkAnswer("0.4", "4/10"));
assertEquals(AnswerChecker.Result.CORRECT, AnswerChecker.checkAnswer("0.4", "2*1/5"));

// Factorial equivalents: 4! = 4*3*2 = 24
assertEquals(AnswerChecker.Result.CORRECT, AnswerChecker.checkAnswer("4!", "4*3*2"));
assertEquals(AnswerChecker.Result.CORRECT, AnswerChecker.checkAnswer("4!", "24"));

// Combination equivalents: C(5,3) = 5!/(3!*2!)
assertEquals(AnswerChecker.Result.CORRECT, AnswerChecker.checkAnswer("C(5,3)", "5!/(3!*2!)"));

// Exponent equivalents: .49^3 = .49*.49*.49
assertEquals(AnswerChecker.Result.CORRECT, AnswerChecker.checkAnswer(".49^3", ".49*.49*.49"));
}

@Test
public void testPiConstant() {
// Test that pi constant works
assertEquals(AnswerChecker.Result.CORRECT, AnswerChecker.checkAnswer("pi", "3.14159265"));
assertEquals(AnswerChecker.Result.CORRECT, AnswerChecker.checkAnswer("pi / 4", "0.78539816"));
assertEquals(AnswerChecker.Result.CORRECT, AnswerChecker.checkAnswer("2*pi", "6.28318531"));
}

@Test
public void testBusStopPuzzleAnswer() {
// The stored answer for the bus stop puzzle (level 2) uses mXparser's iterated sum operator.
// The exact value is Pr[X >= 17 | Y = 19] where the posterior of X is a Gamma(20, 1)
// density truncated to [0, 20]; numerically it is approximately 0.502252198694341.
String correctAnswer =
"(e^(-17)*sum(k,0,19,17^k/k!) - e^(-20)*sum(k,0,19,20^k/k!))/(1 - e^(-20)*sum(k,0,19,20^k/k!))";
assertEquals(AnswerChecker.Result.CORRECT, AnswerChecker.checkAnswer(correctAnswer, "0.5022522"));
assertEquals(AnswerChecker.Result.CORRECT, AnswerChecker.checkAnswer(correctAnswer, "0.502252198694341"));
assertEquals(AnswerChecker.Result.INACCURATE, AnswerChecker.checkAnswer(correctAnswer, "0.5022"));
// The prior probability, before observing the line, is not the correct answer
assertEquals(AnswerChecker.Result.INCORRECT, AnswerChecker.checkAnswer(correctAnswer, "3/20"));
}

@Test
public void testInaccurateAnswer() {
assertEquals(AnswerChecker.Result.INACCURATE, AnswerChecker.checkAnswer("1/3", "0.333"));
Expand All @@ -24,6 +116,7 @@ public void testInaccurateAnswer() {
@Test
public void testIncorrectAnswer() {
assertEquals(AnswerChecker.Result.INCORRECT, AnswerChecker.checkAnswer("1", "2"));
assertEquals(AnswerChecker.Result.INCORRECT, AnswerChecker.checkAnswer("0.50", "0.49"));
// The user's answer is not close enough to be considered merely inaccurate.
assertEquals(AnswerChecker.Result.INCORRECT, AnswerChecker.checkAnswer("1/3", "0.33"));
}
Expand Down
Loading