forked from nayuki/Project-Euler-solutions
-
Notifications
You must be signed in to change notification settings - Fork 0
/
p172.java
145 lines (124 loc) · 6.45 KB
/
p172.java
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
/*
* Solution to Project Euler problem 172
* By Nayuki Minase
*
* http://nayuki.eigenstate.org/page/project-euler-solutions
* https://github.com/nayuki/Project-Euler-solutions
*/
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
public final class p172 implements EulerSolution {
public static void main(String[] args) {
System.out.println(new p172().run());
}
// Highly customizable!
private static final int LENGTH = 18; // >= 1
private static final int MAX_COUNT = 3; // >= ceiling(LENGTH / BASE), else the result is 0
private static final int BASE = 10; // >= 2
/*
* Let's explain how to solve this problem mostly by examples rather than by rigorous arguments.
*
* We want to generate all the sequences of 18 decimal digits where each digit value (from '0' to '9') is used 0 to 3 times.
* We can partition the set of all possible sequences by considering, for each sequence, the multiset of digit value repetition counts.
* For example, the number 111222333444555666 has '1' used 3 times, '2' used 3 times, ..., and '6' used 3 times.
* When we look at these repetition counts, we see that 3 + 3 + 3 + 3 + 3 + 3 = 18. We always need them to sum to 18 ('LENGTH').
*
* There are sequences with other repetition counts too. For example: 121212333444567890, the rep counts are 1 + 3 + 3 + 3 + 3 + 1 + 1 + 1 + 1 + 1.
* In fact, there are exactly 17 ways (partitions) to express 18 as an unordered sum of 10 terms with each term from 0 to 3:
* - 3 + 3 + 3 + 3 + 3 + 3 + 0 + 0 + 0 + 0
* - 3 + 3 + 3 + 3 + 3 + 2 + 1 + 0 + 0 + 0
* - 3 + 3 + 3 + 3 + 3 + 1 + 1 + 1 + 0 + 0
* - 3 + 3 + 3 + 3 + 2 + 2 + 2 + 0 + 0 + 0
* - 3 + 3 + 3 + 3 + 2 + 2 + 1 + 1 + 0 + 0
* - 3 + 3 + 3 + 3 + 2 + 1 + 1 + 1 + 1 + 0
* - 3 + 3 + 3 + 3 + 1 + 1 + 1 + 1 + 1 + 1
* - 3 + 3 + 3 + 2 + 2 + 2 + 2 + 1 + 0 + 0
* - 3 + 3 + 3 + 2 + 2 + 2 + 1 + 1 + 1 + 0
* - 3 + 3 + 3 + 2 + 2 + 1 + 1 + 1 + 1 + 1
* - 3 + 3 + 2 + 2 + 2 + 2 + 2 + 2 + 0 + 0
* - 3 + 3 + 2 + 2 + 2 + 2 + 2 + 1 + 1 + 0
* - 3 + 3 + 2 + 2 + 2 + 2 + 1 + 1 + 1 + 1
* - 3 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 1 + 0
* - 3 + 2 + 2 + 2 + 2 + 2 + 2 + 1 + 1 + 1
* - 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 0
* - 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 1 + 1
* For example, the number 912349441125323088 is associated with the partition (3 + 3 + 3 + 3 + 2 + 2 + 1 + 1 + 0 + 0),
* because '1' is used 3 times, '2' is used 3 times, '3' is used 3 times, '8' is used 2 times, '9' is used 2 times,
* '0' is used 1 time, '5' is used 1 time, '6' is used 0 times, and '7' is used 0 times.
*
* For each partition, we want to take the 10 decimal digit values and assign them to the terms of the partition. Here is one example assignment:
* Frequency: 3 3 3 3 | 2 2 | 1 1 | 0 0
* Digit value: 1 2 3 4 | 8 9 | 0 5 | 6 7
* But note that order does not matter if the frequency is the same - for example, this means the same as above:
* Frequency: 3 3 3 3 | 2 2 | 1 1 | 0 0
* Digit value: 4 1 3 2 | 8 9 | 5 0 | 7 6
*
* For a given partition, how many ways are there to assign digit values to the frequencies?
* Considering the frequencies and the digit values each as a length-10 sequence (like above),
* we know that there are 10! raw ways to arrange the digit values. But order within a frequency does not matter.
* So we divide by the factorial of the repetition count of each frequency. In the example above, the answer is 10! / (4! 2! 2! 2!).
* (Note that this is a multinomial coefficient.)
*
* Now that we have a partition and a digit value assignment to the frequencies, we can generate permutations.
* For the example above, one possible sequence (and the lexicographically lowest) is 011122233344458899 (length 18).
* If we permute this sequence, the partition and digit-frequency assignments will remain the same.
* So we want to count how many permutations this sequence has.
*
* Given a partition and a digit-frequency assignment, how many sequences have this classification?
* Because there are 18 digits, there are 18! raw arrangements of digits for the sequence.
* But for each digit value, it is repeated k times, so we need to divide by k! to suppress identical-looking arrangements.
* In this example, there are 18! / (3! 3! 3! 3! 2! 2! 2! 2! 2! 2!) arrangements.
*
* Now, all of the arguments above have no made use of the specific digit values, so there is a certain symmetry in the set of desired sequences.
* In particular, this means exactly 9/10th of all items have a leading zero, hence we multiply by 9/10 to get the final answer.
*/
public String run() {
BigInteger ways = partitionAndCount(LENGTH, MAX_COUNT, new ArrayList<Integer>());
// Multiply by (base - 1) / base to discount sequences with leading zeros
BigInteger BASE_BI = BigInteger.valueOf(BASE);
ways = ways.multiply(BASE_BI.subtract(BigInteger.ONE));
ways = divideExactly(ways, BASE_BI);
return ways.toString();
}
// Expresses 'LENGTH' as a sum of 'BASE' non-increasing terms, where terms to be added are in the range [0, max].
// e.g. partitionAndCount(7, 2, [3, 3, 2, 2, 1]) asks us to express 18 as a sum of 5 more terms,
// where the new terms have a sum of 7 and each is no greater than 2 and all terms are non-increasing.
private BigInteger partitionAndCount(int sum, int max, List<Integer> terms) {
if (terms.size() == BASE) {
if (sum == 0)
return countWays(terms);
else
return BigInteger.ZERO;
} else {
BigInteger result = BigInteger.ZERO;
for (int i = Math.min(max, sum); i >= 0; i--) {
terms.add(i);
result = result.add(partitionAndCount(sum - i, i, terms));
terms.remove(terms.size() - 1);
}
return result;
}
}
private BigInteger countWays(List<Integer> freqs) {
// The number of times each frequency value occurs
int[] histogram = new int[MAX_COUNT + 1];
for (int x : freqs)
histogram[x]++;
// Multinomial coefficient: BASE! / (histogram[0]! * histogram[1]! * ...)
BigInteger ways = Library.factorial(BASE);
for (int x : histogram)
ways = ways.divide(Library.factorial(x));
// Multinomial coefficient: LENGTH! / (freqs[0]! * freqs[1]! * ...)
ways = ways.multiply(Library.factorial(LENGTH));
for (int x : freqs)
ways = ways.divide(Library.factorial(x));
return ways;
}
private static BigInteger divideExactly(BigInteger x, BigInteger y) {
BigInteger[] temp = x.divideAndRemainder(y);
if (temp[1].signum() != 0)
throw new IllegalArgumentException("Not divisible");
return temp[0];
}
}