Gale-Ryser 定理
证明
给定两个非负整数数列 \(p_1 \ge p_2 \ge \dots \ge p_n\) 以及 \(q_1 \ge q_2 \ge \dots \ge q_m\) 满足 \(\sum_{i = 1}^n p_i = \sum_{i = 1}^m q_i\),存在一个简单二分图使得左部点的度数分别为 \(p_1, p_2, \dots, p_n\)、右部点的度数分别为 \(q_1, q_2, \dots, q_m\) 的充要条件为 \(\forall k \in [1, n], \sum_{i = 1}^k p_i \le \sum_{i = 1}^m \min\{q_i, k\}\)。
必要性
记该二分图的左部点为 \(x_1, x_2, \dots, x_n\)、右部点为 \(y_1, y_2, \dots, y_m\),不妨设 \(\forall i \in [1, n], deg(x_i) = p_i, \forall i \in [1, m], deg(y_i) = q_i\)。
对于左部的任意 \(k\) 个点,考察它们的度数之和。对于每一个右部点 \(y_i\),它最多和左部的 \(k\) 个点中的 \(\min\{q_i, k\}\) 个有边相连。因此左部任意的 \(k\) 个点度数之和 \(\le \sum_{i = 1}^m \min\{q_i, k\}\)。
所以 \(\forall k \in [1, n], \sum_{i = 1}^k p_i \le \sum_{i = 1}^m \min\{q_i, k\}\)。
充分性
考虑归纳构造出该二分图。
若我们已经构造出一个简单二分图满足 \(deg(x_k) < p_k, \forall i \in [1, k - 1], deg(x_i) = p_i, \forall i \in [k + 1, n], deg(x_i) = 0, \forall i \in [1, m], deg(y_i) \le q_i\),考虑如何添加或修改若干条边使得 \(deg(x_k) \leftarrow deg(x_k) + 1\),且除了 \(deg(x_k) < p_k\) 以外的所有条件仍然满足。
我们必然能找到一个 \(y_a\) 使得 \(deg(y_a) < \min\{q_a, k\}\),否则 \(\sum_{i = 1}^k p_i > deg(x_k) + \sum_{i = 1}^{k - 1} p_i = \sum_{i = 1}^m deg(y_i) = \sum_{i = 1}^m \min\{q_i, k\}\),矛盾。
如果还没有边 \((x_k, y_a)\),则加入该边。
否则根据抽屉原理,必然存在 \(b \in [1, k - 1]\) 使得没有边 \((x_b, y_a)\)。又因为 \(deg(x_b) = p_b \ge p_k > deg(x_k)\),所以一定存在 \(y_c\) 使得有边 \((x_b, y_c)\)、没有边 \((x_k, y_c)\)。删去边 \((x_b, y_c)\),加入边 \((x_b, y_a), (x_k, y_c)\)。
不难发现上述操作是使得 \(deg(x_k) \leftarrow deg(x_k) + 1\) 的一个合法构造。
初始 \(k = 1, \forall i \in [1, n], deg(x_i) = 0\),当 \(deg(x_k) < p_k\) 时不断调用上述构造,然后令 \(k \leftarrow k + 1\)。此时若 \(k = n + 1\) 则我们已经构造出一个满足条件的二分图。
容易发现上述充分性的构造实际上是在模拟增广路算法。
例题
CF1740F Conditional Mix
观察发现最后 \(\{a_i\}\) 划分成的每一个集合中数两两不同,并且每一种集合中数两两不同的划分方案都可以通过题目中的操作构造出来。
设 \(\{a_i\}\) 中值为 \(x\) 的数有 \(b_x\) 个,可以发现 \(M\) 能被构造出来的充要条件是能够构造出一个二分图使得左部点的度数可重集为 \(\{b_i\}\),右部点的度数可重集为 \(M\)。
考虑使用 Gale-Ryser 定理进行判定,即将 M 中的数从大到小排序后每一个前缀和都需要满足一个上限。\(dp_{i, j, k}\) 表示从大到小写了 \(i\) 个数,和为 \(j\),最后一个数为 \(k\) 的方案数。
因为 \(i \times k \le n\),所以状态数是 \(O(n^2 \log n)\) 的。直接枚举上一个数转移时间复杂度 \(O(n^3 \log n)\),可以后缀和优化成 \(O(n^2 \log n)\)。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
using ld = long double;
using i128 = __int128;
using u128 = unsigned __int128;
template <typename T>
void chkmax(T &x, const T &y) {
if (x < y) x = y;
}
template <typename T>
void chkmin(T &x, const T &y) {
if (y < x) x = y;
}
constexpr int MAXN = 2010, MOD = 998244353;
void inc(int &x, int y) { x += y, x >= MOD && (x -= MOD); }
int add(int x, int y) { return x += y, x >= MOD ? x - MOD : x; }
int n, a[MAXN], lim[MAXN], dp[MAXN][MAXN], suf[MAXN][MAXN];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1, x; i <= n; ++i) {
cin >> x;
++a[x];
}
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j) lim[i] += min(a[j], i);
for (int i = 1; i <= lim[1]; ++i) dp[i][i] = 1;
int ans = (lim[1] == n);
for (int i = 2; i <= n; ++i) {
for (int j = i - 1; j <= lim[i - 1]; ++j) {
int up = j / (i - 1);
suf[j][up] = dp[j][up];
for (int k = up - 1; k >= 1; --k) suf[j][k] = add(suf[j][k + 1], dp[j][k]);
}
for (int j = i; j <= lim[i]; ++j) {
for (int k = 1; k * i <= j; ++k)
dp[j][k] = (j - k <= lim[i - 1] ? suf[j - k][k] : 0);
}
if (lim[i] == n)
for (int k = 1; k * i <= n; ++k) inc(ans, dp[n][k]);
}
cout << ans << "\n";
return 0;
}
// g++ CF1470F.cpp -o CF1470F -Wall -Wextra -Wshadow -O2 -std=c++14 -fsanitize=address,undefined
JOISC 2023 Day3 Cookies
不难发现我们可以用 Gale-Ryser 定理判断箱子是否合法,即把箱子按容量从大到小排序每个前缀和都需要满足一个上限。
设 \(dp_{i, j, k}\) 表示确定了前 \(i\) 大的箱子,第 \(i\) 大的容量为 \(j\),能否使容量和为 \(k\)。因为 \(i \times j \le \sum_{l = 1}^n a_l\),所以状态数是 \(O(n^2 \log n)\) 级别的。
转移考虑枚举上一个数,后缀和优化可以做到转移总复杂度 \(O(n^2 \log n)\),再用 bitset 优化转移可以做到 \(O(\frac{n^2 \log n}{w})\)。
确定了箱子个数以及箱子容量后,考虑如何输出方案。按任意顺序枚举每种饼干,尽量放进容量较大的箱子内。容易用调整法证明最后合法的方案一定可以调整成这种形式。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
using ld = long double;
using i128 = __int128;
using u128 = unsigned __int128;
template <typename T>
void chkmax(T &x, const T &y) {
if (x < y) x = y;
}
template <typename T>
void chkmin(T &x, const T &y) {
if (y < x) x = y;
}
constexpr int MAXN = 15010;
int n, m, sum, a[MAXN], lim[MAXN];
bool can[MAXN];
vector<bitset<MAXN>> dp[MAXN];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n; ++i) {
cin >> a[i];
sum += a[i];
++lim[1];
--lim[a[i] + 1];
}
for (int i = 1; i <= sum; ++i) lim[i] += lim[i - 1];
for (int i = 1; i <= sum; ++i) lim[i] += lim[i - 1];
cin >> m;
for (int i = 1, x; i <= m; ++i) {
cin >> x;
can[x] = true;
}
if (lim[1] == sum && can[sum]) {
cout << "1\n" << n << " ";
for (int i = 1; i <= n; ++i) cout << i << " \n"[i == n];
return 0;
}
dp[1].resize(lim[1] + 1);
for (int i = 1; i <= lim[1]; ++i)
if (can[i]) dp[1][i][i] = 1;
bitset<MAXN> now;
for (int i = 1; i <= lim[1]; ++i) now[i] = 1;
for (int i = 2; i <= sum; ++i) {
for (int j = lim[i - 1] + 1; j <= lim[i]; ++j) now[j] = 1;
bitset<MAXN> tmp;
dp[i].resize(lim[i] / i + 1);
for (int j = lim[i] / i, k = lim[i - 1] / (i - 1); j >= 1; --j) {
if (!can[j]) continue;
while (k >= j) tmp |= dp[i - 1][k--];
dp[i][j] = tmp << j & now;
if (dp[i][j][sum]) {
cout << i << "\n";
vector<int> vec;
for (; i; --i) {
while (!dp[i][j][sum]) ++j;
vec.emplace_back(j);
sum -= j;
}
vector<vector<int>> wys(vec.size());
priority_queue<pair<int, int>> pq;
for (int l = 0; l < (int)vec.size(); ++l) pq.emplace(vec[l], l);
for (int l = 1; l <= n; ++l) {
vector<pair<int, int>> temp;
while (a[l]--) {
int u = pq.top().second;
temp.emplace_back(pq.top().first - 1, u);
pq.pop();
wys[u].emplace_back(l);
}
for (auto &s : temp) pq.emplace(s);
}
for (auto &l : wys) {
cout << l.size() << " ";
for (auto s : l) cout << s << " ";
cout << "\n";
}
return 0;
}
}
}
cout << "-1\n";
return 0;
}
// g++ cookies.cpp -o cookies -Wall -Wextra -Wshadow -O2 -std=c++14 -fsanitize=address,undefined