PKUSC2019 D1T1 題解

XuYueming發表於2024-04-19

前言

五一網課的例題,但是網上沒有詳細的題解(其實就是都沒放程式碼),所以來寫一篇。題目可以在這裡提交。

題目簡述

\(n\)\(n \leq 5 \times 10 ^5\))個村莊排成一排,每個村莊裡有一個人。第 \(i\) 個村莊裡的人要去第 \(p_i\) 個村莊,且 \(p\)\(1 \sim n\) 的一個排列。他們出行方式是每次交換相鄰兩個村莊的人。

現在政府要建立 \(m\) 個哨卡,第 \(i\) 個哨卡建立在村莊 \(q_i\)\(q_i + 1\) 之間,每次交換的時候,如果中間有一個哨卡,或者交換雙方有一個人經過了一個哨卡,則需要花費 \(1\) 的代價。

政府每年建設一個哨卡。人們好奇,對於前 \(i\) 年建設的 \(i\) 個哨卡,每個人都走到對應村莊的最小代價是多少呢?由於這個問題太難了,所以交給聰明的你去做啦。

題目分析

先觀察樣例:

樣例 #1

樣例輸入 #1

10 8
9 10 7 3 1 5 8 6 2 4
5
1
7
2
3
4
8
9

樣例輸出 #1

15
18
23
26
28
29
31
31

只有第一天 \((5, 6)\) 之間的哨卡時,情況如下:

\[9,10,7,3,1{\color{red}{|}}5,8,6,2,4 \]

好像可以把兩邊都先排個序,不用花費:

\[1,3,7,9,10{\color{red}{|}}2,4,5,6,8 \]

然後先讓第 \(5\) 個人走到 \(10\) 這個位置,分別和 \(2, 4, 5, 6, 8\) 交換了一次,貢獻是 \(5\)

\[1,3,7,9,2{\color{red}{|}}4,5,6,8,10 \]

然後再讓第 \(4\) 個人走到 \(9\) 這個位置,分別和 \(2, 4, 5, 6, 8\) 交換了一次,貢獻是 \(5\)

\[1,3,7,2,4{\color{red}{|}}5,6,8,9,10 \]

然後再讓第 \(3\) 個人走到 \(7\) 這個位置,分別和 \(2, 4, 5, 6\) 交換了一次,貢獻是 \(4\)

\[1,3,2,4,5{\color{red}{|}}6,7,8,9,10 \]

然後再讓第 \(2\) 個人走到 \(3\) 這個位置,和 \(2\) 交換了一次,貢獻是 \(1\)

\[1,2,3,4,5{\color{red}{|}}6,7,8,9,10 \]

總花費 \(15\)。我們發現,這樣移動一定是最優的,因為每個人的移動都是必要且最小的,是一個排序的過程。

再仔細看,要走到 \(10\) 這個位置,分別和 \(2, 4, 5, 6, 8\) 交換了一次,恰好是 \(10\) 和右邊比 \(10\) 小的數。再以 \(2\) 來看,為了走到 \(2\),和左邊比 \(2\) 大的 \(3,7,9,10\) 分別交換了一次。總結一下,跨過哨卡的逆序對都對答案貢獻了一。

其實這就是結論:答案等於經過了任何一個哨卡的逆序對個數。接下來考慮證明它。

證明:

任意一對逆序對必然要交換一次,如果一對逆序對中間跨過了一個哨卡,必然需要花費代價,因此答案至少是這個。

將兩個關卡之間的部分排序,任意一對逆序對只會在相鄰的時候交換一次,交換了所有逆序對即表明它走到了終點,因此答案至多是這個。

以上可以說是這題核心思考了。接下來就可以寫出暴力程式碼了:

read(n, m);
for (int i = 1; i <= n; ++i) read(p[i]);
for (int i = 1; i <= m; ++i){
	int x; read(x), mark[x] = true;
	long long res = 0;
	for (int a = 1; a <= n; ++a)
	for (int b = a + 1; b <= n; ++ b){
		if (p[a] > p[b]){
			bool flag = false;
			for (int x = a; x <= b - 1; ++x) if (mark[x]){
				flag = true;
				break;
			}
			res += flag;
		}
	}
	write(res, '\n');
}

時間複雜度 \(\Theta(m n^3)\),顯然不是正解。考慮增量法,加入一個哨卡,求得只經過這個哨卡的逆序對,即左邊一塊沒有經過哨卡的部分和右邊一塊沒有經過哨卡的部分形成的逆序對數。但是並不好做,將區間拆分並不是我們熟悉的資料結構好做的(當然也可能是我太菜了),所以將詢問倒轉,考慮每次刪掉一個哨卡,並刪除這個僅經過這個哨卡的逆序對數,併合並區間。發現可以用線段樹合併,同時記錄形成了多少個逆序對。時間複雜度 \(\Theta(n \log n)\)

程式碼

當然肯定要放程式碼的。

//#pragma GCC optimize(3)
//#pragma GCC optimize("Ofast", "inline", "-ffast-math")
//#pragma GCC target("avx", "sse2", "sse3", "sse4", "mmx")
#include <iostream>
#include <cstdio>
#define debug(a) cerr << "Line: " << __LINE__ << " " << #a << endl
#define print(a) cerr << #a << "=" << (a) << endl
#define file(a) freopen(#a".in", "r", stdin), freopen(#a".out", "w", stdout)
#define main Main(); signed main(){ return ios::sync_with_stdio(0), cin.tie(0), Main(); } signed Main
using namespace std;

int n, m, p[500010], q[500010];
long long ans[500010];

bool mark[500010];

struct Bit_Tree{
	constexpr inline int lowbit(const int x){
		return x & -x;
	}
	int tree[500010];
	void modify(int p, int v){
		for (int i = p; i <= n; i += lowbit(i)) tree[i] += v;
	}
	int query(int p){
		int res = 0;
		for (int i = p; i; i -= lowbit(i)) res += tree[i];
		return res;
	}
} yzh;

struct DSU{
	int fa[500010];
	void init(){ for (int i = 1; i <= n; ++i) fa[i] = i; }
	int & operator [] (const int x) { return fa[x]; }
	int get(int x){ return fa[x] == x ? x : fa[x] = get(fa[x]); }
} dsu;

struct Segment_Tree{
	struct node{
		int lson, rson;
		int sum;
	} tree[500010 * 40];
	int tot;
	void pushup(int idx){
		tree[idx].sum = tree[tree[idx].lson].sum + tree[tree[idx].rson].sum;
	}
	int merge(int idx, int oidx, int l, int r, long long & ans){
		if (!idx || !oidx) return idx | oidx;
		if (l == r) return tree[idx].sum += tree[oidx].sum, idx;
		int mid = (l + r) >> 1;
		ans += 1ll * tree[tree[idx].rson].sum * tree[tree[oidx].lson].sum;
		tree[idx].lson = merge(tree[idx].lson, tree[oidx].lson, l, mid, ans);
		tree[idx].rson = merge(tree[idx].rson, tree[oidx].rson, mid + 1, r, ans);
		return pushup(idx), idx;
	}
	void modify(int &idx, int l, int r, int p, int val){
		if (l > p || r < p) return;
		if (!idx) tree[idx = ++tot].sum = 0;
		tree[idx].sum += val;
		if (l == r) return;
		int mid = (l + r) >> 1;
		modify(tree[idx].lson, l, mid, p, val);
		modify(tree[idx].rson, mid + 1, r, p, val);
	}
} huan;

int root[500010];

void init(){
	// 獲得初始答案,順便初始化每一棵線段樹
	mark[0] = mark[n] = true;
	for (int i = 1; i <= m; ++i) mark[q[i]] = true;
	for (int i = 1, to; i <= n; i = to + 1){
		for (to = i; ; ++to){
			dsu[to] = i, huan.modify(root[i], 1, n, p[to], 1);
			ans[m + 1] += i - 1 - yzh.query(p[to]);
			if (mark[to]) break;
		}
		for (to = i; ; ++to){
			yzh.modify(p[to], 1);
			if (mark[to]) break;
		}
	}
}

signed main(){
	read(n, m);
	for (int i = 1; i <= n; ++i) read(p[i]);
	for (int i = 1; i <= m; ++i) read(q[i]);
	init();
	for (int i = m; i >= 1; --i){
		// 處理詢問,線段樹合併 q[i] q[i] + 1
		int u = dsu.get(q[i]), v = dsu.get(q[i] + 1);
		root[u] = huan.merge(root[u], root[v], 1, n, ans[i]);
		dsu[v] = u, ans[i] = ans[i + 1] - ans[i];
	}
	for (int i = 1; i <= m; ++i) write(ans[i + 1], '\n');
	return 0;
}