CF1601C Optimal Insertion 解题报告

确实是一道好题 模拟赛打挂了

题意

给定两个序列 a,b,长度分别为 n,m(1n,m106)
)。接下来将 b 中的所有元素以任意方式插入序列 a 中任意位置,请找出一种插入方式使结果序列中的逆序对数量最小化,并输出这个最小值。

Solution

模拟赛时看到这道题确实没啥思路,手玩几个样例发现都是将 b 排序后插入 a 序列, 因此我们证明第一个性质。

最优方案一定是将 b 中序列从小到大排序后插入 a 序列

证明: 不妨设 b 中插入的两个数 x,y(xy)。 尝试交换两个数, 显然交换前后 x 左边的数和 y 右边的数对答案没有影响。现在考虑两数中间的数 z(z)

  1. zxzy, 显然对答案没有影响
  2. x<z<y, 会使答案增加2

由于交换后答案必定至少加1, 所以交换后必然不优。得证。

模拟赛时想到这个贪心就不会了, 看了题解以后确实恍然大悟。

我们引入最优决策点的概念。

对于 b 中的一个数, 它的最优决策点就是将它插入 a 中使得它对逆序对贡献最小的这个位置

可能有点绕, 感性理解一下。

接下来考虑如何计算最优决策点。

我们考虑 b 中的一个数 x, 将 x 先插在 a 的最左边并计算当前逆序对个数。然后将 x 向后交换。 如果与之交换的数比 x 大, 则贡献加 1; 相等则贡献不变; 小于, 则贡献 1。 于是我们可以建一个长度为n的由 1,0,1构成的数列, x 的最优决策点即为数列中最小前缀的位置。

接下来证明第二个性质。

x<y, 则 x 的最优决策点 y 的最优决策点

证明:因为 y>x 所以数列中的 1 仍是 1, 0 会变成 1, 1 会变成 0 或者 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;
}
posted @   LikC1606  阅读(31)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示