本系列内容较长、用到的技术有点杂(指同时用python做数据和用C++跑暴搜),所以将分为上下两段主要板块:
上半(此篇):用python
生成宝可梦属性克制表。
下半:用C++
写暴搜进行枚举。
题目描述
黑箱宝可梦问题
假设存在这样一个未知属性的宝可梦,只能依靠不断殴打它得到的反馈来判断它的属性,每次进攻和正常游戏一样会得到效果拔群、效果一般、效果较差和无效四种情况。
那么你最多需要攻击几次才能确定他的属性是什么?
补充条件:
- 这只宝可梦不会使用任何招式。
- 这只宝可梦可以拥有无限长的血条。
- 你看不到这只宝可梦的样子,不知道它的等级。
- 你拥有所有属性的进攻能力。
- 宝可梦的特性不参与计算。
解题思路
首先,(对于没玩过宝可梦的人),宝可梦有哪些属性?
那么根据资料显示,宝可梦有18种属性。(分别为一般、火、水、草、电、冰、虫、飞行、地面、岩石、格斗、超能力、幽灵、毒、恶、钢、龙、妖精)
而攻击效果和属性之间又有什么关系呢?
攻击效果由攻击方属性、防御方属性决定,总共有18*18=324种组合。
诺,这里有一张表,给出了攻击效果和属性的关系。
(0x
为无效,0.5x
为效果较差,空白(1x
)为效果一般,2x
为效果拔群)。
记住这张表,它将成为本篇后面内容的主角。
在看到这张表之后,思路就变得清晰了起来:
找出一个最小的属性集 S S S,满足:用 S S S中的属性击打该宝可梦,形成一个反馈序列,该序列能够区分所有属性。
标准的描述如下:
计上表为 T = [ t i , j ] ( t i , j ∈ { 0 , 0.5 , 1 , 2 } ) T=[t_{i,j}](t_{i,j} \in \{ 0,0.5,1,2\}) T=[ti,j](ti,j∈{0,0.5,1,2}),计所有属性集合为 A = { a 1 , a 2 , ⋯ , a n } A = \{a_1,a_2, \cdots, a_n \} A={a1,a2,⋯,an},设有一个原属性的子集 S = { x 1 , x 2 , ⋯ , x m } S = \{ x_1,x_2, \cdots, x_m\} S={x1,x2,⋯,xm},当满足 ∀ a , b ∈ A , ∃ x ∈ S , T x , a ≠ T x , b \forall a,b \in A, \exist x \in S, T_{x,a} \neq T_{x,b} ∀a,b∈A,∃x∈S,Tx,a=Tx,b时, S S S即为一个合法属性集。
最终答案即为 min ( ∣ S ∣ ) \min(|S|) min(∣S∣)。
而求解 S S S的问题,我们可以用枚举来解决,即枚举 A A A的所有子集。由于题目是求最小解,因此我们还必须根据集合大小顺序进行枚举。
时间复杂度是绝对够看的,因为 n n n很小(只有18)。考虑最坏的情况,需要枚举所有子集,有 2 n 2^n 2n个,每个子集验证需要 n 2 × ∣ S ∣ n^2 \times |S| n2×∣S∣的复杂度,所以时间复杂度可以直接算成 n 3 2 n n^3 2^n n32n,约为 1.5 × 1 0 9 1.5 \times 10^9 1.5×109,然而实际情况根本不需要跑这么久,因为很明显答案长度不会到18。
开始解决
1. 生成克制关系表
宝可梦的属性有18个,意味着其攻击-属性关系有 18 ∗ 18 = 324 18*18 = 324 18∗18=324项,也就是上面这张表,有324项。
这就比较头疼了。把这个表里面的内容一个一个敲下来?是个办法,但是一项一项输入如此多的数据显然不是一个程序员喜欢干的事情。
那有没有什么办法可以让电脑根据这张图片生成呢?一想到这里,我便有一个大胆的想法。
1.1 生成思路——色斑识别
可以看到上面这张彩色表格的内容无非就是4种色块:

0x
对应黑色,0.5x
对应红色,2x
对应绿色,没红没黑没绿就是1x
。
那么有一种很简单的思路就是:划分单元格,然后根据单元格当中的红、黑、绿色块进行统计,如果某种颜色的数量超过某个阈值,即可判定其内容。
1.2 色斑识别逻辑——相似颜色像素计数
众所周知,计算机图片当中五彩斑斓的颜色由三原色组成: R G B RGB RGB,两个颜色相同,当且仅当其RGB值相等。
由于计算机种图片一般采用8位RGB,故其取值范围为 [ 0 , 255 ] [0,255] [0,255],共可以组成 2 24 ≈ 1.6 × 1 0 7 2^{24} \approx 1.6 \times 10^7 224≈1.6×107种颜色。
那么在这里,两种颜色的相似度,采用这个公式:
令 a = ( r 1 , g 1 , b 1 ) , b = ( r 2 , g 2 , b 2 ) \mathbf{a} = (r_1,g_1,b_1),\mathbf{b} = (r_2,g_2,b_2) a=(r1,g1,b1),b=(r2,g2,b2),则 s i m ( a , b ) = 3 × 25 5 2 − ∣ a − b ∣ 2 sim(\mathbf{a},\mathbf{b}) = 3 \times 255^2 - |\mathbf{a} - \mathbf{b}|^2 sim(a,b)=3×2552−∣a−b∣2。
有人可能会问,怎么会用这么奇怪的公式?别急,它其实一点也不奇怪。
你会发现 ∣ a − b ∣ |\mathbf{a} - \mathbf{b}| ∣a−b∣是一个非常好的向量距离,当两个向量完全相同时它会为 0 0 0,而与一个向量距离相同的向量刚好构成一个球,而当这个球足够小的时候,颜色就也就差不多了。
但是直接用 ∣ a − b ∣ |\mathbf{a} - \mathbf{b}| ∣a−b∣的话他们的值和相似度是负相关的。为了让它正相关,于是用它的最大值减去它,就得到了上面这个公式。
有了相似度,我们再取出来三个色块的颜色(拿windows画图的取色器,你要是没有那就photoshop的取色器):
black = (0,0,0) # 0x色斑
red = (158,36,43) # 0.5x色斑
green = (34,177,76) # 2x色斑
它们显示出来长这样:
那这三种颜色,去和单元格当中的像素一一比对,统计这三种的相似颜色出现的次数,如果超过阈值,即可认为含有该颜色色斑;如果没超过阈值,则认为该单元格内不含有色斑,识别为空白单元格。
这个就是相似颜色像素统计算法 (我起的名,不必在意)
1.3 代码实现
因为涉及到计算机图形处理方面的东西,拿C++写实在难受,于是采用python
当中的PIL
、numpy
等库进行实现。
src_pic = "./baokemeng.png" # 源图片文件名称
from PIL import Image
import numpy as np
import matplotlib.pyplot as plt
black = np.array([0,0,0],dtype=np.uint8)
red = np.array([158,36,43],dtype=np.uint8)
green = np.array([34,177,76],dtype=np.uint8)
def calc_sim(a: np.array, b: np.array) -> float:
'''
计算两个颜色向量的相似度
a, b: 3位向量,dtype=np.uint8,表示两个RGB
'''
a = a.astype(np.float64)
b = b.astype(np.float64)
c = a-b
ans = 195075.0-np.dot(c,c)
return ans
###################
similarity_p = 180000 # 颜色向量相似阈值,这个值可以调
###################
def cnt_colors(unit: np.array) -> tuple:
'''
对一个单元格进行相似颜色像素统计。
unit: (w,h,4),第四维是每个像素的RGB和透明度(在这里恒为255)
'''
cnt_black = 0
cnt_red = 0
cnt_green = 0
for i in range(unit.shape[0]):
for j in range(unit.shape[1]):
now = unit[i,j,0:-1] # 这里要把最后一维不透明度去掉
be_black = calc_sim(now,black)
be_red = calc_sim(now,red)
be_green = calc_sim(now,green)
if be_black >= similarity_p:
cnt_black += 1
if be_red >= similarity_p:
cnt_red += 1
if be_green >= similarity_p:
cnt_green += 1
return cnt_black,cnt_red,cnt_green
pic = Image.open(src_pic)
print(pic.size)
w,h = pic.size[0], pic.size[1]
pic = pic.crop((72,53,w,h)) # 表格裁剪,裁去表头
w,h = pic.size[0], pic.size[1]
w_unit = pic.size[0]//18 # 计算单元格尺寸
h_unit = pic.size[1]//18 #
result = np.full((18,18),2,dtype=np.uint8) # 记录结果的数组(初始为2,是因为1x对应的值为2)
# 0为0x,1为0.5x,2为1x(默认),3为2x
############
exist_patch_p = 70 # 色斑判定阈值,这个可以调。
############
for i in range(18):
for j in range(18):
unit = np.array(pic.crop((w_unit*i, h_unit*j, w_unit*(i+1), h_unit*(j+1))))
##### 这里单元格枚举是按照列顺序枚举的,而存答案是按行存的,所以后面要翻转
cnt_black,cnt_red,cnt_green = cnt_colors(unit)
if(cnt_black >= exist_patch_p):
result[i,j] = 0
if(cnt_red >= exist_patch_p):
result[i,j] = 1
if(cnt_green >= exist_patch_p):
result[i,j] = 3
# 输出来看一下
# if(cnt_black >= 70 or cnt_red >= 70 or cnt_green >= 70):
# plt.imshow(unit)
# plt.title("i: {}, j: {}, sum: {}\n black: {}, red: {}, green: {}"
# .format(i,j,unit.sum(),cnt_black,cnt_red,cnt_green))
# plt.show()
result = result.transpose() # 翻转
# 输出
for i in range(18):
for j in range(18):
print(str(result[i,j])+' ',end="")
print()
1.4 生成结果
输出结果:
2 2 2 2 2 2 2 2 2 2 2 2 1 0 2 2 1 2
2 1 1 2 3 3 2 2 2 2 2 3 1 2 1 2 3 2
2 3 1 2 1 2 2 2 3 2 2 2 3 2 1 2 2 2
2 2 3 1 1 2 2 2 0 3 2 2 2 2 1 2 2 2
2 1 3 2 1 2 2 1 3 1 2 1 3 2 1 2 1 2
2 1 1 2 3 1 2 2 3 3 2 2 2 2 3 2 1 2
3 2 2 2 2 3 2 1 2 1 1 1 3 0 2 3 3 1
2 2 2 2 3 2 2 1 1 2 2 2 1 1 2 2 0 3
2 3 2 3 1 2 2 3 2 0 2 1 3 2 2 2 3 2
2 2 2 1 3 2 3 2 2 2 2 3 1 2 2 2 1 2
2 2 2 2 2 2 3 3 2 2 1 2 2 2 2 0 1 2
2 1 2 2 3 2 1 1 2 1 3 2 2 1 2 3 1 1
2 3 2 2 2 3 1 2 1 3 2 3 2 2 2 2 1 2
0 2 2 2 2 2 2 2 2 2 3 2 2 3 2 1 2 2
2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 2 1 0
2 2 2 2 2 2 1 2 2 2 3 2 2 3 2 1 2 1
2 1 1 1 2 3 2 2 2 2 2 2 3 2 2 2 1 3
2 1 2 2 2 2 3 1 2 2 2 2 2 2 3 3 1 2
拷贝,存至./data.txt
,就算是生成属性表成功啦!
2. 枚举答案
参见 黑箱宝可梦问题解决(下半)。