Dilworth定理及其在算法题中的应用
1. Dilworth定理
Dilworth定理由数学家Robert P. Dilworth于1950年提出,它描述了偏序集中链和反链之间的关系。
- 偏序集:一个满足某种偏序关系集合(即一个自反、反对称、传递的关系)。
- 链:偏序集的一个子集,其中任意两个元素都是可比较的(即全序子集)。
- 反链:偏序集的一个子集,其中任意两个元素都是不可比较的。
Dilworth定理:在任意有限偏序集中,其最小链划分的大小(即将偏序集分解成链的最小数量)等于其最大反链的大小(即反链中元素的最大数量)。
例子:考虑一个序列 [3, 1, 2],其中偏序关系基于元素值和位置(例如,元素值越小越“小”,但位置也影响)。Dilworth定理可以帮助我们找到最小链分解。
2. 序列分解定理
序列分解定理是Dilworth定理在序列上的一个特例。它关注的是序列能否被分解为k个非递减子序列。
- 非递减子序列:一个子序列其中每个元素都不小于前一个元素(即单调非递减)。
- 分解:将原序列的每个元素分配到k个子序列中,使得每个子序列都是非递减的。
序列分解定理:一个序列可以被分解为k个非递减子序列 当且仅当 序列中最长递减子序列的长度不超过k。
这意味着:
- 如果序列中存在一个长度为k+1的递减子序列,那么它无法被分解成k个非递减子序列。
- 反之,如果最长递减子序列的长度为k,那么序列可以被分解成k个非递减子序列。
Dilworth定理为这个结论提供了严格的数学基础:在序列构成的偏序集(基于值和位置)中,反链对应递减子序列,链对应非递减子序列。
3. 定理在算法题中的应用 (CF-2143-D1)
题目链接:[https://codeforces.com/contest/2143/problem/D1]
这个题需要统计所有“好”子序列(即可以被分解为两个非递减子序列的子序列)。根据序列分解定理,这等价于统计所有最长递减子序列长度不超过2的子序列。这可以用dp快速解决:记dp[i][j](其中 i 和 j 表示两个非递减链的最后一个元素值,且 i<=j)为子序列的数量。对于每个新元素 v,我们检查是否能将其添加到第一个链(如果 v>=i)或第二个链(如果 v>=j),并更新状态,最后统计即可。
直观上,如果序列中有三个元素递减(如 a > b > c 且顺序出现),那么这三个元素无法被分配到两个非递减链中(因为每个链必须非递减,但递减关系要求它们分属不同链,但只有两个链,矛盾)。反之,如果没有这样的三个元素,那么总可以通过贪心将序列分解为两个非递减链。
AC代码:
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int mod = 1e9+7;
signed main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
auto solve = [&](){
int n;
cin >> n;
vector<int> a(n);
for (int i=0; i<n; i++) cin >> a[i];
vector<vector<int>> dp(n+1, vector<int>(n+1, 0));
dp[0][0] = 1; // 空序列
for (auto v: a){
vector<vector<int>> cpydp = dp;
for(int i=0; i<=n; i++){
for(int j=0; j<=n; j++){
if(dp[i][j] == 0)
continue;
if(v >= i)
cpydp[v][j] = (cpydp[v][j]+dp[i][j])%mod;
else if(v >= j)
cpydp[i][v] = (cpydp[i][v]+dp[i][j])%mod;
}
}
dp = cpydp;
}
int ans = 0;
for(int i=0; i<=n; i++)
for(int j=0; j<=n; j++)
ans = (ans+dp[i][j])%mod;
cout << ans << "\n";
};
int T;
for(cin>>T; T--; )
solve();
return 0;
}
对于大数据(\(n=2000\)),可以用树状数组优化到 \(O(n^2logn)\)
题目链接:[https://codeforces.com/contest/2143/problem/D2]
AC代码:
/*
对于 dp[v][j]:需要所有 i<=v 的 dp[i][j] 的和
对于 dp[i][v]:需要所有 j<=v 的 dp[i][j] 的和
考虑用树状数组预处理:
列方向前缀和:对于固定的 j,求 i 从 0 到 v 的 dp[i][j] 的和 col[j]
行方向前缀和:对于固定的 i,求 j 从 0 到 v 的 dp[i][j] 的和 row[i]
*/
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int mod = 1e9+7;
#define lowbit(i) i & (-i)
struct Fenwick{
int n;
vector<int> tree;
Fenwick(int sz): n(sz), tree(sz+1, 0) {}
void upd(int pos, int x){
for(int i=pos; i<=n; i+=lowbit(i))
tree[i] = (tree[i]+x)%mod;
}
int summ(int pos){
int res = 0;
for(int i=pos; i>0; i-=lowbit(i))
res = (res+tree[i])%mod;
return res;
}
};
signed main(){
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
auto solve = [&](){
int n;
cin >> n;
vector<int> a(n);
for(int i=0; i<n; i++) cin >> a[i];
vector<vector<int>> dp(n+1, vector<int>(n+1, 0));
dp[0][0] = 1;
vector<Fenwick> row(n+1, Fenwick(n+1)); // 行方向前缀和
vector<Fenwick> col(n+1, Fenwick(n+1)); // 列方向前缀和
// 初始dp[0][0]=1, 由于Fenwick从1开始,upd的时候都要+1
row[0].upd(1, 1);
col[0].upd(1, 1);
for(auto v: a){
vector<int> R(n+1), C(n+1);
// 对于列j,求行0到v的和
for(int j=0; j<=n; j++)
C[j] = col[j].summ(v+1);
// 对于行i,求列0到v的和
for(int i=v+1; i<=n; i++)
R[i] = row[i].summ(v+1);
for(int j=0; j<=n; j++){
if(C[j] == 0) continue;
dp[v][j] = (dp[v][j]+C[j])%mod;
// 更新第v行,第j列
row[v].upd(j+1, C[j]);
col[j].upd(v+1, C[j]);
}
for(int i=v+1; i<=n; i++){
if(R[i] == 0) continue;
dp[i][v] = (dp[i][v]+R[i])%mod;
// 更新第i行,第v列
row[i].upd(v+1, R[i]);
col[v].upd(i+1, R[i]);
}
}
int ans = 0;
for(int i=0; i<=n; i++)
for(int j=0; j<=n; j++)
ans = (ans+dp[i][j])%mod;
cout << ans << "\n";
};
int t;
for(cin>>t; t--;)
solve();
return 0;
}

浙公网安备 33010602011771号