Codeforces Global Round 19


題目傳送門:Codeforces Global Round 19

A. Sorting Parts

題意簡述

給定長度為 \(n\) 的數列 \(a_1, a_2, \ldots, a_n\)

問是否可以將數列分為一段非空前綴和一段非空后綴,將兩部分分別從小到大排序后放回原位置,整個數列卻並沒被從小到大排序。

有多組數據。

數據范圍:\(1 \le n \le {10}^4\)\(1 \le a_i \le {10}^9\)

AC 代碼
#include <cstdio>
#include <algorithm>

#define F(i, a, b) for(int i = a; i <= (b); ++i)

void Solve();

int main() {
	int tests = 1;
	scanf("%d", &tests);
	while (tests--)
		Solve();
	return 0;
}

const int MN = 10005;

int n, a[MN];

void Solve() {
	scanf("%d", &n);
	F(i, 1, n) scanf("%d", a + i);
	puts(std::is_sorted(a + 1, a + n + 1) ? "NO" : "YES");
}

如果原數列已經有序,則無論如何划分,數列都會保持有序。

否則,必然存在兩元素 \(a_i, a_j\)\(i < j\))滿足 \(a_i > a_j\)。只需將它們划分在不同段內,則兩段分別排序后數列仍然不會變得有序。

於是只需要判斷原數列是否有序即可,使用 std::is_sorted 可以簡化代碼。

時間復雜度為 \(\mathcal O(n)\)

B. MEX and Array

題意簡述

設有一長度為 \(k\) 的非負整數數列 \(b_1, b_2, \ldots, b_k\)。將其划分為若干段 \([l_1, r_1], [l_2, r_2], \ldots, [l_c, r_c]\),則此划分的權值被定義為 \(\displaystyle c + \sum_{i = 1}^{c} \operatorname{mex}(\{ b_{l_i}, b_{l_i + 1}, \ldots, b_{r_i} \})\),即划分的段數加上每一段內的數的 mex 之和。定義此數列 \(b_{1 \sim k}\)價值為所有不同划分方式中的最大權值。

給定長度為 \(n\) 的非負整數序列 \(a_1, a_2, \ldots, a_n\),求其所有連續子串的價值之和。

有多組數據。

數據范圍:\(1 \le n \le 100\)\(0 \le a_i \le {10}^9\)

AC 代碼
#include <cstdio>

#define F(i, a, b) for(int i = a; i <= (b); ++i)

void Solve();

int main() {
	int tests = 1;
	scanf("%d", &tests);
	while (tests--)
		Solve();
	return 0;
}

const int MN = 105;

int n, a[MN];

void Solve() {
	scanf("%d", &n);
	F(i, 1, n) scanf("%d", a + i);
	int ans = 0;
	F(i, 1, n)
		ans += (a[i] ? 1 : 2) * i * (n - i + 1);
	printf("%d\n", ans);
}

首先考慮如何求出數列 \(b_1, b_2, \ldots, b_k\) 的價值。

注意到,長度為 \(\mathit{len}\) 的段中所有數的 mex 不會超過 \(len\),而每一段對划分的權值有貢獻 \(1 + \operatorname{mex} \le \mathit{len} + 1\)
又有,將這一段全部拆分為長度為 \(1\) 的段,將對划分的權值有貢獻至少 \(\mathit{len}\)。而如果 \(\operatorname{mex} \ne 0\),意味着這一段內至少有一個 \(0\),於是再對划分的權值貢獻至少 \(1\)

也就是說,數列 \(b_1, b_2, \ldots, b_k\) 的價值即是全部划分為長度為 \(1\) 的段時的權值,即 \(\displaystyle \sum_{i = 1}^{k} \begin{cases} 1 & , b_i \ne 0 \\ 2 & , b_i = 0 \end{cases}\)

\(c_i = \begin{cases} 1 & , b_i \ne 0 \\ 2 & , b_i = 0 \end{cases}\),則答案即為 \(c_{1 \sim n}\) 的每個子串中所有數的和之和。

我們熟知(通過和式變換)這即是 \(\displaystyle \sum_{i = 1}^{n} c_i \cdot i \cdot (n - i + 1)\)

時間復雜度為 \(\mathcal O(n)\)

C. Andrew and Stones

題意簡述

\(n\)\(n \ge 3\))堆石子排成一行,其中第 \(i\) 堆的數量為 \(a_i\)

你需要通過一些操作將除了第 \(1\) 堆和第 \(n\) 堆以外的石子移走。每次操作,你可以:

  • 選取三個下標 \(i, j, k\),滿足 \(1 \le i < j < k \le n\) 且第 \(j\) 堆中至少有 \(2\) 個石子,將第 \(j\) 堆中的 \(2\) 個石子的其中一個移到第 \(i\) 堆中,另一個移到第 \(k\) 堆中。
    即需保證 \(a_j \ge 2\) 並執行 \(a_j \gets a_j - 2\)\(a_i \gets a_i + 1\)\(a_k \gets a_k + 1\)

請判斷是否可以完成目標,如果可以完成,求出最少操作次數,否則報告之。

有多組數據。

數據范圍:\(1 \le n \le {10}^5\)\(1 \le a_i \le {10}^9\)

AC 代碼
#include <cstdio>

#define F(i, a, b) for(int i = a; i <= (b); ++i)

void Solve();

int main() {
	int tests = 1;
	scanf("%d", &tests);
	while (tests--)
		Solve();
	return 0;
}

typedef long long ll;
const int MN = 100005;

int n, a[MN];

void Solve() {
	scanf("%d", &n), n -= 2;
	F(i, 0, n + 1) scanf("%d", a + i);
	if (n == 1 && a[1] % 2)
		return puts("-1"), void();
	bool ok = 0;
	F(i, 1, n)
		if (a[i] != 1)
			ok = 1;
	if (!ok)
		return puts("-1"), void();
	ll ans = 0;
	F(i, 1, n)
		ans += a[i] + (a[i] & 1);
	printf("%lld\n", ans / 2);
}

首先,\(a_1\)\(a_n\) 的值我們不關心,只有中間 \(n - 2\) 個數是值得考慮的,於是讓 \(n\) 減去 \(2\),並且刪去 \(a_1\)\(a_n\) 兩個數,即 \(a\) 數列變為 \(a_{1 \sim n-2}\)

接下來考慮無解的情況:

  • \(n = 1\)\(a_1\) 為奇數。
  • 所有的 \(a_i\) 均為 \(1\)

這兩種情況的無解性是顯然的,接下來展示其他情況均有解,同時盡量減少操作次數。

其他情況即 \(n = 1\)\(a_1\) 為偶數,或 \(n \ge 2\) 且至少有一個 \(a_i \ge 2\)

我們認為一次操作將 \(2\) 個石子移到 \(1\)\(n\) 號堆中是不浪費的,則可以認為每將石子移到非 \(1\)\(n\) 號堆中是浪費了半步操作,我們需要減少浪費的操作數。

\(n = 1\)\(a_1\) 為偶數時有解是顯然的,而且不浪費任何一步操作。

\(n \ge 2\) 且至少有一個 \(a_i \ge 2\) 時,對於每個為奇數的 \(a_i\),我們都需要將至少一個石子移到其中,以將其變為偶數個,從而可以被清空,於是至少要浪費的操作數量為數列中的奇數個數。
此情況下,如果仍然存在奇數數量的石子堆,那么總是可以將數量 \(\ge 2\) 的石子堆中的 \(2\) 個石子的其中一個移動到數量為奇數的石子堆中,以浪費半步操作為代價消除一堆數量為奇數的石子堆;否則所有石子堆中石子數量均為偶數,顯然可以不浪費任何一步操作清空。

於是當有解時,最少操作數為 \(\displaystyle \frac{1}{2} \left( \sum_{i = 1}^{n} a_i + \sum_{i = 1}^{n} [a_i \bmod 2 = 1] \right) = \sum_{i = 1}^{n} \left\lceil \frac{a_i}{2} \right\rceil\)

時間復雜度為 \(\mathcal O(n)\)

D. Yet Another Minimization Problem

題意簡述

給定長度為 \(n\) 的兩個正整數數列 \(a_1, a_2, \ldots, a_n\)\(b_1, b_2, \ldots, b_n\)

你可以對於任意 \(i\)\(1 \le i \le n\))選擇是否交換 \(a_i\)\(b_i\)

你需要最小化 \(\displaystyle \sum_{1 \le i < j \le n} {(a_i + a_j)}^2 + \sum_{1 \le i < j \le n} {(b_i + b_j)}^2\),求出此最小值。

有多組數據。

數據范圍:\(1 \le n \le 100\)\(1 \le a_i, b_i \le 100\)

AC 代碼
#include <cstdio>
#include <algorithm>
#include <bitset>

#define F(i, a, b) for(int i = a; i <= (b); ++i)
#define dF(i, a, b) for(int i = a; i >= (b); --i)

void Solve();

int main() {
	int tests = 1;
	scanf("%d", &tests);
	while (tests--)
		Solve();
	return 0;
}

const int MN = 105;
const int S = 10005;

int n;
int a[MN], b[MN];
std::bitset<S> f[MN];

void Solve() {
	int ans = 0, sum = 0;
	scanf("%d", &n);
	F(i, 1, n) scanf("%d", a + i), ans += a[i] * a[i], sum += a[i];
	F(i, 1, n) scanf("%d", b + i), ans += b[i] * b[i], sum += b[i];
	ans *= n - 2;
	f[0].reset();
	f[0][0] = 1;
	F(i, 1, n)
		f[i] = (f[i - 1] << a[i]) | (f[i - 1] << b[i]);
	int p = (sum + 1) / 2;
	while (!f[n][p]) ++p;
	ans += p * p + (sum - p) * (sum - p);
	printf("%d\n", ans);
}

考慮

\[\begin{aligned} \sum_{1 \le i < j \le n} {(a_i + a_j)}^2 &= \frac{1}{2} \left( \sum_{i = 1}^{n} \sum_{j = 1}^{n} {(a_i + a_j)}^2 - \sum_{i = 1}^{n} {(a_i + a_i)}^2 \right) \\ &= \frac{1}{2} \left( \sum_{i = 1}^{n} \sum_{j = 1}^{n} (a_i^2 + a_j^2 + 2 a_i a_j) - \sum_{i = 1}^{n} 4 a_i^2 \right) \\ &= (n - 2) \sum_{i = 1}^{n} a_i^2 + {\left( \sum_{i = 1}^{n} a_i \right)}^2 \end{aligned} \]

於是要最小化的即是 \(\displaystyle (n - 2) \left( \sum_{i = 1}^{n} a_i^2 + \sum_{i = 1}^{n} b_i^2 \right) + {\left( \sum_{i = 1}^{n} a_i \right)}^2 + {\left( \sum_{i = 1}^{n} b_i \right)}^2\)。其中第一項為定值,於是只需考慮最小化 \(\displaystyle {\left( \sum_{i = 1}^{n} a_i \right)}^2 + {\left( \sum_{i = 1}^{n} b_i \right)}^2\)

\(\displaystyle S = \sum_{i = 1}^{n} a_i + \sum_{i = 1}^{n} b_i\),以及 \(\displaystyle A = \sum_{i = 1}^{n} a_i\)。只需考慮最小化 \(A^2 + {(S - A)}^2\),根據熟知結論,這即是需要讓 \(A\) 盡量接近 \(S / 2\)

枚舉是否交換每對 \(a_i, b_i\) 等價於進行 0/1 背包,使用正常做法或 std::bitset 均可。

時間復雜度為 \(\mathcal O(n^2 v / w)\),其中 \(v\) 表示值域,\(w\) 為字長。

E. Best Pair

題意簡述

給定長度為 \(n\) 的數列 \(a_1, a_2, \ldots, a_n\)

\(\mathit{cnt}_x\) 表示數值 \(x\)\(a_{1 \sim n}\) 中的出現次數。

\(x, y\) 為兩不同的在 \(a\) 中出現過的數值,即需保證 \(\mathit{cnt}_x, \mathit{cnt}_y \ge 1\)\(x \ne y\),定義數值對 \(\langle x, y \rangle\) 的權值為 \((\mathit{cnt}_x + \mathit{cnt}_y) \cdot (x + y)\)

給出 \(m\) 對被禁止的數值對 \(\langle x_i, y_i \rangle\)\(1 \le i \le m\))。保證給出的數值對滿足 \(\mathit{cnt}_{x_i}, \mathit{cnt}_{y_i} \ge 1\)\(x_i \ne y_i\) 且不會給出重復的數值對(\(\langle x, y \rangle\)\(\langle y, x \rangle\) 算作相同)。

你需要求出所有未被禁止的數值對的權值的最大值。

保證至少存在一對權值有定義的未被禁止的數值對。

有多組數據。

數據范圍:\(2 \le n \le 3 \times {10}^5\)\(0 \le m \le 3 \times {10}^5\)\(1 \le a_i \le {10}^9\)

AC 代碼
#include <cstdio>
#include <algorithm>
#include <vector>
#include <set>
#include <map>

#define F(i, a, b) for(int i = a; i <= (b); ++i)

void Solve();

int main() {
	int tests = 1;
	scanf("%d", &tests);
	while (tests--)
		Solve();
	return 0;
}

typedef long long ll;
const int MN = 300005;

int n, m;
int a[MN];
std::vector<int> v[MN];

void Solve() {
	scanf("%d%d", &n, &m);
	F(i, 1, n) scanf("%d", a + i);
	std::map<int, int> b;
	F(i, 1, n) ++b[a[i]];
	F(i, 1, n) v[i].clear();
	for (auto [c, x] : b)
		v[x].push_back(c);
	std::vector<int> vv;
	F(i, 1, n) if (!v[i].empty()) {
		std::reverse(v[i].begin(), v[i].end());
		vv.push_back(i);
	}
	std::set<std::pair<int, int>> f;
	F(i, 1, m) {
		int x, y;
		scanf("%d%d", &x, &y);
		f.insert({x, y});
	}
	ll ans = 0;
	for (int i : vv)
		for (int j : vv) if (j >= i)
			for (int x : v[i]) {
				bool ok = false;
				for (int y : v[j]) if (x != y)
					if (f.find({x, y}) == f.end() && f.find({y, x}) == f.end()) {
						ans = std::max(ans, (ll)(i + j) * (x + y));
						if (y == v[j][0]) ok = true;
						break;
					}
				if (ok) break;
			}
	printf("%lld\n", ans);
}

首先我們有經典結論:不同的 \(\mathit{cnt}\) 的種類數只有 \(\mathcal O(\sqrt{n})\) 種。

這啟發我們枚舉 \(x, y\)\(\mathit{cnt}\):假設枚舉了 \(\mathit{cnt}_x = i\) 以及 \(\mathit{cnt}_y = j\),則枚舉本身的復雜度可以控制在 \(\mathcal O(n)\) 級別。

那么,要最大化 \((i + j) \cdot (x + y)\),即是要最大化 \(x + y\),但需要滿足 \(\mathit{cnt}_x = i\)\(\mathit{cnt}_y = j\),並且 \(\langle x, y \rangle\) 未被禁止。

定義 \(S_i = \{ x \mid \mathit{cnt}_x = i \}\) 表示 \(\mathit{cnt}_x = i\) 的那些數值 \(x\) 構成的集合。則枚舉了一對 \(i, j\) 后,我們可以最多遍歷 \(m'\)\(\langle x, y \rangle\),其中 \(m'\) 表示連接 \(S_i\)\(S_j\) 之間的所有被禁止的數值對的數量。具體流程如下:

  • 我們可以對於每個 \(i\),維護一個 std::vector 按照 \(x\) 從大到小的順序存儲 \(S_i\),在枚舉的時候先從大到小枚舉 \(x\),從大到小遍歷 \(S_j\) 中的 \(y\),在一個 std::set 中查詢這一對 \(\langle x, y \rangle\) 是否是被禁止的,對於每個 \(x\),查找到第一個未被禁止的即可退出,如果最大的 \(y\) 已被匹配,則退出整個對 \(x\) 的枚舉循環。

這個過程是 \(\mathcal O(1) + \mathcal O(m' \log m)\) 的。

總復雜度為 \(\mathcal O(n \log n + m \log m)\)

F. Towers

題意簡述

給定一棵有 \(n\) 個結點的樹,結點編號為 \(1 \sim n\),結點 \(i\) 有權值 \(h_i\)

你可以在結點中建任意多個信號塔,在結點 \(u\) 上建造一個信號強度為 \(e\) 的塔需要花費 \(e\) 的代價(\(e > 0\))。

對於結點 \(u\),我們稱其接收到了信號,當且僅當存在兩座信號塔,它們在樹上的路徑經過 \(u\)(包含端點),且它們的信號強度均 \(\ge h_u\)

請求出為了讓所有結點均接收到信號,需要花費的最少總代價。

數據范圍:\(1 \le n \le 2 \times {10}^5\)\(1 \le h_i \le {10}^9\)

AC 代碼
#include <cstdio>
#include <algorithm>
#include <vector>

#define F(i, a, b) for(int i = a; i <= (b); ++i)

void Solve();

int main() {
	Solve();
	return 0;
}

typedef long long ll;
const int MN = 200005;

int n, h[MN];
std::vector<int> G[MN];

int mx[MN];
ll DFS(int u, int p) {
	mx[u] = 0;
	ll ret = 0;
	for (int v : G[u]) if (v != p) {
		ret += DFS(v, u);
		mx[u] = std::max(mx[u], mx[v]);
	}
	ret += std::max(h[u] - mx[u], 0);
	mx[u] = std::max(mx[u], h[u]);
	return ret;
}

void Solve() {
	scanf("%d", &n);
	F(i, 1, n) scanf("%d", h + i);
	F(i, 2, n) {
		int x, y;
		scanf("%d%d", &x, &y);
		G[x].push_back(y);
		G[y].push_back(x);
	}
	int rt = (int)(std::max_element(h + 1, h + n + 1) - h);
	ll ans = 0;
	int mx1 = 0, mx2 = 0;
	for (int v : G[rt]) {
		ans += DFS(v, rt);
		if (mx[v] > mx2)
			mx2 = mx[v];
		if (mx2 > mx1)
			std::swap(mx1, mx2);
	}
	ans += 2 * h[rt] - mx1 - mx2;
	printf("%lld\n", ans);
}

\(\displaystyle h_\max = \max_{i = 1}^{n} h_i\)。顯然,不會建造信號強度超過 \(h_\max\) 的信號塔,並且信號強度恰等於 \(h_\max\) 的信號塔會建造至少兩座。

假設 \(h_\mathit{rt} = h_\max\),考慮將結點 \(\mathit{rt}\) 提作根。則 \(\mathit{rt}\) 的兩棵不同子樹內都必須建造信號強度恰等於 \(h_\max\) 的信號塔(或需要在 \(\mathit{rt}\) 自身上建造信號強度恰等於 \(h_\max\) 的信號塔)。

在考慮了這一點后,對於任意一個非 \(\mathit{rt}\) 的結點,對其的最佳“覆蓋”(即選取兩個路徑經過它的,信號強度盡可能大的信號塔)總是有此形式:從其子樹內選取一個信號強度最大的信號塔,而另一個信號塔總是可以取到那兩個信號強度為 \(h_\max\) 中的某一個。於是限制即變為:對於每個結點 \(u\),其子樹內必須至少建造一個信號強度 \(\ge h_u\) 的信號塔。

這時就可以進行一個簡單的樹形 DP,如果子樹內的 \(h\) 值的最大值 \(\mathit{mx}\) 小於 \(h_u\),則選取子樹中的信號塔中信號強度最大的一個,將其強度提升至 \(h_u\)(即代價增加 \(h_u - \mathit{mx}\))。

最后對於 \(\mathit{rt}\) 的處理,只需求出子樹中前兩大值將其提升至 \(h_\max\) 即可。

時間復雜度為 \(\mathcal O(n)\)

G. Birthday

題意簡述

你擁有 \(1, 2, \ldots, n\)\(n\) 個整數。

對於每次操作,你可以選擇你擁有的兩個數 \(x, y\),刪除這兩個數,並加入 \(x + y\)\(|x - y|\)

你需要在 \(20 \cdot n\) 次操作內使這 \(n\) 個整數全部相等,並且在此基礎上最小化這 \(n\) 個整數的和。

如果可以實現目標,你需要構造方案,否則報告之。

有多組數據。

數據范圍:\(3 \le n \le 5 \times {10}^4\)

AC 代碼
#include <cstdio>
#include <algorithm>
#include <vector>
#include <array>
#include <bit>

#define F(i, a, b) for(int i = a; i <= (b); ++i)

void Solve();

int main() {
	int tests = 1;
	scanf("%d", &tests);
	while (tests--)
		Solve();
	return 0;
}

std::vector<std::array<int, 2>> ans;
inline void add(int x, int y, int k) {
	ans.push_back({x * k, y * k});
}
inline void expp(int n, int x, int k) {
	while (n != x)
		add(0, n, k),
		add(n, n, k),
		n *= 2;
}

std::vector<std::array<int, 2>> pans[8] = {
	{},
	{},
	{},
	{{1, 3}, {2, 2}},
	{{1, 3}, {2, 2}},
	{{1, 2}, {1, 3}, {2, 4}, {2, 6}, {4, 4}, {3, 5}, {0, 2}, {2, 2}, {0, 4}, {4, 4}},
	{{1, 2}, {1, 3}, {2, 4}, {2, 6}, {4, 4}, {3, 5}, {2, 6}, {0, 4}, {4, 4}},
	{{1, 2}, {3, 5}, {2, 6}, {4, 4}, {1, 0}, {1, 3}, {1, 7}, {2, 6}, {4, 4}}
};

void Solve(int n, int x, int k) {
	if (n == x) {
		Solve(n - 1, x, k);
		return ;
	}
	if (n <= x / 2) {
		Solve(n, x / 2, k);
		F(i, 1, n - 1)
			expp(x / 2, x, k);
		return ;
	}
	if (n <= 7) {
		for (auto [a, b] : pans[n])
			add(a, b, k);
		return ;
	}
	F(i, x / 2 + 1, n)
		add(x - i, i, k);
	// [1 ~ x-n-1], x/2,  k
	// [1 ~ n-x/2], x/2, 2k
	int n1 = x - n - 1;
	int n2 = n - x / 2;
	if (n1 >= 3) {
		Solve(n1, x / 2, k);
		F(i, 1, n1 - 1)
			expp(x / 2, x, k);
	}
	if (n2 >= 3) Solve(n2, x / 2, 2 * k);
	expp(x / 2, x, k);
	if (n1 <= 2)
		F(i, 1, n1)
			expp(i, x, k);
	if (n2 <= 2)
		F(i, 1, n2)
			expp(i, x / 2, 2 * k);
	if (n1 >= 3 && n2 >= 3)
		add(0, x, k);
}

int n;

void Solve() {
	scanf("%d", &n);
	if (n == 2)
		return puts("-1"), void();
	int x = std::bit_ceil((unsigned)n);
	if (n <= 7) {
		ans = pans[n];
	} else {
		ans.clear();
		Solve(n, x, 1);
	}
	add(0, x, 1);
	printf("%d\n", (int)ans.size());
	for (auto [a, b] : ans)
		printf("%d %d\n", a, b);
}

首先,\(n = 2\) 時無解,這個樣例已經告訴我們了,合理猜測其他情況均有解。

首先寫個暴力跑一下 \(n = 3, 4, 5, 6, 7, 8, 9, \ldots\) 的情況,可以猜測最后需要將 \(n\) 個數全部變為大於等於 \(n\) 的最小的 \(2\) 的次冪。

這個結論的證明也很簡單,考慮任意一個奇素數 \(p\),考慮擁有的整數對 \(p\) 取模后的值,可以發現每次操作不可能將不同時為 \(0\) 的兩個整數變為兩個 \(0\)。於是最終所有數都必須變為 \(2\) 的次冪,接下來的構造顯示了總是可以達到大於等於 \(n\) 的最小的 \(2\) 的次冪。

首先,如果 \(n\) 本身就是 \(2\) 的次冪,將 \(n\) 減去 \(1\),顯然與原問題等價。接下來假設 \(n\) 不是 \(2\) 的次冪,並令 \(x = 2^{\lceil \log_2 n \rceil}\) 為大於等於 \(n\) 的最小的 \(2\) 的次冪。

首先一個很自然的想法是盡量將 \(i\)\(x - i\) 配對,得到 \(x\)\(| 2 i - x |\)。比如:

  • \(\{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 \}\) 變為
    \(\{ 1, 2, 3, {\color{red} {2, 4, 6, 8}}, 8, {\color{blue} {16, 16, 16, 16}} \}\)

於是,已得到的 \(x\) 不需要處理,而剩下的數分為三個部分:

  1. \(1, 2, \ldots, x - n - 1\)
  2. \(2, 4, \ldots, 2 \left( n - \frac{x}{2} \right)\)
  3. 一個落單的 \(\frac{x}{2}\)

其中第 2 部分可以看作 \(1, 2, \ldots, n - \frac{x}{2}\) 全體乘以 \(2\),並且最終需要達到 \(x / 2\)(也被除以了 \(2\))。

\(n\) 足夠大時,第 1, 2 部分均至少有一部分的大小 \(\ge 3\),可以遞歸。對於大小 \(\le 2\) 的部分,需要和落單的 \(\frac{x}{2}\) 一起處理。

在遞歸后,需要將已得到的整數批量操作到 \(x\)(因為可能會得到 \(x / 2^i\)),例如當 \(n = 12\) 的時候,遞歸 \(\{ 1, 2, 3 \}\) 得到 \(\{ 4, 4, 4 \}\) 后就需要將它們操作到 \(16\)(具體操作是簡單的)。

我們對於 \(3 \le n \le 7\) 的部分打表,對於 \(n \ge 9\) 的部分,考慮至少進行了一次遞歸,這意味着最后一步操作必然為選取了 \(0\)\(x\) 得到兩個 \(x\),我們可以撤回最后一步操作,並得到一個 \(0\),有了 \(0\) 就意味着我們可以進行倍增:

  • \((0, a) \to (a, a)\)
  • \((a, a) \to (0, 2 a)\)

於是我們將可能落單的 \(1, 2, 4, \frac{x}{2}\) 都倍增至 \(x\),最后補上操作 \((0, x)\) 即可。

代碼實現中直接將遞歸過程定義為生成刪去最后一步操作的操作序列,方便討論。

時間復雜度為 \(\mathcal O(n \log n)\),操作次數可以控制在 \(\mathcal O(n \log n)\) 級別。

H. Minimize Inversions Number

題意簡述

給定一個 \(1 \sim n\) 的排列 \(p_1, p_2, \ldots, p_n\)

對於每個 \(k = 0, 1, \ldots, n\)

  • 你需要在 \(p_1, p_2, \ldots, p_n\) 中取出一個長度為 \(k\) 的子序列,並將其按原順序移動到 \(p\) 的開頭。
  • 你需要求出在所有選取方案中的逆序對數的最小值。

輸出這 \(n + 1\) 個答案。

有多組數據。

數據范圍:\(1 \le n \le 5 \times {10}^5\)

AC 代碼
#include <cstdio>
#include <algorithm>
#include <functional>

#define F(i, a, b) for(int i = a; i <= (b); ++i)
#define dF(i, a, b) for(int i = a; i >= (b); --i)

void Solve();

int main() {
	int tests = 1;
	scanf("%d", &tests);
	while (tests--)
		Solve();
	return 0;
}

typedef long long ll;
const int MN = 500005;

int n, a[MN], c[MN];
int b[MN];
inline void Add(int i, int x) {
	for (; i <= n; i += i & -i)
		b[i] += x;
}
inline int Qur(int i) {
	int s = 0;
	for (; i; i -= i & -i)
		s += b[i];
	return s;
}

void Solve() {
	scanf("%d", &n);
	F(i, 1, n) scanf("%d", &a[i]);
	ll ans = 0;
	F(i, 1, n) b[i] = 0;
	F(i, 1, n) {
		int d = Qur(a[i]);
		ans += i - 1 - d;
		c[i] = i - 1 - 2 * d;
		Add(a[i], 1);
	}
	F(i, 1, n) b[i] = 0;
	dF(i, n, 1) {
		c[i] -= 2 * Qur(a[i]);
		Add(a[i], 1);
	}
	std::sort(c + 1, c + n + 1, std::greater<>());
	printf("%lld ", ans);
	for (int k = 1; k <= n; ++k) {
		ans -= c[k] + (k - 1);
		printf("%lld%c", ans, " \n"[k == n]);
	}
}

考慮 \(k = 1\) 時的情況,即選取一個數移動到首位。

  • 如果選取了 \(p_i\),逆序對數將減少 \(\displaystyle \sum_{j = 1}^{i - 1} [p_j > p_i] - \sum_{j = 1}^{i - 1} [p_j < p_i]\),即位置 \(i\) 之前的與 \(p_i\) 構成的逆序對數減去順序對數。
  • 將這個數記為 \(d_i\)

顯然,當 \(k = 1\) 時,即是選擇 \(d_i\) 最大的那個移動到首位即可。

接下來考慮當 \(k \ge 2\) 時,逆序對數的變化量。可以看出,如果按從左到右的順序將選取的子序列中的每個元素移到首位,則變化量恰好為子序列的 \(d_i\) 之和。然而如此操作后得到的排列與定義不同:選取的子序列被翻轉了,於是需要將子序列翻轉回來,故原序列的逆序對數將減少「選取的子序列中的順序對數減去逆序對數」。

即,假設選取的子序列下標為 \(\{ i_1, i_2, \ldots, i_k \}\),子序列為 \(q_1, q_2, \ldots, q_k\)\(q_i = p_{i_j}\)),則原序列的逆序對數等於 \(\displaystyle \operatorname{inv}(p_{1 \sim n}) - \left( \sum_{j = 1}^{k} d_{i_j} + \left( \left( \binom{k}{2} - \operatorname{inv}(q_{1 \sim k}) \right) - \operatorname{inv}(q_{1 \sim k}) \right) \right)\),其中 \(\displaystyle \binom{k}{2} - \operatorname{inv}(q_{1 \sim k})\) 一項即為順序對數。

整理一下即得到需要最大化 \(\displaystyle \sum_{j = 1}^{k} d_{i_j} - 2 \operatorname{inv}(q_{1 \sim k})\)

這時我們需要一個關鍵結論:對於每個逆序對 \((i, j)\)(即 \(i < j\)\(p_i > p_j\)),如果 \(i\) 被選取在子序列中,則 \(j\) 也被選取在子序列中一定不更劣。我們使用反證 + 調整法證明此結論:

  • 假設存在逆序對 \((i, j)\)\(p_i\) 被選取而 \(p_j\) 未被選取,我們將證明存在比此種情況不更劣的情況。

  • 由於滿足此條件的逆序對存在,考慮選取其中 \(i, j\) 距離最小的那一對,即 \(j - i\) 最小的且滿足 \(i\) 被選取而 \(j\) 未被選取的逆序對。

  • 由此,在 \(i, j\) 之間不存在:

    • 值在 \(p_j\)\(p_i\) 之間的元素;
    • 值大於 \(p_i\) 且已被選取的元素;
    • 值小於 \(p_j\) 且未被選取的元素。

    否則,將與 \(j - i\) 的最小性矛盾。

  • 考慮將 \(i\) 從子序列中刪去,並加入 \(j\),考察元素 \(p_k\) 對最終逆序對數量的貢獻:

    • 如果 \(k < i\)\(p_k\) 被選取:\(p_k\)\(p_i, p_j\) 的相對位置無變化,貢獻 \(0\)
    • 如果 \(k < i\)\(p_k\) 未被選取:只有 \(p_k\) 介於 \(p_j\)\(p_i\) 之間時,會有貢獻 \(-2\),否則貢獻 \(0\)
    • 如果 \(k > j\)\(p_k\) 未被選取:\(p_k\)\(p_i, p_j\) 的相對位置無變化,貢獻 \(0\)
    • 如果 \(k > j\)\(p_k\) 被選取:只有 \(p_k\) 介於 \(p_j\)\(p_i\) 之間時,會有貢獻 \(-2\),否則貢獻 \(0\)
    • 如果 \(i < k < j\):只有「\(p_k > p_i\)\(p_k\) 被選取」或「\(p_k < p_j\)\(p_k\) 未被選取」時才有貢獻 \(1\),其他情況均貢獻 \(-1\)

    然而前文已說明並不存在這兩類(「\(p_k > p_i\)\(p_k\) 被選取」或「\(p_k < p_j\)\(p_k\) 未被選取」)使得貢獻為正數的情況,再加上 \(i, j\) 本身從逆序對變為順序對的 \(-1\) 貢獻,於是總變化量必然為負數。

  • 因為每次調整后選取的子序列的下標之和必然增加,調整法將在有限步內結束。證畢。

於是,前文需最大化的 \(\displaystyle \sum_{j = 1}^{k} d_{i_j} - 2 \operatorname{inv}(q_{1 \sim k})\) 中的第二項可改為 \(\displaystyle \sum_{j = 1}^{k} \sum_{l = i_j + 1}^{n} [p_l < p_{i_j}]\)

\(\displaystyle c_i = d_i - 2 \sum_{j = i + 1}^{n} [p_j < p_i]\)。即是要最大化 \(\displaystyle \sum_{j = 1}^{k} c_{i_j}\)

\(c\) 從大到小排序后逐個選取即可。

所有 \(c_i\) 的計算和最初逆序對數 \(\operatorname{inv}(a_{1 \sim n})\) 的計算均可在 \(\mathcal O(n \log n)\) 內完成。

時間復雜度為 \(\mathcal O(n \log n)\)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM