Loading

偏序集与 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);
    }
}
posted @ 2025-09-18 21:40  lajishift  阅读(11)  评论(0)    收藏  举报