Book Store
1. Readme
书店
为了尝试鼓励,畅销 5 书系列书籍的销售,书店决定提供多书购买的折扣。
这五本书中的任何一本都要花 8 美元.
但是,如果您购买两本不同的书籍,那么这两本书将获得 5%的折扣。
如果您购买 3 本不同的书籍,您将获得 10%的折扣。
如果您购买 4 本不同的书籍,您将获得 20%的折扣。
如果您全部购买 5 件,即可获得 25%的折扣。
注意:如果您购买了 4 本书,但不同的书只有 3 本,那么您可以在 3 本书中获得 10%的折扣,但是第 4 本书的价格仍为 8 美元。
你的任务是写一段代码,来计算购物车(这 5 本书的任意搭配)的价格(只包含同一系列的书籍),给予尽可能大的折扣。
例如,购物车若是下面的书,那价格是多少?
- 第一本书的 2 份
- 第二本书的 2 份
- 第三本书的 2 份
- 第四本书的 1 份
- 第五本书的 1 份
将这 8 本书分组的一种方法是:
- 第 1 组 5 本书 -> 25%折扣(第 1,第 2,第 3,第 4,第 5)
- 还有 1 组 3 本书 -> 10%折扣(第 1 名,第 2 名,第 3 名)
这将总共给出:
- 5 本书,25%的折扣
- 还有 3 本书可享受 10%的折扣
导致:
- 5 x(8 - 2.00)== 5 x 6.00 == $ 30.00
- 还有 3 x(8 - 0.80)== 3 x 7.20 == 21.60 美元
总计 51.60 美元
但是,将这 8 本书分组的另一种方法是:
- 第 1 组 4 本书 -> 20%折扣(第 1,第 2,第 3,第 4)
- 还有 1 组 4 本书 -> 20%折扣(第 1,第 2,第 3,第 5)
这将总共给出:
- 4 本书,20%的折扣
- 还有 4 本书,20%的折扣
导致:
- 4 x(8 - 1.60)== 4 x 6.40 == 25.60 美元
- 还有 4 x(8 - 1.60)== 4 x 6.40 == 25.60 美元
总计 51.20 美元
51.20 美元是最大折扣的价格.
资源
灵感来自 Cyber-Dojo 的哈利波特卡塔.http://cyber-dojo.org
2. 开始你的表演
pub fn lowest_price(books: &[u32]) -> u32 { unimplemented!( "Find the lowest price of the bookbasket with books {:?}", books ) }
3. 测试代码查看
# #![allow(unused_variables)] #fn main() { // Tests for book-store // // Generated by [script][script] using [canonical data][canonical-data] // // [script]: https://github.com/exercism/rust/blob/master/bin/init_exercise.py // [canonical-data]: https://raw.githubusercontent.com/exercism/problem-specifications/master/exercises/book-store/canonical_data.json /// Process a single test case for the property `total` /// /// All cases for the `total` property are implemented /// in terms of this function. /// /// Expected input format: ('basket', 'targetgrouping') fn process_total_case(input: (Vec<u32>, Vec<Vec<u32>>), expected: u32) { assert_eq!(lowest_price(&input.0), expected) } // Return the total basket price after applying the best discount. // Calculate lowest price for a shopping basket containing books only from // a single series. There is no discount advantage for having more than // one copy of any single book in a grouping. #[test] /// Only a single book fn test_only_a_single_book() { process_total_case((vec![1], vec![vec![1]]), 800); } #[test] //#[ignore] /// Two of the same book fn test_two_of_the_same_book() { process_total_case((vec![2, 2], vec![vec![2], vec![2]]), 1_600); } #[test] //#[ignore] /// Empty basket fn test_empty_basket() { process_total_case((vec![], vec![]), 0); } #[test] //#[ignore] /// Two different books fn test_two_different_books() { process_total_case((vec![1, 2], vec![vec![1, 2]]), 1_520); } #[test] //#[ignore] /// Three different books fn test_three_different_books() { process_total_case((vec![1, 2, 3], vec![vec![1, 2, 3]]), 2_160); } #[test] //#[ignore] /// Four different books fn test_four_different_books() { process_total_case((vec![1, 2, 3, 4], vec![vec![1, 2, 3, 4]]), 2_560); } #[test] //#[ignore] /// Five different books fn test_five_different_books() { process_total_case((vec![1, 2, 3, 4, 5], vec![vec![1, 2, 3, 4, 5]]), 3_000); } #[test] //#[ignore] /// Two groups of four is cheaper than group of five plus group of three fn test_two_groups_of_four_is_cheaper_than_group_of_five_plus_group_of_three() { process_total_case( ( vec![1, 1, 2, 2, 3, 3, 4, 5], vec![vec![1, 2, 3, 4], vec![1, 2, 3, 5]], ), 5_120, ); } #[test] //#[ignore] /// Group of four plus group of two is cheaper than two groups of three fn test_group_of_four_plus_group_of_two_is_cheaper_than_two_groups_of_three() { process_total_case( (vec![1, 1, 2, 2, 3, 4], vec![vec![1, 2, 3, 4], vec![1, 2]]), 4_080, ); } #[test] //#[ignore] /// Two each of first 4 books and 1 copy each of rest fn test_two_each_of_first_4_books_and_1_copy_each_of_rest() { process_total_case( ( vec![1, 1, 2, 2, 3, 3, 4, 4, 5], vec![vec![1, 2, 3, 4, 5], vec![1, 2, 3, 4]], ), 5_560, ); } #[test] //#[ignore] /// Two copies of each book fn test_two_copies_of_each_book() { process_total_case( ( vec![1, 1, 2, 2, 3, 3, 4, 4, 5, 5], vec![vec![1, 2, 3, 4, 5], vec![1, 2, 3, 4, 5]], ), 6_000, ); } #[test] //#[ignore] /// Three copies of first book and 2 each of remaining fn test_three_copies_of_first_book_and_2_each_of_remaining() { process_total_case( ( vec![1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 1], vec![vec![1, 2, 3, 4, 5], vec![1, 2, 3, 4, 5], vec![1]], ), 6_800, ); } #[test] //#[ignore] /// Three each of first 2 books and 2 each of remaining books fn test_three_each_of_first_2_books_and_2_each_of_remaining_books() { process_total_case( ( vec![1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 1, 2], vec![vec![1, 2, 3, 4, 5], vec![1, 2, 3, 4, 5], vec![1, 2]], ), 7_520, ); } #[test] //#[ignore] /// Four groups of four are cheaper than two groups each of five and three fn test_four_groups_of_four_are_cheaper_than_two_groups_each_of_five_and_three() { process_total_case( ( vec![1, 1, 2, 2, 3, 3, 4, 5, 1, 1, 2, 2, 3, 3, 4, 5], vec![ vec![1, 2, 3, 4], vec![1, 2, 3, 5], vec![1, 2, 3, 4], vec![1, 2, 3, 5], ], ), 10_240, ); } #}
4. 答案
# #![allow(unused_variables)] #fn main() { use std::cell::RefCell; use std::cmp::Ordering; use std::collections::hash_map::DefaultHasher; use std::collections::{BTreeSet, HashSet}; use std::hash::{Hash, Hasher}; use std::mem; type Book = u32; type GroupedBasket = Vec<Group>; type Price = u32; const BOOK_PRICE: Price = 800; #[derive(Debug, Clone, PartialEq, Eq)] struct Group(RefCell<BTreeSet<Book>>); impl Group { fn new() -> Group { Group(RefCell::new(BTreeSet::new())) } fn new_containing(book: Book) -> Group { let g = Group::new(); g.0.borrow_mut().insert(book); g } fn price(&self) -> Price { (self.0.borrow().len() as Price) * BOOK_PRICE * match self.0.borrow().len() { 2 => 95, 3 => 90, 4 => 80, 5 => 75, _ => 100, } / 100 } } impl Ord for Group { // we want to order groups first by qty contained DESC, then by lowest value ASC fn cmp(&self, other: &Group) -> Ordering { match other.0.borrow().len().cmp(&self.0.borrow().len()) { Ordering::Equal => { if self.0.borrow().len() == 0 { Ordering::Equal } else { self.0 .borrow() .iter() .next() .unwrap() .cmp(other.0.borrow().iter().next().unwrap()) } } otherwise => otherwise, } } } impl PartialOrd for Group { fn partial_cmp(&self, other: &Group) -> Option<Ordering> { Some(self.cmp(other)) } } impl Hash for Group { fn hash<H: Hasher>(&self, hasher: &mut H) { self.0.borrow().hash(hasher); } } fn basket_price(basket: &GroupedBasket) -> Price { basket.iter().map(|g| g.price()).sum() } /// Compute the hash of a GroupedBasket /// /// Note that we don't actually care at all about the _values_ within /// the groups, only their lengths. Therefore, let's hash not the actual /// GB but its lengths. fn hash_of(basket: &GroupedBasket) -> u64 { let lengths = basket .iter() .map(|g| g.0.borrow().len()) .collect::<Vec<_>>(); let mut hasher = DefaultHasher::new(); lengths.hash(&mut hasher); hasher.finish() } pub fn lowest_price(books: &[Book]) -> Price { DecomposeGroups::new(books) .map(|gb| basket_price(&gb)) .min() .unwrap_or(0) } struct DecomposeGroups { prev_states: HashSet<u64>, next: Option<GroupedBasket>, } impl Iterator for DecomposeGroups { type Item = GroupedBasket; fn next(&mut self) -> Option<Self::Item> { // our goal here: produce a stream of valid groups, differentiated by their // counts, from most compact to most dispersed. // // Algorithm: // - Start with the most compact groups possible // - If the number of groups == 0 or the max population of any group == 1, return None // - For every item in the most populous group: // - Try removing it and adding it to a smaller group. // - Can any smaller group accept it? if yes, move it there and return // - If it cannot be added to any smaller group, try the next item from this set // - If no item from the most populous group can be added to any smaller group, // then move the last item from the most populous group into a new group, alone, // and return let return_value = self.next.clone(); if let Some(groups) = mem::replace(&mut self.next, None) { if !(groups.is_empty() || groups.iter().all(|g| g.0.borrow().len() == 1)) { let mut hypothetical; for mpg_book in groups[0].0.borrow().iter() { for (idx, other_group) in groups[1..].iter().enumerate() { if !other_group.0.borrow().contains(mpg_book) { hypothetical = groups.clone(); hypothetical[0].0.borrow_mut().remove(mpg_book); hypothetical[1 + idx].0.borrow_mut().insert(*mpg_book); hypothetical.sort(); let hypothetical_hash = hash_of(&hypothetical); if !self.prev_states.contains(&hypothetical_hash) { self.prev_states.insert(hypothetical_hash); mem::replace(&mut self.next, Some(hypothetical)); return return_value; } } } } // we've gone through all the items of the most populous group, // and none of them can be added to any other existing group. // We need to create a new group; let book = { let backing_bt = groups[0].0.borrow(); let mut book_iter = backing_bt.iter(); book_iter.next().unwrap().clone() }; hypothetical = groups.clone(); hypothetical[0].0.borrow_mut().remove(&book); hypothetical.push(Group::new_containing(book)); hypothetical.sort(); self.prev_states.insert(hash_of(&hypothetical)); mem::replace(&mut self.next, Some(hypothetical)); } } return_value } } impl DecomposeGroups { fn new(books: &[Book]) -> DecomposeGroups { let mut book_groups = GroupedBasket::new(); 'nextbook: for book in books { for idx in 0..book_groups.len() { if !book_groups[idx].0.borrow().contains(&book) { book_groups[idx].0.borrow_mut().insert(*book); continue 'nextbook; } } // if we're here, we still haven't found a place for the book. // better add it to a new group book_groups.push(Group::new_containing(*book)); } book_groups.sort(); DecomposeGroups { next: Some(book_groups), prev_states: HashSet::new(), } } } #}