Dilworth 定理 学习笔记
这个定理还是很重要的,一般用于偏序集上 求最小链划分 和 求最大反链 的相互转换。
前置知识
偏序集
偏序集是由集合 \(S\) 和定义在 \(S\) 上的一个偏序关系 \(R\) 构成,记作 \((S, R)\)。
偏序关系
对于一个二元关系 \(R \subset S \times S\),如果满足:
- 自反性:\(\forall x \in S\), \(x \ R \ x\) 成立。
- 反对称性: \(\forall x, y \in S\),若 \(x \ R \ y 且 y \ R \ x\),则有 \(x = y\)。
- 传递性:\(\forall x, y, z \in S\),若 \(x \ R \ y,y \ R \ z\),则有 \(x \ R \ z\)。
则称 \(R\) 是一个偏序关系。
显然 \(\leq\) 就是在 \(N\) 上的一个偏序关系, \((N, \leq)\) 也是最简单的偏序集。
为了方便,后文均用 \(\leq\) 指代 \(R\)。
其余一些定义
链:偏序集的一个 全序子集 称作 链。一条链是一个集合,满足集合内的元素 两两可比。
反链:一个反链也是一个子集,满足集合内的元素 两两不可比。
最长链:偏序集的一个最大链称作 最长链。
最长反链:偏序集的一个最大反链称作 最长反链。
最小链划分:偏序集最少能被划分成多少条链。
最小反链划分:偏序集最少能被划分成多少条反链。
\(Dilworth\) 定理
- 对于任意 有限 偏序集,最小链划分 等于 最长反链的大小。
- 对于任意 有限 偏序集, 最小反链划分 等于 最长链的大小。
证明对应用用处不大,等到以后有时间了再补吧。
应用
导弹拦截
题意:
给定一个正整数序列 \(\{a_i\}\),求最少能被划分成多少个 单调不降 的子序列。
分析:
定义二元组 \((i, a_i)\) 为偏序集中的元素。定义二元关系 \(\leq\) 为:若 \((i, a_i), (j, a_j)\) 满足 \(i \leq j 且 a_i \leq a_j\) 那么有 \((i, a_i) \leq (j, a_j)\)。
那么 \((\{(i, a_i)\}, \leq)\) 构成一个偏序集。我们要求的就是偏序集的最小链划分。
根据 \(Dilworth\) 定理,最小链划分等于最长反链大小。在这种定义下两个元素 \((i, a_i), (j, a_j)\)不可比当且仅当 \(i \leq j 且 a_i > a_j\)。因此一条反链就是一个单调递减子序列,最长反链大小就是最长单调递减子序列长度。
「TJOI2015」组合数学
题意:
给定一张 \(n \times m\) 的网格图 \(a_{i, j}\),第 \(i\) 行第 \(j\) 列有 \(a_{i, j}\) 个宝石。
每次你都从 \((1, 1)\) 出发,只能向右或向下走,走到 \((n, m)\) 停止,你可以将经过的位置上的宝石拿走一个。
问最少走多少次才能将所有宝石都拿走。
\(1 \leq n, m \leq 10^3\)。
分析:
定义二元组 \((i, j)\) 为偏序集中的元素。定义偏序关系 \(\leq\) 为:若 \((a \leq c 且 b \leq d) 且 (a < c和b<d 至少有一个成立)\), 则称 \((a, b) \leq (c, d)\)。也就是 \((a, b)\) 能走至少一步到 \((c, d)\),则有 \((a, b) \leq (c, d)\)
将每个 \((i, j)\) 复制 \(a_{i, j}\) 次,那么要求的就是偏序集的最小链划分。
根据 \(Dilworth\) 定理,转化为求最长反链大小:在这里只需要满足任意两个元素相互不可至少走一步到达,那么一行显然最多只能选一个位置,并且贪心的肯定要把位置上的二元组都拿走。
因此可以 \(dp\): \(dp_{i, j}\) 表示从上到下考虑了前 \(i\) 行,当前最靠左的位置所在列为 \(j\) 的答案。转移是枚举 \(k > j\),从 \(dp_{i - 1, k}\) 取 \(\max\)。显然可以后缀和优化。
复杂度 \(O(nm)\)。
CODE:
// 根据 Dilworth 定理,可以从右上角到左下角 dp
#include<bits/stdc++.h>
using namespace std;
const int N = 1e3 + 10;
int T, n, m, a[N][N], dp[N][N]; // dp[i][j] 表示考虑到第 i 行, 当前在第 j 列的最大值
inline void solve() {
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i ++ )
for(int j = 1; j <= m; j ++ )
scanf("%d", &a[i][j]);
memset(dp, 0, sizeof dp);
for(int i = 1; i <= n; i ++ ) {
int mx = 0;
for(int j = m; j >= 1; j -- ) {
dp[i][j] = dp[i - 1][j];
mx = max(mx, dp[i - 1][j + 1]);
dp[i][j] = max(dp[i][j], mx + a[i][j]);
}
}
int res = 0;
for(int i = 1; i <= m; i ++ ) res = max(res, dp[n][i]);
cout << res << endl;
}
int main() {
scanf("%d", &T);
while(T -- ) solve();
return 0;
}
[ABC237Ex] Hakata
题意:
给定字符串 \(S\),问你最多从 \(S\) 中选出多少 回文子串,满足选出的任意两个回文子串 \(s, t\),都不满足 \(s\) 是 \(t\) 的子串 或 \(t\) 是 \(s\) 的子串。
\(1 \leq |S| \leq 200\),字符集为小写字母。
分析:
定义偏序关系 \(\leq\) 为:若回文子串 \(s\) 是 \(t\) 的子串,则有 \(s \leq t\)。
那么答案就是将所有 本质不同 的回文子串取出来后的最长反链大小,根据 \(Dilworth\) 定理转化为求最小链划分。
考虑将所有满足偏序关系的回文子串连一条从较短串到较长串的有向边,那么一定会形成一张 \(DAG\)。我们要做的就是将所有点划分成最少的集合,满足任意一个集合内的点都是一条路径的子集。
这等于于求这张 \(DAG\) 的 可交最小路径覆盖。只需要先跑一遍传递闭包求出所有可达关系,重建图后求出 拆点后的二分图最大匹配,然后拿总点数减去最大匹配就是答案。
算起来复杂度好像不太对?但是我们有结论:长度为 \(n\) 的字符串本质不同回文子串数量为 \(O(n)\) 级别。
这样复杂度就对了, \(floyd\) 复杂度 \(O(n^3)\),\(Dinic\) 求二分图最大匹配复杂度为 \(O(m\sqrt{n} = n^{2.5})\)。
CODE:
#include<bits/stdc++.h>
using namespace std;
const int N = 210;
const int M = N * 2;
const int INF = 1e8;
int c, l[M], r[M], L[N * N], R[N * N], odr[N * N], tot, n;
char s[N];
inline bool check(int l, int r) {
while(l <= r) {
if(s[l] != s[r]) return 0;
l ++; r --;
}
return 1;
}
inline bool cmp(int x, int y) {
int len = min(R[x] - L[x] + 1, R[y] - L[y] + 1);
for(int i = 1; i <= len; i ++ ) {
if(s[L[x] + i - 1] < s[L[y] + i - 1]) return 1;
else if(s[L[y] + i - 1] < s[L[x] + i - 1]) return 0;
}
return R[x] - L[x] + 1 == len;
}
inline bool chk(int x, int y) {
if(r[x] - l[x] > r[y] - l[y]) return 0;
for(int i = l[y]; i <= r[y] - (r[x] - l[x] + 1) + 1; i ++ ) {
bool flag = 1;
for(int j = 1; j <= r[x] - l[x] + 1; j ++ ) {
if(s[j + l[x] - 1] != s[i + j - 1]) {flag = 0; break;}
}
if(flag) return 1;
}
return 0;
}
bool bok[M];
struct edge {
int v, last, cap;
} E[M * M + 4 * M];
int head[M], len;
inline void add(int u, int v, int cap) {
E[len] = (edge) {v, head[u], cap}; head[u] = len ++;
E[len] = (edge) {u, head[v], 0}; head[v] = len ++;
}
int node[M][2], S, T, nc;
int d[M], cur[M];
inline bool bfs(int s, int t) {
memset(d, -1, sizeof d); d[s] = 0; cur[s] = head[s];
queue< int > q; q.push(s);
while(!q.empty()) {
int u = q.front(); q.pop();
for(int i = head[u]; ~i; i = E[i].last) {
int ver = E[i].v, cap = E[i].cap;
if(d[ver] == -1 && cap) {
d[ver] = d[u] + 1;
if(ver == t) return 1;
cur[ver] = head[ver];
q.push(ver);
}
}
}
return 0;
}
int Find(int u, int t, int limit) {
if(u == t) return limit;
int res = 0;
for(int i = cur[u]; ~i && limit; i = E[i].last) {
int ver = E[i].v, cap = E[i].cap;
if(d[ver] == d[u] + 1 && cap) {
int v = Find(ver, t, min(limit, cap));
res += v, E[i].cap -= v, E[i ^ 1].cap += v, limit -= v;
if(limit) d[ver] = -1;
}
cur[u] = i;
}
return res;
}
inline int dinic(int s, int t) {
int res = 0, flow;
while(bfs(s, t)) while(flow = Find(s, t, INF)) res += flow;
return res;
}
int main() {
scanf("%s", s + 1); n = strlen(s + 1);
for(int l = 1; l <= n; l ++ )
for(int r = l; r <= n; r ++ )
if(check(l, r)) L[++ tot] = l, R[tot] = r;
for(int i = 1; i <= tot; i ++ ) odr[i] = i;
sort(odr + 1, odr + tot + 1, cmp);
for(int i = 1; i <= tot; i ++ ) {
if(i == 1 || !cmp(odr[i], odr[i - 1])) l[++ c] = L[odr[i]], r[c] = R[odr[i]];
}
memset(head, -1, sizeof head);
S = ++ nc; T = ++ nc;
for(int i = 1; i <= c; i ++ ) {
node[i][0] = ++ nc; node[i][1] = ++ nc;
add(S, node[i][0], 1); add(node[i][1], T, 1);
}
for(int i = 1; i <= c; i ++ ) {
for(int j = 1; j <= c; j ++ ) {
if(i != j && chk(i, j)) add(node[i][0], node[j][1], 1);
}
}
printf("%d\n", c - dinic(S, T));
return 0;
}
[CTSC2008] 祭祀
题意:
给定一张 \(n\) 个点, \(m\) 条边的 \(DAG\),选出最多的点满足选出的任意两点互不可达。
- 输出最多能选多少个点。
- 构造一组选出最多点的方案。
- 对每个点判断它能否出现在一组选了最多点的方案中。
\(1 \leq n \leq 100, 1 \leq m \leq 1000\)。
分析:
实际上就是求最长反链的大小,构造一组最长反链,判断每个点能否在一条最长反链中。
对于第一问,显然和上一题做法类似,不再赘述。
我们先来看第三问:
假设我们求出了最长反链大小为 \(k\)。如何判断 \(x\) 能否在一条最长反链中呢?
强制选 \(x\),将所有能到达 \(x\) 或能被 \(x\) 到达的点都删掉。对于剩下的偏序集,再求一遍最大反链
的大小。如果此时求出的答案为 \(k - 1\),那么 \(x\) 就可以出现在一条最长反链中,否则不行。
根据这个思路,好像也可以做第二问了:
每次枚举一个点 \(x\) 删掉然后做上面的判断,如果 \(x\) 可以在一条最大反链中,那么把 \(x\) 选中然后把所有与 \(x\) 有偏序关系的点删掉。否则直接把 \(x\) 删掉不影响构造答案。一直这样做知道构造一组方案。每个点显然只会被枚举删除一次,复杂度和上面那一问一样。
但是第二问还有更优的做法:
首先需要会 构造二分图的最大点独立集,如果不会可以参考我的这篇博客。
然后我们考虑对于 拆点后的二分图,求出一组 最大点独立集。如果一个点 \(x\) 被拆成的两个点 \(x_L, x_R\) 都在这个独立集中,那么就把 \(x\) 放到答案点集 \(S\) 中。
证明:
- \(S\) 是一条反链
首先我们选出来的点 \(x\) 都满足 \(x_L\) 未被标记 且 \(x_R\) 被标记。
考虑 \(S\) 中如果存在两个点 \(u, v\) 满足偏序关系,那么一定存在一条 \(u_L \to v_R\) 的有向边。
如果 \(v_R\) 是 非匹配点 那么 \(u_L\) 一定被标记,与条件矛盾。
如果 \(v_R\) 是 匹配点 那么所有和它相连的点都被标记, \(u_L\) 也一定被标记,与条件矛盾。
因此不存在选出两个点具有偏序关系,所以 \(S\) 是一条反链。
- \(S\) 是一条最长反链
设独立集为 \(I\),那么 \(|I| = 2n - k\)(最大独立集等于总点数减最大匹配数),考虑 \(I - S\) 的含义:相当于有多少个点 \(x\) 满足 [\(x_L\) 未被标记 和 \(x_R\) 被标记其中之一成立],因此 \(|I - S| \leq n\),所以 \(|S| \geq |I| - n = n - k\)。我们知道 \(S\) 的大小最大为 \(n - k\),所以 \(S\) 的大小就等于 \(n - k\),是一条最长反链。
然后就做完了,这样构造的复杂度 \(O(m)\)。
总复杂度 \(O(nm\sqrt{n} = n^{3.5})\)。
CODE:
#include<bits/stdc++.h>
#define pb emplace_back
using namespace std;
const int N = 110;
const int M = 1010;
const int INF = 1e8;
int n, m, f[N][N];
bool vis[N * 2], bok[N];
namespace Flow { // 每次能删掉一个点,以及相关点,求最小可交路径覆盖
struct edge {
int v, last, cap;
} E[N * N + 4 * N];
int head[N * 2], tot, S, T, node[N][2], nc;
int d[N * 2], cur[N * 2];
int era[N];
inline void add(int u, int v, int cap) {
E[tot] = (edge) {v, head[u], cap}; head[u] = tot ++;
E[tot] = (edge) {u, head[v], 0}; head[v] = tot ++;
}
inline bool bfs(int s, int t) {
memset(d, -1, sizeof d); d[s] = 0; cur[s] = head[s];
queue< int > q; q.push(s);
while(!q.empty()) {
int u = q.front(); q.pop();
for(int i = head[u]; ~i; i = E[i].last) {
int ver = E[i].v, cap = E[i].cap;
if(d[ver] == -1 && cap) {
d[ver] = d[u] + 1;
if(ver == t) return true;
cur[ver] = head[ver];
q.push(ver);
}
}
}
return false;
}
int Find(int u, int t, int lim) {
if(u == t) return lim;
int res = 0;
for(int i = cur[u]; lim && ~i; i = E[i].last) {
int ver = E[i].v, cap = E[i].cap;
if(d[ver] == d[u] + 1 && cap) {
int v = Find(ver, t, min(lim, cap));
res += v; lim -= v; E[i].cap -= v; E[i ^ 1].cap += v;
if(lim) d[ver] = -1;
}
cur[u] = i;
}
return res;
}
inline int dinic(int s, int t) {
int res = 0, flow;
while(bfs(s, t)) while(flow = Find(s, t, INF)) res += flow;
return res;
}
inline int del(int x) { // 删掉 x 和所有 x 能到的以及能到 x 的
memset(head, -1, sizeof head); nc = 0; tot = 0;
memset(era, 0, sizeof era);
S = ++ nc, T = ++ nc; int cnt = 0;
for(int i = 1; i <= n; i ++ ) {
if(i == x || f[i][x] || f[x][i]) era[i] = 1, cnt ++;
}
for(int i = 1; i <= n; i ++ ) {
if(!era[i]) {
node[i][0] = ++ nc, node[i][1] = ++ nc;
add(S, node[i][0], 1);
add(node[i][1], T, 1);
}
}
for(int i = 1; i <= n; i ++ ) {
if(era[i]) continue;
for(int j = 1; j <= n; j ++ ) {
if(!era[j])
if(f[i][j]) add(node[i][0], node[j][1], 1);
}
}
return n - cnt - dinic(S, T);
}
}
void dfs(int x) {
if(vis[x]) return;
vis[x] = 1;
for(int i = Flow::head[x]; ~i; i = Flow::E[i].last) {
int ver = Flow::E[i].v, cap = Flow::E[i].cap;
if(ver == Flow::S || ver == Flow::T) continue;
if(cap == 0) dfs(ver);
}
}
int main() {
scanf("%d%d", &n, &m);
for(int i = 1; i <= m; i ++ ) {
int u, v; scanf("%d%d", &u, &v);
f[u][v] = 1;
}
for(int i = 1; i <= n; i ++ )
for(int j = 1; j <= n; j ++ )
for(int k = 1; k <= n; k ++ )
f[j][k] |= (f[j][i] & f[i][k]);
int res = Flow::del(0);
cout << res; cout << '\n';
return 0;
}
CF590E Birthday
掉毛题目卡我半天。
题意:
给定 \(n\) 个仅包含 \(a,b\) 的字符串 \(\{s_i\}\),保证它们互不相同。
你需要去掉尽量少的字符串,使得剩下的字符串中不存在某一个串是另一个串的子串。
输出答案和一组构造。
\(1 \leq n \leq 750, \sum |s_i| \leq 10^7\)。
分析:
偏序关系显然是子串关系,题目要求的也是最长反链大小及一组构造。
显然只要我们能求出子串关系然后就变得和上一题一模一样了。
显然是建立 \(ACAM\) 然后通过一些方法维护。这道题比较恶心,字符串总长度太大导致你甚至不能写任何关于 \(AC\) 自动机的 \(dfs\) 函数,否则就会爆栈。
具体维护方法是这样:建立 \(fail\) 指针时顺便对每个节点 \(p\)维护出 \(fail\) 树跳祖先第一个能跳到的某个字符串末尾状态,记作 \(pos_p\)。然后对每个字符串 \(i\) 重新在 \(AC\) 自动机跑一遍从根到末尾状态节点的路径,将经过的 \(p\) 对应的 \(pos_p\) 和 \(i\) 连一条有向边。如果经过的 \(p\) 不是当前字符串末尾状态但是是另一个字符串的末尾也连一条有向边。最后跑一遍传递闭包即可。
复杂度 \(O(n^3 + n^{2.5})\)。
CODE:
// 又是这种构造最大反链的题
#include<bits/stdc++.h>
#define pb emplace_back
using namespace std;
struct IO{
static const int S=1<<21;
char buf[S],*p1,*p2;int st[105],Top;
~IO(){clear();}
inline void clear(){fwrite(buf,1,Top,stdout);Top=0;}
inline void pc(const char c){Top==S&&(clear(),0);buf[Top++]=c;}
inline char gc(){return p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++;}
inline IO&operator >> (char&x){while(x=gc(),x==' '||x=='\n'||x=='\r');return *this;}
template<typename T>inline IO&operator >> (T&x){
x=0;bool f=0;char ch=gc();
while(!isdigit(ch)){if(ch=='-') f^=1;ch=gc();}
while(isdigit(ch)) x=(x<<3)+(x<<1)+ch-'0',ch=gc();
f?x=-x:0;return *this;
}
inline IO&operator << (const char c){pc(c);return *this;}
template<typename T>inline IO&operator << (T x){
if(x<0) pc('-'),x=-x;
do{st[++st[0]]=x%10,x/=10;}while(x);
while(st[0]) pc('0'+st[st[0]--]);return *this;
}
} fin, fout;
const int M = 1e7 + 10;
const int N = 760;
const int INF = 1e8;
int n, et[M], l[N], r[N];
int q[M], g[N][N];
short s[M];
struct ACAM {
int tot, pos[M], tr[M][2], fail[M], Top;
inline void ins(int idx) {
int p = 0;
for(int i = l[idx]; i <= r[idx]; i ++ ) {
if(!tr[p][s[i]]) tr[p][s[i]] = ++ tot;
p = tr[p][s[i]];
}
et[p] = idx;
}
inline void build() {
int l = 1, r = 0;
for(int i = 0; i <= 1; i ++ )
if(tr[0][i]) q[++ r] = tr[0][i];
while(l <= r) {
int u = q[l ++];
for(int i = 0; i <= 1; i ++ ) {
if(tr[u][i]) {
fail[tr[u][i]] = tr[fail[u]][i];
pos[tr[u][i]] = (et[fail[tr[u][i]]] ? et[fail[tr[u][i]]] : pos[fail[tr[u][i]]]);
q[++ r] = tr[u][i];
}
else tr[u][i] = tr[fail[u]][i];
}
}
}
inline void get(int idx) {
int p = 0;
for(int i = l[idx]; i <= r[idx]; i ++ ) {
p = tr[p][s[i]];
if(pos[p]) g[idx][pos[p]] = 1;
if(i != r[idx] && et[p]) g[idx][et[p]] = 1;
}
}
} AC;
struct Flow {
struct edge {
int v, last, cap;
} E[N * N + 4 * N];
int head[N * 2], tot, node[N][2], S, T, nc;
int cur[N * 2], d[N * 2];
bool bok[N * 2];
inline void add(int u, int v, int cap) {
E[tot] = (edge) {v, head[u], cap}; head[u] = tot ++;
E[tot] = (edge) {u, head[v], 0}; head[v] = tot ++;
}
inline void build(int n) {
memset(head, -1, sizeof head);
S = ++ nc; T = ++ nc;
for(int i = 1; i <= n; i ++ ) {
node[i][0] = ++ nc; node[i][1] = ++ nc;
add(S, node[i][0], 1); add(node[i][1], T, 1);
}
}
inline bool bfs(int s, int t) {
memset(d, -1, sizeof d); d[s] = 0;
queue< int > q; q.push(s); cur[s] = head[s];
while(!q.empty()) {
int u = q.front(); q.pop();
for(int i = head[u]; ~i; i = E[i].last) {
int ver = E[i].v, cap = E[i].cap;
if(d[ver] == -1 && cap) {
d[ver] = d[u] + 1;
if(ver == t) return 1;
cur[ver] = head[ver];
q.push(ver);
}
}
}
return 0;
}
int Find(int u, int t, int limit) {
if(u == t) return limit;
int res = 0;
for(int i = cur[u]; limit && ~i; i = E[i].last) {
int ver = E[i].v, cap = E[i].cap;
if(d[ver] == d[u] + 1 && cap) {
int v = Find(ver, t, min(cap, limit));
res += v; limit -= v; E[i].cap -= v; E[i ^ 1].cap += v;
if(limit) d[ver] = -1;
}
cur[u] = i;
}
return res;
}
inline int dinic(int s, int t) {
int res = 0, flow;
while(bfs(s, t)) {
while(flow = Find(s, t, INF)) res += flow;
}
return res;
}
void dfs(int x) {
if(bok[x]) return ;
bok[x] = 1;
for(int i = head[x]; ~i; i = E[i].last) {
int ver = E[i].v, cap = E[i].cap;
if(!cap) dfs(ver);
}
}
inline void solve() { // 求出最大反链大小, 以及构造一组方案
int res = n - dinic(S, T);
fout << res; fout.pc('\n'); //printf("%d\n", res);
for(int i = 1; i <= n; i ++ ) {
for(int j = head[node[i][1]]; ~j; j = E[j].last) {
if(E[j].v == T && E[j].cap == 1) dfs(node[i][1]);
}
}
for(int i = 1; i <= n; i ++ ) {
if(!bok[node[i][0]] && bok[node[i][1]]) fout << i, fout.pc(' ');
}
fout.pc('\n');
}
} f;
inline void floyd() {
for(int i = 1; i <= n; i ++ )
for(int j = 1; j <= n; j ++ )
for(int k = 1; k <= n; k ++ )
g[j][k] |= (g[j][i] & g[i][k]);
for(int i = 1; i <= n; i ++ )
for(int j = 1; j <= n; j ++ )
if(i != j && g[i][j]) f.add(f.node[i][0], f.node[j][1], 1);
}
int main() {
fin >> n;
for(int i = 1; i <= n; i ++ ) {
l[i] = r[i - 1] + 1;
r[i] = r[i - 1];
char c = fin.gc();
while(c != '\n') s[++ r[i]] = c - 'a', c = fin.gc();
AC.ins(i);
}
AC.build(); f.build(n);
for(int i = 1; i <= n; i ++ ) AC.get(i);
floyd();
f.solve();
return 0;
}

浙公网安备 33010602011771号