深入浅出Dilworth's Theorem

Dilworth's Theorem

Dilworth's Theorem 是组合数学中一个重要的定理。平常做题时发现它往往能从对立面简化一个非常复杂的问题,或者为某种贪心策略提供理论基础。碰到了很多次,但是都当结论用了,所以这次我想深入浅出地仔细学习一下这个定理。

前置知识

理解这个定理需要有一些离散数学的基础。如果有的话可以直接跳到 \(Dilworth's\ Theorem\)

传递闭包

对于某个集合 \(S\) 我们有定义在集合上的二元关系 \(R\)

我们在 \(S\) 上还定义了一个二元关系 \(T\),若关系 \(T\) 满足 \(\forall x,y \in S\), \(xTy→\exists x_0=x,x_1,x_2,…,x_k=y\) 使得 \(x_iRx_{i+1}(0\le i\lt k)\) 均成立

,那么就称关系 \(T\) 为关系 \(R\) 的传递闭包。

用人话解释,举个例子。

在有向图 \(G\) 的邻接矩阵中,对于两个点 \(u, v\) 我们可以看邻接矩阵中是否有直接边 \(e_{u\rightarrow v}\) ,以此来判断 \(u\rightarrow v\) 可达 。那么如何处理两个没有直接边,但他们依然可达的点呢?例如一条链的起点 \(a\) 和终点 \(b\)\(a\rightarrow i \rightarrow ... \rightarrow j \rightarrow b\)

我们用 \(Floyd\) 求出传递闭包(也是一个邻接矩阵),得到的结果就是:传递闭包中 \(a, b\) 有直接边 \(e_{a\rightarrow b}\),而原图邻接矩阵中没有。同理对于任意一组点对 \(<u, v>\),只要在原图中可达,传递闭包中就包含直接边。

更简洁的解释:对于图 \(G=(V,E)\),定义 \(R\) 为可达。对于 \(x,y\in V\)\(xRy\) 成立当且仅当 \(E\) 中存在 \(x\rightarrow y\),那么 \(R\) 的传递闭包 \(T\) 为:\(xTy\) 成立当且仅当 \(E\) 中存在 \(x\rightarrow y\)路径

偏序

对于集合 \(A\),若定义在集合 \(A\) 上的关系 \(R\) 满足:

  • 自反性,\(∀x∈A\) 都有 \(xRx\)
  • 反对称性,\(∀x,y∈A\),若 \(xRy,yRx\)\(x=y\)
  • 传递性,\(∀x,y,z∈A\),若 \(xRy,yRz\)\(xRz\)

\(R\) 为定义在 \(A\) 上的非严格偏序关系,记作 \(\preccurlyeq\)。很像 \(\le\) ,可以把它理解为 \(\le\),但又不是传统意义上的 \(\le\)

同理:定义严格偏序,对于集合 \(A\),若定义在集合 \(A\) 上的关系 \(R\) 满足:

  • 反自反性,\(∀x∈A\)\(xRx\) 都不成立
  • 非对称性,\(∀x,y∈A\)\(xRy\) 都不成立
  • 传递性,\(∀x,y,z∈A\),若 \(xRy,yRz\)\(xRz\)

\(R\) 为定义在 \(A\) 上的非严格偏序关系,记作 \(\prec\)

实际上可以将偏序关系理解成 \(DAG\)。对于一个 \(G=(V,E)\) 满足 \(E=\{(u,v)|u\prec v , u,v∈V\}\),那么 \(G\)\(DAG\),其传递闭包就是其本身。

通过我们对 \(\le\) 的直觉,可以很容易的推导出以下结论:

  • 若集合 \(A\) 上定义了一个非严格偏序 \(≼\)。那么严格偏序 \(≺\)\(a≺b\) 当且仅当 \(a≼b\)\(a≠b\)
  • 若集合 \(A\) 上定义了一个严格偏序 \(≺\)。那么非严格偏序 \(≼\)\(a≼b\) 当且仅当 \(a≺b\)\(a=b\)
  • 给定集合 \(A\) 上的一个非严格偏序 \(≼\)。那么其逆关系 \(≽\) 也是非严格偏序。
  • 给定集合 \(A\) 上的一个严格偏序 \(≺\)。那么其逆关系 \(≻\) 也是严格偏序。

Dilworth's Theorem

进入正题,先简单介绍 \(Dilworth's\ Theorem\),然后谈其应用。证明比较巧妙,并不复杂,但需要组合数学例如鸽笼原理等前置知识,为了避免篇幅过长就省去了。

首先是一些必要的名词:对于偏序集 \((A,R)\)

  • 链(Chain): \(A′⊆A\)\(∀x,y∈A′\) 都有 \(xRy\)\(yRx\)
  • 反链(Antichain):\(A′⊆A\)\(∀x,y∈A′,xRy\)\(yRx\) 都不成立。
  • 链覆盖(Cover of chains):\(S=\{S_1,S_2,…,S_k\}\) 其中 \(S_1,S_2,…,S_k\) 均为链且 \(∀x∈A,∃i∈[1,k]\) 使得 \(x∈S_i\)。该链覆盖的大小为 \(k\)

Dilworth's Theorem:一个偏序集的最大反链大小等于其最小链覆盖。

通过一个非常强的应用理解一下这个定理,链覆盖与二分图匹配的对应关系:

在一个二分图的匹配中,一个非匹配点一定是某个链的起点(因为没有任何边进入这个点)。所以链覆盖集的势 \(=\) 非匹配点的数量。匹配越大,非匹配点就越少(显然),链覆盖集的势就越大。得出一个强力的结论:\(|链覆盖集| = |V| - |最大匹配|\)

等价的结论是:\(|最大独立集| = |最小可相交路径覆盖(最小链覆盖)| = |V| - |最大匹配|\)

接下来看经典的例题,例如导弹拦截。但是我们说这一题。题意是:

  1. 给你一个 \(DAG\) ,问你最多选出多少个点,使得两两不可达。
  2. 输出一种方案
  3. 输出每个点是否有可能在所有方案中出现(没有一个点可以到达它)

这道题就把我们上面所有东西揉起来了。首先这道题是一个明显的求孤立点集。

方法如下:

可不可达是一个偏序集。我们按照上面说的,令偏序关系 \(R\) 为是否可达。

  1. 求传递闭包
  2. 无法到达,也就是说 \(xRy\) 不成立,即无法比较。即求最大反链长,即最小链覆盖。
    • 如何求最小链覆盖?
      • 拆点。建二分图。
      • 最小链覆盖 = 点数 - 新建的二分图的最大匹配
  3. 先解决第三问:如果满足第三问,即没有一个点可以到它。那么与其有 \(R\) 关系的就都不能选(它能到的和能到它的)。所以我们可以枚举点能否出现在最优构造方案中,删去它及其可达点。然后跑一边匹配,如果最大反链长(流量)减少 \(1\) 说明这个点可以选。可以选我们就把它选上,继续枚举。这样许多点都会被删掉,复杂度应该是不差的 \(O(n^2)\) (我感觉的,不严谨)
  4. 受第三问启发,第二问同理。模拟选点的过程,暴力枚举一边即可。

代码在这

点击查看代码
/***
 *    author: wrz
 *    created: 17.06.2022 14:52:37
 */
#include <bits/stdc++.h>

using namespace std;

#ifdef LOCAL
#include "../template/debug.hpp"
#else
#define debug(...) 42
#endif

template <typename T>
class flow_graph {
 public:
  static constexpr T eps = (T) 1e-9;

  struct edge {
    int from;
    int to;
    T c;
    T f;
  };

  vector<vector<int>> g;
  vector<edge> edges;
  int n;
  int st;
  int fin;
  T flow;

  flow_graph(int _n, int _st, int _fin) : n(_n), st(_st), fin(_fin) {
    assert(0 <= st && st < n && 0 <= fin && fin < n && st != fin);
    g.resize(n);
    flow = 0;
  }

  void clear_flow() {
    for (const edge &e : edges) {
      e.f = 0;
    }
    flow = 0;
  }

  int add(int from, int to, T forward_cap, T backward_cap) {
    assert(0 <= from && from < n && 0 <= to && to < n);
    int id = (int) edges.size();
    g[from].push_back(id);
    edges.push_back({from, to, forward_cap, 0});
    g[to].push_back(id + 1);
    edges.push_back({to, from, backward_cap, 0});
    return id;
  }
};

template <typename T>
class dinic {
public:
  flow_graph<T> &g;

  vector<int> ptr;
  vector<int> d;
  vector<int> q;

  dinic(flow_graph<T> &_g) : g(_g) {
    ptr.resize(g.n);
    d.resize(g.n);
    q.resize(g.n);
  }

  bool expath() {
    fill(d.begin(), d.end(), -1);
    q[0] = g.fin;
    d[g.fin] = 0;
    int beg = 0, end = 1;
    while (beg < end) {
      int i = q[beg++];
      for (int id : g.g[i]) {
        const auto &e = g.edges[id];
        const auto &back = g.edges[id ^ 1];
        if (back.c - back.f > g.eps && d[e.to] == -1) {
          d[e.to] = d[i] + 1;
          if (e.to == g.st) {
            return true;
          }
          q[end++] = e.to;
        }
      }
    }
    return false;
  }

  T dfs(int v, T w) {
    if (v == g.fin) {
      return w;
    }
    int &j = ptr[v];
    while (j >= 0) {
      int id = g.g[v][j];
      const auto &e = g.edges[id];
      if (e.c - e.f > g.eps && d[e.to] == d[v] - 1) {
        T t = dfs(e.to, min(e.c - e.f, w));
        if (t > g.eps) {
          g.edges[id].f += t;
          g.edges[id ^ 1].f -= t;
          return t;
        }
      }
      j--;
    }
    return 0;
  }

  T max_flow() {
    while (expath()) {
      for (int i = 0; i < g.n; i++) {
        ptr[i] = (int) g.g[i].size() - 1;
      }
      T big_add = 0;
      while (true) {
        T add = dfs(g.st, numeric_limits<T>::max());
        if (add <= g.eps) {
          break;
        }
        big_add += add;
      }
      if (big_add <= g.eps) {
        break;
      }
      g.flow += big_add;
    }
    return g.flow;
  }

  vector<bool> min_cut() {
    max_flow();
    vector<bool> ret(g.n);
    for (int i = 0; i < g.n; i++) {
      ret[i] = (d[i] != -1);
    }
    return ret;
  }
};

int main() {
  ios::sync_with_stdio(false);
  cin.tie(0);
  int n, m;
  cin >> n >> m;
  vector<vector<int>> reach(n, vector<int>(m));
  for (int i = 0; i < m; i++) {
    int u, v;
    cin >> u >> v;
    u--, v--;
    reach[u][v] = 1;
  }
  for (int k = 0; k < n; k++) {
    for (int i = 0; i < n; i++) {
      for (int j = 0; j < n; j++) {
        reach[i][j] |= reach[i][k] & reach[k][j];
      }
    }
  }
  int S = 2 * n, T = 2 * n + 1;
  flow_graph<int> g(2 * n + 2, S, T);
  for (int i = 0; i < n; i++) {
    g.add(S, i, 1, 0);
    g.add(n + i, T, 1, 0);
  }
  for (int i = 0; i < n; i++) {
    for (int j = 0; j < n; j++) {
      if (reach[i][j]) {
        g.add(i, n + j, 1, 0);
      }
    }
  }
  dinic<int> d(g);
  int ans = n - d.max_flow();
  cout << ans << '\n';
  vector<bool> blocked(n, false);
  int ans1 = ans, old = 0;
  vector<int> seq;
  for (int v = 0; v < n; v++) {
    if (blocked[v]) {
      seq.push_back(0);
      continue;
    }
    flow_graph<int> g2(2 * n + 2, S, T);
    for (int i = 0; i < n; i++) {
      if (i == v) {
        continue;
      }
      if (!reach[i][v] && !reach[v][i] && !blocked[i]) {
        for (int j = 0; j < n; j++) {
          if (j == v) {
            continue;
          }
          if (!reach[v][j] && !reach[j][v] && !blocked[j]) {
            if (reach[i][j]) {
              g2.add(i, n + j, 1, 0);
            }
          }
        }
      }
    }
    int cnt = 0;
    for (int i = 0; i < n; i++) {
      if (i == v) {
        cnt++;
        continue;
      }
      if (!reach[i][v] && !reach[v][i] && !blocked[i]) {
        g2.add(S, i, 1, 0);
        g2.add(n + i, T, 1, 0);
      } else if (!blocked[i] && (reach[i][v] || reach[v][i])) {
        cnt++;
      }
    }
    dinic<int> d2(g2);
    int max_flow = d2.max_flow();
    if (n - cnt - old - max_flow == ans1 - 1) {
      seq.push_back(1);
      ans1 = n - cnt - old - max_flow;
      blocked[v] = true;
      old += cnt;
      for (int i = 0; i < n; i++) {
        if (reach[i][v] || reach[v][i]) {
          blocked[i] = true;
        }
      }
    } else {
      seq.push_back(0);
    }
  }
  for (int i : seq) {
    cout << i;
  }
  cout << '\n';
  seq.clear();
  for (int v = 0; v < n; v++) {
    flow_graph<int> g3(2 * n + 2, S, T);
    for (int i = 0; i < n; i++) {
      if (i == v) {
        continue;
      }
      if (!reach[i][v] && !reach[v][i]) {
        for (int j = 0; j < n; j++) {
          if (j == v) {
            continue;
          }
          if (!reach[j][v] && !reach[v][j]) {
            if (reach[i][j]) {
              g3.add(i, n + j, 1, 0);
            }
          }
        }
      }
    }
    int cnt = 0;
    for (int i = 0; i < n; i++) {
      if (i == v) {
        cnt++;
        continue;
      }
      if (!reach[i][v] && !reach[v][i]) {
        g3.add(S, i, 1, 0);
        g3.add(n + i, T, 1, 0);
      } else {
        cnt++;
      }
    }
    dinic<int> d3(g3);
    if (n - d3.max_flow() - cnt == ans - 1) {
      seq.push_back(1);
    } else {
      seq.push_back(0);
    }
  }
  for (int i : seq) {
    cout << i;
  }
  return 0;
}

Erdős–Szekeres's Theorem

Erdős–Szekeres's Theorem 是 Dilworth's Theorem 的一个简单推论。

Theorem (Erdős–Szekeres): 对于 \(mn+1\) 个互不相同实数组成的数列 \((m, n\in \N^+\)),一定存在长为 \(m+1\) 的递增子列或长为 \(n+1\) 的递减子列。

或者说其几何意义:

二维欧式平面上任意 \(mn+1\) 个点总能构造出 \(m+1\) 条正斜率线段或 \(n+1\) 条负斜率线段(只要该坐标系下任意两点横纵坐标都不同)

单单这个定理可以直接证明,但为什么说他是 Dilworth's Theorem 的一个简单推论?

考虑数列中的最长递减子序列长度为 \(l(l\lt \sqrt n)\)。根据 Dilworth's Theorem 我们可以将原数列划分成 \(l\) 个递增子序列;由于所有序列长度的和为 \(n\),至少其中一个的长度是 \(\sqrt n\)

这说明如果最长递减子序列的长度比 \(\sqrt n\) 小,那么存在一个递增子序列,其长度至少为 \(\sqrt n\)。同理, 最长递增子序列的长度比 \(\sqrt n\) 小,那么存在一个递减子序列,其长度至少为 \(\sqrt n\)

我见过的应用:

考虑一个长度为 \(n\) 的排列 \(p\)。以下两个结论至少有一个成立:

  • 存在一个长度为 \(\sqrt n\) 的递增序列
  • 存在一个长度为 \(\sqrt n\) 的递减序列
posted @ 2022-06-17 17:56  Nicknamezz  阅读(654)  评论(0编辑  收藏  举报