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}