大規模 VRP 與啟發式演算法

VRP 是 NP-Hard 問題。當客戶數量超過 100 個時,OR-Tools 的 exact solver 可能需要數小時才能找到最佳解。實務上,我們使用啟發式演算法來找到「夠好」的解。

Clarke-Wright 節省法 (Savings Algorithm)

這是實務上最常用的 VRP 啟發式演算法,由 Clarke 與 Wright 在 1964 年提出。

import numpy as np

def clarke_wright_savings(dist_matrix, demands, capacity, depot=0):
    """
    Clarke-Wright 節省法
    
    核心概念:合併兩條路線能節省的距離 = d(0,i) + d(0,j) - d(i,j)
    """
    num_nodes = len(dist_matrix)
    
    # 第一階段:每個節點一條獨立路線
    routes = [[i] for i in range(num_nodes) if i != depot]
    
    # 計算所有節點對的節省值
    savings = []
    for i in range(1, num_nodes):
        for j in range(i + 1, num_nodes):
            saving = dist_matrix[depot][i] + dist_matrix[depot][j] - dist_matrix[i][j]
            if saving > 0:  # 只保留有節省效果的
                savings.append((saving, i, j))
    
    # 按節省值排序(從大到小)
    savings.sort(reverse=True, key=lambda x: x[0])
    
    # 第二階段:合併路線
    node_to_route = {}
    for idx, route in enumerate(routes):
        for node in route:
            node_to_route[node] = idx
    
    for saving, i, j in savings:
        route_i = node_to_route[i]
        route_j = node_to_route[j]
        
        if route_i == route_j:
            continue  # 已經在同一路線
        
        # 檢查容量限制
        merged_demand = sum(demands[node] for node in routes[route_i] + routes[route_j])
        if merged_demand > capacity:
            continue  # 超過容量
        
        # 合併路線
        # 確保 i 在末端,j 在開端
        if routes[route_i][-1] == i and routes[route_j][0] == j:
            new_route = routes[route_i] + routes[route_j]
        elif routes[route_i][0] == i and routes[route_j][-1] == j:
            new_route = routes[route_j] + routes[route_i]
        elif routes[route_i][-1] == i and routes[route_j][-1] == j:
            new_route = routes[route_i] + list(reversed(routes[route_j]))
        elif routes[route_i][0] == i and routes[route_j][0] == j:
            new_route = list(reversed(routes[route_j])) + routes[route_i]
        else:
            continue
        
        # 合併
        routes[route_i] = new_route
        routes[route_j] = []
        
        # 更新映射
        for node in new_route:
            node_to_route[node] = route_i
    
    # 過濾空路線
    return [r for r in routes if r]

# 測試
np.random.seed(42)
n = 50  # 50 個客戶
locations = np.random.rand(n + 1, 2) * 100

# 距離矩陣
dist = np.zeros((n + 1, n + 1))
for i in range(n + 1):
    for j in range(n + 1):
        dist[i][j] = np.sqrt((locations[i][0]-locations[j][0])**2 +
                             (locations[i][1]-locations[j][1])**2)

demands = [0] + list(np.random.randint(50, 200, n))
capacity = 500

print(f"\n=== Clarke-Wright 節省法 ===")
print(f"客戶數: {n}, 車輛容量: {capacity}")

routes = clarke_wright_savings(dist, demands, capacity)

print(f"\n路線數量: {len(routes)}")
for idx, route in enumerate(routes):
    route_dist = sum(dist[node][route[(i+1) % len(route)]] 
                    for i, node in enumerate(route))
    route_demand = sum(demands[node] for node in route)
    print(f"路線 {idx}: {route} (距離: {route_dist:.1f}, 載重: {route_demand})")

解鎖完整教學內容

本章為付費內容。加入專案即可解鎖超過 5000 字的深度解析,包含 10 個以上神級 Prompt 與真實 Source Code 範例!