-
背景技术
我们可能遇到这种需求:把M个人分为{N1,N2,N3,...Nk}共k组,有约束条件(如第i组人数为3人,或者所有组别人数相同),给出优化目标(如使组之间的人员年龄之和差最小),求最优分组方式。
传统的思路会想到for循环。定义M个循环变量,i1,i2,i3,...,iM,在M层循环内部计算优化目标,检验约束条件。但这样做,有以下弊端:
- 代码冗长繁琐
- 代码容易出错,包括打字错误与逻辑错误
- 不易检查代码,不易维护
本文给出一种可以完全替代分组优化情景中的高阶循环的算法。
-
内容
首先,把分组结果表示为一个M长度的K进制字符串S,若S的第i位取a,说明第i个人分到第a组(0<=a<K)。每种分组方式的字符串S唯一。
S与数值X一一对应,S即X的K进制字符串表示。对于任意一个X(0<=X<Pow(K,M)),可以把X转化为K进制字符串,并在高位补0,得到M长度的S。
遍历满足约束条件的X的所有可能值,得到所有的S。取出S的每一位,得到分组结果(第i为为m说明第i人分到第m组),计算优化目标,得到最优解。
-
具体实施方式
以9人分3组,每组人数一致,求年龄和之方差最小值为例。
- 遍历准备。9人分3组的所有可能性有Pow(3,9)(3的9次方=19683)种。X遍历0到Pow(3,9)-1的所有值,求对应的3进制字符串S。
- 考虑约束条件。遍历S中每个字符,计算出字符0、字符1、字符2的数量,如果数量不等,即不满足约束条件,直接返回,不计算优化值。
- 对于满足约束条件的分组方式Sx,遍历Sx中每个字符,若Sx[i]==0, 把第i人的年龄值累加到第0组的年龄和中;若Sx[i]==1, 把第i人的年龄值累加到第1组的年龄和中;若Sx[i]==2, 把第i人的年龄值累加到第2组的年龄和中。计算出该分组方式的方差。若当前值比历史最优值更优,更新最优值。
import math
def to_base3(n, length=9):
# 转换为三进制字符串
n = (int)(n)
if n == 0:
base3_string = "0"
else:
digits = []
while n:
digits.append(str(n % 3))
n //= 3
base3_string = ''.join(digits[::-1])
# 填充到指定长度
return base3_string.zfill(length)
def D(x1,x2,x3):
x1 = (float)(x1)
x2 = (float)(x2)
x3 = (float)(x3)
ave = (x1+x2+x3)/3.0
return (x1-ave)*(x1-ave)+(x2-ave)*(x2-ave)+(x3-ave)*(x3-ave)
LEN = 9
ID = math.pow(3,9)
group = [1,2,3,4,5,6,7,8,9]
Dmin = 0
result = ""
isFirsttime = True
for i in range((int)(ID)):
s = to_base3(i)
cnt0,cnt1,cnt2 = 0,0,0
list0,list1,list2 = [],[],[]
for j in range(9):
c = s[j]
if(c=='0'):
cnt0+=1
list0.append(j)
if(c=='1'):
cnt1+=1
list1.append(j)
if(c=='2'):
cnt2+=1
list2.append(j)
if(cnt0==3 and cnt1==3 and cnt2==3):
x1 = group[list0[0]]+group[list0[1]]+group[list0[2]]
x2 = group[list1[0]]+group[list1[1]]+group[list1[2]]
x3 = group[list2[0]]+group[list2[1]]+group[list2[2]]
d = D(x1,x2,x3)
if(isFirsttime):
isFirsttime = False
Dmin = d
result = s
else:
if(d<Dmin):
Dmin = d
result = s
print("group allocation "+result)
print("min D ")
print(Dmin)
运行结果
group allocation 012120201
min D
0.0