拉格朗日乘数法

昨天的模拟测考到了这样的一道题目:

给定一个 \(n\times n\) 的矩阵 \(w\),构造一个有 \(n\) 个数的非负实数序列 \(x\),需要保证其中所有数的和为 \(1\) ,求 \(\sum\limits^n_{i=1}\sum\limits^n_{j=i+1}w_{i, j}x_ix_j\) 的最大值。\(n\leq 10\),保证 \(w_{i, i} = 0, w_{i, j} = w_{j, i}\)

当时我非常兴奋,因为之前也考过一道非常相似的题目,只是 \(w\) 一定是 01 矩阵,且 \(n\) 的范围扩大到了 \(20\)

之前的题目解法

不难发现这道题可以转化为,给定一个有 \(n\) 个点的无向图 \(G\),连边给定,给每个点赋一个点权,且边权为所连接的两点点权之和,需要求出这个图的边权之和最大值 \(f(G)\)

注意到在一个完全图的时候能够尽量应用到每个 \(w_{i, j}\),所以 \(f(G)\) 一定有一个下界:设 \(G\) 的最大团大小为 \(k\),那么可以令团内所有点权为 \(\frac{1}{k}\),有\(f(G) \geq \frac{k-1}{2k}\)

尝试找到比这更优的做法,发现这很难做到。有一个结论:\(f(G) = \frac{k-1}{2k}\)

Proof

考虑归纳证明,当 \(n=1\) 时显然成立。

假设当图大小为当前 \(n-1\) 时成立。如果最优解中序列 \(x\) 存在一项为 \(0\),那么去掉就回到了 \(n-1\)。若 \(G\) 是完全图,那么结论显然成立。接下来只需证明当 \(G\) 不是完全图且 \(x\) 为正实数序列时成立即可。

设得出的最优解为 \(x_1, \cdots, x_n\),与 \(x\) 有连边的点集为 \(e_x\)。不妨设 \(1\notin e_2\)。由于 \(x\) 为最优解,因此 \(\sum\limits_{i\in{e_1}} x_i = \sum\limits_{j\in{e_2}} x_j\)。于是对 \(x_1\)\(x_2\) 进行一定的偏移:新序列 \(x_1+\Delta, x_2 - \Delta, x_3, \cdots, x_n\) 的答案为最优解答案减去 \(\Delta (\sum\limits_{i\in{e_1}} x_i - \sum\limits_{j\in{e_2}} x_j))\),并无变化。于是令 \(\Delta = -x_1\),就可以把第一项消掉,仍是最优解。

因此只需要得出图的最大团大小了。

但是这道题的 \(w_{i, j}\) 值域改变,由于不好计算甚至难以定义 “带权最大团”,上道题的确定性解法无法再次应用。当然,两题依然可以用算法优化的模拟退火。但随机化不是好做法,我们寻求一个具有确定性的、可以稳定得出真正解的做法。

我们可以使用 “拉格朗日乘数法”。它适用于求多元函数 \(f(x_1, \cdots, x_n)\) 在另一个多元函数 \(g(x_1, \cdots, x_n) = 0\) 时的极值。只需要引入新变量 \(\lambda\),求拉格朗日函数 \(F(x_1, \cdots, x_n, \lambda) = f(x_1, \cdots, x_n) + \lambda g(x_1, \cdots, x_n)\) 的极值即可。这点可以通过对每一元求偏导后结果定为 \(0\) 做到。

具体的,这里有一道数学题用以演示:已知 \(a^2+b^2=1\),求 \((a+1)(b+2)\) 的最大值。

直接计算不好做。但发现这两个式子可以由两个多元函数表示:\(f(a, b) = (a+1)(b+2), \phi(a, b) = a^2+b^2-1 = 0\)。有 \(F(a, b, \lambda) = f(a, b) + \lambda\phi(a, b) = (a+1)(b+2) + \lambda(a^2+b^2-1)\)

分别对 \(a\)\(b\)\(\lambda\) 求偏导:\(F_a^{'}=2a\lambda+b+2=0, F_b^{'} = 2b\lambda +a+1=0, F_\lambda^{'}=a^2+b^2-1=0\)。解三元方程即可。

于是这道题可以用此方法做:\(f(x_1, \cdots, x_n)=\sum\limits^n_{i=1}\sum\limits^n_{j=i+1}w_{i, j}x_ix_j, \phi(x_1, \cdots, x_n) = (\sum x_i) - 1=0\)\(F(x_1, \cdots, x_n, \lambda) = f(x_1, \cdots, x_n) + \lambda\phi(x_1, \cdots, x_n)\)

分别对 \(x_i\)\(\lambda\) 求偏导:\(F_{x_i}^{'}=\sum\limits_{j=1}^n w_{i, j}x_j + \lambda = 0, F_\lambda^{'}=(\sum x_i) - 1=0\)\(n\) 元方程组暴力高斯消元即可。

但是有个 bug:直接做最优解可能会使 \(x_i<0\),而这样是不合法的。我们只需要枚举哪些 \(x_i\)\(0\),剩下的都是正数,找出合法的去最大值即可。时间复杂度 \(O(n^32^n)\)

Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cmath>
#include <iomanip>
#include <iostream>
#include <numeric>
#include <vector>

using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 11 + 1;
constexpr double kE = 1e-9;

int T, n;
int w[kN][kN];
double a[kN][kN], b[kN];
int p[kN], c;

bool G() {
  for (int i = 1, o; i <= c; i++) {
    o = i;
    for (int j = i + 1; j <= c; j++) {
      if (fabs(a[o][i]) < fabs(a[j][i])) {
        o = j;
      }
    } 
    if (fabs(a[o][i]) < kE) {
      return 0;
    }
    swap(a[i], a[o]), swap(b[i], b[o]);
    for (int j = 1; j <= c; j++) {
      if (i == j) {
        continue;
      }
      double d = a[j][i] / a[i][i];
      for (int k = 1; k <= c; k++) {
        a[j][k] -= a[i][k] * d;
      }
      b[j] -= d * b[i];
    }
  }
  for (int i = 1; i < c; i++) {
    b[i] /= a[i][i];
    if (b[i] < -kE) {
      return 0;
    }
  }
  return 1;
};

int main() {
  cin.tie(0)->sync_with_stdio(0);
  cin >> T;
  cout << fixed << setprecision(7);
  while (T--) {
    cin >> n;
    for (int i = 1; i <= n; i++) {
      for (int j = 1; j <= n; j++) {
        cin >> w[i][j];
      }
    }
    double ans = 0;
    for (int s = 1; s < 1 << n; s++) {
      c = 0;
      for (int i = 0; i < n; i++) {
        if (s >> i & 1) {
          p[++c] = i + 1;
        }
      }
      for (int i = 1; i <= c; i++) {
        for (int j = 1; j <= c; j++) {
          a[i][j] = w[p[i]][p[j]];
        }
        a[i][c + 1] = 1, b[i] = 0;
      }
      c++, a[c][c] = 0, b[c] = 1;
      for (int i = 1; i < c; i++) {
        a[c][i] = 1;
      }
      if (!G()) {
        continue;
      }
      double ret = 0;
      for (int i = 1; i < c - 1; i++) {
        for (int j = i + 1; j < c; j++) {
          ret += b[i] * b[j] * w[p[i]][p[j]];
        }
      }
      ans = max(ans, ret);
    }
    cout << ans << '\n';
  }
  return 0;
}

\(\\\)

练习题

T1

P2179 骑行川蔵

形式化题意:有 \(n\) 个路段,每个路段有三个实数描述:\(s_i, k_i, v_i^{'}\),又给定一个实数 \(E_U\) 表示一个人的体力值。构造一个长度为 \(n\) 的序列 \(v\),经过路段 \(i\) 所需体力值 \(e_i\)\(k_i(v_i-v_i^{'})^2s_i\),让序列满足 \((\sum\limits_{i=1}^n e_i) - E_U = 0\),且 \(\sum\limits_{i=1}^n \frac{s_i}{v_i}\) 的值最小。\(n\leq 10^4\)

Solution

由题有 \(f(v_1, \cdots, v_n)=\sum\limits_{i=1}^n \frac{s_i}{v_i}, \phi(v_1, \cdots, v_n) = \sum\limits_{i=1}^n k_i(v_i-v_i^{'})^2s_i - E_U = 0\)\(F(v_1, \cdots, v_n, \lambda)=F(v_1, \cdots, v_n) + \lambda\phi(v_1, \cdots, v_n)\)

分别对 \(v_i, \lambda\) 求偏导。\(f_{v_i}^{'} = -\frac{s_i}{v_i^2}+2\lambda k_i(v_i-v_i^{'})s_i=0, f_\lambda^{'}=\sum\limits_{i=1}^n k_i(v_i - v_i^{'})^2s_i - E_U = 0\)。但这题 \(n\) 是一万级别的了,无法再直接高斯消元。考虑分析 \(f_{v_i}^{'}\) 所得式子的性质。

\[-\frac{s_i}{v_i^2}+2\lambda k_i(v_i-v_i^{'})s_i = 0 \rightarrow 2\lambda k_i(v_i-v_i^{'})si=\frac{si}{v_i} \rightarrow 2\lambda k_i v_i^2 (v_i-v_i^{'})=1 \rightarrow v_i^2(v_i-v_i^{'}) = \frac{1}{2\lambda k_i} \]

至此,我们可以看出一些性质:首先显然 \(v_i^2(v_i-v_i^{'})\)\(\lambda\) 一定时随 \(v_i\) 单调递增。其次,当 \(\lambda\) 上升时,\(\frac{1}{2\lambda k_i}\) 下降,\(v_i\) 下降,又有 \(e_i\) 上升,所以 \(e_i\)\(v_i\) 一定时随 \(\lambda\) 单调递增。

有了这两个单调性,可以在内层对于当前给定 \(\lambda\) 求出 \(\sum\limits_{i=1}^n e_i\) 最小值,再在外层二分 \(\lambda\) 的值。时间复杂度 \(O(n\log^2 V)\)

\(\\\)

Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <iomanip>
#include <iostream>
#include <numeric>
#include <vector>

using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 1e4 + 1;
constexpr double kE = 1e-12;

int n;
double eu;
double s[kN], k[kN], v0[kN];
double v[kN];

bool J(double h) {
  double e = 0;
  for (int i = 1; i <= n; i++) {
    double l = max(v0[i], 0.0), r = 1e5;
    while (l < r - kE) {
      double m = (l + r) / 2;
      if (2 * h * k[i] * m * m * (m - v0[i]) < 1.0 + kE) {
        l = m;
      } else {
        r = m;
      }
    }
    v[i] = l;
    e += k[i] * (v[i] - v0[i]) * (v[i] - v0[i]) * s[i];
  }
  return e < eu + kE;
}

int main() {
  cin.tie(0)->sync_with_stdio(0);
  cin >> n >> eu;
  for (int i = 1; i <= n; i++) {
    cin >> s[i] >> k[i] >> v0[i];
  }
  double l = 0, r = 1e5;
  while (l < r - kE) {
    double m = (l + r) / 2;
    if (J(m)) {
      r = m;
    } else {
      l = m;
    }
  }
  double ans = 0;
  for (int i = 1; i <= n; i++) {
    ans += s[i] / v[i];
  }
  cout << fixed << setprecision(7) << ans << '\n';
  return 0;
}

T2

CF1344D Résumé Review

给定一个长度为 \(n\) 的序列 \(a\),构造一个自然数序列 \(b\) 使第 \(i\) 个数都不超过 \(a_i\),且每个数之和为 \(k\),并使 \(\sum\limits_{i = 1}^nb_i(a_i-b_i^2)\) 最大。\(n\leq 10^5, 1\leq a_i\leq 10^9\)

Solution

暂时忽略 \(b_i\leq a_i\) 的限制。有 \(f(b_1, \cdots, b_n)=\sum\limits_{i = 1}^nb_i(a_i-b_i^2), \phi(b_1, \cdots, b_n)=k-(\sum\limits_{i=1}^n b_i)\)\(F(b_1, \cdots, b_n, \lambda)=f(b_1, \cdots, b_n) + \lambda\phi(b_1, \cdots, b_n)\)

\(b_i\) 求偏导:\(F_{b_i}^{'}=a_i-3b_i^2-\lambda=0\)\(\lambda=a_i-3b_i^2\)。而 \(b_i\) 因它是自然数而随 \(\lambda\) 单调递增。于是可以二分 \(\lambda\) 算出所有 \(b_i\)

然后再考虑加入 \(b_i\leq a_i\)。显然只需要让 \(\lambda\) 的下界为 \(\max\limits_{i=1}^n a_i-3a_i^2\) 即可。但是 \(\lambda\) 二分后答案为其下界时,可能算出来的序列 \(b\) 不满足 \(\sum\limits_{i=1}^n b_i = k\)

注意到最优解一定是 \(a\) 升序排序后的一段前缀所对应的 \(b\) 卡到了 \(b_i=a_i\)。可以在外层再二分前缀长度,就可以得到实数最优解 \(b\)

感性理解一下自然数解 \(ans_i\) 一定是实数解 \(b_i\) 向上或向下取整。可以先默认向下取整,然后往序列 \(ans\) 中再找出 \(c = k-(\sum\limits^n_{i=1} ans_i)\) 个数 + 1。这点很容易做到,找出 + 1 后对答案贡献最大的前 \(c\) 个数加上即可。

时间复杂度 \(O(n\log n\log V)\)

\(\\\)

Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cmath>
#include <iostream>
#include <numeric>
#include <vector>

using namespace std;
using LL = long long;
using LD = long double;
using PII = pair<int, int>;
constexpr int kN = 1e5 + 1;
constexpr double kE = 1;
#define w first
#define i second

int n, p[kN], a[kN], b[kN];
LL k;
PII q[kN];
vector<PII> v;

bool J(int x) {
  double c = k - accumulate(a + 1, a + x + 1, 0ll);
  if (c < 0) {
    return 0;
  }
  bool t = 0;
  LD l = -1e19, r = 1e19;
  for (int i = x + 1; i <= n; i++) {
    l = max(l, a[i] - LD(3) * a[i] * a[i]);
    r = min(r, LD(1) * a[i]);
  }
  while (l < r - kE) {
    LD m = (l + r) / 2, p = 0;
    for (int i = x + 1; i <= n; i++) {
      p += sqrt((a[i] - m) / 3);
    }
    if (p > c) {
      l = m, t = 1;
    } else {
      r = m;
    }
  }
  for (int i = x + 1; i <= n; i++) {
    b[i] = sqrt((a[i] - l) / 3);
  }
  return !t;
}

int main() {
  cin.tie(0)->sync_with_stdio(0);
  cin >> n >> k;
  for (int i = 1; i <= n; i++) {
    cin >> a[i];
    q[i] = {a[i], i};
  }
  sort(q + 1, q + n + 1);
  for (int i = 1; i <= n; i++) {
    p[q[i].i] = i;
  }

  sort(a + 1, a + n + 1);
  int l = 0, r = n + 1;
  while (l <= r) {
    int m = (l + r) / 2;
    if (J(m)) {
      l = m + 1;
    } else {
      r = m - 1;
    }
  }
  copy_n(a + 1, l, b + 1);
  J(l);
  for (int i = 1; i <= n; i++) {
    k -= b[i];
    if (b[i] < a[i]) {
      v.emplace_back(a[i] - 3 * b[i] * b[i] - 3 * b[i], i);
    }
  }
  sort(v.begin(), v.end(), greater<>());
  for (int i = 1; i <= k; i++) {
    b[v[i - 1].i]++;
  }
  for (int i = 1; i <= n; i++) {
    cout << b[p[i]] << ' ';
  }
  return 0;
}
posted @ 2023-11-10 22:18  Lightwhite  阅读(19)  评论(0编辑  收藏  举报