贪心做题笔记

\(\color{#52A41A}(1)\) CF1684D Traps

  • \(n\) 个数字 \(a_1 \sim a_n\) 排成一排,你需要从左到右依次越过所有数。

    两种越过 \(i\) 的方式:

    1. 花费 \(a_i\) 的代价越过;
    2. 花费 \(0\) 的代价越过,后面的 \(a_i\) 都加 \(1\)

    现在你拥有最多 \(k\) 次操作二的机会,求最小的代价总和。

  • \(n \le 2 \times 10^5\)

一定会使用 \(k\) 次操作二。否则可以在最后一个使用操作一的位置改用操作二,使答案更优。

假设这 \(k\) 次操作二的地方为 \(i_1, i_2, \dots, i_k\),我们考虑其中一个位置 \(i_j\) 的收益(收益指在 \(i_j\) 位置由操作一改为操作二后答案会变小多少):

  • 本身的代价 \(a_{i_j}\) 变成了 \(0\),收益增加 \(a_{i_j}\)
  • 位置 \(i_j + 1 \sim n\) 中,除了位置 \(i_{j + 1}, i_{j + 2}, \dots, i_{k}\),代价都会加一(因为它们在跳跃时代价都是 \(0\)),收益减少 \(n - i_j - (k - j)\)

综上,总收益为:

\[\sum_{j=1}^k (a_{i_j} - n + i_j + k - j) \]

整理得:

\[-nk + k^2 - \frac{k(k+1)}2 + \sum_{j=1}^k(a_{i_j} + i_j) \]

显然我们希望让收益越大越好,所以我们得目标是最大化这个式子的值。

其中 \(-nk + k^2 - \frac{k(k+1)}2\) 为定值,我们希望最大化 \(\sum_{j=1}^k(a_{i_j} + i_j)\)。所以我们将所有值按照 \(a_i + i\) 排序并取前 \(k\) 大即可。

$\color{blue}\text{Code}$
#include <bits/stdc++.h>

using namespace std;

#define int long long
typedef long long ll;
typedef unsigned long long LL;
typedef pair<int, int> PII;

struct FASTREAD {
	template <typename T>
	FASTREAD& operator >>(T& x) {
		x = 0; bool flg = false; char c = getchar();
		while (c < '0' || c > '9') flg |= (c == '-'), c = getchar();
		while (c >= '0' && c <= '9') x = (x << 3) + (x << 1) + c - '0', c = getchar();
		if (flg) x = -x; return *this;
	}
	template <typename T>
	FASTREAD& operator >>(vector<T>& x) {
		for (auto it = x.begin(); it != x.end(); ++ it ) (*this) >> *it;
		return *this;
	}
}fin;

struct FASTWRITE {
	template <typename T>
	FASTWRITE& operator <<(T x) {
		if (x < 0) x = -x, putchar('-');
		static int buf[35]; int top = 0;
		do buf[top ++ ] = x % 10, x /= 10; while (x);
		while (top) putchar(buf[ -- top] + '0');
		return *this;
	}
	FASTWRITE& operator <<(char x) {
		putchar(x); return *this;
	}
	template <typename T>
	FASTWRITE& operator <<(vector<T> x) {
		for (auto it = x.begin(); it != x.end(); ++ it ) (*this) << *it, putchar(' ');
		putchar('\n');
		return *this;
	}
}fout;

const int N = 1e6 + 10;
const int P = 998244353;

int res;

void Luogu_UID_748509() {
	int n, k; fin >> n >> k;
	int res = 0;
	vector<int> a(n); fin >> a;
	for (int t : a) res += t;
	res -= -n * k + k * k - k * (k - 1) / 2;
	for (int i = 0; i < n; ++ i ) a[i] += i;
	sort(a.begin(), a.end(), greater<int>());
	for (int i = 0; i < k; ++ i ) res -= a[i];
	fout << res << '\n';
	return;
}

signed main() {
	int Testcases = 1;
	fin >> Testcases;
	while (Testcases -- ) Luogu_UID_748509();
	return 0;
}

\(\color{#52A41A}(2)\) CF1029E Tree with Small Distances

  • 给定一颗 \(n\) 个节点的树,以 \(1\) 为根。

    求最少在树中加几条边,使得任意一点到 \(1\) 的距离均小于等于 \(2\)

  • \(n \le 2 \times 10^5\)

不难发现最优策略中,每条加的边都有端点 \(1\)

第一步,最自然的想法就是将 \(1\)最深的叶节点连边。其实不然,最优的策略是连接 \(1\) 和叶子节点的父亲。这样能把这个叶子节点的所有兄弟和它父亲的父亲都管控到。

接下来上一步的点就不需要考虑了。我们要做的仍然是连接 \(1\) 和最深的点的父亲。如此迭代即可。

实现上,我们可以维护大根堆,以节点的深度从大到小排序。每次取出堆顶即可。

$\color{blue}\text{Code}$
#include <bits/stdc++.h>

using namespace std;

//#define int long long
typedef long long ll;
typedef unsigned long long LL;
typedef pair<int, int> PII;

struct FASTREAD {
	template <typename T>
	FASTREAD& operator >>(T& x) {
		x = 0; bool flg = false; char c = getchar();
		while (c < '0' || c > '9') flg |= (c == '-'), c = getchar();
		while (c >= '0' && c <= '9') x = (x << 3) + (x << 1) + c - '0', c = getchar();
		if (flg) x = -x; return *this;
	}
	template <typename T>
	FASTREAD& operator >>(vector<T>& x) {
		for (auto it = x.begin(); it != x.end(); ++ it ) (*this) >> *it;
		return *this;
	}
}fin;

struct FASTWRITE {
	template <typename T>
	FASTWRITE& operator <<(T x) {
		if (x < 0) x = -x, putchar('-');
		static int buf[35]; int top = 0;
		do buf[top ++ ] = x % 10, x /= 10; while (x);
		while (top) putchar(buf[ -- top] + '0');
		return *this;
	}
	FASTWRITE& operator <<(char x) {
		putchar(x); return *this;
	}
	template <typename T>
	FASTWRITE& operator <<(vector<T> x) {
		for (auto it = x.begin(); it != x.end(); ++ it ) (*this) << *it, putchar(' ');
		putchar('\n');
		return *this;
	}
}fout;

const int N = 1e6 + 10;
const int P = 998244353;

int n;
vector<int> g[N];
int dep[N];
bool st[N];
int fa[N];

void dfs(int u, int f) {
	fa[u] = f;
	for (int v : g[u]) if (v != f) {
		dep[v] = dep[u] + 1;
		dfs(v, u);
	}
	return;
}

void Luogu_UID_748509() {
	fin >> n;
	for (int i = 1; i < n; ++ i ) {
		int x, y; fin >> x >> y;
		g[x].push_back(y);
		g[y].push_back(x);
	}
	dep[1] = 1;
	int res = 0;
	dfs(1, -1);
	priority_queue<PII> q; 
	for (int i = 1; i <= n; ++ i ) if (dep[i] > 3) q.push({dep[i], i});
	while (q.size()) {
		int u = q.top().second; q.pop();
		if (st[u]) continue;
		u = fa[u];
		st[u] = true;
		for (int v : g[u]) {
			st[v] = true;
		}
		++ res;
	}
	fout << res;
}

signed main() {
	int Testcases = 1;
//	fin >> Testcases;
	while (Testcases -- ) Luogu_UID_748509();
	return 0;
}

\(\color{#FFC116}(3)\) CF1197C Array Splitting

  • 给出一个长度为 \(n\) 的严格单调增的序列,将其分成 \(k\) 段,使得每一段的极差的和最小,求这个最小的和。
  • \(k, n \le 3 \times 10^5\)

推式子。

若这 \(k\) 段分别为 \([i_1, i_2 - 1], [i_2, i_3 - 1], \dots, [i_k, i_{k + 1} - 1]\),其中 \(i_1 = 1, i_{k + 1} = n + 1\)。那么极差和为:

\[a_{i_2 - 1} - a_{i_1} + a_{i_3 - 1} - a_{i_2} + a_{i_4 - 1} - a_{i_3} + \dots + a_{i_{k + 1} - 1} - a_{i_k} \]

整理一下,把 \(+a_{i_j - 1}\)\(-a_{i_j}\) 放在一起:

\[-a_{i_1} + a_{i_{k+1} - 1} + (a_{i_2 - 1} - a_{i_2}) + (a_{i_3 - 1} - a_{i_3}) + \dots + (a_{i_k - 1} - a_{i_k}) \]

其中 \(-a_{i_1} + a_{i_{k+1} - 1}\)\(a_n - a_1\) 是一定的。我们希望让这个式子的值最小,就意味着我们要最小化 \((a_{i_2 - 1} - a_{i_2}) + (a_{i_3 - 1} - a_{i_3}) + \dots + (a_{i_k - 1} - a_{i_k})\)。因此求 \(d_i = a_{i - 1} - a_i\) 的前 \(k - 1\) 小即可。

$\color{blue}\text{Code}$
#include <bits/stdc++.h>

using namespace std;

//#define int long long
typedef long long ll;
typedef unsigned long long LL;
typedef pair<int, int> PII;

struct FASTREAD {
	template <typename T>
	FASTREAD& operator >>(T& x) {
		x = 0; bool flg = false; char c = getchar();
		while (c < '0' || c > '9') flg |= (c == '-'), c = getchar();
		while (c >= '0' && c <= '9') x = (x << 3) + (x << 1) + c - '0', c = getchar();
		if (flg) x = -x; return *this;
	}
	template <typename T>
	FASTREAD& operator >>(vector<T>& x) {
		for (auto it = x.begin(); it != x.end(); ++ it ) (*this) >> *it;
		return *this;
	}
}fin;

struct FASTWRITE {
	template <typename T>
	FASTWRITE& operator <<(T x) {
		if (x < 0) x = -x, putchar('-');
		static int buf[35]; int top = 0;
		do buf[top ++ ] = x % 10, x /= 10; while (x);
		while (top) putchar(buf[ -- top] + '0');
		return *this;
	}
	FASTWRITE& operator <<(char x) {
		putchar(x); return *this;
	}
	template <typename T>
	FASTWRITE& operator <<(vector<T> x) {
		for (auto it = x.begin(); it != x.end(); ++ it ) (*this) << *it, putchar(' ');
		putchar('\n');
		return *this;
	}
}fout;

const int N = 1e6 + 10;
const int P = 998244353;

int a[N], d[N];

void Luogu_UID_748509() {
	int n, k; fin >> n >> k;
	for (int i = 1; i <= n; ++ i ) {
		fin >> a[i];
		d[i] = a[i - 1] - a[i];
	}
	sort(d + 2, d + n + 1);
	int res = a[n] - a[1];
	for (int i = 2; i <= k; ++ i ) res += d[i];
	fout << res;
}

signed main() {
	int Testcases = 1;
//	fin >> Testcases;
	while (Testcases -- ) Luogu_UID_748509();
	return 0;
}

\(\color{#3498D8}(4)\) CF1038D Slime

  • 给定 \(n\) 个数 \(a_i\)。每次可以选择两个相邻的 \(a_i, a_{i + 1}\) 将其合并为 \(a_i - a_{i + 1}\)\(a_{i + 1} - a_i\)。求 \(n - 1\) 次操作后的数的最大值。
  • \(n \le 5 \times 10^5\)

多手玩几组可以发现,最终的答案一定是对每个 \(a_i\) 乘上 \(\pm 1\) 的系数后求和。因为题目的操作为 \(a_i - a_{i + 1}\)\(a_{i + 1} - a_i\),也就是将相邻两个数分别乘上 \(\pm 1\)

所以我们可以对于每个负数乘 \(-1\) 变成正数,正数乘 \(1\) 保持正数,再求和即为答案。其实就是每个数的绝对值之和。

注意会有一个问题。将每个 \(a_i\)\(\pm 1\) 的过程中,不能做到将所有 \(a_i\) 全部乘相同的系数。所以在所有 \(a_i\) 同号时贪心选择某个数乘另外一个系数即可。

$\color{blue}\text{Code}$
#include <bits/stdc++.h>

using namespace std;

#define int long long
typedef long long ll;
typedef unsigned long long LL;
typedef pair<int, int> PII;

struct FASTREAD {
	template <typename T>
	FASTREAD& operator >>(T& x) {
		x = 0; bool flg = false; char c = getchar();
		while (c < '0' || c > '9') flg |= (c == '-'), c = getchar();
		while (c >= '0' && c <= '9') x = (x << 3) + (x << 1) + c - '0', c = getchar();
		if (flg) x = -x; return *this;
	}
	template <typename T>
	FASTREAD& operator >>(vector<T>& x) {
		for (auto it = x.begin(); it != x.end(); ++ it ) (*this) >> *it;
		return *this;
	}
}fin;

struct FASTWRITE {
	template <typename T>
	FASTWRITE& operator <<(T x) {
		if (x < 0) x = -x, putchar('-');
		static int buf[35]; int top = 0;
		do buf[top ++ ] = x % 10, x /= 10; while (x);
		while (top) putchar(buf[ -- top] + '0');
		return *this;
	}
	FASTWRITE& operator <<(char x) {
		putchar(x); return *this;
	}
	template <typename T>
	FASTWRITE& operator <<(vector<T> x) {
		for (auto it = x.begin(); it != x.end(); ++ it ) (*this) << *it, putchar(' ');
		putchar('\n');
		return *this;
	}
}fout;

const int N = 1e6 + 10;
const int P = 998244353;

void Luogu_UID_748509() {
	int n, res = 0; fin >> n;
	vector<int> a(n); fin >> a;
	if (n == 1) {
		fout << a[0];
		return;
	}
	sort(a.begin(), a.end());
	int mn = a[0], mx = a[n - 1];
	if (mn >= 0) {
		res = -a[0];
		for (int i = 1; i < n; ++ i ) res += a[i];
	}
	else if (mx <= 0) {
		res = a[n - 1];
		for (int i = 0; i + 1 < n; ++ i ) res -= a[i];
	}
	else {
		for (int t : a) res += abs(t);
	}
	fout << res;
	return;
}

signed main() {
	int Testcases = 1;
//	fin >> Testcases;
	while (Testcases -- ) Luogu_UID_748509();
	return 0;
}

\(\color{#FFC116}(5)\) CF804A Find Amir

  • 有一张 \(n\) 个节点的完全图,其中连接 \(i, j\) 两点的边的边权为 \((i + j) \bmod (n + 1)\)。求走完所有城市所需的最小花费(起点任选)。
  • \(n \le 10^5\)

方案是 \(1 \to n \to 2 \to (n - 1) \to 3 \to \dots\),边权分别为 \(0, 1, 0, 1, \dots\)

所以答案为边数的一半,即 \(\left \lfloor \dfrac {n-1}2 \right \rfloor\)

$\color{blue}\text{Code}$
#include <bits/stdc++.h>

using namespace std;

int main() {
    int n; cin >> n;
    cout << (n - 1) / 2;
}

\(\color{#BFBFBF}(6)\)

给定 \(n\) 个区间 \([a_i, b_i]\)。若将所有有交集的区间合并,最后有多少区间。

\(a_i\) 排序。记录当前正在尝试合并的区间的左右端点 \(l, r\)。初始 \(l = a_1, r = b_1\)

枚举 \(i = (2, 3, \dots, b)\)。此时:

  • 如果 \(r \ge a_i\):合并。实现上就是 \(l\) 不变,\(r \gets \max(r, b_i)\)
  • 如果 \(r < a_i\):重开。实现上就是 \(l \gets a_i\)\(r \gets b_i\)

\(\color{#BFBFBF}(7)\)

给定 \(n\) 个区间 \([a_i, b_i]\)

给定 \(m\) 个人,每个人有一个区间 \([c_i, d_i]\)\(k_i\)

如果 \([a_i, b_i]\)\([c_j, d_j]\) 包含,那么第 \(j\) 个人就可以选择区间 \(i\)

每个区间至多被一个人选择,每个人至多选 \(k_i\) 个区间。

求是否所有区间都能被某个人选中。

把区间和人放在一起,并全部以左端点排序。左端点相同时把人排在前面。

然后按顺序枚举 \(i = (1, 2, \dots, n + m)\)。如果第 \(i\) 个是区间,那么我们需要找到一个人 \(j\) 且满足:

  • \(a_j \le c_i\)
  • \(b_j \ge d_i\)

由于我们是按 \(a, c\) 排序的,所以第一个条件即 \(j < i\)。问题也就变成了要在 \(i\) 之前找到一个人 \(j\),且人的右端点在区间的右端点的右面。

此时如果有多个满足条件的人,那么我们应该贪心地找右端点最小的,也就是找 \(b_j\) 最小的。

实现上维护 set。枚举到人时加入 \((d_i, k_i)\)。枚举到区间时找到最小的大于等于 \(b_i\)\(d_j\),并将 \(k_j\) 减一。如果 \(k_j = 0\) 将其移除 set

\(\color{#BFBFBF}(8)\)

给定长度为 \(n\) 的序列 \(a\)

对于一个 \(x\),选择 \(a_i\) 的代价为 \(|a_i - x|\)

给定代价之和的上限 \(B\)。求一个 \(x\),使得选择的 \(a_i\) 最多。

可以发现,如果将 \(a\) 从小到大排序,那么最终选择的 \(a_i\) 一定是连续的。

若确定了区间 \([l, r]\),那么代价之和为 \(f(l, r) = \sum_{i = l}^r |a_i - x|\)。很显然将 \(x\) 确定为这些数的中位数 \(f(l, r)\) 最小。

枚举左端点 \(l\)。此时需要找到最大的 \(r\) 满足 \(f(l, r) \le B\)。此时可以 \(\mathcal O(n \log n)\) 二分做。

同时可以发现随着 \(l\) 变大,\(r\) 一定不会变小。所以可以 \(\mathcal O(n)\) 双指针。

\(\color{#BFBFBF}(9)\)

给定长度为 \(n\) 的序列 \(a\)

至多 \(m\) 次操作,每次可以将一个大于 \(0\)\(a_i\) 减一。

你需要进行若干次操作后,使得存在 \(a_k = 0\),且 \(\max(|a_i - a_{i + 1}|)\) 最小。

求这个最小值。

二分答案 \(x\)。然后求在不一定存在 \(a_k = 0\) 的条件,所有的 \(|a_i - a_{i + 1}|\) 都小于等于 \(x\) 的最小操作次数。

先从左往右调整 \(a_i \gets \min(a_i, a_{i - 1} + x)\)。然后还会有不满足的,于是再从右往左调整 \(a_i \gets \min(a_i, a_{i + 1} + x)\)。记录操作次数。

然后枚举 \(k = (1, 2, \dots, n)\) 表示将 \(a_k \gets 0\)。那么序列会变成这样:

\[a_1, \cdots, l \times x, \cdots, 2x, x, 0, x, 2x ,\cdots, r \times x, \cdots, a_n \]

也就是说在 \(a_k\) 出形成了一个辐射状的等差数列,这个等差数列往左延申到 \(k - l\),往右延申到 \(k + r\)

可以发现随着 \(k\) 的递增,\(k - l\)\(k + r\) 都不会变小。所以还是双指针。

\(\color{#BFBFBF}(10)\)

给定 \(n\) 个区间 \([a_i, b_i]\)

你需要选择 \(m\) 个区间,使得它们至少有一个公共点且区间长度的极差最小。

如果有若干个区间被选中,那么如果其中有 \(m\) 个区间有至少一个公共点,就代表如果将每个区间内的位置加 \(1\) 的话存在一个位置大于等于 \(m\)

我们首先按照区间长度从小到大排序。然后枚举最小长度 \(l\),找到最大长度 \(r\),使得将 \(l \sim r\) 这些区间按照上述操作加一后存在一个位置大于等于 \(m\)。那么此时 \(r - l\) 即为答案。

找这个 \(r\) 可以二分做。同时观察到,随着 \(l\) 变大,\(r\) 一定不会变小。所以 \(\mathcal O(n)\) 双指针。

\(\color{#BFBFBF}(11)\)

给定长度为 \(n\) 的仅含 \(\texttt {ab}\) 的字符串。

你可以改变至多 \(k\) 个字符,求改变后最大的连续相同字符的数量。

二分答案 \(x\)。要做的就是判断是否存在一个长度为 \(x\) 的区间,使得能够在进行不超过 \(k\) 次的情况下,将其变成相同的。于是统计区间内 \(\texttt{ab}\) 字符分别数显的次数,然后与 \(k\) 比大小即可。

也可以双指针。假设我们要全变成 \(\texttt{a}\),那么仍然是维护两个指针 \(l, r\),表示 \(l \sim r\)\(\texttt b\) 的数量不超过 \(k\)。于是找到最大的 \(r\) 然后计算最大长度 \(r - l + 1\) 即可。

\(\color{#BFBFBF}(12)\)

给定长度为 \(n\) 的序列 \(a\)

定义 \(f(l, r)\) 表示区间 \([l, r]\) 内不同元素的数量。

随机选取 \(l, r\),求 \(f(l, r)\) 的期望。

一个区间内相同的元素只会对答案产生一次贡献。不妨将这次贡献放在最左面的元素上。

考虑这个问题:第 \(i\) 个元素上一次出现在 \(j\) 的位置,有多少个区间 \([l, r]\)\(i\) 元素可以贡献的?

显然需要满足两个条件:

  • \(j < l \le i\)
  • \(i \le r \le n\)

那么答案即 \((i - j) \times (n - i + 1)\)

于是统计每个元素在它之前最后一次出现的位置。然后根据上述公式累加答案即可。

posted @ 2024-03-20 13:34  2huk  阅读(12)  评论(0编辑  收藏  举报