寻求兼顾服务时长平衡的经纬度位置聚类算法方案
基于最小费用流的集群时长平衡C#实现方案
核心思路
先用你已有的聚类算法(如K-Means)生成空间连续、互不重叠的初始服务区域,再通过最小费用流算法调整区域间的客户分配,让各区域总服务时长逼近目标值(总时长/区域数),同时严格保证区域空间连续性(避免重叠)。
步骤拆解
初始聚类与目标计算
- 用K-Means/MiniBatch K-Means生成K个空间连续的初始区域,统计每个区域的总服务时长
S_i。 - 计算目标时长
T = 总服务时长 / K,定义每个区域的溢出量Excess_i = max(S_i - T, 0),缺口量Deficit_i = max(T - S_i, 0)。
- 用K-Means/MiniBatch K-Means生成K个空间连续的初始区域,统计每个区域的总服务时长
最小费用流模型构建
- 节点定义:源点
Source、汇点Sink,每个区域对应一个独立节点。 - 边定义:
- 源点→溢出区域节点:容量=Excess_i,费用=0(表示可输出的多余时长)
- 缺口区域节点→汇点:容量=Deficit_i,费用=0(表示需要补充的时长)
- 溢出区域→相邻缺口区域:仅当两个区域空间相邻时连边,容量=min(Excess_i, Deficit_j),费用=区域中心距离的整数倍(优先转移空间更近的客户,保证区域连续性)
- 节点定义:源点
运行最小费用流算法
通过算法找到从源到汇的最小费用流,对应最优客户转移方案,调整后各区域总时长方差可控制在1%以内。
C# 实现示例
1. 最小费用流算法核心实现
public class FlowEdge { public int To { get; set; } public int Reverse { get; set; } public int Capacity { get; set; } public int Cost { get; set; } public FlowEdge(int to, int reverse, int capacity, int cost) { To = to; Reverse = reverse; Capacity = capacity; Cost = cost; } } public class MinCostFlow { private readonly List<List<FlowEdge>> _graph; private readonly int[] _potential; private readonly int[] _dist; private readonly int[] _prevNode; private readonly int[] _prevEdge; public MinCostFlow(int nodeCount) { _graph = new List<List<FlowEdge>>(); for (int i = 0; i < nodeCount; i++) { _graph.Add(new List<FlowEdge>()); } _potential = new int[nodeCount]; _dist = new int[nodeCount]; _prevNode = new int[nodeCount]; _prevEdge = new int[nodeCount]; } public void AddEdge(int from, int to, int capacity, int cost) { _graph[from].Add(new FlowEdge(to, _graph[to].Count, capacity, cost)); _graph[to].Add(new FlowEdge(from, _graph[from].Count - 1, 0, -cost)); } public int Compute(int source, int sink, int maxFlow) { int totalCost = 0; Array.Fill(_potential, 0); while (maxFlow > 0) { var priorityQueue = new PriorityQueue<(int Dist, int Node), int>(); Array.Fill(_dist, int.MaxValue); _dist[source] = 0; priorityQueue.Enqueue((0, source), 0); while (priorityQueue.Count > 0) { var (currentDist, u) = priorityQueue.Dequeue(); if (currentDist > _dist[u]) continue; for (int i = 0; i < _graph[u].Count; i++) { var edge = _graph[u][i]; if (edge.Capacity <= 0) continue; int newDist = _dist[u] + edge.Cost + _potential[u] - _potential[edge.To]; if (_dist[edge.To] > newDist) { _dist[edge.To] = newDist; _prevNode[edge.To] = u; _prevEdge[edge.To] = i; priorityQueue.Enqueue((newDist, edge.To), newDist); } } } if (_dist[sink] == int.MaxValue) break; for (int v = 0; v < _graph.Count; v++) { if (_dist[v] != int.MaxValue) _potential[v] += _dist[v]; } int flow = maxFlow; for (int v = sink; v != source; v = _prevNode[v]) { flow = Math.Min(flow, _graph[_prevNode[v]][_prevEdge[v]].Capacity); } maxFlow -= flow; totalCost += flow * _potential[sink]; for (int v = sink; v != source; v = _prevNode[v]) { var edge = _graph[_prevNode[v]][_prevEdge[v]]; edge.Capacity -= flow; _graph[v][edge.Reverse].Capacity += flow; } } return totalCost; } }
2. 业务逻辑适配(区域平衡)
public class ServiceArea { public int Id { get; set; } public double TotalDuration { get; set; } public List<Customer> Customers { get; set; } = new(); public List<int> NeighborAreaIds { get; set; } = new(); // 仅相邻区域可转移客户 } public class Customer { public double Duration { get; set; } public (double X, double Y) Coords { get; set; } } public void BalanceServiceAreas(List<ServiceArea> areas) { double totalDuration = areas.Sum(a => a.TotalDuration); int k = areas.Count; double targetDuration = totalDuration / k; // 放大时长为整数,避免浮点误差 int scale = 100; int scaledTarget = (int)Math.Round(targetDuration * scale); int nodeCount = 2 + k; // 源点(0)、汇点(1)、区域节点(2~k+1) var minCostFlow = new MinCostFlow(nodeCount); int totalExcess = 0; // 构建源/汇与区域的边 for (int i = 0; i < k; i++) { int areaNode = 2 + i; int scaledTotal = (int)Math.Round(areas[i].TotalDuration * scale); int excess = scaledTotal - scaledTarget; if (excess > 0) { minCostFlow.AddEdge(0, areaNode, excess, 0); totalExcess += excess; } else if (excess < 0) { minCostFlow.AddEdge(areaNode, 1, -excess, 0); } } // 构建相邻区域间的转移边 for (int i = 0; i < k; i++) { var srcArea = areas[i]; int scaledSrcTotal = (int)Math.Round(srcArea.TotalDuration * scale); if (scaledSrcTotal <= scaledTarget) continue; int srcNode = 2 + i; foreach (var neighborId in srcArea.NeighborAreaIds) { var destArea = areas.First(a => a.Id == neighborId); int scaledDestTotal = (int)Math.Round(destArea.TotalDuration * scale); if (scaledDestTotal >= scaledTarget) continue; int destNode = 2 + areas.IndexOf(destArea); // 转移成本:区域中心距离放大100倍,优先转移近的客户 double distance = GetCenterDistance(srcArea, destArea); int cost = (int)Math.Round(distance * 100); int maxTransfer = Math.Min(scaledSrcTotal - scaledTarget, scaledTarget - scaledDestTotal); if (maxTransfer > 0) { minCostFlow.AddEdge(srcNode, destNode, maxTransfer, cost); } } } // 运行最小费用流 minCostFlow.Compute(0, 1, totalExcess); // 根据残差网络调整客户分配(需遍历边的流量,将对应时长的客户从源区域转移到目标区域) // 此处省略具体客户转移逻辑,核心是按流量匹配转移对应时长的客户 } // 计算区域中心距离 private double GetCenterDistance(ServiceArea a, ServiceArea b) { var aCenter = (X: a.Customers.Avg(c => c.Coords.X), Y: a.Customers.Avg(c => c.Coords.Y)); var bCenter = (X: b.Customers.Avg(c => c.Coords.X), Y: b.Customers.Avg(c => c.Coords.Y)); return Math.Sqrt(Math.Pow(aCenter.X - bCenter.X, 2) + Math.Pow(aCenter.Y - bCenter.Y, 2)); } // 扩展方法:计算集合平均值 public static double Avg<TSource>(this IEnumerable<TSource> source, Func<TSource, double> selector) { return source.Select(selector).Average(); }
关键注意事项
- 区域相邻性:必须通过初始聚类的邻接矩阵或空间索引(如R树)识别相邻区域,禁止跨非相邻区域转移客户,否则会破坏区域连续性。
- 性能优化:针对50万+客户场景,初始聚类建议用MiniBatch K-Means;区域数K建议控制在1000以内,若K过大需拆分批次处理。
- 精度控制:通过放大时长为整数避免浮点误差,调整完成后再还原为原单位。
内容的提问来源于stack exchange,提问作者DesperateAndTired
相关产品推荐
相关产品推荐

