以一道题目为例
题目大意
每个房间位于第 i i i 行第 j j j 列,包含坚果数量 A i j A_{ij} Aij,且所有 A i j A_{ij} Aij 是 1 1 1 到 N ( N + 1 ) 2 \frac{N(N+1)}{2} 2N(N+1) 的一个排列。从对角线位置 ( i , i ) (i, i) (i,i) 出发,可选择的房间必须满足行号 ≥ i \ge i ≥i 且列号 ≤ i \le i ≤i。因此,对于坚果数量 v v v,设其所在房间的行号为 R v R_v Rv、列号为 L v L_v Lv,则它只能被一个满足 L v ≤ i ≤ R v L_v \le i \le R_v Lv≤i≤Rv 的对角线位置 i i i 选择。
问题转化
有 N N N 个位置 i = 0 , 1 , ... , N − 1 i = 0, 1, \dots, N-1 i=0,1,...,N−1,每个坚果 v v v 对应一个区间 [ L v , R v ] [L_v, R_v] [Lv,Rv]。需要选择 n n n 个 ( 0... n − 1 ) (0 ... n-1) (0...n−1)不同的坚果,并为每个被选坚果分配一个位置 i i i,使得 i i i 在区间内且所有分配的位置互不相同。目标是最大化被选坚果的数值之和。
解题过程
(零)题外话
在见到这个问题的头3h,发现可以构造一个 O ( n 2 ) O(n^2) O(n2) 个点 O ( n 2 ) O(n^2) O(n2) 条边的费用流模型,具体的,可以这样考虑 S → v S \rightarrow v S→v ,这里的 v v v 指的是坚果的价值,再有 i → T i \rightarrow T i→T,这里的 i i i 指的是位置 ( 0... n − 1 ) (0...n-1) (0...n−1) 下标,然后我们发现,对于每个 i i i 只能是有至多 O ( n ) O(n) O(n) 个坚果连向他。所以我们可以辅助以线段树求前k大的方法进行优化建图。
但是无可避免的是有 O ( n 2 ) O(n^2) O(n2) 条 v → i v \rightarrow i v→i 类型的边,所以考虑图的形态,S、i 与 v、T 构成二分图的左图和右图,于是考虑模拟费用流优化建图,我们保留 S、i、T 这样 O ( n ) O(n) O(n) 个点,于是 S → i S \rightarrow i S→i 只需要保留 O(n) 条边,压缩 S → v → i S \rightarrow v \rightarrow i S→v→i 的边(仅使用其中费用最大的那条)。
经过一通计算,发现每次跑一条增广路的时间复杂度是 O ( n 2 ) O(n^2) O(n2) (SPFA,点和边都是 O ( n ) O(n) O(n))。然后要跑 O ( n ) O(n) O(n) 次增广,复杂度升天了。遂寄。
(一)建模
建立映射:对于每个坚果 v v v,记录其列号 L v L_v Lv 和行号 R v R_v Rv。不难从图中发现,坚果 v v v 只能在 i ∈ [ L v , R v ] i \in [L_v, R_v] i∈[Lv,Rv] 时被选择。
那么,就转化成了有 O ( n 2 ) O(n^2) O(n2) 个三元组区间 { L v , R v , v } \{L_v, R_v, v\} {Lv,Rv,v}。
我们希望选出其中 n n n 个区间,使得 ∑ v \sum v ∑v 最大。
(二)匹配存在的充要条件(Hall 定理)
先给出答案
对于区间匹配问题,存在分配方案(系统不同代表系)的充要条件是:对于任意 0 ≤ x ≤ y < n 0 \le x \le y < n 0≤x≤y<n,设 f ( x , y ) f(x,y) f(x,y) 为所选区间中满足 x ≤ L v x \le L_v x≤Lv 且 R v ≤ y R_v \le y Rv≤y 的个数,则必须满足
f ( x , y ) ≤ y − x + 1. f(x,y) \le y - x + 1. f(x,y)≤y−x+1.
即完全包含在 [ x , y ] [x, y] [x,y] 内的区间个数不超过区间长度。
详细讲解其原因
首先,Hall定理讲的是对于一个匹配是可以被完全匹配的,那么对于其任意一个子集 S S S,能(通过边)选择的不可重集合 T T T,需要有在任意时刻都必须满足 ∣ S ∣ ≤ ∣ T ∣ |S| \leq |T| ∣S∣≤∣T∣。
那么对于这个问题而言,S指代选择的区间,这些区间都要有不同的对角线上的点进行匹配;而这些"可选择的区间"只会使用其中的一个点,于是应该满足所有的 [ x , y ] [x, y] [x,y] 区间都要满足区间内被选择的点的数量是小于等于区间长度的才可以。
上述问题等价于:对于 x ≤ L v , R v ≤ y x \leq L_v, R_v \leq y x≤Lv,Rv≤y,需要有 [ x , y ] [x, y] [x,y] 中的被选择的完整区间 [ L v , R v ] [L_v, R_v] [Lv,Rv] 的个数是小于等于 y − x + 1 y - x + 1 y−x+1 的,对于所有 [ x , y ] [x, y] [x,y] 区间都必须成立。
于是,我们推出:
f ( x , y ) ≤ y − x + 1. f(x,y) \le y - x + 1. f(x,y)≤y−x+1.
证明
设已选区间集合为 S S S,定义 cnt ( x , y ) \text{cnt}(x, y) cnt(x,y) 为 S S S 中完全包含在 [ x , y ] [x, y] [x,y] 内的区间数量。合法匹配的充要条件是:对所有 x ≤ y x \leq y x≤y,有 cnt ( x , y ) ≤ y − x + 1 \text{cnt}(x, y) \leq y - x + 1 cnt(x,y)≤y−x+1。
加入新区间 [ L , R ] [L, R] [L,R] 后,所有满足 x ≤ L x \leq L x≤L 且 y ≥ R y \geq R y≥R 的 cnt ( x , y ) \text{cnt}(x, y) cnt(x,y) 会增加 1。因此,加入前必须保证对所有这些 ( x , y ) (x, y) (x,y) 都有 cnt ( x , y ) < y − x + 1 \text{cnt}(x, y) < y - x + 1 cnt(x,y)<y−x+1。
综上,我们考虑一则贪心策略。
(三)贪心策略
要最大化坚果值之和,应优先考虑值较大的坚果。从大到小依次尝试加入每个坚果,加入后检查是否仍存在分配方案。若存在则加入,否则跳过。
我们需要维护 f ( x , y ) f(x,y) f(x,y) 并检查其在任意时刻都需要是合法的。
按照一个比较套路的贪心操作:对于 [ L v , R v ] [L_v, R_v] [Lv,Rv],尽可能选择靠前的点。
但是我们会发现一个问题,如果按顺序放入的区间为 [ 1 , 3 ] , [ 1 , 3 ] , [ 2 , 2 ] [1,3], [1,3], [2,2] [1,3],[1,3],[2,2], [ 2 , 2 ] [2,2] [2,2] 这个区间显然是可以被取的,但是由于以上情况, [ 2 , 2 ] [2,2] [2,2] 是放不进来的。
所以,我们不妨维护 k e y [ i ] key[i] key[i] 当前这个下标 i i i 选择的区间的(坚果的值)标号为 k e y [ i ] key[i] key[i] 。
然后,还需要考虑这个区间能不能放进来,我们可以维护个 l i m [ i ] lim[i] lim[i] ,以 i i i 作为左端点,要到 l i m [ i ] lim[i] lim[i] 时候才能找到第一个空位(结合上述Hall定理)。
那么,对于 [ L v , R v ] [L_v, R_v] [Lv,Rv],只有当 R v ≥ l i m [ L v ] R_v \geq lim[L_v] Rv≥lim[Lv] 时候才能将该区间放进来。
然后,放进来,难道是放在 l i m [ L v ] lim[L_v] lim[Lv] 吗?最好的结果是这样的,但是坏的情况是,这个点已经被其他区间选择了,那么在这个情况下,我们尽可能让右端点靠后的节点去向后匹配去吧!(这段可以看看代码,我通过造样例调出来的)
代码
cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 2e3 + 5, maxVal = maxn * (maxn + 1) / 2;
int n, a[maxn][maxn], up;
struct node {
int l, r;
node(int a = 0, int b = 0):l(a), r(b) {}
} t[maxVal];
int key[maxn], lim[maxn];
ll Solve() {
for(int i = 0; i < n; i ++) lim[i] = i;
ll ans = 0;
int flag = 0;
for(int v = up, x, y; v >= 1 && (flag ^ n); v --) {
x = t[v].l;
y = t[v].r;
if(lim[x] > y) continue;
flag ++;
ans += v;
int pos = lim[x], now, tmp = v;
while(key[pos]) {
now = key[pos];
if(t[tmp].r < t[now].r) {
swap(tmp, now);
}
key[pos] = now;
pos ++;
}
key[pos] = tmp;
lim[n] = n;
for(int l = n - 1; l >= 0; l --) {
if(!key[l]) lim[l] = l;
else {
lim[l] = lim[l + 1];
if(key[l] && t[key[l]].r >= lim[l]) lim[l] = l;
}
}
}
return ans;
}
void solve(int N, vector<vector<int>> A, long long& answer, vector<int>& solution) {
n = N;
up = n * (n + 1) / 2;
for(int i = 0; i < n; i ++) {
for(int j = 0; j <= i; j ++) {
a[i][j] = A[i][j];
t[a[i][j]] = node(j, i);
}
}
answer = Solve();
for(int i = 0; i < n; i ++) solution.push_back(key[i]);
}