前言
题目链接:洛谷;AtCoder。
形式化题意
建议阅读原题面。
给定长度为 \(n\) 的序列 \(a, b, c, d\),求最少修改多少次 \(a_i\),使得存在一个排列 \(p\),满足 \(a_i=c_{p_i} \land b_i \leq d_{p_i}\)。
\(n \leq 2 \times 10^5\),\(b, d\) 单减,保证有解。
题目分析
首先你必须分析出来,这是一个最大权二分图完美匹配问题,不然连最基础的状压都不会写,然后爆零(当然说的是我自己)。
考虑把 \(X_i = (a_i, b_i)\) 看做左部点,\(Y_j = (c_j, d_j)\) 看做右部点,所有满足 \(b_i \leq d_j\) 的 \(X_i, Y_j\) 之间连边,边权是 \([a_i \neq c_j]\),我们所求的就是最小花费完美匹配。
考虑状压 DP。记 \(f_{S}\) 表示右部点匹配上的点集为 \(S\),和前 \(|S|\) 个左部点匹配,最小的花费是多少。初始 \(f_\varnothing = 0\),答案是 \(f_Y\)。转移的时候,每次选取一个点转移即可:\(f_{S\cup\{Y_i\}} \gets \min \Big\{f_{S\cup\{Y_i\}}, f_S + [a_{|S|+1} \neq c_i]\Big\}\)。时间复杂度 \(\mathcal{O}(n2^n)\)。
以上是我没写出来的部分分。我们考虑直接跑 KM 算法求是 \(\mathcal{O}(n^3)\),不太行。那么说明这个二分图一定有特殊的性质,因为如果是一般的二分图的话,我们会的算法 KM 已经最快了。(费用流显然也不太对。)
我们猜测一下算法,肯定需要贪心。我们不妨按照某一个顺序枚举右部点,尝试把它通过一条 \(0\) 边和某一左部点匹配。
性质一
对于一个右部点 \(Y_i\),通过一条边权为 \(0\) 边,尝试和 \(b_j\) 大的 \(X_j\) 配对,肯定不劣。我们将这个匹配的 \(X_j\) 记作 \(\pi(i)\)。
\(b_j\) 大小关乎 \(X_j\) 与多少右部点有边,显然 \(b_j\) 小的连出的边更多。倘若我们选择了 \(b_j\) 更小的 \(X_j\),一定可以将其替换为满足 \(b_{j'}\geq b_j\) 的 \(X_{j'}\),而替换下来的 \(X_j\) 留着可能和别的右部点匹配,从而不劣。
我们能匹配就让他们匹配上。这么做是对的基于所有点的贡献相同,如果我们按照一个恰当的顺序贪心,如果现在不进行匹配,之后让别的点和 \(X_j\) 匹配不会让答案更优,所以这么贪心是不劣的。
接下来思考,什么时候我们发现他们不能匹配呢?倘若匹配他们后,把它们删去,剩下的图还不能形成一个完美匹配,那我们就不能让他们匹配。
是否存在完美匹配?这是一个很典的问题。
定理一:Hall's marriage theorem(霍尔定理)
wikipedia
Hall's condition is that any subset of vertices from one group has a neighbourhood of equal or greater size.
即,一张二分图是否存在完美匹配的充要条件是,对于左部点 / 右部点的任意子集的邻居点集(有边相连的右部点 / 左部点)大小不小于该子集的大小。不妨将该条件称作 Hall 判定条件。
Necessity Proof
假设二分图 \(G = (X\sqcup Y, E)\) 存在完美匹配 \(M\subseteq E\),那么 \(\forall S \subseteq X\),记 \(T = \Big\{y\in Y \mid x\in S \land(x, y)\in M \Big\}\),记 \(S\) 的邻居 \(N_G(S) = \Big\{y\in Y \mid x\in S \land(x, y)\in E \Big\}\),由于 \(M \subseteq E\),我们有 \(T \subseteq N_G(S)\),故 \(|N_G(S)|\geq|T|=|S|\)。对于 \(Y\) 同理。必要性证毕。
Sufficiency Proof
归纳证明。对于 \(|X| = 1\),显然成立。假设 \(|X|<n\) 命题成立,对于 \(|X|=n>1\),分如下两类讨论:
- \(\forall S\subseteq X, N_G(S) \geq |S|+1\):
将任意 \(x\in X\) 与任意 \(y\in N_G(\{x\})\) 匹配,剩下的子图 \(G'\) 依然满足命题条件,而 \(X' = X \setminus \{x\}\),根据归纳假设,成立。 - \(\exists S\subseteq X, N_G(S) = |S|\):
将 \(S\) 与 \(N_G(S)\) 组成 \(|S|\) 组匹配,剩下的子图 \(G'\) 依然满足命题条件(倘若 \(\exists S' \subseteq X', |N_{G'}(S')| < |S'|\),则 \(N_G(S\cup S')=N_G(S)\cup N_{G'}(S')\),从而有 \(|N_G(S\cup S')|<|S\cup S'|\),与假设矛盾),故根据归纳假设,成立。
对于任意子集的话……难道我们要枚举子集?显然不现实。这时候,似乎需要用到这个二分图的一些性质了。
性质二
我们发现,对于一个左部点 \(X_i\),它所连的右部点是一段前缀,不妨设为 \(Y_1\sim Y_{p_i}\),这个可以用双指针处理。类似同理,右部点连到的左部点总是一段后缀。
证明显然,自证不难。
这个性质很自然得到,有什么用呢?
性质三
我们考虑一个右部点的子集 \(S\),记 \(Y_{y_0} = arg \max\limits_{Y_i\in S} d_i\),\(S'=\operatorname{suf}(y_0)=\{Y_i\mid d_i \leq d_{y_0}\}\)。
我怎么听不懂你在讲什么?
如果我们把左右部点,分别按照 \(b, d\) 从小到大的顺序,从上到下排列,\(Y_{y_0}\) 就是 \(S\) 中最靠上的点,\(S'\) 就是 \(Y_{y_0}\) 及以下的右部点构成的点集(不妨将 \(Y_i\) 及以下的右部点构成的点集记作 \(\operatorname{suf}(i)\))。
\(S'\) 满足 Hall 判定条件是 \(S\) 满足 Hall 判定条件的充分条件。因为 \(S'\) 相比 \(S\),邻居没变 \(N_G(S') = N_G(S) = N_G(\{Y_{y_0}\})\),而 \(|S'| \geq |S|\)。
如此,我们把需要判定的子集个数由 \(2^n\) 简化至 \(n\) 个后缀。原问题似乎变得十分可做。慢着,我们还有一个问题一直没有解决,那就是按照什么顺序枚举右部点呢?
先说结论:按照 \(d_i\) 从小到大枚举,即按照下标从后往前枚举右部点。
我们考虑 \(c_i = c_j \land d_i \geq d_j\) 的右部点 \(Y_i,Y_j\),且有 \(\pi(i) = \pi(j) = \pi_0\)。让 \(Y_j\) 和 \(\pi_0\) 匹配,而不是 \(Y_i\) 和 \(\pi_0\) 匹配,原因是,两者唯一的差别就是考虑 \(d_k\in(d_j, d_i)\) 的 \(Y_k\),\(\operatorname{suf}(k)\) 在 \(Y_j\) 和 \(\pi_0\) 匹配后,我们会删去 \(Y_j\) 这个右部点,而无论是 \(Y_i / Y_j\) 和 \(\pi_0\) 匹配,\(N_G\Big(\operatorname{suf}(k)\Big)\) 不变。导致让 \(Y_j\) 先匹配,\(Y_k\) 更有可能满足 Hall 判定条件。
至于 \(\pi(i)\) 的维护,对每种 \(a_i/c_i\) 开一个栈,然后双指针即可。我们可以写出如下 \(\mathcal{O}(n^2)\) 代码:
#include <cstdio>
#include <iostream>
#include <stack>
using namespace std;const int N = 200010;int n, a[N], b[N], c[N], d[N];
int p[N];stack<int> st[N];bool diedL[N], diedR[N];bool check() {int pcnt = 0; // 后缀右部点有多少int N_G = 0; // 邻居左部点有多少for (int i = n, j = n; i >= 1; --i) {if (diedR[i]) continue;++pcnt;for (; j >= 1 && p[j] >= i; --j)N_G += !diedL[j];if (N_G < pcnt)return false;}return true;
}int main() {scanf("%d", &n);for (int i = 1; i <= n; ++i)scanf("%d%d", &a[i], &b[i]);for (int i = 1; i <= n; ++i)scanf("%d%d", &c[i], &d[i]);for (int i = 1; i <= n; ++i) {for (p[i] = p[i - 1]; p[i] + 1 <= n && d[p[i] + 1] >= b[i]; ++p[i]);}int ans = 0;for (int i = n, j = n; i >= 1; --i) {for (; j >= 1 && p[j] >= i; --j)st[a[j]].push(j);if (st[c[i]].empty()) {++ans;continue;}int pi_i = st[c[i]].top();diedL[pi_i] = diedR[i] = true;if (check()) {st[c[i]].pop();} else {++ans;diedL[pi_i] = diedR[i] = false;}}printf("%d", ans);return 0;
}
那么我们需要优化的部分就是 check
了。
我们考虑将判定条件 \(\forall i, \Bigg|N_G\Big(\operatorname{suf}(i)\Big)\Bigg|\geq|\operatorname{suf}(i)|\) 移项变为 \(\forall i, \Bigg|N_G\Big(\operatorname{suf}(i)\Big)\Bigg|-|\operatorname{suf}(i)|\geq 0\),用数据结构维护左侧式子,为了方便叙述,将式子表示为 \(\forall i, u_i - v_i \geq 0\)。
发现我们在匹配 \(\pi(i), Y_i\) 时,将 \(Y_1 \sim Y_{p_{\pi(i)}}\) 的 \(u\) 减了一,将 \(Y_1\sim Y_{i-1}\) 的 \(v\) 减少了一。至于之后 check
的时候不考虑 \(Y_i\),可以直接将 \(u_i \gets \infty\)。或者除了上述操作,将 \(Y_i\) 的 \(v\) 也减一,就不需要特殊处理 \(Y_i\)。
感性理解一下。由于 \(Y_1 \sim Y_{p_{\pi(i)}}\) 包含了 \(Y_i\),再把 \(v_i\) 减一相当于它 \(u_i, v_i\) 都不变。考虑 \(v_i = v_{i+1} + 1\),我们又有 \(u_i-v_i=u_i-1-v_{i+1}\),而 \(u_i-1\geq u_{i+1}\),所以 \(Y_i\) 和后缀里某个点等价,如果不合法,后缀某个点会出现问题。
于是,我们可以使用一棵线段树,方便地执行区间加减操作,查询全局的最小值,看看如果 \(\geq 0\) 就合法,否则不合法。
时间复杂度:\(\mathcal{O}(n \log n)\)。瓶颈在于线段树维护区间加减一,全局最值查询,和 这题 最后瓶颈相同。
代码
$\mathcal{O}(n2^n)$ 状压
namespace $ya {int f[1 << 17 | 736520];void tomin(int &a, int b) {if (b < a)a = b;
}void solve() {memset(f, 0x3f, sizeof(*f) << n);f[0] = 0;using uint = unsigned;for (uint st = 0; st < 1u << n; ++st) {if (f[st] == 0x3f3f3f3f)continue;int i = __builtin_popcount(st) + 1;for (int j = 1; j <= p[i]; ++j)if (!(st & 1u << (j - 1))) {tomin(f[st | 1u << (j - 1)], f[st] + (a[i] != c[j]));}}printf("%d", f[(1u << n) - 1]);
}}
$\mathcal{O}(n^2)$ 暴力判断合法
#include <cstdio>
#include <iostream>
#include <stack>
using namespace std;const int N = 200010;int n, a[N], b[N], c[N], d[N];
int p[N];stack<int> st[N];bool diedL[N], diedR[N];bool check() {int pcnt = 0; // 后缀右部点有多少int N_G = 0; // 邻居左部点有多少for (int i = n, j = n; i >= 1; --i) {if (diedR[i]) continue;++pcnt;for (; j >= 1 && p[j] >= i; --j)N_G += !diedL[j];if (N_G < pcnt)return false;}return true;
}int main() {scanf("%d", &n);for (int i = 1; i <= n; ++i)scanf("%d%d", &a[i], &b[i]);for (int i = 1; i <= n; ++i)scanf("%d%d", &c[i], &d[i]);for (int i = 1; i <= n; ++i) {for (p[i] = p[i - 1]; p[i] + 1 <= n && d[p[i] + 1] >= b[i]; ++p[i]);}int ans = 0;for (int i = n, j = n; i >= 1; --i) {for (; j >= 1 && p[j] >= i; --j)st[a[j]].push(j);if (st[c[i]].empty()) {++ans;continue;}int pi_i = st[c[i]].top();diedL[pi_i] = diedR[i] = true;if (check()) {st[c[i]].pop();} else {++ans;diedL[pi_i] = diedR[i] = false;}}printf("%d", ans);return 0;
}
$\mathcal{O}(n\log n)$ 数据结构维护
#include <cstdio>
#include <iostream>
#include <stack>
using namespace std;const int N = 200010;int n, a[N], b[N], c[N], d[N];
int p[N];stack<int> st[N];#define lson (idx << 1 )
#define rson (idx << 1 | 1)int tag[N << 2], mi[N << 2];inline void pushtag(int idx, int v) {tag[idx] += v, mi[idx] += v;
}inline void pushdown(int idx) {if (!tag[idx]) return;pushtag(lson, tag[idx]);pushtag(rson, tag[idx]);tag[idx] = 0;
}void modify(int idx, int trl, int trr, int l, int r, int v) {if (l <= trl && trr <= r) return pushtag(idx, v);pushdown(idx);int mid = (trl + trr) >> 1;if (l <= mid) modify(lson, trl, mid, l, r, v);if (r > mid) modify(rson, mid + 1, trr, l, r, v);mi[idx] = min(mi[lson], mi[rson]);
}#undef lson
#undef rsonint main() {scanf("%d", &n);for (int i = 1; i <= n; ++i)scanf("%d%d", &a[i], &b[i]);for (int i = 1; i <= n; ++i)scanf("%d%d", &c[i], &d[i]);for (int i = 1; i <= n; ++i) {for (p[i] = p[i - 1]; p[i] + 1 <= n && d[p[i] + 1] >= b[i]; ++p[i]);}for (int i = 1; i <= n; ++i) {modify(1, 1, n, 1, p[i], 1);modify(1, 1, n, 1, i, -1);}int ans = 0;for (int i = n, j = n; i >= 1; --i) {for (; j >= 1 && p[j] >= i; --j)st[a[j]].push(j);if (st[c[i]].empty()) {++ans;continue;}int pi_i = st[c[i]].top();modify(1, 1, n, 1, p[pi_i], -1);modify(1, 1, n, 1, i, 1);if (mi[1] >= 0) {st[c[i]].pop();} else {++ans;modify(1, 1, n, 1, p[pi_i], 1);modify(1, 1, n, 1, i, -1);}}printf("%d", ans);return 0;
}