贪心做题笔记
\(\color{#52A41A}(1)\) CF1684D Traps
有 \(n\) 个数字 \(a_1 \sim a_n\) 排成一排,你需要从左到右依次越过所有数。
两种越过 \(i\) 的方式:
- 花费 \(a_i\) 的代价越过;
- 花费 \(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)\)。
综上,总收益为:
整理得:
显然我们希望让收益越大越好,所以我们得目标是最大化这个式子的值。
其中 \(-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_j - 1}\) 和 \(-a_{i_j}\) 放在一起:
其中 \(-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_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)\)。
于是统计每个元素在它之前最后一次出现的位置。然后根据上述公式累加答案即可。