Bron-Kerbosh算法求解极大团
参考资料:https://www.jianshu.com/p/437bd6936dad
这篇文章讲得很好,本文的代码也是参照这篇文章,用python实现。
什么是极大团?
团、极大团、最大团的定义请问这篇文章:https://www.jianshu.com/p/dabbc78471d7
为什么会用到极大团?
了解到极大团这个知识,是在构建任务指派模型的时候接触到的。在对任务进行指派时,每个任务都有其开始时间、结束时间,在不考虑员工资质的情况下,任务之间可能会存在时间上的冲突,如下图所示。这幅图中存在哪些极大团
呢?
根据极大团的原理:如果一个团不被其他任一团所包含,即它不是其他任一团的真子集,则称该团为图G的极大团(maximal clique)。
怎么用到模型上?
- 回到上面两幅图,需要对第一幅图中的5个任务进行人员指派,应该需要以下约束条件:
当两任务存在时间冲突时:
x
i
,
k
+
x
j
,
k
≤
1
,
i
,
j
∈
N
,
k
∈
K
x_{i,k}+x_{j,k}\le 1 , i,j\in N, k\in K
xi,k+xj,k≤1,i,j∈N,k∈K
其中,
i
,
j
i,j
i,j是任务的序号,
k
k
k是员工的序号。
展开看(这里先不管员工):
x
1
+
x
2
≤
1
,
x
1
+
x
3
≤
1
,
x
1
+
x
4
≤
1
,
x
2
+
x
3
≤
1
,
x
2
+
x
4
≤
1
,
x
3
+
x
4
≤
1
,
x
3
+
x
5
≤
1
,
x
4
+
x
5
≤
1
x_1 + x_2 \le 1 \ , \ x_1 + x_3 \le 1 \ , \ x_1 + x_4 \le 1 \ , \ x_2 + x_3 \le 1 \ , \\x_2 + x_4 \le 1 \ , \ x_3 + x_4 \le 1 \ , \ x_3 + x_5 \le 1 \ , \ x_4 + x_5 \le 1
x1+x2≤1 , x1+x3≤1 , x1+x4≤1 , x2+x3≤1 ,x2+x4≤1 , x3+x4≤1 , x3+x5≤1 , x4+x5≤1
- 用极大团构建约束,第二幅图列出了5个任务的所有极大团,因为极大团中所包含的任务都是存在时间冲突的,所以在一个团中,只能有一个为1。
c l i q u e 1 , 2 , 3 , 4 → x 1 + x 2 + x 3 + x 4 ≤ 1 c l i q u e 3 , 4 , 5 → x 3 + x 4 + x 5 ≤ 1 clique{1,2,3,4} \to x_1 + x_2 + x_3 +x_4 \le 1 \\ clique{3,4,5} \to x_3 + x_4 + x_5 \le 1 clique1,2,3,4→x1+x2+x3+x4≤1clique3,4,5→x3+x4+x5≤1
从上面的两种约束可以看出,使用极大团构建约束,约束的数量将会大大减少,特别是当任务的数量更大的时候。
现在就是如何找到极大团。
Bron-Kerbosch算法
算法的思路、过程等,我建议你看这篇文章:极大团(maximal clique)算法:Bron-Kerbosch算法
下面给出用python编写求解极大团的代码。
首先这是未改进版本的BK算法,以下也给出了几个小算例。
import copy
import time
import pandas as pd
import numpy as np
# BronKerbosch,cnt记录目前是第几层
def BronKerbosch(d, rn, pn, xn):
# 判断P、X是否为空,为空则找到最大值
if pn == 0 and xn == 0:
route.append(copy.deepcopy(R[d]))
# 遍历P中的每一个v,len(P)==0时,搜索到终点
for j in range(pn):
# 取出P中的第j个点
v = P[d][j]
R[d+1] = [] # 因为后面是直接在list后面添加元素,所以先将下一层的list清空
for k in range(rn):
R[d+1].append(R[d][k])
R[d+1].append(v) # 将v节点,添加到R集合中
# 用来分别记录下一层中P集合和X集合中节点的个数
tp, tx = 0, 0
# 更新X集合(下一层X集合),保证X集合中的点都能与R集合中所有的点相连接
X[d + 1] = [] # 因为后面是直接在list后面添加元素,所以先将下一层的list清空
for k in X[d]:
if conf[v][k]:
X[d + 1].append(k)
tx += 1
# 更新P集合时,同时以R、X两个集合作为依据,在X中的元素不能出现在P中
P[d+1] = [] # 因为后面是直接在list后面添加元素,所以先将下一层的list清空
for k in P[d]:
if conf[v][k] and k not in X[d]:
P[d+1].append(k)
tp += 1
# 递归进入下一层
BronKerbosch(d+1, rn+1, tp, tx)
# 完成后,将操作的节点,放入X中,开始下面的寻找
X[d].append(v)
xn += 1
# 任务冲突矩阵
def conflict(data):
m = len(data)
M = [[0]*m]*m
M = np.array(M)
for k in range(m):
for j in range(k+1,m):
if data.taskStartTime[j] <= data.taskStartTime[k] <= data.taskEndTime[j] \
or data.taskStartTime[j] <= data.taskEndTime[k] <= data.taskEndTime[j]:
M[j,k] = 1
M[k,j] = 1
return M
# 获取冲突矩阵
st = time.time()
data = pd.read_excel('data.xlsx')
data.taskStartTime = pd.to_datetime(data.taskStartTime)
data.taskEndTime = pd.to_datetime(data.taskEndTime)
conf = conflict(data)
# conf = [[0, 1, 1, 0], [1, 0, 1, 1], [1, 1, 0, 0], [0, 1, 0, 0]]
# conf = [[0, 1, 0, 1, 1], [1, 0, 1, 0, 1], [0, 1, 0, 0, 1], [1, 0, 0, 0, 1], [1, 1, 1, 1, 0]]
# conf = [[0,1,1,1,0,0,0],[1,0,1,1,1,0,0],[1,1,0,1,0,0,0],[1,1,1,0,1,1,0],[0,1,0,1,0,0,1],[0,0,0,1,0,0,0],[0,0,0,0,1,0,0]]
# 节点总数
n = len(conf)
# 定义三个集合,R已确定的极大团顶点的集合,P未处理顶点集,X以搜过的并且属于某个极大团的顶点集合
R, P, X = [[]]*n, [[]]*n, [[]]*n
P[0] = [i for i in range(n)] # 初始化未处理集合
route = []
BronKerbosch(0, 0, len(conf), 0)
print(len(route))
for key in route:
print(key)
print(time.time() - st)
下面是经过改进的代码,改进的原理也请看上面那篇文章。
import copy
import time
import pandas as pd
import numpy as np
# BronKerbosch,cnt记录目前是第几层
def BronKerbosch(d, rn, pn, xn, u):
# 判断P、X是否为空,为空则找到最大值
if pn == 0 and xn == 0:
route.append(copy.deepcopy(R[d]))
if len(P[d]) > 0:
u = P[d][0] # 记录最近放入P集合中的元素
# 遍历P中的每一个v,len(P)==0时,搜索到终点
for j in range(pn):
# 取出P中的第j个点
v = P[d][j]
# 判断u,v是否是邻居,是则跳过
if u != -1 and conf[u][v] == 1: continue
R[d + 1] = [] # 因为后面是直接在list后面添加元素,所以先将下一层的list清空
for k in range(rn):
R[d + 1].append(R[d][k])
R[d + 1].append(v) # 将v节点,添加到R集合中
# 用来分别记录下一层中P集合和X集合中节点的个数
tp, tx = 0, 0
# 更新X集合(下一层X集合),保证X集合中的点都能与R集合中所有的点相连接
X[d + 1] = [] # 因为后面是直接在list后面添加元素,所以先将下一层的list清空
for k in X[d]:
if conf[v][k]:
X[d + 1].append(k)
tx += 1
# 更新P集合时,同时以R、X两个集合作为依据,在X中的元素不能出现在P中
P[d + 1] = [] # 因为后面是直接在list后面添加元素,所以先将下一层的list清空
for k in P[d]:
if conf[v][k] and k not in X[d]:
P[d + 1].append(k)
tp += 1
# 递归进入下一层
BronKerbosch(d + 1, rn + 1, tp, tx, u)
# 完成后,将操作的节点,放入X中,开始下面的寻找
X[d].append(v)
xn += 1
# print("==========")
# 任务冲突矩阵
def conflict(data):
m = len(data)
M = [[0]*m]*m
M = np.array(M)
for k in range(m):
for j in range(k+1,m):
if data.taskStartTime[j] <= data.taskStartTime[k] <= data.taskEndTime[j] \
or data.taskStartTime[j] <= data.taskEndTime[k] <= data.taskEndTime[j]:
M[j,k] = 1
M[k,j] = 1
return M
# 获取冲突矩阵
st = time.time()
data = pd.read_excel('data.xlsx')
data.taskStartTime = pd.to_datetime(data.taskStartTime)
data.taskEndTime = pd.to_datetime(data.taskEndTime)
conf = conflict(data)
# conf = [[0, 1, 1, 0], [1, 0, 1, 1], [1, 1, 0, 0], [0, 1, 0, 0]]
# conf = [[0, 1, 0, 1, 1], [1, 0, 1, 0, 1], [0, 1, 0, 0, 1], [1, 0, 0, 0, 1], [1, 1, 1, 1, 0]]
# conf = [[0, 1, 1, 1, 0, 0, 0], [1, 0, 1, 1, 1, 0, 0], [1, 1, 0, 1, 0, 0, 0], [1, 1, 1, 0, 1, 1, 0],
# [0, 1, 0, 1, 0, 0, 1], [0, 0, 0, 1, 0, 0, 0], [0, 0, 0, 0, 1, 0, 0]]
# 节点总数
n = len(conf)
# 每个节点的边数
node_sum = []
for i in range(n):
node_sum.append(sum(conf[i]))
# 定义三个集合,R已确定的极大团顶点的集合,P未处理顶点集,X以搜过的并且属于某个极大团的顶点集合
R, P, X = [[]] * n, [[]] * n, [[]] * n
# 初始化,以边数做倒序
P[0] = sorted(range(len(node_sum)), key=lambda k: node_sum[k], reverse=True)
route = []
BronKerbosch(0, 0, len(conf), 0, -1)
print(len(route))
for key in route:
print(key)
print(time.time() - st)
任务数较小时,改进前后的求解时间上没有很大的差异,这里我用了一份其他的数据进行测试,任务数70多个,改进前求解需要24秒左右,改进后仅需0.4秒。
(本文为学习记录用)
代码中的数据:
链接:https://pan.baidu.com/s/1zHq-CQs3kts_Nfj4C_XM2Q
提取码:e5dq