当前位置: 代码迷 >> 综合 >> 算法设计技巧: Primal-Dual
  详细解决方案

算法设计技巧: Primal-Dual

热度:57   发布时间:2023-12-15 10:55:32.0

原始对偶(Primal-Dual)是一种求解优化问题的思想, 在凸优化和组合优化问题中有重要应用. 本文以线性规划问题为例解释原始对偶算法的设计思路, 进而介绍如何设计基于原始对偶的近似算法(一般用于求解NP-hard问题).

互补松弛条件

考虑线性规划的原始问题(P)和对偶问题(D)如下:

min ? c T x s.t.  A x ≥ b x ≥ 0 (P) \begin{aligned} \min\ & c^Tx \tag{P}\\ \text{s.t. } & Ax \geq b\\ & x \geq 0 \end{aligned} min s.t. ?cTxAxbx0?(P)

max ? b T y s.t.  A T y ≤ c y ≥ 0 (D) \begin{aligned} \max\ & b^T y \tag{D}\\ \text{s.t. } & A^Ty \leq c \\ & y \geq 0 \end{aligned} max s.t. ?bTyATycy0?(D)

互补松弛条件(Complementary Slackness Condition)

  • Primal complementary slackness conditions
    x j > 0 ? [ A T y ] j = c j x_j > 0\Rightarrow [A^Ty]_j = c_j xj?>0?[ATy]j?=cj?
  • Dual complementary slackness conditions
    y i > 0 ? [ A x ] i = b i y_i > 0 \Rightarrow [Ax]_i = b_i yi?>0?[Ax]i?=bi?
    其中 [ ? ] i [\cdot]_i [?]i?是表达式的第 i i i个分量.

当互补松弛条件成立且 x x x y y y分别是原问题(P)和对偶问题(D)的可行解, 那么它们也是对应问题的最优解. 回顾线性规划的单纯形算法(Simplex Algorithm), 它从原问题(P)的一个可行解出发, 在满足互补松弛条件的前提下, 使得其对偶变量朝着对偶可行解的方向迭代. Primal-Dual的思想是从对偶可行解出发, 在满足互补松弛条件的前提下, 使得原始变量朝着可行解的方向迭代. 在经典的组合优化问题例如Maximal Flow, Minimum Cost Flow, Maximum Weight Matching等都有Primal-Dual算法的身影.

下面我们介绍如何利用Primal-Dual的思想设计近似算法.

松弛的互补松弛条件

x x x, y y y分别为(P)和(D)的可行解. 存在常数 α ≥ 1 , β ≥ 1 \alpha\geq 1, \beta \geq 1 α1,β1满足如下条件:

  • Relaxed primal complementary slackness
    x j > 0 ? [ A T y ] j ≥ c j / α x_j > 0\Rightarrow [A^Ty]_j \geq c_j/\alpha xj?>0?[ATy]j?cj?/α
  • Relaxed dual complementary slackness
    y i > 0 ? [ A x ] i ≤ β b i y_i > 0 \Rightarrow [Ax]_i \leq \beta b_i yi?>0?[Ax]i?βbi?

基于Primal-Dual的近似算法从对偶可行解出发, 始终满足松弛的(Relaxed)互补松弛条件, 且朝着原问题可行解的方向迭代.

由于 x x x y y y分别是(P)和(D)的可行解, OPT为原问题最优解的目标值. 此外, 根据上述 松弛的 互补松弛条件, 我们有
c T x ≤ α ( A T y ) T x = α y T ( A x ) ≤ α β y T b = α β b T y ≤ α β ? OPT . c^Tx \leq \alpha(A^Ty)^T x = \alpha y^T(Ax) \leq \alpha \beta y^Tb = \alpha \beta b^T y \leq \alpha\beta\cdot \text{OPT}. cTxα(ATy)Tx=αyT(Ax)αβyTb=αβbTyαβ?OPT.

换句话说, 上述算法得到的目标值不超过最优值的 α β \alpha \beta αβ倍. 当 α β \alpha\beta αβ越小, 算法的解与最优解越近.

基于Prima-Dual的近似算法

以最小化问题为例, 基于Primal-Dual的近似算法框架如下:

  1. 把问题用整数规划建模, 然后考虑其松弛的线性规划问题(P)以及对应的对偶问题(D).
  2. 初始化对偶可行解 y y y以及原始问题的不可行 x x x. (一般来说, 初始化时 x = 0 , y = 0 x=0, y=0 x=0,y=0.)
  3. 用某种方式增加 y i y_i yi?直到某个约束 [ A y ] j = c j / α [Ay]_j = c_j /\alpha [Ay]j?=cj?/α且始终保持 y y y的可行性. 然后根据对偶约束增加 x i x_i xi?的值直到 x x x成为原问题(整数规划问题)的可行解.
  4. 对偶问题的解是OPT的下界. 算法的近似比是 α β \alpha\beta αβ.

(更多细节参考这里)

Set Cover

Set Cover是一个经典的组合优化问题. 我们已知:

  • 元素的集合 E = { 1 , 2 , … , n } E=\{1, 2, \ldots, n\} E={ 1,2,,n}
  • m m m个集合 S 1 , S 2 , … , S m S_1, S_2, \ldots, S_m S1?,S2?,,Sm?, 其中 S j ? E S_j\subseteq E Sj??E
  • 每个集合的费用 c j c_j cj?, j = 1 , 2 , … , m j=1,2, \ldots, m j=1,2,,m

目标是找到一些集合 S j 1 , S j 2 , … , S j k S_{j_1}, S_{j_2},\ldots, S_{j_k} Sj1??,Sj2??,,Sjk??使得它们覆盖 E E E, 即 ∪ i = 1 k S j i = E \cup_{i=1}^k S_{j_i} = E i=1k?Sji??=E, 且总成本最低.

如下图所示 E = { 1 , 2 , … , 9 } E=\{1, 2, \ldots, 9\} E={ 1,2,,9}, S = { S 1 , S 2 , … , S 6 } \mathcal{S} = \{S_1, S_2, \ldots, S_6\} S={ S1?,S2?,,S6?}. 根据定义, C = { S 1 , S 2 , S 5 , S 6 } \mathcal{C} = \{S_1, S_2, S_5, S_6\} C={ S1?,S2?,S5?,S6?}是一个set cover. 当给定每个集合 S j S_j Sj?的权重时, 我们的目标是要找到一个总成本最低的set cover.

整数规划

S = { S 1 , S 2 , … , S m } \mathcal{S} = \{S_1, S_2, \ldots, S_m\} S={ S1?,S2?,,Sm?}. ? S ∈ S \forall S\in \mathcal{S} ?SS, 定义决策变量 x S ∈ { 0 , 1 } x_S\in \{0, 1\} xS?{ 0,1}, 代表是否选择 S S S. 上述问题可以描述成如下整数规划.
min ? ∑ S ∈ S c S x S s.t.  ∑ S ? e x S ≥ 1 , ? e ∈ E x S ∈ { 0 , 1 } \begin{aligned} \min\ & \sum_{S\in \mathcal{S}}c_Sx_S \\ \text{s.t. } & \sum_{S\ni e} x_S \geq 1, \quad \forall e\in E \\ & x_S \in \{0, 1\} \end{aligned} min s.t. ?SS?cS?xS?S?e?xS?1,?eExS?{ 0,1}?

其LP relaxation的对偶问题如下:
max ? ∑ e ∈ E y e s.t.  ∑ e ∈ S y e ≤ c S , ? S ∈ S y e ≥ 0 , ? e ∈ E . \begin{aligned} \max\ & \sum_{e\in E} y_e \\ \text{s.t. } & \sum_{e\in S}y_e \leq c_S, \quad \forall S\in \mathcal{S} \\ & y_e \geq 0, \quad \forall e\in E. \end{aligned} max s.t. ?eE?ye?eS?ye?cS?,?SSye?0,?eE.?

互补松弛条件

  • Primal complementary slackness
    x S > 0 ? ∑ e ∈ S y e = c S x_S > 0 \Rightarrow \sum_{e\in S}y_e = c_S xS?>0?eS?ye?=cS?
  • Dual complementary slackness
    y e > 0 ? ∑ S ? e x S = 1 y_e > 0\Rightarrow \sum_{S\ni e}x_S = 1 ye?>0?S?e?xS?=1

思路: Relax对偶互补条件: 存在 β > 1 \beta>1 β>1, 满足
y e > 0 ? ∑ S ? e x S ≤ β . y_e > 0 \Rightarrow \sum_{S\ni e}x_S \leq \beta. ye?>0?S?e?xS?β.

Primal-Dual算法

  1. 初始化 x S = 0 , ? S ∈ S x_S=0, \forall S\in\mathcal{S} xS?=0,?SS, y e = 0 , ? e ∈ E y_e=0,\forall e\in E ye?=0,?eE.
  2. 如果存在 e ∈ E e\in E eE未被覆盖, 则增加 y e y_e ye?直到存在 S ∈ S S\in \mathcal{S} SS使得 ∑ e ∈ S y e = c S \sum_{e\in S} y_e = c_S eS?ye?=cS?, 然后把所有满足条件的 S S S添加到结果中(即, x S = 1 x_S=1 xS?=1), 并把相应的元素标记未已覆盖.

说明 上述 y e y_e ye?的取值可以在区间 [ 0 , max ? c S ] [0, \max c_S] [0,maxcS?]通过二分查找得到.

近似比

f e = ∣ { S j ∣ e ∈ S j } ∣ f_e = |\{S_j | e \in S_j\}| fe?={ Sj?eSj?}, 代表元素 e e e在所有 S j S_j Sj?中出现的次数. 令 f = max ? f e f = \max f_e f=maxfe?, 我们有 ∑ S ? e x S ≤ f \sum_{S\ni e}x_S \leq f S?e?xS?f. 因此, 该算法的近似比为 f f f, 即 ALG ≤ β ? OPT \text{ALG}\leq \beta \cdot \text{OPT} ALGβ?OPT.

Python实现

class SetCoverPD(object):""" 基于Primal-Dual的近似算法求解Set Cover问题."""def __init__(self, m, sets, costs):"""注意: 输出参数的数值必须是整数.:param m: 元素个数. 元素的编号从0开始, e.g. 0, 1, ..., m-1:param sets: 元素(下标)的集合, list of sets, e.g. [{0, 2}, {1, 2, 3, 6}, {3, 4, 5}, {2, 4, 6}]:param costs: 每个元素集合的cost, e.g. [2, 3, 4, 1]"""self._m = mself._sets = setsself._costs = costsself._result = []  # 计算结果: 保存结果集合在sets中的编号self._y = [0] * self._m  # 对偶决策变量def solve(self):"""Primal-Dual算法."""uncovered_elements = set(range(self._m))ub = max(self._costs)while uncovered_elements:# 如果存在未被覆盖的元素i, 通过二分查找y[i]的值使得它满足条件:# y(S)=c(S), for some S 包含元素i. (称S为tight set.)i = uncovered_elements.pop()tight_sets = self._binary_search(i, 0, ub)# 把关于i的所有tight sets添加到结果集for ts in tight_sets:uncovered_elements -= self._sets[ts]self._result.append(ts)def _is_some_set_satisfied(self, i):""" 给定i(已知y[i]), 判断是否存在集合S满足y(S)>=c(S),其中y(S)代表S中元素对应y值之和, c(S)代表集合S的cost."""for j in range(len(self._sets)):set_j = self._sets[j]if i not in set_j:continuesum_y = sum([self._y[k] for k in set_j])if sum_y >= self._costs[j]:return Truereturn Falsedef _binary_search(self, i, lb, ub):"""用二分法查找y[i]使得y(S) = c(S) for some S (S称为tight set):param i: 元素的下标:param lb: y_i的下界:param ub: y_i的上界:return: list of tight sets"""if ub - lb <= 1:self._y[i] = ubreturn self._get_tight_sets(i)mid = (lb + ub) // 2self._y[i] = midif not self._is_some_set_satisfied(i):return self._binary_search(i, mid, ub)else:return self._binary_search(i, lb, mid)def _get_tight_sets(self, i):"""已知y[i], 找到所有tight sets(满足y(S)=c(S))."""tight_sets = []for j in range(len(self._sets)):set_j = self._sets[j]if i not in set_j:continuesum_y = sum([self._y[k] for k in set_j])if abs(sum_y - self._costs[j]) < 1e-6:tight_sets.append(j)return tight_setsdef print_result(self):print("solution:", [self._sets[i] for i in self._result])print("total cost:", sum([self._costs[i] for i in self._result]))if __name__ == '__main__':sc = SetCoverPD(9,  # m[{
    0, 1}, {
    2, 5, 8}, {
    3, 5}, {
    4, 6}, {
    1, 2, 3, 4}, {
    6, 7, 8}],  # sets[4, 2, 5, 7, 8, 1]  # costs)sc.solve()sc.print_result()
  相关解决方案