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

news/2025/2/4 19:14:33/文章来源:https://www.cnblogs.com/XuYueming/p/18697527

前言

题目链接:洛谷;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\),分如下两类讨论:

  1. \(\forall S\subseteq X, N_G(S) \geq |S|+1\)
    将任意 \(x\in X\) 与任意 \(y\in N_G(\{x\})\) 匹配,剩下的子图 \(G'\) 依然满足命题条件,而 \(X' = X \setminus \{x\}\),根据归纳假设,成立。
  2. \(\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;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/878680.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2025.2.4 鲜花

交通网络 题解?hzoi898 交通网络 题解?Underground 是那个纯音乐啦~Ans 注意到:一个只能说真话,一个绝不说假话。这题有四样读法,你知道么?出一个毒瘤 ds 的最好方式就是把序列问题直接出到树上,考察选手树剖能力。正确的题意:给定一棵树,在时刻 \([tl, tr]\) 链加,查…

云手机和模拟器究竟有什么区别?一分钟带你理清楚

云手机和模拟器究竟有什么区别?一分钟带你理清楚概念 云手机:是在云端运行的虚拟手机,基于云计算技术和端云一体虚拟化技术,通过云服务器实现云服务。具备真实的ARM硬件,和真实手机的架构几乎一样,用户通过网络连接,在不同终端设备上远程操作,可轻松安装、管理APP。 模…

函数笔记

想了很久还是来更这个了。 0x00 一次函数 一般式 \(y=kx+b\),特殊的,当 \(b=0\),称作正比例函数。 图象 一次函数的图像是过\(\left(0,b\right)\)、\(\left( -\dfrac{b}{k},0\right)\)的直线。 而正比例函数的图像则是过原点的一条直线。 根据参数的符号,我们可以确定图象…

java基础(中)

java基础 Scanner对象Scanner类是用于获取用户输入的import java.util.Scanner;public class Hello {public static void main(String[] args){// 创建一个scanner对象来接收键盘数据Scanner scanner = new Scanner(System.in);System.out.println("Enter your name: &qu…

java基础1

java基础2 Scanner对象Scanner类是用于获取用户输入的import java.util.Scanner;public class Hello {public static void main(String[] args){// 创建一个scanner对象来接收键盘数据Scanner scanner = new Scanner(System.in);System.out.println("Enter your name: &q…

PCIe扫盲——TLP Header详解(三)

Completions Completions的TLP Header的格式如下图所示:这里来解释一下Completion Status Codes000b (SC) Successful Completion:表示请求(Request)被正确的处理; 001b (UR) Unsupported Request:表示请求是非法的或者不能被Completer所识别的。在PCIe V1.1以及之后的版…

03-requests库和session

接口测试经常会用到抓包工具,用来抓取接口测试中发送的HTTP请求信息和接收的响应信息。然后查看里面的具体内容。 fiddler是一款常用的HTTP抓包工具,抓包原理是代理式抓包。 Filters设置过滤项Inspectors查看请求消息和响应消息,点击Raw查看原始的请求消息和响应消息 首先客…

探索Java动态代理的奥秘:JDK vs CGLIB

动态代理是一种在 运行时动态生成代理类的技术,无需手动编写代理类代码。它通过拦截目标方法的调用,实现对核心逻辑的 无侵入式增强(如日志、事务、权限控制等)。一、关于动态代理 1.1 简介 动态代理是一种在 运行时动态生成代理类 的技术,无需手动编写代理类代码。它通过…

Esp32s3(立创实战派)移植LVGL

Esp32s3(立创实战派)移植LVGL 移植: 使用软件EEZ studio 创建工程选择带EEZ Flow的,可以使用该软件更便捷的功能根据屏幕像素调整画布为320*240复制ui文件至工程 将生成的ui文件夹复制到main文件夹同级目录(ui文件夹在工程文件src中) 工程结构:修改声明头文件路径 首先将u…

字节系AI代码编辑器Trae:免费双模大模型+「Builder模式」

字节跳动技术团队近日正式推出AI代码编辑器Trae(官网:https://www.trae.ai/),这款定位为「自适应AI IDE」的开发工具,凭借其创新功能组合在技术圈引发关注。作为面向下一代开发者的智能编程平台,Trae正在重新定义人机协作的编码体验。 一、核心亮点解析双模大模型免费开放…

RocketMQ实战—3.基于RocketMQ升级订单系统架构

大纲 1.基于MQ实现订单系统核心流程的异步化改造 2.基于MQ实现订单系统和第三方系统的解耦 3.基于MQ实现将订单数据同步给大数据团队 4.秒杀系统的技术难点以及秒杀商详页的架构设计 5.基于MQ实现秒杀系统的异步化架构 6.全面引入MQ的订单系统架构的思维导图1.基于MQ实现订单系…

昆明理工大学2025通信复试真题及答案-通信核心课程综合

0854通信工程信号与系统 吴大正信号与线性系统分析昆工昆明理工大学通信工程817信号与系统考研信号与信息处理通信与信息系统通信工程F002通信核心课程综合通信工程(含宽带网络、移动通信等)