偏序集与 Dilworth 定理
理论知识
偏序集
若集合 \(S\) 和其上的一个二元关系 \(\preceq\) 满足性质:
- 自反性:\(\forall a \in S, a \preceq a\)
- 反对称性:\(\forall a, b \in S, a \preceq b \land b \preceq a \Rightarrow a = b\)
- 传递性:\(\forall a, b, c \in S, a \preceq b \land b \preceq c \Rightarrow a \preceq c\)
则 \(S\) 是偏序集,\(\preceq\) 是其上的偏序。
一个显然的例子是 \(\mathbb{N}, \mathbb{Z}, \mathbb{Q}, \mathbb{R}\) 都关于二元关系 \(\leq\) 构成偏序。
全序集
类比偏序集理解。
若集合 \(S\) 和其上的一个二元关系 \(\preceq\) 满足性质:
- 自反性:\(\forall a \in S, a \preceq a\)
- 反对称性:\(\forall a, b \in S, a \preceq b \land b \preceq a \Rightarrow a = b\)
- 传递性:\(\forall a, b, c \in S, a \preceq b \land b \preceq c \Rightarrow a \preceq c\)
- 连接性:\(\forall a, b \in S, a \not = b \Rightarrow a \preceq b \lor b \preceq a\)
则 \(S\) 为全序集,\(\preceq\) 是其上的全序。
偏序集的链与反链
偏序集 \(S\) 上的链是其全序子图 \(T\),具体地,\(\forall a, b \in T, a \not = b \Rightarrow a \preceq b \lor b \preceq a\)。
即链上的任意两点都可以比较(可以比较即在某一顺序上满足偏序关系)。
偏序集 \(S\) 上的反链是集合 \(T\) 满足,\(\forall a, b \in T, a \not = b \Rightarrow a \not \preceq b \land b \not \preceq a\)。
即反链上任意两点都不可以比较。
Dilworth 定理
Dilworth 定理告诉我们,偏序集的最小链覆盖 = 偏序集的最长反链。
CF1630F
题意
给你一个长度为 \(n\) 的序列 \(a\),构建一个无向图:若 \(a_i | a_j\),则在 \(i\) 和 \(j\) 中连边。
求最少删除多少个点,才能使得剩下的图是二分图。
思路
首先,我们知道倍数关系是一个偏序关系,即 \(a_i | a_j, a_j | a_k \rightarrow a_i | a_k\)。
所以,一旦出现了一个长度 \(\ge 3\) 的链,那么就会出现奇环,就不可能是二分图。
也就是说,最后的图中每个点的度数都 \(le 1\),可以将边定向(不妨设是大的数向小的数连边),转换成每个点要么有入度,要么有出度。
考虑拆点,\(x_0\) 表示 \(x\) 这个数有出度,\(x_1\) 则表示这个点有入度。
拆点后,将互相矛盾的状态连边(无向边),得到一个新图,显然,这个图的最大独立集就是最后能保留下来最多的数个数。
但是,普通图的最大独立集是 NPC,考虑怎么转换这个图。
因为我们就是从一个有偏序关系的图得到的这个图,很自然地想让它也有偏序关系。
把这个图定向,变成有向图(大连向小),显然得到了一个 DAG,同时还是偏序集。
那么这个时候,答案是这个图的最大独立集,也是偏序集的最长反链。
根据 Dilworth 定理,可以转换为偏序集的最小链覆盖,做完了。
代码
const int N = 5e6 + 5;
const int M = 5e6 + 5;
const int inf = 0x3f3f3f3f;
int n, m;
int s, t;
namespace dinic{
int head[N], cur[N];
int dep[N];
struct edge{
int v, cap, nxt;
} e[M];
int tot = 1;
void add(int u, int v, int cap = 1){
e[++tot] = {v, cap, head[u]};
head[u] = tot;
e[++tot] = {u, 0, head[v]};
head[v] = tot;
}
bool bfs(){
rep(i, s, t) dep[i] = inf;
queue<int> q;
q.push(s); dep[s] = 0;
cur[s] = head[s];
while(!q.empty()){
int u = q.front(); q.pop();
for(int i = head[u]; i; i = e[i].nxt){
int v = e[i].v;
if(e[i].cap <= 0 || dep[v] != inf) continue;
q.push(v);
cur[v] = head[v];
dep[v] = dep[u] + 1;
if(v == t) return 1;
}
}
return 0;
}
int dfs(int u, int flow){
if(u == t) return flow;
int k = 0, ans = 0;
for(int &i = cur[u]; i; i = e[i].nxt){
if(flow <= 0) break;
int v = e[i].v;
if(e[i].cap > 0 && (dep[v] == dep[u] + 1)){
k = dfs(v, min(flow, e[i].cap));
if(k == 0) dep[v] = inf;
e[i].cap -= k;
e[i ^ 1].cap += k;
ans += k;
flow -= k;
}
}
return ans;
}
int mxflow(){
int mxflow = 0;
while(bfs()) mxflow += dfs(s, inf);
return mxflow;
}
void clear(int n){
tot = 1;
s = 0, t = 4 * n + 1;
rep(i, s, t) head[i] = 0;
}
}
int a[N], pos[N];
// i: 只有因数 + 左部点 | i + n: 只有因数 + 右部点 | i + 2n: 只有倍数 + 左部点 | i + 3n: 只有倍数 + 右部点
int id(int x, int i, int j){ // i: 因数 / 倍数 | j: 左部 / 右部
return x + i * 2 * n + j * n;
}
void add(int u, int v){
dinic::add(id(u, 0, 0), id(v, 0, 1));
dinic::add(id(u, 0, 0), id(v, 1, 1));
dinic::add(id(u, 1, 0), id(v, 1, 1));
}
void solve_test_case(){
n = read();
dinic::clear(n);
rep(i, 1, n) a[i] = read(), pos[a[i]] = i;
rep(i, 1, n){
dinic::add(id(i, 0, 0), id(i, 1, 1));
dinic::add(s, id(i, 0, 0)), dinic::add(s, id(i, 1, 0));
dinic::add(id(i, 0, 1), t), dinic::add(id(i, 1, 1), t);
}
rep(i, 1, n){
rep(j, 2, (int)(5e4 / a[i])){
if(!pos[a[i] * j]) continue;
add(pos[a[i] * j], i);
}
}
write(dinic::mxflow() - n);
rep(i, 1, n) pos[a[i]] = 0;
}
补充:最小链覆盖
即,在一个图上用最少的链覆盖这个图的所有边。
求法如下:
拆点,将每个点拆成入点和出点。
把每条边转换,得到了一个二分图。
此时,每一个匹配都会将两个链合并,于是最小链覆盖 = 总点数 - 最大匹配 = 总点数 - 最大流
[KOI 2025 #2] 存放箱子
题意
你有 \(n\) 个箱子,每个箱子有参数 \(c, s\),分别表示它可以嵌套一个大小 \(\le c\) 的箱子、这个箱子自己的大小。
问对于任意一个前缀的箱子,至少需要多少组嵌套才能把他们都囊括。
保证 \(c \lt s\)。
\(n \le 2 \times 10 ^ 5\)。
思路
需要想到嵌套的图论建模,\(i\) 能嵌套 \(j\) 表述为:
\(c_j \lt s_j \le c_i \lt s_i\)
可以看出是一个偏序关系。
然后一个多个箱子构成的嵌套其实是一个链,即求偏序集上的最小链覆盖。
Dilworth 定理转换为最大反链,要求反链里的箱子两两不能比较,即:
\(\forall i, j (c_i \lt c_j \wedge s_i \gt s_j) \vee (c_i \gt c_j \wedge s_i \lt s_j)\)
画到数轴上,变成区间 \([c_i, s_i)\),枚举 \(i\) 的时候维护区间加和全局 max 即可,线段树可以做到。
代码
const int N = 2e5 + 5;
int n;
int c[N], s[N];
struct node{
int mx;
int add;
} t[N << 3];
#define ls(x) (x << 1)
#define rs(x) ((x << 1) | 1)
#define mid ((l + r) >> 1)
void push_up(int x){
t[x].mx = max(t[ls(x)].mx, t[rs(x)].mx);
}
void hard(int x, int v){
t[x].mx += v;
t[x].add += v;
}
void push_down(int x){
if(!t[x].add) return;
hard(ls(x), t[x].add);
hard(rs(x), t[x].add);
t[x].add = 0;
}
void modify(int x, int l, int r, int ql, int qr, int v){
if(ql <= l && r <= qr){
hard(x, v);
return;
}
push_down(x);
if(ql <= mid) modify(ls(x), l, mid, ql, qr, v);
if(qr > mid) modify(rs(x), mid + 1, r, ql, qr, v);
push_up(x);
}
int query(int x, int l, int r, int ql, int qr){
if(ql <= l && r <= qr){
return t[x].mx;
}
if(ql > mid) return query(rs(x), mid + 1, r, ql, qr);
if(qr <= mid) return query(ls(x), l, mid, ql, qr);
return max(query(ls(x), l, mid, ql, qr), query(rs(x), mid + 1, r, ql, qr));
}
#undef ls
#undef rs
#undef mid
vector<int> vec;
void solve_test_case(){
n = read();
rep(i, 1, n){
s[i] = read(), c[i] = read();
vec.push_back(s[i]);
vec.push_back(c[i]);
}
sort(vec.begin(), vec.end());
vec.erase(unique(vec.begin(), vec.end()), vec.end());
rep(i, 1, n){
s[i] = lower_bound(vec.begin(), vec.end(), s[i]) - vec.begin() + 1;
c[i] = lower_bound(vec.begin(), vec.end(), c[i]) - vec.begin() + 1;
}
rep(i, 1, n){
modify(1, 1, 2 * n, c[i], s[i] - 1, 1);
write(t[1].mx);
}
}

浙公网安备 33010602011771号