「JOISC 2016 Day 4」最差记者 2 题解

前言

题目链接:洛谷AtCoder

形式化题意

建议阅读原题面。

给定长度为 n 的序列 a,b,c,d,求最少修改多少次 ai,使得存在一个排列 p,满足 ai=cpibidpi

n2×105b,d 单减,保证有解。

题目分析

首先你必须分析出来,这是一个最大权二分图完美匹配问题,不然连最基础的状压都不会写,然后爆零(当然说的是我自己)。

考虑把 Xi=(ai,bi) 看做左部点,Yj=(cj,dj) 看做右部点,所有满足 bidjXi,Yj 之间连边,边权是 [aicj],我们所求的就是最小花费完美匹配。

考虑状压 DP。记 fS 表示右部点匹配上的点集为 S,和前 |S| 个左部点匹配,最小的花费是多少。初始 f=0,答案是 fY。转移的时候,每次选取一个点转移即可:fS{Yi}min{fS{Yi},fS+[a|S|+1ci]}。时间复杂度 O(n2n)

以上是我没写出来的部分分。我们考虑直接跑 KM 算法求是 O(n3),不太行。那么说明这个二分图一定有特殊的性质,因为如果是一般的二分图的话,我们会的算法 KM 已经最快了。(费用流显然也不太对。)

我们猜测一下算法,肯定需要贪心。我们不妨按照某一个顺序枚举右部点,尝试把它通过一条 0 边和某一左部点匹配。

性质一

对于一个右部点 Yi,通过一条边权为 0 边,尝试和 bj 大的 Xj 配对,肯定不劣。我们将这个匹配的 Xj 记作 π(i)

bj 大小关乎 Xj 与多少右部点有边,显然 bj 小的连出的边更多。倘若我们选择了 bj 更小的 Xj,一定可以将其替换为满足 bjbjXj,而替换下来的 Xj 留着可能和别的右部点匹配,从而不劣。

我们能匹配就让他们匹配上。这么做是对的基于所有点的贡献相同,如果我们按照一个恰当的顺序贪心,如果现在不进行匹配,之后让别的点和 Xj 匹配不会让答案更优,所以这么贪心是不劣的。

接下来思考,什么时候我们发现他们不能匹配呢?倘若匹配他们后,把它们删去,剩下的图还不能形成一个完美匹配,那我们就不能让他们匹配。

是否存在完美匹配?这是一个很典的问题。

定理一: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=(XY,E) 存在完美匹配 ME,那么 SX,记 T={yYxS(x,y)M},记 S 的邻居 NG(S)={yYxS(x,y)E},由于 ME,我们有 TNG(S),故 |NG(S)||T|=|S|。对于 Y 同理。必要性证毕。

Sufficiency Proof

归纳证明。对于 |X|=1,显然成立。假设 |X|<n 命题成立,对于 |X|=n>1,分如下两类讨论:

  1. SX,NG(S)|S|+1
    将任意 xX 与任意 yNG({x}) 匹配,剩下的子图 G 依然满足命题条件,而 X=X{x},根据归纳假设,成立。
  2. SX,NG(S)=|S|
    SNG(S) 组成 |S| 组匹配,剩下的子图 G 依然满足命题条件(倘若 SX,|NG(S)|<|S|,则 NG(SS)=NG(S)NG(S),从而有 |NG(SS)|<|SS|,与假设矛盾),故根据归纳假设,成立。

对于任意子集的话……难道我们要枚举子集?显然不现实。这时候,似乎需要用到这个二分图的一些性质了。

性质二

我们发现,对于一个左部点 Xi,它所连的右部点是一段前缀,不妨设为 Y1Ypi,这个可以用双指针处理。类似同理,右部点连到的左部点总是一段后缀。

证明显然,自证不难。

这个性质很自然得到,有什么用呢?

性质三

我们考虑一个右部点的子集 S,记 Yy0=argmaxYiSdiS=suf(y0)={Yididy0}

我怎么听不懂你在讲什么?

如果我们把左右部点,分别按照 b,d 从小到大的顺序,从上到下排列,Yy0 就是 S 中最靠上的点,S 就是 Yy0 及以下的右部点构成的点集(不妨将 Yi 及以下的右部点构成的点集记作 suf(i))。

S 满足 Hall 判定条件是 S 满足 Hall 判定条件的充分条件。因为 S 相比 S,邻居没变 NG(S)=NG(S)=NG({Yy0}),而 |S||S|

如此,我们把需要判定的子集个数由 2n 简化至 n 个后缀。原问题似乎变得十分可做。慢着,我们还有一个问题一直没有解决,那就是按照什么顺序枚举右部点呢?

先说结论:按照 di 从小到大枚举,即按照下标从后往前枚举右部点。

我们考虑 ci=cjdidj 的右部点 Yi,Yj,且有 π(i)=π(j)=π0。让 Yjπ0 匹配,而不是 Yiπ0 匹配,原因是,两者唯一的差别就是考虑 dk(dj,di)Yksuf(k)Yjπ0 匹配后,我们会删去 Yj 这个右部点,而无论是 Yi/Yjπ0 匹配,NG(suf(k)) 不变。导致让 Yj 先匹配,Yk 更有可能满足 Hall 判定条件。

至于 π(i) 的维护,对每种 ai/ci 开一个栈,然后双指针即可。我们可以写出如下 O(n2) 代码:

#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 了。

我们考虑将判定条件 i,|NG(suf(i))||suf(i)| 移项变为 i,|NG(suf(i))||suf(i)|0,用数据结构维护左侧式子,为了方便叙述,将式子表示为 i,uivi0

发现我们在匹配 π(i),Yi 时,将 Y1Ypπ(i)u 减了一,将 Y1Yi1v 减少了一。至于之后 check 的时候不考虑 Yi,可以直接将 ui。或者除了上述操作,将 Yiv 也减一,就不需要特殊处理 Yi

感性理解一下。由于 Y1Ypπ(i) 包含了 Yi,再把 vi 减一相当于它 ui,vi 都不变。考虑 vi=vi+1+1,我们又有 uivi=ui1vi+1,而 ui1ui+1,所以 Yi 和后缀里某个点等价,如果不合法,后缀某个点会出现问题。

于是,我们可以使用一棵线段树,方便地执行区间加减操作,查询全局的最小值,看看如果 0 就合法,否则不合法。

时间复杂度:O(nlogn)。瓶颈在于线段树维护区间加减一,全局最值查询,和 这题 最后瓶颈相同。

代码

O(n2n) 状压
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]);
}
}
O(n2) 暴力判断合法
#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;
}
O(nlogn) 数据结构维护
#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 rson
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]);
}
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;
}
posted @   XuYueming  阅读(12)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示