问题描述
用户可以从每月的 1~最大月份天数(这里统一为 31 天),选出不重复的 7 个数字,进行投注
开奖时会产生7个不同的数字,
根据彩票的数字和开奖彩票的相近程度,划分为1~7等奖,
问题分析
假定玩家在开奖前共计投注 N 张彩票,需要计算出这其中每张彩票的奖项。由于彩票至少为N,所以可以预期算法不会好于O(N),如果在判断一张彩票时,步数过多,那么随着彩票的增多,开奖速度毫无疑问会大幅下降。所以如何判断彩票是中几等奖,直接影响开奖效率。
算法1
以上面的开奖数字1,2,3,4,5,6,7来说,一张彩票的形式是1,2,3,4,5,6,7那么就是1等奖,1等级只有1种可能;而2等奖有(1,2,3,4,5,6,*、*,2,3,4,5,6,7)2种可能;同理3等级有(1,2,3,4,5,*,*、*,2,3,4,5,6,*、*,*,3,4,5,6,7)3种情况,依此类推4等奖有4种可能,5等奖有5种可能,6等奖有6种可能,7等奖有7种可能,共计1+2+3+4+5+6+7=28种情况
1.可以在开奖时,生成1,2,3,4,5,6,7的所有中奖形式,时间花费为O(7)*O(7) = O(49),这项可以忽略
2.尝试对彩票从1等奖到7等奖进行7个奖项的判断;而7个奖项中判断最少次数的1等级只有1种可能,而7等级最多有 7种可能;时间花费为O(7)
3.需要根据开奖等级从开奖数字中取出至多7种可能;时间花费为O(7)
4.另外这里还没有被判断彩票的所有可能,在进行比较之前需要计算出被判定彩票的1~7等奖的所有形式;花费为O(7)*O(7),对每张彩票都要进行这不操作,这部分不能忽略
5.需要根据开奖等级从一张彩票中取出至多7种可能;时间花费为O(7)
这种情况下,1等奖判断需要比较1次……7等奖至多需要7*7,也就是49次,时间复杂度为O(N)*O(7)*O(7)*O(7)*O(7)*O(7) = O(16801N),看起来常数项不小。
实际表现
算法1,实际表现不如人意,处理100000彩票,需要花费12s左右,而且由于总是从1等级判断直到7等奖,它的花费总是 O(N)*O(7)*O(7)*O(7)*O(7)*O(7) = O(16801N);要知道实际情况下,中奖的彩票是极少数的,中大奖的彩票就更加少得可怜,另外一张奖品连7等奖都没有中的话,那是不可能中更高奖项的,因此算法1进行了很多不必要的比较,生成了不必要的中奖可能。
算法2
对算法1不足的地方进行改进
其中,由于中奖的彩票是极少数。现在优先从7等奖开始判断,如果中了7等奖,再去判断是否中了6等奖,一旦不中奖直接丢掉剩下奖项的判断。
另外,被判断的奖票不再每次生成7等奖的所有情况,而是判断每1级别的奖项就生成每1级别的奖项可能。这里时间花费O(49)降低至O(7),总的时间花费降低为 O(2401N)
总的来说,这番改良,可以将预期时间花费为之前的 1/7
实际表现
算法2相比算法1有些许改进,同样处理100000彩票,花费了 1.76s。总的来说,算法1是一个直观的算法,而算法2仅仅是算法1的改良版本。他们的效率我仍然不能接受,所以有了算法3
算法3
想要获得更快的算法,需要对问题进行抽象,同时需要存储更多彩票的信息。
这个问题的关键就是如果快速判断一张彩票的中奖情况,
中奖的判断又可以分为1.是否中奖,2.中了几等奖。这在算法1中,是直接将彩票和中奖彩票进行比较的,没有先关注是否中奖,这势必会进行很多不必要的计算。
所谓中奖与否,可以抽象为,一组有序数字,相对于 “目标” 组有序数字的相同情况,毕竟有1个数字相同一定中7等奖,2个数字有可能中6等奖……
这里中奖与否问题准确来讲变成了,是否可能中奖,毕竟有2个数字相同只是有可能中 6 等奖,要中 6 等奖,除非 这两个数字的排列顺序,能在 “目标” 组数组中找到对应
中奖与否跟数字的顺序有关,原先的数据结构不能获得每个数字的 “坐标” 信息
到这里,算法就较为明了了。
先找出,一张彩票和开奖彩票有几个共同相同数字,
如果相同数字 1 个,那么直接中 7 等奖,处理结束
如果相同数字 0 个,那么不中奖,同样处理结束
如果数字 >= 2 个,进入判断中几等奖的流程
中几等奖的问题,就是一组数字相对于目标组数字的相同连续序列情况,
我们要求最大中几等奖,可以抽象为,求一组数字相对于目标组数字的,最大相同连续序列中数字的个数
算法总的来说分为两步
预先使用 map 记录开奖彩票每个数字的下标信息,散列表插入时间平均O(1),插入7次,这部分时间花费可以忽略
1.对每张彩票,确定相同的数字个数。由于数字范围1~31,可以选择同时遍历一张彩票以及开奖彩票数组,并将他们放入到大小而 32 的数组中,放入时如果该数字个数 == 2 就认为找到了一个相同的数字,并且额外记录这个数字在数组中的下标。这部分时间操作O(7) 在于 N 张彩票,共计花费 O(7N)
2.找到两组数字中,相同又相序的最大序列。还是同时遍历一张彩票和开奖彩票,处理共有的数字,对于两组数字:如果当前数字坐标比上一个数字坐标大1,我们就认为数字连续,否则中断。只有两组数字都处于连续状态时,我们可以累加这张彩票相对于开奖奖票的相同相序数字个数,否则相同相序数字个数归1,重新开始。直至最后一个数字。这步的时间花费最多为O(7),再加上有N张票,共计至多O(7N)
3.两步共计时间花费 O(14N),可以看到常数项很小,基本可以算是一个良好的线性算法啦。
实际表现
经过前面的分析,不那么直观的算法3,效率还行。处理100000彩票,花费仅有 0.02s 。当彩票量是1000000时,花费也是线性增长到 0.19s
算法实现
这里为了减少篇幅,只展示了算法3 也就是函数 Open_lottery3(),其他是一些辅助函数
package gameproblem
import (
"bufio"
"fmt"
"math/rand"
"os"
"time"
"github.com/vertgenlab/gonomics/fileio"
)
//single lottery num
type lot_item struct {
seven_num [7]int
luck_val int
}
const LOTTERY_COUNT int = 100000 // // 100000
//1,2,3,4,5,6,7
// 6 等奖 有连续两个数字 想通
// 5 等奖 ……3
// 4…… 4
//3 5
//2 6
//1 7
//生成 10万张奖票
func Init_lottery_file() {
var err error
// the WriteFile method returns an error if unsuccessful
if _, err := os.Stat("lotteries.txt"); err == nil {
fmt.Printf("File exists\n")
return
}
f, err := os.Create("lotteries.txt")
if err != nil {
fmt.Printf("File create failed\n")
return
}
all_lotteris := make([]lot_item, LOTTERY_COUNT)
for i := 0; i < LOTTERY_COUNT; i++ {
lottery := Get_a_lottery()
var lot_item1 lot_item
for j := 0; j < 7; j++ {
lot_item1.seven_num[j] = lottery[j]
}
all_lotteris[i] = lot_item1
}
for i := 0; i < len(all_lotteris); i++ {
lottery_str := fileio.IntSliceToString(all_lotteris[i].seven_num[:])
_, err := f.WriteString(lottery_str + "\n")
// handle this error
if err != nil {
// print it out
fmt.Println(err)
break
}
}
f.Close()
}
func get_all_lotties() ([]lot_item, int) {
all_lotteris := make([]lot_item, LOTTERY_COUNT)
f, err := os.Open("lotteries.txt")
if err != nil {
fmt.Println(err)
return all_lotteris, -1
}
scanner := bufio.NewScanner(f)
// optionally, resize scanner's capacity for lines over 64K, see next example
j := 0
for scanner.Scan() {
txt := scanner.Text()
lottery := fileio.StringToIntSlice(txt)
// fmt.Println(lottery)
var lot_item1 lot_item
for i := 0; i < 7; i++ {
lot_item1.seven_num[i] = lottery[i]
}
if j < LOTTERY_COUNT {
all_lotteris[j] = lot_item1
all_lotteris[j].luck_val = -1
}
j++
}
return all_lotteris, 0
}
func deleteElement(slice []int, index int) []int {
return append(slice[:index], slice[index+1:]...)
}
func Get_a_lottery() [7]int {
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}
lottery := [7]int{0, 0, 0, 0, 0, 0}
for i := 0; i < 7; i++ {
randindex := rand.Intn(len(numbers))
lottery[i] = numbers[randindex]
numbers = deleteElement(numbers, randindex)
}
return lottery
}
func get_all_num_from_a_lottery_n(seven []int, n int) []string {
i := n
dis := 7 - i
iCount := 0
var num_strs []string
// var num reward_num2
for start_pos := 0; start_pos+dis <= 7; start_pos++ {
tmp_num := seven[start_pos : start_pos+dis]
tmp_num_str := fileio.IntSliceToString(tmp_num)
num_strs = append(num_strs, tmp_num_str)
// all_possible_reward.reward_str.num_str[n] = append(all_possible_reward.reward_str.num_str[n], tmp_num_str)
// for k := 0; k < len(tmp_num); k++ {
// num.num[iCount][k] = tmp_num[k]
// }
iCount++
}
// all_possible_reward.reward_str.num_str[n] = append(all_possible_reward.reward_str.num_str[n], num_strs)
return num_strs
}
func get_all_num_from_a_lottery(seven []int) s_all_rewad {
var all_possible_reward s_all_rewad
for i := 0; i < 7; i++ { //0~6 依次处理 1到 7等级
dis := 7 - i
iCount := 0
var num_strs []string
for start_pos := 0; start_pos+dis <= 7; start_pos++ {
tmp_num := seven[start_pos : start_pos+dis]
tmp_num_str := fileio.IntSliceToString(tmp_num)
num_strs = append(num_strs, tmp_num_str)
for k := 0; k < len(tmp_num); k++ {
all_possible_reward.reward.num[i][iCount][k] = tmp_num[k]
}
iCount++
}
all_possible_reward.reward_str.num_str = append(all_possible_reward.reward_str.num_str, num_strs)
}
return all_possible_reward
}
func Open_lottery3() {
all_lotteris, rtcode := get_all_lotties()
if 0 != rtcode {
fmt.Printf("get lotteries failed,open lottery suspend \n")
return
}
fmt.Printf("read lottery %d 张\n", len(all_lotteris))
var seven []int
seven = []int{28, 29, 1, 9, 6, 10, 2} //开奖的号码 1,2,3,4,5,6,7
var m_seven map[int]int
m_seven = make(map[int]int)
for y := 0; y < 7; y++ {
m_seven[seven[y]] = y //记录文字信息
}
var compare_times int = 0
start := time.Now().UnixMilli()
for x := 0; x < LOTTERY_COUNT; x++ { //O(N)
tmpseven := all_lotteris[x].seven_num
numbers := [32]int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
sameCount := 0
var m_tmpseven map[int]int
m_tmpseven = make(map[int]int)
for j := 0; j < 7; j++ { //7
//将开奖数字以及一张彩票融进 numbers
numbers[tmpseven[j]]++
if 2 == numbers[tmpseven[j]] {
//不关心具体相同数字是多少,只关心相同的个数
//额外记录 下标
m_tmpseven[tmpseven[j]] = j
sameCount++
}
numbers[seven[j]]++
if 2 == numbers[seven[j]] {
//不关心具体相同数字是多少,只关心相同的个数
sameCount++
m_tmpseven[seven[j]] = j
}
}
if 0 == sameCount {
continue
}
//如果相同的只有一个那么直接就是 6 等奖
if 1 == sameCount {
all_lotteris[x].luck_val = 7
continue
}
//统计相同的张数 就是最大能中的奖项
//此时从5等级开始,只不过上限确定了
//此时关注 是否有序性
//得出最大的相同连续序列
//两边相邻,才算,单方面相邻不算
//由于此时相同的数字至少有两个,所以每次循环最多 7+7-2 = 12 次
var last_pos int = -2
var last_tmp_pos int = -2
var tmp_cont_count int = 1 //连续挨着的最大数字个数,为最终的奖项
var max_cont_count int = 1
for k := 0; k < 7; k++ { //7
if 2 != numbers[tmpseven[k]] {
continue
}
cur_pos := m_seven[tmpseven[k]]
cur_tmp_pos := m_tmpseven[tmpseven[k]]
var is_seven_cont bool = false
var is_tmp_seven_cont bool = false
if cur_pos == (last_pos + 1) {
is_seven_cont = true
}
//当前pos 比 前一个 大 1 就算相邻
if cur_tmp_pos == (last_tmp_pos + 1) {
is_tmp_seven_cont = true
}
if true == is_tmp_seven_cont && true == is_seven_cont {
tmp_cont_count++ //累加 连续数
} else {
if tmp_cont_count > max_cont_count {
max_cont_count = tmp_cont_count //可能找到更大的数
}
tmp_cont_count = 1 //清零 重置
}
last_pos = cur_pos
last_tmp_pos = cur_tmp_pos
}
if tmp_cont_count > max_cont_count {
max_cont_count = tmp_cont_count //可能找到更大的数
}
if 0 < max_cont_count {
all_lotteris[x].luck_val = 7 - max_cont_count + 1
continue
}
// if -1 != all_lotteris[x].luck_val {
// fmt.Printf("lucky lottery: %d reward: lottery: ", all_lotteris[x].luck_val+1)
// fmt.Println(all_lotteris[x].seven_num)
// if 0 == all_lotteris[x].luck_val {
// fmt.Printf("has a specal reward\n")
// }
// }
}
end := time.Now().UnixMilli()
fmt.Printf("find all lucky lotteries costs %.2f s, costs : %d times\n", float64(end-start)/1000.0, compare_times)
//对每张彩票,从 6等级开始判断
// for x := 0; x < LOTTERY_COUNT; x++ {
// if -1 != all_lotteris[x].luck_val {
// fmt.Printf("lucky lottery: %d reward: lottery: ", all_lotteris[x].luck_val)
// fmt.Println(all_lotteris[x].seven_num)
// }
// }
}