diff --git a/app/build.gradle b/app/build.gradle
index 9c95df5..1c232ea 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -24,7 +24,7 @@ android {
defaultConfig {
applicationId "atorch.statspuzzles"
- minSdkVersion 19
+ minSdkVersion 21
targetSdkVersion 35
versionCode 32
versionName "4.0"
@@ -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"
}
diff --git a/app/libs/mXparser-v.4.2.0-jdk.1.7.jar b/app/libs/mXparser-v.4.2.0-jdk.1.7.jar
deleted file mode 100644
index db068df..0000000
Binary files a/app/libs/mXparser-v.4.2.0-jdk.1.7.jar and /dev/null differ
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 9d67dd7..0cb5b78 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -2,7 +2,7 @@
Probability Puzzles
This is version 4.0, released in October 2025 — 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\u00A0Dartmouth book and these\u00A0MIT notes. You could also read through some probability problems at\u00A0math.stackexchange.com. If you prefer to watch lectures on youtube, try\u00A0these or\u00A0these.
- \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\u00A0C(5,3) with a capital C for \"5 choose 3.\" You can also type lowercase e for the base of the natural logarithm. Visit\u00A0mathparser.org 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\u00A0C(5,3) with a capital C for \"5 choose 3.\" You can also type lowercase e for the base of the natural logarithm. Visit\u00A0mathparser.org for more information.
\n\nIf you\'d like to contribute to the app (or peek at solutions), have a look at its\u00A0github repo!
Challenge yourself with probability puzzles — install the app at https://play.google.com/store/apps/details?id=atorch.statspuzzles
@@ -244,7 +244,7 @@
- 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 — 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.
- Before Monty does anything, the probability that your initial choice is correct is 1/10. Does that change after he shows you 8 goats?
- 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.
- - 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?
+ - 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 — 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.
- 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.
- 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.
- 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)!
@@ -614,6 +614,7 @@
- gambler_broke_11_bets_small
- vote_counting_small
+
- Linearity of expectation will help: name the foxes 1, 2, …, 5, and let X_i be an indicator for whether fox i is followed by a hound. The answer is E[X_1 + … + X_5] = E[X_1] + … + 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.
@@ -634,6 +635,7 @@
- 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)).
- 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.
- 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 — the portion from the origin to the orange circle — 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.
+ - 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] — 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! + … + 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.
- 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.
@@ -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?
+ - You want to catch a bus that arrives every 20 minutes, like clockwork — 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?
- 5*7/12
@@ -702,6 +707,7 @@
- 0.5*(1 + e^(-2))
- 1/42
- 40/100
+ - (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!))
diff --git a/app/src/test/java/atorch/statspuzzles/AnswerCheckerTest.java b/app/src/test/java/atorch/statspuzzles/AnswerCheckerTest.java
index a97c1de..b76c208 100644
--- a/app/src/test/java/atorch/statspuzzles/AnswerCheckerTest.java
+++ b/app/src/test/java/atorch/statspuzzles/AnswerCheckerTest.java
@@ -7,8 +7,8 @@ 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
@@ -16,6 +16,98 @@ public void testCorrectAnswer() {
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"));
@@ -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"));
}