記事ディレクトリ
記事の背景
整数計画法モデリング+ソルバー計算という手法を実践して、マスターキーを見つけたような気がしましたが、すべての組み合わせ最適化問題はこの方法で解けます。
しかし実際には、多くの古典的な組み合わせ最適化問題には、ナップザック問題の動的計画法アルゴリズムや代入問題のハンガリアン アルゴリズムなど、比較的古典的な解決アルゴリズムがあることも学びました。
これにより、私の中に「これらの古典的なアルゴリズムの価値は何だろう?」という疑問が生まれました。それでも学習して使用する必要がありますか?
この記事の続きでは、上記の疑問に答えるために、ナップサック問題と代入問題の解決策と効果について詳しく説明します。
バックパックの問題
ナップザック問題は次のように説明できます。n个重量为 w 1 , w 2 , ⋅ ⋅ ⋅ , w n w_1, w_2,···, w_n w1、w2、⋅⋅⋅、wん、价值为 v 1 , v 2 , ⋅ ⋅ ⋅ , v n v_1,v_2,···,v_n v1、v2、⋅⋅⋅、vん最大積載量がWWのアイテムWのバックパックの場合、バックパックに積み込むことができる最高の価値を持つこれらのアイテムのサブセットを見つけます。
数学的モデル
整数計画法
当面は古典的なアルゴリズムを無視し、整数計画問題として直接モデル化します。
定义 x i x_{i} バツ私は为第 i i iアイテムをバックパックに入れるかどうかは、値が 0 の場合は入れられていないことを意味し、値が 1 の場合は入れられたことを意味します。
このとき、以下の整数計画モデル
max ∑ i = 1 nvixi st ∑ i = 1 nwixi ≤ W , i = 1 , 2 , ... , nxi ∈ { 0 , 1 } , i = 1 , 2 , ... となる。 . , n max \quad \sum_{i=1}^nv_{i}x_{i} \\ \text{st} \quad \sum_{i=1}^nw_ix_{i}≤W, \quad i = 1,2,...,n \\ \nonumber x_{i} \in \{0,1\} 、\quad i=1,2,...,n\\マ×i = 1∑んv私はバツ私はセントi = 1∑んw私はバツ私は≤わ、私=1 、2 、... 、nバツ私は∈{
0 ,1 } 、私=1 、2 、... 、n
動的プログラミング
ナップザック問題を解決するための古典的なアルゴリズムについては、LeetCode を使用したことがある人なら誰でも、それが動的計画法アルゴリズムであることを知っているはずです。動的計画法のアルゴリズム原理はこの記事の焦点ではないので、ここではリンクのみを示します。興味のある人は自分で調べてください。
シミュレーション
次のコードは、Python で実装されたナップザック問題を解決するための整数計画アルゴリズムと動的計画アルゴリズムに基づいています。NNを調整することでNの値によって、ナップザック問題の規模が変化する可能性があります。したがって、最適解の品質や解の速度など、異なる問題サイズの下で 2 つのアルゴリズムの結果を直感的に比較して、アルゴリズムの機能を評価できます。
from ortools.linear_solver import pywraplp
import numpy as np
import time
def calc_by_ortools(N, w, v, W):
# 声明ortools求解器,使用SCIP算法
solver = pywraplp.Solver.CreateSolver('SCIP')
# 优化变量,0-1变量
x = {
}
for j in range(N):
x[j] = solver.IntVar(0, 1, 'x[%i]' % j)
# 目标函数
obj_expr = [v[j][0] * x[j] for j in range(N)]
solver.Maximize(solver.Sum(obj_expr))
# 约束条件
cons_expr = [w[j][0] * x[j] for j in range(N)]
solver.Add(solver.Sum(cons_expr) <= W)
# 模型求解
status = solver.Solve()
# 打印模型结果
if status == pywraplp.Solver.OPTIMAL:
# 求解成功,打印最优目标函数值
print('ortools, best_f =', solver.Objective().Value())
else:
# 求解不成功,提示未收敛
print('not converge.')
def calc_by_dp(weight, value, bag_weight):
# 初始化: 全为0
dp = [0] * (bag_weight + 1)
# 先遍历物品, 再遍历背包容量
for i in range(len(weight)):
for j in range(bag_weight, weight[i][0] - 1, -1):
# 递归公式
dp[j] = max(dp[j], dp[j - weight[i][0]] + value[i][0])
print('dp, best_f =', dp[-1])
if __name__ == '__main__':
# 设置随机种子,确保每次运行生成的随机数相同
np.random.seed(0)
# 设定物品数量N,重量w,价值v,背包可承重W
N = 1000
w = np.random.randint(1, 10, (N, 1))
v = np.random.randint(1, 100, (N, 1))
W = int(N / 10)
print('N = ', N)
# 使用ortools求解,并统计计算耗时
t0 = time.time()
calc_by_ortools(N, w, v, W)
print('ortools计算耗时:{}'.format(time.time() - t0))
# 使用动态规划方法求解,并统计计算耗时
t1 = time.time()
calc_by_dp(w, v, W)
print('dp计算耗时:{}'.format(time.time() - t1))
次の表は、異なるNNの 2 つのアルゴリズムを示しています。Nに関する詳細なパフォーマンス データ。 ortools は整数計画アルゴリズムを指し、 dp は動的計画アルゴリズムを指します。
解の品質の観点から見ると、どちらのアルゴリズムも大域的な最適解を見つけることができるため、違いはありません。
ただし、解決効率の点で 2 つのアルゴリズムには大きな違いがあります: N<1000 の場合、ortools の計算時間は dp よりも大きくなりますが、絶対値は非常に小さくなります; N=1000 の場合、計算時間はortools と dp の差はすでに比較的小さいです。小さいため、 NNを増やし続けます。N以降、 ortools の計算時間は dp よりも短くなり、 dp の計算時間は明らかに ortools の計算時間よりも速く増加します。
N | アルゴリズム | 最適なソリューション | 時間がかかる、 |
---|---|---|---|
10 | オルツール | 89 | 0.0085 |
DP | 89 | 0.0000 | |
100 | オルツール | 616 | 0.0117 |
DP | 616 | 0.0007 | |
1000 | オルツール | 6154 | 0.0424 |
DP | 6154 | 0.0629 | |
10000 | オルツール | 60509 | 0.4257 |
DP | 60509 | 7.6769 | |
100000 | オルツール | 617258 | 5.111 |
DP | 617258 | 730.8 |
一見すると、この比較には何の問題もないように思えます。しかし、まとめているときに突然、ortools は C++ ベースで書かれていて、dp は Python で書かれていることを思い出しました。dp ではプログラミング言語が失われるのでしょうか。そこで、Java に変更して再試行してみました (理由は聞かないでください) C++ は使わないでください。頼めば使わないでしょう)。
以下は Java バージョンのアルゴリズム実装ですが、全体的なロジックは Python と一致しているため、詳細は説明しません。
import java.util.Random;
import com.google.ortools.Loader;
import com.google.ortools.linearsolver.MPConstraint;
import com.google.ortools.linearsolver.MPObjective;
import com.google.ortools.linearsolver.MPSolver;
import com.google.ortools.linearsolver.MPVariable;
public class ZeroOnePack {
// 预加载本地库
static {
Loader.loadNativeLibraries();
}
public static void DP(int W, int N, int[] weight, int[] value){
//动态规划
int[] dp = new int[W +1];
for(int i=1;i<N+1;i++){
//逆序实现
for(int j = W; j>=weight[i-1]; j--){
dp[j] = Math.max(dp[j-weight[i-1]]+value[i-1],dp[j]);
}
}
// 打印最优解
System.out.println("DP, best_f: " + dp[W]);
}
public static void orToolsMethod(int W, int N, int[] weight, int[] value){
// 声明求解器
MPSolver solver = MPSolver.createSolver("SCIP");
if (solver == null) {
System.out.println("Could not create solver SCIP");
return;
}
// 优化变量
MPVariable[] x = new MPVariable[N];
for (int j = 0; j < N; ++j) {
x[j] = solver.makeIntVar(0.0, 1, "");
}
// 目标函数
MPObjective objective = solver.objective();
for (int j = 0; j < N; ++j) {
objective.setCoefficient(x[j], value[j]);
}
// 约束条件
objective.setMaximization();
MPConstraint constraint = solver.makeConstraint(0, W, "");
for (int j = 0; j < N; ++j) {
constraint.setCoefficient(x[j], weight[j]);
}
// 模型求解
MPSolver.ResultStatus resultStatus = solver.solve();
if (resultStatus == MPSolver.ResultStatus.OPTIMAL) {
// 求解成功,打印最优目标函数值
System.out.println("ortools, best_f = " + objective.value());
} else {
// 求解不成功,提示未收敛
System.err.println("The problem does not have an optimal solution.");
}
}
public static void main(String[] args) {
//设置随机种子,确保每次运行生成的随机数相同
Random rand =new Random(0);
// 设定物品数量N,重量weight,价值value,背包可承重W
int N = 1000000;
int[] weight=new int[N];
for(int i=0;i<weight.length;i++){
weight[i]= rand.nextInt(10) + 1;
}
int[] value=new int[N];
for(int i=0;i<value.length;i++){
value[i]= rand.nextInt(100) + 1;
}
int W = (int) N / 10;
System.out.println("N = " + N);
// 使用ortools求解,并统计计算耗时
long start = System.currentTimeMillis();
orToolsMethod(W, N, weight, value);
System.out.println("cost time: " + (System.currentTimeMillis() - start) + " ms");
// 使用动态规划方法求解,并统计计算耗时
start = System.currentTimeMillis();
DP(W, N, weight, value);
System.out.println("cost time: " + (System.currentTimeMillis() - start) + " ms");
}
}
次の表は、異なるNNの 2 つのアルゴリズム (Java バージョン) を示しています。Nの詳細なパフォーマンス データについては
Java の使用により、dp の計算効率が大幅に向上しました。たとえば、N = 100000 N=100000N=100000では、Java では 2 秒かかりますが、Python では最大 730 秒かかります。
しかしそれでも、アルゴリズム比較の結論は変わりません。どちらのアルゴリズムも最適解を見つけることができますが、計算効率の観点からは、ortools が最初に遅れをとり、その後リードします。
N | アルゴリズム | 最適なソリューション | 時間がかかります、ミリ秒 |
---|---|---|---|
10 | オルツール | 78 | 9 |
DP | 78 | 0 | |
100 | オルツール | 481 | 10 |
DP | 481 | 0 | |
1000 | オルツール | 6224 | 17 |
DP | 6224 | 3 | |
10000 | オルツール | 60442 | 81 |
DP | 60442 | 22 | |
100000 | オルツール | 603439 | 2405 |
DP | 603439 | 2039年 | |
200000 | オルツール | 1207108 | 3128 |
DP | 1207108 | 6994 | |
1000000 | オルツール | 6037100 | 15614 |
DP | 6037100 | 135898 |
結果分析
ナップザック問題は最適化原理と余効なしの原理を満たすため、動的計画法アルゴリズムを使用してナップザック問題に対する大域的最適解を得ることができます。解法プロセス中の動的計画法アルゴリズムの時間計算量はO ( n W ) O(nW)です。O ( nW )ですが、 WW以来Wは単なる入力データであり、入力スケールnnとして表現できます。nの指数形式であるため、これは擬似多項式アルゴリズムです。つまり、多項式アルゴリズムではありません。したがって、NNNが増加すると、dp の計算時間は非常に急速に増加します。
整数計画法が大域的最適解を得ることができることには疑いの余地はありませんが、それ自体は多項式アルゴリズムではないため、NNのようにNが増加すると、計算時間も大幅に増加しますが、比較データからは、整数計画法の方が動的計画法アルゴリズムよりも効率的です。
割り当て問題
バックパックの問題を分析した後、課題の問題を勉強しましょう。
割り当ての問題は次のように説明できます。nnn個人割り当てnnタスクはn 個あります。1 人に割り当てられるタスクは 1 つだけであり、1 つのタスクは 1 人にのみ割り当てられます。タスクを 1 人に割り当てるには、報酬の支払いが必要です。報酬総額が確実に満たされるようにタスクを割り当てる方法を見つけてください。支払われるのは最低額です。
数学的モデル
整数計画法
支払われる報酬を行列C n × n \pmb C_{n\times n}として設定します。Cn × n,其中 c i , j c_{i,j} c私、 jii代目を代表する個人iには jj番目がj個のタスクに対して支払われる必要がある報酬。
xi , j x_{i,j}を定義しますバツ私、 j为第 i i 個人iにjjj task の値が 0 の場合は割り当てられていないことを意味し、値が 1 の場合は割り当てられていることを示します。
このとき、次の数理計画モデルが成立する:
min ∑ i = 1 n ∑ j = 1 nci , jxi , j st ∑ j = 1 nxi , j = 1 , i = 1 , 2 , . . . , n ∑ i = 1 nxi , j = 1 , j = 1 , 2 , . . . , nxi , j ∈ { 0 , 1 } , i , j = 1 , 2 , . . . , n min \quad \sum_{i =1} ^n\sum_{j=1}^nc_{i,j}x_{i,j} \\ \text{st} \quad \sum_{j=1}^nx_{i,j}=1 , \quad i=1,2,...,n \\ \nonumber \sum_{i=1}^nx_{i,j}=1, \quad j=1,2,...,n \\ \nonumber x_ {i,j} \in \{0,1\} 、\quad i,j=1,2,...,n\\分i = 1∑んj = 1∑んc私、 jバツ私、 jセントj = 1∑んバツ私、 j=1 、私=1 、2 、... 、ni = 1∑んバツ私、 j=1 、j=1 、2 、... 、nバツ私、 j∈{
0 ,1 } 、私、j=1 、2 、... 、n
ハンガリーのアルゴリズム
割り当て問題の古典的なアルゴリズムは、ハンガリアン アルゴリズムです。ただし、このアルゴリズムは動的プログラミング アルゴリズムほど実装が簡単ではないため、既製のツールキットを見つける方が簡単です。この記事では、scipy.optimize パッケージの Linear_sum_assignment モジュールを使用します。このアルゴリズムの原理は文献で見つけることができます: 2D 長方形割り当てアルゴリズムの実装について. その本質は依然としてハンガリーのアルゴリズムであると言われていますが、記事の結論に影響を与えないため、著者はそれを注意深く研究しませんでした~
シミュレーション
次のコードは、Python で実装された代入問題を解決するための整数計画法アルゴリズムとハンガリアン アルゴリズムに基づいています。NNを調整することでNの値によって、割り当て問題のサイズが変化する可能性があります。したがって、解の質や解の速度など、異なる問題サイズの下でこれら 2 つのアルゴリズムの結果を簡単に比較して、アルゴリズムの能力を評価できます。
from ortools.linear_solver import pywraplp
from scipy.optimize import linear_sum_assignment
import numpy as np
import time
def calc_by_ortools(C):
# 声明ortools求解器,使用SCIP算法
solver = pywraplp.Solver.CreateSolver('SCIP')
m = C.shape[0]
n = C.shape[1]
# 优化变量,0-1变量
x = {
}
for i in range(m):
for j in range(n):
x[i, j] = solver.IntVar(0, 1, 'x[%i,%i]' % (i, j))
# 目标函数
obj_expr = [C[i][j] * x[i, j] for i in range(m) for j in range(n)]
solver.Minimize(solver.Sum(obj_expr))
# 约束条件
for i in range(m):
cons_expr = [x[i, j] for j in range(n)]
solver.Add(solver.Sum(cons_expr) == 1)
for j in range(n):
cons_expr = [x[i, j] for i in range(m)]
solver.Add(solver.Sum(cons_expr) == 1)
# 模型求解
status = solver.Solve()
# 打印模型结果
if status == pywraplp.Solver.OPTIMAL:
# 求解成功,打印最优目标函数值
print('ortools, best_f =', solver.Objective().Value())
else:
# 求解不成功,提示未收敛
print('not converge.')
def calc_by_scipy(C):
# 调用工具包:linear_sum_assignment
row_ind, col_ind = linear_sum_assignment(C)
# 打印最优目标函数值
print('scipy, best_f =', cost[row_ind, col_ind].sum())
if __name__ == '__main__':
# 设置随机种子,确保每次运行生成的随机数相同
np.random.seed(0)
# 设定报酬矩阵的维度
N = 1000
# 报酬范围是10~100间的随机值
cost = np.random.randint(10, 100, (N, N))
print('N = ', N)
# 使用ortools求解,并统计计算耗时
t0 = time.time()
calc_by_ortools(cost)
print('ortools计算耗时:{}'.format(time.time() - t0))
# 使用求解scipy中的 modified Jonker-Volgenant algorithm求解,并统计计算耗时
t1 = time.time()
calc_by_scipy(cost)
print('scipy计算耗时:{}'.format(time.time() - t1))
次の表は、異なるNNの 2 つのアルゴリズムを示しています。Nに関する詳細なパフォーマンス データ。 ortools は整数計画法アルゴリズムを指し、scipy はハンガリー アルゴリズムを指します。
解の品質の観点から見ると、2 つのアルゴリズムは常に全体的な最適解を見つけることができるため、違いはありません。
解決時間の点では、ortools は常に scipy よりも長く、NNを使用するとNが増加するにつれて、scipy の解析時間はゆっくりと増加しますが、ortools は非常に早く増加します。
N | アルゴリズム | 最適なソリューション | 時間がかかる、 |
---|---|---|---|
10 | オルツール | 222 | 0.0136 |
サイピー | 222 | 0 | |
50 | オルツール | 621 | 0.1599 |
サイピー | 621 | 0.0001 | |
100 | オルツール | 1087 | 0.9516 |
サイピー | 1087 | 0.0003 | |
300 | オルツール | 3034 | 9.9593 |
サイピー | 3034 | 0.0047 | |
500 | オルツール | 5004 | 24.89 |
サイピー | 5004 | 0.0118 | |
1000 | オルツール | 10000 | 177.5 |
サイピー | 10000 | 0.0396 |
結果分析
整数計画法アルゴリズムの結果は分析されません。基本的にはナップザック問題と同じです。ハンガリーのアルゴリズムを簡単に見てみましょう。
从算法原理上来说,它是针对指派问题的特点,找到的一个多项式算法,所以耗时非常短。
总结
其实分析分析后,结论已经呼之欲出了:将组合优化问题建模为整数规划问题来求解,本质上使用的是一种通用方案,只是由于很多公司都致力于迭代优化求解器的效率,所以目前来看,这个通用方案的整体表现还不错;但那些针对特定问题的特定算法,可以理解为一种个性化解决方案,旨在通过利用问题自身的特征,探查更高效的解决方案。
基于这个理解,在实际问题的求解中,应该优先将问题建模为有个性化求解算法同时问题复杂度是多项式的经典问题;其次是建模为有个性化求解算法但问题复杂度不是多项式的经典问题,此时需要对比经典算法和整数规划的效率和精度;最后才是直接建模为整数规划问题。
参考文献
背包问题,动态规划Python代码:https://blog.csdn.net/m0_51370744/article/details/127120649
背包问题,动态规划java代码:https://blog.csdn.net/baidu_41602099/article/details/110383230
指派问题和匈牙利算法:https://zhuanlan.zhihu.com/p/103125599
指派问题scipy算法原理:https://sci-hub.se/10.1109/TAES.2016.140952