深入浅出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| - |最大匹配|\)
接下来看经典的例题,例如导弹拦截。但是我们说这一题。题意是:
- 给你一个 \(DAG\) ,问你最多选出多少个点,使得两两不可达。
- 输出一种方案
- 输出每个点是否有可能在所有方案中出现(没有一个点可以到达它)
这道题就把我们上面所有东西揉起来了。首先这道题是一个明显的求孤立点集。
方法如下:
可不可达是一个偏序集。我们按照上面说的,令偏序关系 \(R\) 为是否可达。
- 求传递闭包
- 无法到达,也就是说 \(xRy\) 不成立,即无法比较。即求最大反链长,即最小链覆盖。
- 如何求最小链覆盖?
- 拆点。建二分图。
- 最小链覆盖 = 点数 - 新建的二分图的最大匹配
- 如何求最小链覆盖?
- 先解决第三问:如果满足第三问,即没有一个点可以到它。那么与其有 \(R\) 关系的就都不能选(它能到的和能到它的)。所以我们可以枚举点能否出现在最优构造方案中,删去它及其可达点。然后跑一边匹配,如果最大反链长(流量)减少 \(1\) 说明这个点可以选。可以选我们就把它选上,继续枚举。这样许多点都会被删掉,复杂度应该是不差的 \(O(n^2)\) (我感觉的,不严谨)
- 受第三问启发,第二问同理。模拟选点的过程,暴力枚举一边即可。
点击查看代码
/***
* 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\) 的递减序列