排班问题应用非常广泛,之前给咖驿站做【自动化排班表】的时候遇到过。当时在Matlab里设计相应的评价函数,采用【粒子群算法】把这个问题解决了。在b站也有up主采用【遗传算法】去实现了排班问题的求解,链接如下:
https://www.bilibili.com/video/BV1xb4y1Y7d3?spm_id_from=333.337.search-card.all.click
这一次采用回溯算法去解决这个问题,编程语言为C++。
排班问题可以抽象为如何调节两大主体的供需关系。
以值班为例,主体分别为【值班人员】与【值班时间】。比如说有2位值班人员,2个值班时间,【值班人员A1】的需求是【值班时间B1】,【值班人员A2】的需求是【值班时间B2】,那么可以抽象为如下的矩阵:
1 0
0 1
那么排班问题即为如何保证【每个人值班次数不少于一定限制】,【每个值班时间不超过一定限制】。那么具体到矩阵而言,即为
【如何将一定数量的1置为0,从而保证每行不超过规定限制,每列不超过规定限制。】
/*
* paiban 排班问题主函数
* mat 待排班的矩阵 每行为值班人员 每列为值班时间
* rqm 行要求,最终结果应每行小于等于rqm
* rqn 列要求,最终结果应每列大于等于rqn
* 返回值 1 该问题有解 0 该问题无解
*/
bool paiban(vector<vector<int>>& mat, int rqm, int rqn) {
int m = mat.size(), n = mat[0].size();
bool ans = false, flag = false;
// 如果第i个值班时间最多只有小于rqn个值班人员,那么这一算法无解
for (int i = 0; i < n; ++i) {
// col_count列元素求和
if (col_count(mat, i) < rqn) return false;
}
// 否则可能有解
for (int j = 0; j < n; ++j) { // j为列
if (flag) break;
for (int i = 0; i < m; ++i) { // i为行
if (mat[i][j] == 1 && col_count(mat, j) > rqn) {
mat[i][j] = 0;
ans = ans || backtracking(mat, m, n, i, j, rqm, rqn);
if (ans) {
flag = true;
break;
}
mat[i][j] = 1;
}
}
}
return ans;
}
对于vector
/*
* col_count 列求和函数
* mat 待排班的矩阵 每行为值班人员 每列为值班时间
* c 列数
* 返回值 该列元素之和
*/
int col_count(vector<vector<int>> mat, int c) {
int m = mat.size(), ans = 0;
for (int i = 0; i < m; ++i) {
ans += mat[i][c];
}
return ans;
}
回溯算法搜索解空间:
/*
* backtracking 回溯函数
* mat 待排班的矩阵 每行为值班人员 每列为值班时间
* m 行数
* n 列数
* r 搜索起始点所在行
* c 搜索起始点所在列
* rqm 行要求,最终结果应每行小于等于rqm
* rqn 列要求,最终结果应每列大于等于rqn
* 返回值 1 搜索到解 0 未搜索到解
*/
bool backtracking(vector<vector<int>>& mat, int m, int n, int r, int c, int rqm, int rqn) {
// 先确定出口条件
if (meetrequire(mat, m, n, rqm, rqn)) return true;
bool ans = false, flag = false;
// 遍历完第c列
if (r < m - 1) {
for (int i = r + 1; i < m; ++i) {
if (mat[i][c] == 1 && col_count(mat, c) > 2) {
mat[i][c] = 0;
ans = ans || backtracking(mat, m, n, i, c, rqm, rqn);
if (ans) {
flag = true;
break;
}
mat[i][c] = 1;
}
}
}
// 再遍历c+1列及之后
if (c < n - 1) {
for (int j = c + 1; j < n; ++j) { // j为列
if (flag) break;
for (int i = 0; i < m; ++i) { // i为行
if (mat[i][j] == 1 && col_count(mat, j) > 2) {
mat[i][j] = 0;
ans = ans || backtracking(mat, m, n, i, j, rqm, rqn);
if (ans) {
flag = true;
break;
}
mat[i][j] = 1;
}
}
}
}
return ans;
}
回溯算法的出口是搜索到满足条件的mat并返回true:
/*
* meetrequire 判断是否搜索到符合条件的矩阵
* mat 待排班的矩阵 每行为值班人员 每列为值班时间
* m 行数
* n 列数
* rqm 行要求,最终结果应每行小于等于rqm
* rqn 列要求,最终结果应每列大于等于rqn
* 返回值 1 符合条件 0 不符合条件
*/
bool meetrequire(vector<vector<int>>& mat, int m, int n, int rqm, int rqn) {
// 检查每一行
for (int i = 0; i < m; ++i) {
if (count(mat[i].begin(), mat[i].end(), 1) > rqm) return false;
}
// 检查每一列
for (int i = 0; i < n; ++i) {
if (col_count(mat, i) < rqn) return false;
}
return true;
}
以上是通过回溯算法求解排班问题的思路及C++代码。