count/count.rs
1// Copyright (C) 2017 Steve Sprang
2//
3// This program is free software: you can redistribute it and/or modify
4// it under the terms of the GNU General Public License as published by
5// the Free Software Foundation, either version 3 of the License, or
6// (at your option) any later version.
7//
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11// GNU General Public License for more details.
12//
13// You should have received a copy of the GNU General Public License
14// along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16//! Finds all n-card deals that contain no SuperSets.
17//!
18//! The [description of SuperSet](http://magliery.com/Set/SuperSet.html) indicates that the
19//! odds are good that a 9-card deal will contain a SuperSet. It's known that the smallest deal
20//! guaranteed to contain a Set is 21 cards. What is the smallest deal guaranteed to contain a
21//! SuperSet?
22//!
23//! Results on *AMD Ryzen 7 1800x @ 3.9GHz* with 16 threads:
24//!
25//! deal | supersets | no supersets | total | % without | time
26//! ------+-------------------+--------------+-------------------+------------+-----------------
27//! 4 | 63_180 | 1_600_560 | 1_663_740 | 96.20253 % | PT0.003547802S
28//! 5 | 4_696_380 | 20_925_216 | 25_621_596 | 81.67023 % | PT0.050105706S
29//! 6 | 155_521_080 | 169_019_136 | 324_540_216 | 52.07957 % | PT0.658883744S
30//! 7 | 2_808_519_480 | 668_697_120 | 3_477_216_600 | 19.23082 % | PT7.795403335S
31//! 8 | 31_413_675_150 | 750_578_400 | 32_164_253_550 | 2.33358 % | PT37.613810149S
32//! 9 | 260_868_122_190 | 19_712_160 | 260_887_834_350 | 0.00756 % | PT70.994455511S
33//! 10 | 1_878_392_407_320 | 0 | 1_878_392_407_320 | 0.00000 % | PT67.419270885S
34//!
35//! Donald Knuth wrote two very efficient programs that find all the deals that contain no Sets
36//! (SETSET and SETSET-ALL here: <https://cs.stanford.edu/~uno/programs.html>). At some point
37//! I'd like to study these programs and apply the same techniques here.
38//!
39//! As it is, this program runs in about 3 minutes on my machine. It makes use of the fact that
40//! there is an isomorphism between a `core::Card` and its index. It only uses `core::Card`
41//! objects directly when initializing the `SETS` lookup table, and otherwise just works with
42//! the cards by index. It recursively builds up a hand of cards, and abandons branches of the
43//! search tree as soon as the hand contains a SuperSet.
44//!
45//! As implemented, we have to count each deal size explicitly. We will undercount if we also
46//! count smaller deals as we are counting a larger deal size. By abandoning branches of the
47//! search tree as soon as a SuperSet is found, we don't reach every sub-deal that might be
48//! SuperSet-free.
49//!
50
51#[macro_use]
52extern crate clap;
53extern crate core;
54#[macro_use]
55extern crate prettytable;
56extern crate rayon;
57extern crate time;
58
59use prettytable::Table;
60use prettytable::format::consts;
61use rayon::prelude::*;
62use std::ops::Range;
63use std::cmp;
64use time::{Duration, PreciseTime};
65
66use core::card::*;
67use core::deck::cards;
68use core::pair_iter::PairIter;
69use core::utils::pretty_print;
70
71const VERSION: &str = env!("CARGO_PKG_VERSION");
72/// The number of cards composing a SuperSet.
73const SUPERSET_SIZE: usize = 4;
74
75struct Combination {
76 /// Cards available to combine. `usize` stands in for `core::Card` here.
77 deck: Vec<usize>,
78 /// Current combination.
79 hand: Vec<usize>,
80 /// Number of times we've dealt N cards and found no SuperSets.
81 null_count: u64,
82}
83
84struct Count {
85 /// Stuck hands.
86 no_supersets: u64,
87 /// Total possible combinations.
88 combinations: u64,
89 /// Duration of computation.
90 time: Duration,
91}
92
93fn count_null_supersets(deal_size: usize) -> Count {
94 let start_time = PreciseTime::now();
95 let sum = (deal_size - 1..81)
96 .into_par_iter()
97 .map(|x| deal_hands(x, deal_size))
98 .sum();
99
100 Count {
101 no_supersets: sum,
102 combinations: choose(81, deal_size as u64),
103 time: start_time.to(PreciseTime::now()),
104 }
105}
106
107fn deal_hands(start: usize, deal_size: usize) -> u64 {
108 // our deck of cards is really a deck of card indices
109 let cards = (0..81).collect::<Vec<usize>>();
110
111 let mut data = Combination {
112 deck: cards,
113 hand: Vec::with_capacity(deal_size),
114 null_count: 0,
115 };
116
117 data.hand.push(data.deck[start]);
118 deal_another_card(&mut data, (deal_size - 2)..start);
119 data.hand.pop();
120
121 data.null_count
122}
123
124fn deal_another_card(data: &mut Combination, range: Range<usize>) {
125 let depth = range.start;
126
127 for y in range {
128 let next_card = data.deck[y];
129
130 if data.hand.len() >= (SUPERSET_SIZE - 1) && contains_superset(&data.hand, next_card) {
131 // There's already at least one SuperSet, so we can skip this branch
132 continue;
133 }
134
135 if depth == 0 {
136 // The hand is full and it doesn't contain a SuperSet
137 data.null_count += 1;
138 } else {
139 // recursively add another card
140 data.hand.push(next_card);
141 deal_another_card(data, (depth - 1)..y);
142 data.hand.pop();
143 }
144 }
145}
146
147fn generate_table() {
148 let mut table = Table::new();
149 table.set_format(*consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
150 table.set_titles(row![r => "deal", "supersets", "no supersets", "total", "% without", "time"]);
151
152 for deal in 4.. {
153 let count = count_null_supersets(deal);
154
155 // calculate derivable stats
156 let sets = count.combinations - count.no_supersets;
157 let percentage = (count.no_supersets as f64 / count.combinations as f64) * 100.;
158
159 table.add_row(row![r => &deal.to_string(),
160 &pretty_print(sets),
161 &pretty_print(count.no_supersets),
162 &pretty_print(count.combinations),
163 &format!("{:.5} %", percentage),
164 &count.time.to_string()]);
165 table.printstd();
166 println!();
167
168 if count.no_supersets == 0 {
169 break;
170 }
171 }
172}
173
174////////////////////////////////////////////////////////////////////////////////
175// main
176////////////////////////////////////////////////////////////////////////////////
177
178fn main() {
179 clap_app!(count =>
180 (version: VERSION)
181 (about: "Finds all n-card deals that contain no SuperSets."))
182 .get_matches();
183
184 // initialize lookup table
185 build_lookup();
186
187 println!("Finding all n-card deals that contain no SuperSets.");
188 println!("This could take some time...\n");
189 generate_table();
190}
191
192////////////////////////////////////////////////////////////////////////////////
193// Support Functions
194////////////////////////////////////////////////////////////////////////////////
195
196/// Computes the binomial coefficient (n k). This function overflows
197/// for (81 k) where 18 < k < 63. Could use `BigUint`, but this is
198/// sufficient for the values needed here.
199///
200/// https://en.wikipedia.org/wiki/Binomial_coefficient
201fn choose(n: u64, k: u64) -> u64 {
202 let m = cmp::min(k, n - k) + 1;
203 (1..m).fold(1, |product, i| product * (n + 1 - i) / i)
204}
205
206/// Lookup table for Sets.
207static mut SETS: [[usize; 81]; 81] = [[0; 81]; 81];
208
209fn build_lookup() {
210 let cards = cards();
211
212 for (&a, &b) in (0..81).collect::<Vec<_>>().pairs() {
213 let c = (cards[a], cards[b]).complete_set().index();
214 unsafe {
215 SETS[a][b] = c;
216 // `complete_set()` is commutative
217 SETS[b][a] = c;
218 }
219 }
220}
221
222/// Make nested unchecked accesses less clunky.
223macro_rules! lookup {
224 ($a:ident, $b:ident) => {
225 SETS.get_unchecked($a).get_unchecked($b);
226 }
227}
228
229fn is_superset(a: usize, b: usize, c: usize, d: usize) -> bool {
230 unsafe {
231 lookup!(a, b) == lookup!(c, d)
232 || lookup!(a, c) == lookup!(b, d)
233 || lookup!(a, d) == lookup!(b, c)
234 }
235}
236
237/// This function assumes that `hand` does not already contain a
238/// SuperSet. It only tests combinations that include `extra`.
239#[allow(clippy::needless_range_loop)]
240fn contains_superset(hand: &[usize], extra: usize) -> bool {
241 for a in 2..hand.len() {
242 for b in 1..a {
243 for c in 0..b {
244 if is_superset(hand[a], hand[b], hand[c], extra) {
245 return true;
246 }
247 }
248 }
249 }
250
251 false
252}