// Copyright (C) 2017 Steve Sprang
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//! Finds all n-card deals that contain no SuperSets.
//!
//! The [description of SuperSet](http://magliery.com/Set/SuperSet.html) indicates that the
//! odds are good that a 9-card deal will contain a SuperSet. It's known that the smallest deal
//! guaranteed to contain a Set is 21 cards. What is the smallest deal guaranteed to contain a
//! SuperSet?
//!
//! Results on *AMD Ryzen 7 1800x @ 3.9GHz* with 16 threads:
//!
//! deal | supersets | no supersets | total | % without | time
//! ------+-------------------+--------------+-------------------+------------+-----------------
//! 4 | 63_180 | 1_600_560 | 1_663_740 | 96.20253 % | PT0.003547802S
//! 5 | 4_696_380 | 20_925_216 | 25_621_596 | 81.67023 % | PT0.050105706S
//! 6 | 155_521_080 | 169_019_136 | 324_540_216 | 52.07957 % | PT0.658883744S
//! 7 | 2_808_519_480 | 668_697_120 | 3_477_216_600 | 19.23082 % | PT7.795403335S
//! 8 | 31_413_675_150 | 750_578_400 | 32_164_253_550 | 2.33358 % | PT37.613810149S
//! 9 | 260_868_122_190 | 19_712_160 | 260_887_834_350 | 0.00756 % | PT70.994455511S
//! 10 | 1_878_392_407_320 | 0 | 1_878_392_407_320 | 0.00000 % | PT67.419270885S
//!
//! Donald Knuth wrote two very efficient programs that find all the deals that contain no Sets
//! (SETSET and SETSET-ALL here: ). At some point
//! I'd like to study these programs and apply the same techniques here.
//!
//! As it is, this program runs in about 3 minutes on my machine. It makes use of the fact that
//! there is an isomorphism between a `core::Card` and its index. It only uses `core::Card`
//! objects directly when initializing the `SETS` lookup table, and otherwise just works with
//! the cards by index. It recursively builds up a hand of cards, and abandons branches of the
//! search tree as soon as the hand contains a SuperSet.
//!
//! As implemented, we have to count each deal size explicitly. We will undercount if we also
//! count smaller deals as we are counting a larger deal size. By abandoning branches of the
//! search tree as soon as a SuperSet is found, we don't reach every sub-deal that might be
//! SuperSet-free.
//!
#[macro_use]
extern crate clap;
extern crate core;
#[macro_use]
extern crate prettytable;
extern crate rayon;
extern crate time;
use prettytable::Table;
use prettytable::format::consts;
use rayon::prelude::*;
use std::ops::Range;
use std::cmp;
use time::{Duration, PreciseTime};
use core::card::*;
use core::deck::cards;
use core::pair_iter::PairIter;
use core::utils::pretty_print;
const VERSION: &str = env!("CARGO_PKG_VERSION");
/// The number of cards composing a SuperSet.
const SUPERSET_SIZE: usize = 4;
struct Combination {
/// Cards available to combine. `usize` stands in for `core::Card` here.
deck: Vec,
/// Current combination.
hand: Vec,
/// Number of times we've dealt N cards and found no SuperSets.
null_count: u64,
}
struct Count {
/// Stuck hands.
no_supersets: u64,
/// Total possible combinations.
combinations: u64,
/// Duration of computation.
time: Duration,
}
fn count_null_supersets(deal_size: usize) -> Count {
let start_time = PreciseTime::now();
let sum = (deal_size - 1..81)
.into_par_iter()
.map(|x| deal_hands(x, deal_size))
.sum();
Count {
no_supersets: sum,
combinations: choose(81, deal_size as u64),
time: start_time.to(PreciseTime::now()),
}
}
fn deal_hands(start: usize, deal_size: usize) -> u64 {
// our deck of cards is really a deck of card indices
let cards = (0..81).collect::>();
let mut data = Combination {
deck: cards,
hand: Vec::with_capacity(deal_size),
null_count: 0,
};
data.hand.push(data.deck[start]);
deal_another_card(&mut data, (deal_size - 2)..start);
data.hand.pop();
data.null_count
}
fn deal_another_card(data: &mut Combination, range: Range) {
let depth = range.start;
for y in range {
let next_card = data.deck[y];
if data.hand.len() >= (SUPERSET_SIZE - 1) && contains_superset(&data.hand, next_card) {
// There's already at least one SuperSet, so we can skip this branch
continue;
}
if depth == 0 {
// The hand is full and it doesn't contain a SuperSet
data.null_count += 1;
} else {
// recursively add another card
data.hand.push(next_card);
deal_another_card(data, (depth - 1)..y);
data.hand.pop();
}
}
}
fn generate_table() {
let mut table = Table::new();
table.set_format(*consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
table.set_titles(row![r => "deal", "supersets", "no supersets", "total", "% without", "time"]);
for deal in 4.. {
let count = count_null_supersets(deal);
// calculate derivable stats
let sets = count.combinations - count.no_supersets;
let percentage = (count.no_supersets as f64 / count.combinations as f64) * 100.;
table.add_row(row![r => &deal.to_string(),
&pretty_print(sets),
&pretty_print(count.no_supersets),
&pretty_print(count.combinations),
&format!("{:.5} %", percentage),
&count.time.to_string()]);
table.printstd();
println!();
if count.no_supersets == 0 {
break;
}
}
}
////////////////////////////////////////////////////////////////////////////////
// main
////////////////////////////////////////////////////////////////////////////////
fn main() {
clap_app!(count =>
(version: VERSION)
(about: "Finds all n-card deals that contain no SuperSets."))
.get_matches();
// initialize lookup table
build_lookup();
println!("Finding all n-card deals that contain no SuperSets.");
println!("This could take some time...\n");
generate_table();
}
////////////////////////////////////////////////////////////////////////////////
// Support Functions
////////////////////////////////////////////////////////////////////////////////
/// Computes the binomial coefficient (n k). This function overflows
/// for (81 k) where 18 < k < 63. Could use `BigUint`, but this is
/// sufficient for the values needed here.
///
/// https://en.wikipedia.org/wiki/Binomial_coefficient
fn choose(n: u64, k: u64) -> u64 {
let m = cmp::min(k, n - k) + 1;
(1..m).fold(1, |product, i| product * (n + 1 - i) / i)
}
/// Lookup table for Sets.
static mut SETS: [[usize; 81]; 81] = [[0; 81]; 81];
fn build_lookup() {
let cards = cards();
for (&a, &b) in (0..81).collect::>().pairs() {
let c = (cards[a], cards[b]).complete_set().index();
unsafe {
SETS[a][b] = c;
// `complete_set()` is commutative
SETS[b][a] = c;
}
}
}
/// Make nested unchecked accesses less clunky.
macro_rules! lookup {
($a:ident, $b:ident) => {
SETS.get_unchecked($a).get_unchecked($b);
}
}
fn is_superset(a: usize, b: usize, c: usize, d: usize) -> bool {
unsafe {
lookup!(a, b) == lookup!(c, d)
|| lookup!(a, c) == lookup!(b, d)
|| lookup!(a, d) == lookup!(b, c)
}
}
/// This function assumes that `hand` does not already contain a
/// SuperSet. It only tests combinations that include `extra`.
#[allow(clippy::needless_range_loop)]
fn contains_superset(hand: &[usize], extra: usize) -> bool {
for a in 2..hand.len() {
for b in 1..a {
for c in 0..b {
if is_superset(hand[a], hand[b], hand[c], extra) {
return true;
}
}
}
}
false
}