小朋友学TopCoder(5):SRM144 DIV1 550-point

96
海天一树X
2017.12.11 18:38* 字数 1645

Problem Statement

In most states, gamblers can choose from a wide variety of different lottery games. The rules of a lottery are defined by two integers (choices and blanks) and two boolean variables (sorted and unique). choices represents the highest valid number that you may use on your lottery ticket. (All integers between 1 and choices, inclusive, are valid and can appear on your ticket.) blanks represents the number of spots on your ticket where numbers can be written.

The sorted and unique variables indicate restrictions on the tickets you can create. If sorted is set to true, then the numbers on your ticket must be written in non-descending order. If sorted is set to false, then the numbers may be written in any order. Likewise, if unique is set to true, then each number you write on your ticket must be distinct. If unique is set to false, then repeats are allowed.

Here are some example lottery tickets, where choices = 15 and blanks = 4:
{3, 7, 12, 14} -- this ticket is unconditionally valid.
{13, 4, 1, 9} -- because the numbers are not in nondescending order, this ticket is valid only if sorted = false.
{8, 8, 8, 15} -- because there are repeated numbers, this ticket is valid only if unique = false.
{11, 6, 2, 6} -- this ticket is valid only if sorted = false and unique = false.

Given a list of lotteries and their corresponding rules, return a list of lottery names sorted by how easy they are to win. The probability that you will win a lottery is equal to (1 / (number of valid lottery tickets for that game)). The easiest lottery to win should appear at the front of the list. Ties should be broken alphabetically (see example 1).

Definition

Class: Lottery
Method: sortByOdds
Parameters: String[]
Returns: String[]
Method signature: String[] sortByOdds(String[] rules)
(be sure your method is public)

Limits

Time limit (s): 2.000
Memory limit (MB): 64

Constraints

  • rules will contain between 0 and 50 elements, inclusive.
  • Each element of rules will contain between 11 and 50 characters, inclusive.
  • Each element of rules will be in the format "<NAME>:<CHOICES><BLANKS><SORTED><UNIQUE>" (quotes for clarity). The underscore character represents exactly one space. The string will have no leading or trailing spaces.
  • <NAME> will contain between 1 and 40 characters, inclusive, and will consist of only uppercase letters ('A'-'Z') and spaces (' '), with no leading or trailing spaces.
  • <CHOICES> will be an integer between 10 and 100, inclusive, with no leading zeroes.
  • <BLANKS> will be an integer between 1 and 8, inclusive, with no leading zeroes.
  • <SORTED> will be either 'T' (true) or 'F' (false).
  • <UNIQUE> will be either 'T' (true) or 'F' (false).
  • No two elements in rules will have the same name.

Examples

0)
{"PICK ANY TWO: 10 2 F F"
,"PICK TWO IN ORDER: 10 2 T F"
,"PICK TWO DIFFERENT: 10 2 F T"
,"PICK TWO LIMITED: 10 2 T T"}

Returns: 
{ "PICK TWO LIMITED",
  "PICK TWO IN ORDER",
  "PICK TWO DIFFERENT",
  "PICK ANY TWO" }

The "PICK ANY TWO" game lets either blank be a number from 1 to 10. Therefore, there are 10 * 10 = 100 possible tickets, and your odds of winning are 1/100.
The "PICK TWO IN ORDER" game means that the first number cannot be greater than the second number. This eliminates 45 possible tickets, leaving us with 55 valid ones. The odds of winning are 1/55.
The "PICK TWO DIFFERENT" game only disallows tickets where the first and second numbers are the same. There are 10 such tickets, leaving the odds of winning at 1/90.
Finally, the "PICK TWO LIMITED" game disallows an additional 10 tickets from the 45 disallowed in "PICK TWO IN ORDER". The odds of winning this game are 1/45.

1)
{"INDIGO: 93 8 T F",
 "ORANGE: 29 8 F T",
 "VIOLET: 76 6 F F",
 "BLUE: 100 8 T T",
 "RED: 99 8 T T",
 "GREEN: 78 6 F T",
 "YELLOW: 75 6 F F"}

Returns: { "RED",  "ORANGE",  "YELLOW",  "GREEN",  "BLUE",  "INDIGO",  "VIOLET" }

Note that INDIGO and BLUE both have the exact same odds (1/186087894300). BLUE is listed first because it comes before INDIGO alphabetically.

2)
{}

Returns: { }

Empty case

分析

(一)例题分析(以Example 0为例)

(1)对于“PICK ANY TWO: 10 2 F F”而言,数字不排序可重复,总共有10 ^ 2种彩票组合。你抽到的那张彩票,获奖概率是1/100。

(2)对于"PICK TWO DIFFERENT: 10 2 F T",数字不排序不重复,总共有A(10, 2) = 90种彩票组合。你抽到的那张彩票,获奖概率是1/90。

(3)对于"PICK TWO LIMITED: 10 2 T T",数字非降序不重复(即升序),总共的彩票组合数为C(10, 2) = 45。
注意,这里只是数量上等价于组合数,意义还是有所不同的。因为C(10, 2)是表示从10个里不分顺序任取两个,比如(10,9)和(9,10)是同一种组合。而升序排列的话,(10,9)是不符合要求的,(9,10)才能算一种。
所以升序排序只是数量上等价于组合数,意义并不一样。

(4)对于"PICK TWO IN ORDER: 10 2 T F”,数字非降序可重复。
因为数字可重复,所以非降序不代表升序。比如(9,9)就符合题意,但并不是升序。
规律:设非降序可重复的取法为Get(n, k),设则Get(n, k) = C(n + k - 1, k)。
所以这里共有Get(10,2) = C(10 + 2 - 1, 2) = 55种彩票组合。你抽到的那张彩票,获奖概率是1/55。

下面用数字直观说明一下Get(n, k) = C(n + k - 1, k)的道理。
例1:从整数1~10中取两个数,取法为非降序可重复。求总共有多少种取法

10 10
9 10
9 9
8 10
8 9
8 8
…… ……

例2:从整数1~11中取两个数,取法为升序(升序说明不可重复)。求总共有多少种取法

10 11
9 11
9 10
8 11
8 10
8 9
…… ……

注意:
从例1和例2的表格可以观察到,这两种的取法总数是一样的。
例2的取法数在数量上等于C(11, 2)
例1的取法总数Get(10, 2) = C(11, 2) = C(10 + 2 - 1, 2)

例3:从整数1~3中取三个数,取法为非降序可重复。求总共有多少种取法

3 3 3
2 3 3
2 2 3
2 2 2
1 3 3
1 2 3
1 2 2
1 1 3
1 1 2
1 1 1

例4:从整数1~4中取三个数,取法为升序(升序说明不可重复)。求总共有多少种取法

3 4 5
2 4 5
2 3 5
2 3 4
1 4 5
1 3 5
1 3 4
1 2 5
1 2 4
1 2 3

注意:
从例1和例2的表格可以观察到,这两种的取法总数是一样的。
例4的取法在数量上等于C(5, 3)
例3的取法总数Get(3, 3) = C(5, 3) = C(3+3-1, 3)

(二)数据类型选择

(1)choices取值范围为[10, 100]
Java语言byte类型的取值范围为[-2 ^ 7, 2 ^ 7 - 1],即[-128, 127]。所以若使用java,choices定义成byte就行。
C++里没有byte类型,只能定义成int型。

(2)blanks的取值范围为[1, 8]
若使用Java,blanks定义为byte类型;
若使用C++,blanks定义为int类型。

(3)彩票组合数totalTickets
choices的最大值为100,blanks的最大值为8,彩票组合数的可能的最大数:从100个数里有重复地取8位,有100 ^ 8 = 10 ^ 16种组合。

Java中,int类型为4字节,long类型为8字节。int类型的取值范围为[-2 ^ 31, 2 ^ 31 - 1],即[-2147483648, 2147483647],放不下10 ^ 16。
long类型的取值范围为[-2 ^ 63, 2 ^ 63 - 1],即[-9223372036854775808, 9223372036854775807],约为[-2 ^ 19, 2 ^ 19],放得下10 ^ 16。
所以Java中需要把totalTickets定义为long类型。

C++中,int和long都是4字节,放不下10 ^ 16。long long为8 个字节,放得下10 ^ 16。
所以C++中需要把totalTickets定义为long long类型

Java程序

import java.util.List;
import java.util.ArrayList;
import java.util.Collections;

public class Lottery {
    public String[] sortByOdds(String[] rules) {
        List<Rule> listOfRules = new ArrayList<Rule>();
        for(String ruleDesc: rules) {
            listOfRules.add(new Rule(ruleDesc));
        }
        
        // 按正序(升序)排序,会调用Rule的compareTo方法
        Collections.sort(listOfRules);
        
        // 把排序后的名字放到sortedRules,即是最终所求的结果
        String[] sortedRules = new String[rules.length];
        int i = 0;
        for(Rule rule : listOfRules) {
            sortedRules[i++] = rule.name;
        }
        
        return sortedRules;
    }
    
    // 内部类
    class Rule implements Comparable<Rule> {
        // Java四种访问权限:public, protected, private, default
        // 如果不显式写出,则为default,同一个包内都可以访问
        // 又因为这是个内部类,所以下面的属性只有Lottery才能访问
        String name;
        byte choices;
        byte blanks;
        boolean sorted;         // 是否排序
        boolean unique;         // 是否不重复
        long totalTickets;      // 有多少种票
        double winningOdds;     // 中奖的概率
        
        // 构造函数
        Rule (String description) {
            String[] nameAndRules = description.split(": ");
            name = nameAndRules[0];
            String[] rules = nameAndRules[1].split(" ");
            choices = (byte)Integer.parseInt(rules[0]);
            blanks = (byte)Integer.parseInt(rules[1]);
            sorted = rules[2].equals("T") ? true : false;
            unique = rules[3].equals("T") ? true : false;
            totalTickets =  getTotalTickets();
            winningOdds = 1.0 / totalTickets;
        }
        
        // 共有多少种票
        private long getTotalTickets() {
            if(!sorted) {
                if(unique) {
                    return permutation(choices, blanks);
                } else {
                    return (long)Math.pow(choices, blanks);
                }
            } else {
                if(unique) {
                    return combination(choices, blanks);
                } else {
                    return combination(choices + blanks - 1, blanks);
                }
            }
        }
        
        // 递归求排列数
        private long permutation(int n, int k) {
            if(0 == k) {
                return 1;
            } else {
                return permutation(n, k - 1) * (n - k + 1);
            }
        }
        
        // 递归求组合数
        private long combination(int n, int k) {
            if(0 == k) {
                return 1;
            } else {
                return combination(n, k - 1) * (n - k + 1) / k;
            } 
        }
        
        // 题目要求,赢率大(总票数小)的排在前,赢率小(总票数大)的排在后
        // 相同赢率,按字母升序排列(见例1)
        public int compareTo(Rule that) {
            if(this.totalTickets < that.totalTickets) {
                return -1;
            } else if(this.totalTickets > that.totalTickets) {
                return 1;
            } else {
                return this.name.compareTo(that.name);
            }
        }
    }
    
    // 测试结果,可不提交到TopCoder Arena中
    public static void main(String[] args) {
        Lottery lot = new Lottery();
        String[] rules ={"PICK ANY TWO: 10 2 F F", 
                        "PICK TWO IN ORDER: 10 2 T F", 
                        "PICK TWO DIFFERENT: 10 2 F T", 
                        "PICK TWO LIMITED: 10 2 T T"};
        // String[] rules = {"INDIGO: 93 8 T F",
                        // "ORANGE: 29 8 F T",
                        // "VIOLET: 76 6 F F",
                        // "BLUE: 100 8 T T",
                        // "RED: 99 8 T T",
                        // "GREEN: 78 6 F T",
                        // "YELLOW: 75 6 F F"};
        // String[] rules = {};
        String[] result = lot.sortByOdds(rules);
        
        for(int i = 0; i < result.length; i++) {
            System.out.println(result[i]);
        }
    }
}

运行结果:

result.png

程序解释:
(1)这里Rule是个内部类。在Java中,假如一个类只被另一个类使用,那么可以把这个类定义为另一个类的内部类。
(2)求排列和组合的算法,可以参考
小朋友学算法(7):求排列数和组合数
(3)compareTo方法是Comparable接口提供的唯一方法
(4)因为Lottery中的注释部分采用中文写,编码格式为UTF-8,所以Windows下要用“javac -encoding UTF-8 Lottery.java”。若用“javac Lottery.java”会报错。




更多内容请关注微信公众号


wechat.jpg
TopCoder