CF1601C Optimal Insertion 解题报告
确实是一道好题 模拟赛打挂了
题意
给定两个序列 ,长度分别为
)。接下来将 中的所有元素以任意方式插入序列 中任意位置,请找出一种插入方式使结果序列中的逆序对数量最小化,并输出这个最小值。
Solution
模拟赛时看到这道题确实没啥思路,手玩几个样例发现都是将 排序后插入 序列, 因此我们证明第一个性质。
最优方案一定是将 中序列从小到大排序后插入 序列
证明: 不妨设 中插入的两个数 。 尝试交换两个数, 显然交换前后 左边的数和 右边的数对答案没有影响。现在考虑两数中间的数
- 若 或 , 显然对答案没有影响
- 若 , 会使答案增加2
由于交换后答案必定至少加1, 所以交换后必然不优。得证。
模拟赛时想到这个贪心就不会了, 看了题解以后确实恍然大悟。
我们引入最优决策点的概念。
对于 中的一个数, 它的最优决策点就是将它插入 中使得它对逆序对贡献最小的这个位置
可能有点绕, 感性理解一下。
接下来考虑如何计算最优决策点。
我们考虑 中的一个数 , 将 先插在 的最左边并计算当前逆序对个数。然后将 向后交换。 如果与之交换的数比 大, 则贡献加 ; 相等则贡献不变; 小于, 则贡献 。 于是我们可以建一个长度为的由 构成的数列, 的最优决策点即为数列中最小前缀的位置。
接下来证明第二个性质。
若 , 则 的最优决策点 的最优决策点
证明:因为 所以数列中的 仍是 , 会变成 , 会变成 或者 , 因此最小前缀的位置只可能会后移, 得证。
我们使用线段树维护这个过程。
Code
#include<bits/stdc++.h>
using namespace std;
#define ll long long
inline int read() {
int x = 0, f = 1; char c = getchar();
while (c < '0' || c > '9') {if (c == '-') f = -f; c = getchar();}
while (c >= '0' && c <= '9') {x = (x << 3) + (x << 1) + (c ^ 48); c = getchar();}
return x * f;
}
#define Min(x, y) (x < y ? x : y)
const int N = 1e6 + 10, INF = 0x3f3f3f3f;
int T, n, m, tot;
int a[N], b[N], p[N<<1], c[N<<1];
ll ans;
struct point{
int v, id;
bool operator <(const point &o) const {return v < o.v;}
}sa[N];
inline int lowbit(int x) {return x & -x;}
inline void add(int x, int v) {for (; x <= n+m; x += lowbit(x)) c[x] += v;}
inline int ask(int x) {int res = 0; for (; x; x -= lowbit(x)) res += c[x]; return res;}
struct Tree {
int s, tag;
#define s(x) tree[x].s
#define tag(x) tree[x].tag
// tag 是懒标记, s 是数列前缀
}tree[N<<2];
#define ls p << 1
#define rs p << 1 | 1
inline void pushup(int p) {s(p) = Min(s(ls), s(rs));}
inline void build(int p, int l, int r) {
tag(p) = 0;
if (l == r) {s(p) = l; return;}
int mid = l + r >> 1;
build (ls, l, mid); build (rs, mid+1, r);
pushup(p);
}
inline void pushdown(int p) {
if (tag(p)) {
s(ls) += tag(p); s(rs) += tag(p);
tag(ls) += tag(p); tag(rs) += tag(p);
tag(p) = 0;
}
}
inline void modify(int p, int l, int r, int L, int R, int v) {
if (L <= l && r <= R) {
tag(p) += v; s(p) += v;
return;
}
pushdown(p); int mid = l + r >> 1;
if (L <= mid) modify(ls, l, mid, L, R, v);
if (R > mid) modify(rs, mid+1, r, L, R, v);
pushup(p);
}
inline int query(int p, int l, int r, int L, int R) {
if (L <= l && r <= R) return s(p);
pushdown(p);
int minn = INF, mid = l + r >> 1;
if (L <= mid) minn = Min(minn, query(ls, l, mid, L, R));
if (R > mid) minn = Min(minn, query(rs, mid+1, r, L, R));
return minn;
} //以上均为线段树基本操作
int main () {
T = read();
while (T --) {
n = read(), m = read();
for (int i = 1; i <= n; ++ i) a[i] = read(), p[++tot] = a[i];
for (int i = 1; i <= m; ++ i) b[i] = read(), p[++tot] = b[i];
sort(p + 1, p + 1 + tot); tot = unique(p + 1, p + 1 + tot) - p - 1;
for (int i = 1; i <= n; ++ i) a[i] = lower_bound(p + 1, p + 1 + tot, a[i]) - p;
for (int i = 1; i <= m; ++ i) b[i] = lower_bound(p + 1, p + 1 + tot, b[i]) - p; // 离散化
for (int i = 1; i <= n; ++ i) sa[i] = (point) {a[i], i};
sort(b + 1, b + 1 + m); sort(sa+1, sa + 1 + n);
build (1, 1, n);
ans = 0; sa[n+1].v = INF;
for (int i = 1, j = 1; i <= n + 1; ++ i) while (j <= m && b[j] <= sa[i].v) ans += i - 1, j ++; // 先将 b 中所有数放到 a 的最左边
for (int i = n; i >= 1; -- i) ans += ask(a[i]-1), add(a[i], 1); // 树状数组求逆序对
for (int i = 1, j = 1; i <= m; ++ i) {
if (b[i] != b[i-1]) {
while (j <= n && sa[j].v < b[i]) {
if (sa[j].v == b[i-1]) modify(1, 1, n, sa[j].id, n, -1);
else modify(1, 1, n, sa[j].id, n, -2);
++ j;
}
int k = j;
while (k <= n && sa[k].v == b[i])
modify(1, 1, n, sa[k].id, n, -1), ++ k;
}
ans += Min(0, query(1, 1, n, 1, n));
}
printf("%lld\n", ans);
for (int i = 1; i <= n + m; ++ i) c[i] = 0;
tot = 0;
}
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现