簡體   English   中英

將代理分配給具有固定開始時間和結束時間的任務的 CP/MILP 問題名稱是什么?

[英]What is the CP/MILP problem name for the assignment of agents to tasks with fixed start time and end time?

我正在嘗試解決將代理分配給任務的約束滿足優化問題。 然而,與基本的分配問題不同的是,如果任務不重疊,則可以將代理分配給許多任務。 每個任務都有固定的開始時間和結束時間。 代理根據一些一元和二元約束分配給任務。

變量 = 任務集

域 = 一組兼容的代理(對於每個變量)

約束=一元&二元

優化 fct = some liniar function

問題示例:為我們知道到達和離開時間的卡車分配停車位(或車隊)。

我對文獻中是否有此類問題的准確名稱感興趣。 我認為這是某種分配問題。 另外,如果您曾經遇到過這個問題,您是如何解決的?

謝謝你。

我會將其解釋為:具有沖突的矩形賦值問題可以說比多項式可解決的賦值問題更難(一般來說是NP 難)。

另一個答案中顯示的演示可能有效並且 ortools 的 cp-sat 很棒,但我沒有看到一個很好的理由在這里使用基於離散時間的推理就像它已經完成的那樣: interval-variablesedge-finding 和 co。 基於調度約束(+沖突分析/解釋)。 這些東西完全是矯枉過正,開銷會很明顯。 我認為沒有必要對時間進行推理,而只是對時間引發的沖突進行推理

編輯:可以 label 這兩種方法(鏈接+建議)作為compact formulationextended formulation 只要可擴展性不是問題,擴展公式通常會顯示出更強的松弛和更好的(求解)結果。 對於更大的數據,緊湊的方法可能會再次變得更加可行(這里很難猜測,因為調度傳播器並不便宜)。

我會建議:

  • (1)按照基本的分配問題公式+ 調整使其成為矩形-> 允許一個工人處理多個任務,同時處理所有任務(一個和等式下降)
  • (2) 添加完整性= 將變量標記為二進制 -> 因為問題不再滿足完全單模
  • (3)添加約束,禁止沖突
  • (4) 添加約束:剩余的東西(例如兼容性

現在這一切都很簡單,但我會針對 (3) 提出一個非天真的改進

  • 沖突可以解釋為穩定集多胞體
  • 您的沖突是由先驗定義的時間窗口及其重疊引起的(正如我所解釋的;這是整個答案背后的核心假設)
  • 這是一個區間圖(因為有時間窗)
  • 所有區間圖都是弦圖
  • 弦圖允許在多時間內枚舉所有最大派系(暗示只有多項式多)
  • 所有最大派系的集合(枚舉)定義了穩定集多面體的面
  • 那些(集合中每個元素的約束)我們添加為約束!
  • (這里使用的圖上的穩定集多胞形也允許非常非常強大的半定松弛,但很難預見在哪些情況下這實際上會有所幫助,因為 SDP 更難處理:樹搜索中的熱啟動;可擴展性;……)

這將導致多尺寸整數規划問題,這在使用良好的 IP 求解器時應該非常好(商業廣告或如果需要開源: Cbc > GLPK )。

關於小demo(三)

import itertools
import networkx as nx

# data: inclusive, exclusive
# --------------------------
time_windows = [
  (2, 7),
  (0, 10),
  (6, 12),
  (12, 20),
  (8, 12),
  (16, 20)
]

# helper
# ------
def is_overlapping(a, b):
  return (b[1] > a[0] and b[0] < a[1])

# raw conflicts
# -------------
binary_conflicts = [] 
for a, b in itertools.combinations(range(len(time_windows)), 2):
  if is_overlapping(time_windows[a], time_windows[b]):
    binary_conflicts.append( (a, b) )

# conflict graph
# --------------
G = nx.Graph()
G.add_edges_from(binary_conflicts)

# maximal cliques
# ---------------
max_cliques = nx.chordal_graph_cliques(G)

print('naive constraints: raw binary conflicts')
for i in binary_conflicts:
  print('sum({}) <= 1'.format(i))

print('improved constraints: clique-constraints')
for i in max_cliques:
  print('sum({}) <= 1'.format(list(i)))

Output:

naive constraints: raw binary conflicts
sum((0, 1)) <= 1
sum((0, 2)) <= 1
sum((1, 2)) <= 1
sum((1, 4)) <= 1
sum((2, 4)) <= 1
sum((3, 5)) <= 1
improved constraints: clique-constraints
sum([1, 2, 4]) <= 1
sum([0, 1, 2]) <= 1
sum([3, 5]) <= 1

有趣的事實:

  • 商業整數規划求解器甚至 Cbc 甚至可能會在某種程度上嘗試對集團約束進行相同的推理,盡管沒有假設弦度,這是一個 NP 難問題
  • ortools 的 cp-sat 求解器也有一個代碼路徑(同樣:一般 NP-hard 案例)
    • 應該在表達基於沖突的 model 時觸發(更難決定這種基於一般離散時間的調度模型的利用)

注意事項

實施/可擴展性

還有一些懸而未決的問題,例如:

  • 復制每個工人的 max-clique 約束與以某種方式合並它們
  • 更有效/更聰明地發現沖突(排序)
  • 它會擴展到數據嗎:圖表有多大/我們需要多少沖突和約束

但這些事情通常遵循實例統計(又名“不要盲目決定”)。

我不知道您所描述的特定變體的名稱 - 也許其他人會。 然而,這似乎確實適合 CP/MIP 求解器; 我會使用 OR-Tools CP-SAT 求解器 go,它免費、靈活且通常運行良好。

下面是 Python 的參考實現,假設每輛車都需要分配一個團隊且沒有重疊,並且目標是盡量減少使用中的團隊數量。 該框架允許直接 model 允許/禁止分配(查看文檔

from ortools.sat.python import cp_model
model = cp_model.CpModel()

## Data
num_vehicles = 20
max_teams = 10

# Generate some (semi-)interesting data
interval_starts = [i % 9 for i in range(num_vehicles)]
interval_len = [ (num_vehicles - i) % 6 for i in range(num_vehicles)]
interval_ends = [ interval_starts[i] + interval_len[i] for i in range(num_vehicles)]


### variables

# t, v is true iff vehicle v is served by team t
team_assignments = {(t, v): model.NewBoolVar("team_assignments_%i_%i" % (t, v)) for t in range(max_teams) for v in range(num_vehicles)}

#intervals for vehicles. Each interval can be active or non active, according to team_assignments
vehicle_intervals = {(t, v): model.NewOptionalIntervalVar(interval_starts[v], interval_len[v], interval_ends[v], team_assignments[t, v], 'vehicle_intervals_%i_%i' % (t, v)) 
                     for t in range(max_teams) for v in range(num_vehicles)}

team_in_use = [model.NewBoolVar('team_in_use_%i' % (t)) for t in range(max_teams)]

## constraints
# non overlap for each team
for t in range(max_teams):
    model.AddNoOverlap([vehicle_intervals[t, v] for v in range(num_vehicles)])
    
# each vehicle must be served by exactly one team
for v in range(num_vehicles):
    model.Add(sum(team_assignments[t, v] for t in range(max_teams)) == 1)

# what teams are in use?
for t in range(max_teams):
    model.AddMaxEquality(team_in_use[t], [team_assignments[t, v] for v in range(num_vehicles)])

#symmetry breaking - use teams in-order
for t in range(max_teams-1):
    model.AddImplication(team_in_use[t].Not(), team_in_use[t+1].Not())


# let's say that the goal is to minimize the number of teams required
model.Minimize(sum(team_in_use))

solver = cp_model.CpSolver()

# optional
# solver.parameters.log_search_progress = True     
# solver.parameters.num_search_workers = 8
# solver.parameters.max_time_in_seconds = 5

result_status = solver.Solve(model)


if (result_status == cp_model.INFEASIBLE): 
    print('No feasible solution under constraints')
elif (result_status == cp_model.OPTIMAL):
    print('Optimal result found, required teams=%i' % (solver.ObjectiveValue()))
elif (result_status == cp_model.FEASIBLE):                        
    print('Feasible (non optimal) result found')
else:
    print('No feasible solution found under constraints within time')  

# Output:
#
# Optimal result found, required teams=7        

編輯:

@sascha 提出了一種分析(提前已知)時間 window 重疊的漂亮方法,這將使它可以作為分配問題解決。

因此,雖然上面的公式可能不是最佳的(盡管它可能是,這取決於求解器的工作方式),但我嘗試用建議的 max-clique 方法替換無重疊條件 - 下面的完整代碼。

我對中等規模的問題(100 輛和 300 輛汽車)進行了一些實驗,從經驗來看,在較小的問題(~100 輛)上,這確實有所改善——在最佳解決方案的時間上平均提高了 15%; 但我找不到對較大(~300)問題的重大改進。 這可能是因為我的公式不是最優的; 因為 CP-SAT 求解器(也是一個很好的 IP 求解器)足夠聰明; 或者因為我錯過了什么:)

代碼:

(這基本上與上面的代碼相同,其邏輯支持使用 .network 方法而不是從@sascha 的答案中復制的無重疊方法):

from timeit import default_timer as timer
from ortools.sat.python import cp_model
model = cp_model.CpModel()

run_start_time = timer()

## Data
num_vehicles = 300
max_teams = 300

USE_MAX_CLIQUES = True

# Generate some (semi-)interesting data
interval_starts = [i % 9 for i in range(num_vehicles)]
interval_len = [ (num_vehicles - i) % 6 for i in range(num_vehicles)]
interval_ends = [ interval_starts[i] + interval_len[i] for i in range(num_vehicles)]

if (USE_MAX_CLIQUES):
    ## Max-cliques analysis
    # for the max-clique approach
    time_windows = [(interval_starts[i], interval_ends[i]) for i in range(num_vehicles)]

    def is_overlapping(a, b):
      return (b[1] > a[0] and b[0] < a[1])

    # raw conflicts
    # -------------
    binary_conflicts = [] 
    for a, b in itertools.combinations(range(len(time_windows)), 2):
      if is_overlapping(time_windows[a], time_windows[b]):
        binary_conflicts.append( (a, b) )

    # conflict graph
    # --------------
    G = nx.Graph()
    G.add_edges_from(binary_conflicts)

    # maximal cliques
    # ---------------
    max_cliques = nx.chordal_graph_cliques(G)

##

### variables

# t, v is true iff point vehicle v is served by team t
team_assignments = {(t, v): model.NewBoolVar("team_assignments_%i_%i" % (t, v)) for t in range(max_teams) for v in range(num_vehicles)}

#intervals for vehicles. Each interval can be active or non active, according to team_assignments
vehicle_intervals = {(t, v): model.NewOptionalIntervalVar(interval_starts[v], interval_len[v], interval_ends[v], team_assignments[t, v], 'vehicle_intervals_%i_%i' % (t, v)) 
                     for t in range(max_teams) for v in range(num_vehicles)}

team_in_use = [model.NewBoolVar('team_in_use_%i' % (t)) for t in range(max_teams)]

## constraints
# non overlap for each team
if (USE_MAX_CLIQUES):
    overlap_constraints = [list(l) for l in max_cliques]
    for t in range(max_teams):
        for l in overlap_constraints:
            model.Add(sum(team_assignments[t, v] for v in l) <= 1)
else:        
    for t in range(max_teams):
        model.AddNoOverlap([vehicle_intervals[t, v] for v in range(num_vehicles)])
        

    
# each vehicle must be served by exactly one team
for v in range(num_vehicles):
    model.Add(sum(team_assignments[t, v] for t in range(max_teams)) == 1)

# what teams are in use?
for t in range(max_teams):
    model.AddMaxEquality(team_in_use[t], [team_assignments[t, v] for v in range(num_vehicles)])

#symmetry breaking - use teams in-order
for t in range(max_teams-1):
    model.AddImplication(team_in_use[t].Not(), team_in_use[t+1].Not())


# let's say that the goal is to minimize the number of teams required
model.Minimize(sum(team_in_use))

solver = cp_model.CpSolver()

# optional
solver.parameters.log_search_progress = True     
solver.parameters.num_search_workers = 8
solver.parameters.max_time_in_seconds = 120

result_status = solver.Solve(model)


if (result_status == cp_model.INFEASIBLE): 
    print('No feasible solution under constraints')
elif (result_status == cp_model.OPTIMAL):
    print('Optimal result found, required teams=%i' % (solver.ObjectiveValue()))
elif (result_status == cp_model.FEASIBLE):                        
    print('Feasible (non optimal) result found, required teams=%i' % (solver.ObjectiveValue()))
else:
    print('No feasible solution found under constraints within time')  
    
print('run time: %.2f sec ' % (timer() - run_start_time))

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM