若干贪心总结
对于贪心题,有一些通用的 \(trick\)。
- 分析题目中每个行为的贡献,我们要尽量使每一步的贡献最大/小。
- 当直接分析效果不好时,我们可以以小见大。如p11830,先分析每一个数的贡献,再去考虑优化方法。
- 有些题目是出自一些比较经典的模型,可以看这里
- 有很大一部分题目是关于最佳操作顺序的,我们可以任意找两个元素通过推式子的方式去分析,进而得出排序规则。
- 注意有些题目需要分类讨论。
特别的,对于反贪:
反贪主要有两种形式,一种是边做操作边反贪,一种是做完所有操作后再进行退款。实际做题时,要对于两种形式都分析一下,选取更合适的方式去做。
P11830 [省选联考 2025] 幸运数字
题目描述
小 X 有 \(n\) 个正整数二元组 \((a_i, b_i) (1 \leq i \leq n)\)。他将会维护初始为空的可重集 \(S\),并对其进行 \(n\) 轮操作。第 \(i (1 \leq i \leq n)\) 轮操作中,他会在 \(S\) 中加入 \(a_i\) 个 \(b_i\)。
设 \(m = \sum \limits_{i=1}^{n} a_i\),在所有操作结束后,小 X 会得到一个包含 \(m\) 个正整数的可重集 \(S\)。最后他会计算 \(S\) 的中位数,即 \(S\) 中第 \(\left\lfloor \frac{m+1}{2} \right\rfloor\) 小的数,作为他的幸运数字。
想知道小 X 幸运数字的小 Y 不知道这 \(n\) 个二元组的具体数值是多少,但她得知了每个数的范围。具体地,对于每个 \(1 \leq i \leq n\),小 Y 知道 \(a_i \in [l_{i,1}, r_{i,1}]\) 且 \(b_i \in [l_{i,2}, r_{i,2}]\)。
小 Y 想知道在满足以上条件的情况下,有多少个数可能成为小 X 的幸运数字。
输入格式
本题有多组测试数据。输入的第一行两个整数 \(c, T\),分别表示测试点编号和测试数据组数,接下来输入每组测试数据。样例满足 \(c = 0\)。
对于每组测试数据,第一行一个整数 \(n\),表示二元组的个数,接下来 \(n\) 行,第 \(i (1 \leq i \leq n)\) 行四个整数 \(l_{i,1}, r_{i,1}, l_{i,2}, r_{i,2}\),描述二元组每个数的范围。
输出格式
对于每组测试数据,输出一行一个整数,表示可能的幸运数字个数。
输入输出样例 #1
输入 #1
0 4
2
1 2 1 1
1 1 2 2
2
1 1 1 2
1 1 2 3
2
1 2 1 2
2 3 3 4
4
1 2 1 4
3 4 1 2
3 4 2 3
3 4 3 4
输出 #1
1
2
4
3
说明/提示
【样例 1 解释】
该组样例共有 \(4\) 组测试数据。
- 对于第一组测试数据,若取 \((a_1, b_1) = (1, 1), (a_2, b_2) = (1, 2)\),则得到 \(S = \{1, 2\}\),其中位数为 \(1\);若取 \((a_1, b_1) = (2, 1), (a_2, b_2) = (1, 2)\),则得到 \(S = \{1, 1, 2\}\),其中位数为 \(1\)。因此仅有 \(1\) 为可能计算出的中位数,因此答案为 \(1\)。
- 对于第二组测试数据,若取 \((a_1, b_1) = (1, 1), (a_2, b_2) = (1, 2)\),则得到 \(S = \{1, 2\}\),其中位数为 1;若取 \((a_1, b_1) = (1, 2), (a_2, b_2) = (1, 3)\),则得到 \(S = \{2, 3\}\),其中位数为 \(2\)。可以证明不存在其他可能计算出的中位数,因此答案为 \(2\)。
- 对于第三组测试数据,可以证明有且仅有 \(1, 2, 3, 4\) 为可能计算出的中位数,因此答案为 \(4\)。
- 对于第四组测试数据,可以证明有且仅有 \(1, 2, 3\) 为可能计算出的中位数,因此答案为 \(3\)。
【样例 2】
见选手目录下的 lucky/lucky2.in 与 lucky/lucky2.ans。
该组样例共有 \(60\) 组测试数据,所有数据均满足 \(n = 4\)。其中测试数据 \(1 \sim 20\) 满足特殊性质 AB,测试数据 \(21 \sim 40\) 满足特殊性质 A。
【样例 3】
见选手目录下的 lucky/lucky3.in 与 lucky/lucky3.ans。
该组样例共有 \(4\) 组测试数据,所有数据均满足 \(n = 2\,000\)。其中测试数据 \(1\) 满足特殊性质 AB,测试数据 \(2\) 满足特殊性质 A,测试数据 \(3\) 满足特殊性质 B。
【样例 4】
见选手目录下的 lucky/lucky4.in 与 lucky/lucky4.ans。
该组样例共有 \(2\) 组测试数据,所有数据均满足 \(n = 2 \times 10^5\)。其中测试数据 \(1\) 满足特殊性质 A,测试数据 \(2\) 满足特殊性质 B。
【子任务】
设 \(\sum n\) 为单个测试点内所有测试数据的 \(n\) 的和。对于所有测试点,
- \(1 \leq T \leq 400\),
- \(1 \leq n \leq 2 \times 10^5\),\(1 \leq \sum n \leq 6 \times 10^5\),
- \(\forall 1 \leq i \leq n\),\(1 \leq l_{i,1} \leq r_{i,1} \leq 10^9\),\(1 \leq l_{i,2} \leq r_{i,2} \leq 10^9\)。
::cute-table{tuack}
| 测试点编号 | \(n \leq\) | \(\sum n \leq\) | 特殊性质 A | 特殊性质 B |
|---|---|---|---|---|
| \(1\) | \(4\) | \(400\) | 是 | 是 |
| \(2\) | ^ | ^ | ^ | 否 |
| \(3\) | \(2\,000\) | \(10^4\) | ^ | 是 |
| \(4\) | ^ | ^ | ^ | 否 |
| \(5\) | ^ | ^ | 否 | 是 |
| \(6\) | ^ | ^ | ^ | 否 |
| \(7\) | \(2 \times 10^5\) | \(6 \times 10^5\) | 是 | 是 |
| \(8\) | ^ | ^ | ^ | 否 |
| \(9\) | ^ | ^ | 否 | 是 |
| \(10\) | ^ | ^ | ^ | 否 |
- 特殊性质 A:\(\forall 1 \leq i \leq n\),\(r_{i,1}, r_{i,2} \leq n\)。
- 特殊性质 B:\(\forall 1 \leq i \leq n\),\(l_{i,1} = r_{i,1}\)。
题解:
对于每一个数\(w\),
我们考虑这个数如何成为中位数。
设所有数中,有 \(a\) 个数\(<w\),\(b\) 个数 \(=w\),\(c\) 个数 \(>w\)。
很容易发现,我们要使 \(b\) 尽量大。
所以,对于所有\(l_{i,2}<w<r_{i,2}\)的区间,都要选择放 \(r_{i,1}\) 个 \(w\)。
\(b\) 的值就确定下来了。
接下来考虑 \(a\) 和 \(c\)。
受到总个数的限制,我们设 \(a \in [l,r]\),\(c \in [L,R]\)。
不难发现,如果 \(w\) 是中位数,那么 \(c \leq a+b\) 且 \(c+b>a\)。
联立得 \(c \in [a-b+1,a+b]\)
将其与 \(a\) 与 \(c\) 原本的取值范围联立,得\(c \in [\max(L,l-b+1),\min(R,r+b)]\)。
那么只要 \(w\) 是否存在于任何一个区间中且 \(c\) 的取值范围不为空,\(w\) 便可以成为中位数。
\(O(V)\) 枚举 \(w\),\(O(N)\) 处理 \(c\) 的值域,我们便得到了一个 \(O(VN)\) 的算法。
考虑优化,我们考虑每一个区间,不难发现每一个区间都会为 \([l_{i,2},r_{i,2}]\) 的 \(w\) 提供 \(r_{i,1}\) 的贡献,为 \((r_{i,2},\infty)\) 的 \(l\) 提供 \(l_{i,1}\) 的贡献,为 \((r_{i,2},\infty)\) 的 \(r\) 提供 \(r_{i,1}\) 的贡献,为 \([1,l_{i,2})\) 的 \(L\) 提供 \(l_{i,1}\) 的贡献,为 \([1,l_{i,2})\) 的 \(R\) 提供 \(r_{i,1}\) 的贡献。
我们可以将 \(l_{i,2}\) 和 \(r_{i,2}\) 全部离散化存下来,再用差分处理每个区间的贡献,最后用前缀和汇总。
Code:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 2e5 + 7;
int CC, Task;
int n;
ll ls[N << 1], tot;
ll l1[N], r1[N], l2[N], r2[N];
ll b[N << 1], l[N << 1], r[N << 1], L[N << 1], R[N << 1], num[N << 1];
void init(){
for(int i = 1; i <= n; i++){
l1[i] = r1[i] = l2[i] = r2[i] = 0;
}
for(int i = 1; i <= (n << 1); i++){
ls[i] = b[i] = l[i] = r[i] = L[i] = R[i] = num[i] = 0;
}
tot = 0;
}
void Main(){
cin >> n;
init();
for(int i = 1; i <= n; i++){
cin >> l1[i] >> r1[i] >> l2[i] >> r2[i];
ls[++tot] = l2[i];
ls[++tot] = r2[i] + 1;
}
sort(ls + 1, ls + (n << 1) + 1);
int m = unique(ls + 1, ls + (n << 1) + 1) - ls - 1;
for(int i = 1; i <= n; i++){
int x = lower_bound(ls + 1, ls + m + 1, l2[i]) - ls, y = lower_bound(ls + 1, ls + m + 1, r2[i] + 1) - ls;
b[x] += r1[i], b[y] -= r1[i];
l[y] += l1[i], r[y] += r1[i];
L[1] += l1[i], L[x] -= l1[i];
R[1] += r1[i], R[x] -= r1[i];
num[x]++;
num[y]--;
}
ll ans = 0;
for(int i = 1; i < m; i++){
b[i] += b[i - 1];
l[i] += l[i - 1];
r[i] += r[i - 1];
L[i] += L[i - 1];
R[i] += R[i - 1];
num[i] += num[i - 1];
if(num[i] && max(l[i] - b[i] + 1, L[i]) <= min(r[i] + b[i], R[i])){
ans += ls[i + 1] - ls[i];
}
}
printf("%lld\n", ans);
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin >> CC >> Task;
while(Task--){
Main();
}
return 0;
}
P2168 [NOI2015] 荷马史诗
题目背景
追逐影子的人,自己就是影子 —— 荷马
题目描述
Allison 最近迷上了文学。她喜欢在一个慵懒的午后,细细地品上一杯卡布奇诺,静静地阅读她爱不释手的《荷马史诗》。但是由《奥德赛》和《伊利亚特》 组成的鸿篇巨制《荷马史诗》实在是太长了,Allison 想通过一种编码方式使得它变得短一些。
一部《荷马史诗》中有 \(n\) 种不同的单词,从 \(1\) 到 \(n\) 进行编号。其中第 \(i\) 种单词出现的总次数为 \(w_i\)。Allison 想要用 \(k\) 进制串 \(s_i\) 来替换第 \(i\) 种单词,使得其满足如下要求:
对于任意的 \(1\leq i, j\leq n\) ,\(i\ne j\) ,都有:\(s_i\) 不是 \(s_j\) 的前缀。
现在 Allison 想要知道,如何选择 \(s_i\),才能使替换以后得到的新的《荷马史诗》长度最小。在确保总长度最小的情况下,Allison 还想知道最长的 \(s_i\) 的最短长度是多少?
一个字符串被称为 \(k\) 进制字符串,当且仅当它的每个字符是 \(0\) 到 \(k-1\) 之间(包括 \(0\) 和 \(k-1\) )的整数。
字符串 \(str1\) 被称为字符串 \(str2\) 的前缀,当且仅当:存在 \(1 \leq t\leq m\) ,使得 \(str1 = str2[1..t]\)。其中,\(m\) 是字符串 \(str2\) 的长度,\(str2[1..t]\) 表示 \(str2\) 的前 \(t\) 个字符组成的字符串。
输入格式
输入的第 \(1\) 行包含 \(2\) 个正整数 \(n, k\) ,中间用单个空格隔开,表示共有 \(n\) 种单词,需要使用 \(k\) 进制字符串进行替换。
接下来 \(n\) 行,第 \(i + 1\) 行包含 \(1\) 个非负整数 \(w_i\),表示第 \(i\) 种单词的出现次数。
输出格式
输出包括 \(2\) 行。
第 \(1\) 行输出 \(1\) 个整数,为《荷马史诗》经过重新编码以后的最短长度。
第 \(2\) 行输出 \(1\) 个整数,为保证最短总长度的情况下,最长字符串 \(s_i\) 的最短长度。
输入输出样例 #1
输入 #1
4 2
1
1
2
2
输出 #1
12
2
输入输出样例 #2
输入 #2
6 3
1
1
3
3
9
9
输出 #2
36
3
说明/提示
【样例解释】
样例 1 解释
用 \(X(k)\) 表示 \(X\) 是以 \(k\) 进制表示的字符串。
一种最优方案:令 \(00(2)\) 替换第 \(1\) 种单词, \(01(2)\) 替换第 \(2\) 种单词, \(10(2)\) 替换第 \(3\) 种单词,\(11(2)\) 替换第 \(4\) 种单词。在这种方案下,编码以后的最短长度为:
最长字符串 \(s_i\) 的长度为 \(2\) 。
一种非最优方案:令 \(000(2)\) 替换第 \(1\) 种单词,\(001(2)\) 替换第 \(2\) 种单词,\(01(2)\) 替换第 \(3\) 种单词,\(1(2)\) 替换第 \(4\) 种单词。在这种方案下,编码以后的最短长度为:
最长字符串 \(s_i\) 的长度为 \(3\) 。与最优方案相比,文章的长度相同,但是最长字符串的长度更长一些。
样例 2 解释
一种最优方案:令 \(000(3)\) 替换第 \(1\) 种单词,\(001(3)\) 替换第 \(2\) 种单词,\(01(3)\) 替换第 \(3\) 种单词, \(02(3)\) 替换第 \(4\) 种单词, \(1(3)\) 替换第 \(5\) 种单词, \(2(3)\) 替换第 \(6\) 种单词。
【数据规模与约定】
所有测试数据的范围和特点如下表所示(所有数据均满足 \(0 < w_i \leq 10^{11}\)):
::cute-table{tuack}
| 测试点编号 | \(n\) 的规模 | \(k\) 的规模 | 备注 |
|---|---|---|---|
| \(1\) | \(n=3\) | \(k=2\) | |
| \(2\) | \(n=5\) | ^ | ^ |
| \(3\) | \(n=16\) | ^ | 所有 \(w_i\) 均相等 |
| \(4\) | \(n=1\,000\) | ^ | \(w_i\) 在取值范围内均匀随机 |
| \(5\) | ^ | ^ | |
| \(6\) | \(n=100\,000\) | ^ | ^ |
| \(7\) | ^ | ^ | 所有 \(w_i\) 均相等 |
| \(8\) | ^ | ^ | |
| \(9\) | \(n=7\) | \(k=3\) | ^ |
| \(10\) | \(n=16\) | ^ | 所有 \(w_i\) 均相等 |
| \(11\) | \(n=1\,001\) | ^ | ^ |
| \(12\) | \(n=99\,999\) | \(k=4\) | ^ |
| \(13\) | \(n=100\,000\) | ^ | ^ |
| \(14\) | ^ | ^ | ^ |
| \(15\) | \(n=1\,000\) | \(k=5\) | ^ |
| \(16\) | \(n=100\,000\) | \(k=7\) | \(w_i\) 在取值范围内均匀随机 |
| \(17\) | ^ | ^ | |
| \(18\) | ^ | \(k=8\) | \(w_i\) 在取值范围内均匀随机 |
| \(19\) | ^ | \(k=9\) | |
| \(20\) | ^ | ^ | ^ |
【提示】
选手请注意使用 64 位整数进行输入输出、存储和计算。
【评分方式】
对于每个测试点:
- 若输出文件的第 \(1\) 行正确,得到该测试点 \(40\%\) 的分数;
- 若输出文件完全正确,得到该测试点 \(100\%\) 的分数。
题解:
不难发现,这一题其实本质就是构造一棵 \(k\) 叉的哈夫曼树。
按照贪心的思想,构造过程其实就是每次选取权值最小的 \(k\) 个节点,并将其合并在一个子树下。
但这样合并有一个问题,若 \((n-1)mod(k-1)!=0\),最后合并到根节点的时候根节点的儿子会不足 \(k\) 个,导致无法实现最优。
而解决方法也很简单,加入一些权值为0的节点就可以了。
Code:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5 + 7;
const ll inf = 1e18;
int n, k;
ll w[N];
struct node{
ll w, h;
bool operator<(const node &a)const{
if(w != a.w) return w > a.w;
else return h > a.h;
}
};
priority_queue<node>q;
bool cmp1(ll x, ll y){
return x > y;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin >> n >> k;
for(int i = 1; i <= n; i++){
cin >> w[i];
q.push({w[i], 1});
}
while((n - 1) % (k - 1) != 0){
q.push({0, 1});
n++;
}
ll ans = 0;
while(q.size() >= k){
ll sum = 0, hh = -1;
for(int i = 1; i <= k; i++){
sum += q.top().w;
hh = max(hh, q.top().h);
q.pop();
}
ans += sum;
q.push({sum, hh + 1});
}
printf("%lld\n%lld\n", ans, q.top().h - 1);
return 0;
}
P2751 [IOI 1996 / USACO4.2] 工序安排 Job Processing
题目描述
一家工厂的流水线正在生产一种产品,这需要两种操作:操作 \(A\) 和操作 \(B\)。每个操作只有一些机器能够完成。

上图显示了按照下述方式工作的流水线的组织形式。\(A\) 型机器从输入库接受工件,对其施加操作 \(A\),得到的中间产品存放在缓冲库。\(B\) 型机器从缓冲库接受中间产品,对其施加操作 \(B\),得到的最终产品存放在输出库。所有的机器平行并且独立地工作,每个库的容量没有限制。每台机器的工作效率可能不同,一台机器完成一次操作需要一定的时间。
给出每台机器完成一次操作的时间,计算完成 \(A\) 操作的最晚时间点的最小值,和完成 \(B\) 操作的最晚时间点的最小值。
注:
- 机器在一次操作中干掉一个工件;
输入格式
第一行:三个用空格分开的整数:\(N\),工件数量(\(1\leq N\leq1000\));\(M_1\),\(A\) 型机器的数量(\(1\leq M_1\leq30\));\(M_2\),\(B\) 型机器的数量(\(1\leq M_2\leq30\))。
第二行:\(M_1\) 个整数,表示 \(A\) 型机器完成一次操作的时间;接着是 \(M_2\) 个整数,\(B\) 型机器完成一次操作的时间。(所有 \(A\) 型机器与 \(B\) 型机器完成一次操作的时间均为 \([1,20]\) 内的整数)
输出格式
只有一行。输出两个整数:完成所有 \(A\) 操作的最晚时间点的最小值,和完成所有 \(B\) 操作的最晚时间点的最小值(\(A\) 操作必须在 \(B\) 操作之前完成)。
输入输出样例 #1
输入 #1
5 2 3
1 1 3 1 4
输出 #1
3 5
说明/提示
题目翻译来自 NOCOW。
USACO Training Section 4.2
题解:
注意点:是所有零件做完A步骤后才会开始做B步骤。
我们可以先将A步骤和B步骤看作两个独立的子问题处理。
不难发现,若是希望总和的最大值最小,一定是要A的正数第 \(i\) 配上B的倒数第 \(i\) 个。
Code:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e3 + 7, M = 37;
int n, m1, m2;
int a[M], b[M];
int ans1[N], ans2[N];
struct machine{
int h, t;
bool operator>(const machine &a)const{
if(h + t != a.h + a.t) return h + t > a.h + a.t;
else return h > a.h;
}
};
priority_queue<machine, vector<machine>, greater<machine> >q;
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin >> n >> m1 >> m2;
for(int i = 1; i <= m1; i++){
cin >> a[i];
}
for(int i = 1; i <= m2; i++){
cin >> b[i];
}
for(int i = 1; i <= m1; i++){
q.push({a[i], 0});
}
for(int i = 1; i <= n; i++){
machine t = q.top();
q.pop();
q.push({t.h, t.t + t.h});
ans1[i] = t.t + t.h;
}
sort(ans1 + 1, ans1 + n + 1);
printf("%d ", ans1[n]);
while(!q.empty()) q.pop();
for(int i = 1; i <= m2; i++){
q.push({b[i], 0});
}
for(int i = 1; i <= n; i++){
machine t = q.top();
q.pop();
q.push({t.h, t.t + t.h});
ans2[i] = t.t + t.h;
}
int s = 0;
sort(ans2 + 1, ans2 + n + 1);
for(int i = 1; i <= n; i++){
s = max(s, ans1[i] + ans2[n - i + 1]);
}
printf("%d", s);
return 0;
}
P1484 种树
题目描述
cyrcyr 今天在种树,他在一条直线上挖了 \(n\) 个坑。这 \(n\) 个坑都可以种树,但为了保证每一棵树都有充足的养料,cyrcyr 不会在相邻的两个坑中种树。而且由于 cyrcyr 的树种不够,他至多会种 \(k\) 棵树。假设 cyrcyr 有某种神能力,能预知自己在某个坑种树的获利会是多少(可能为负),请你帮助他计算出他的最大获利。
输入格式
第一行,两个正整数 \(n,k\)。
第二行,\(n\) 个整数,第 \(i\) 个数表示在直线上从左往右数第 \(i\) 个坑种树的获利。
输出格式
输出一个数,表示 cyrcyr 种树的最大获利。
输入输出样例 #1
输入 #1
6 3
100 1 -1 100 1 -1
输出 #1
200
说明/提示
对于 \(20\%\) 的数据,\(n\leq 20\)。
对于 \(50\%\) 的数据,\(n\leq 6000\)。
对于 \(100\%\) 的数据,\(1 \le n\leq 300000\),\(1 \le k\leq \dfrac{n}{2}\),在一个地方种树获利的绝对值在 \(10^6\) 以内。
题解:
一道很经典的反悔贪心题目。
若抛开数据范围不谈,我们很容易想到DP做法。
但数据范围很明显不允许我们这么做。
我们可以模拟一下选位置的过程。
不难发现,若\(x\)是序列中的最大值,要是要选择两个点,只有两种选择:选除去 \(x\) 两侧的一个点,或是不选择 \(x\),选择 \(x-1\) 和 \(x+1\)。
按照反悔贪心的套路去做的话,我们可以先选择 \(x\),再在后续的操作中判断是否需要反悔。
我们可以建立大根堆去储存每个点的权值,并根据位置建立一个双向链表。
每次操作时取出堆顶(一定要是未访问的),并将 \(i-1\) 和 \(i+1\) 合并为一个节点,下标设为\(i\),用于反悔,则该节点的权值应为 \(a_{l_i}+a_{r_i}-a_i\)。同时将 \(i-1\) 和 \(i+1\) 设为已访问,原因也很显然,若是需要反悔,应该取合并后的新节点而不是这两个节点。同时更新链表。
Code:
#include<bits/stdc++.h>
using namespace std;
const int N = 3e5 + 7;
typedef long long ll;
int n, k;
ll a[N];
bool vis[N];
struct node{
ll w, id;
bool operator<(const node &a)const{
return w < a.w;
}
};
priority_queue<node>q;
int l[N], r[N];
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin >> n >> k;
for(int i = 1; i <= n; i++){
cin >> a[i];
q.push({a[i], i});
l[i] = i - 1;
r[i] = i + 1;
}
r[0] = 1;
l[n + 1] = n;
ll ans = 0;
for(int i = 1; i <= k; i++){
while(vis[q.top().id]) q.pop();
node t = q.top();
q.pop();
if(t.w < 0) break;
ans += t.w;
int c = t.id;
vis[l[c]] = vis[r[c]] = 1;
a[c] = a[l[c]] + a[r[c]] - a[c];
t.w = a[c];
l[c] = l[l[c]];
r[l[c]] = c;
r[c] = r[r[c]];
l[r[c]] = c;
q.push(t);
}
printf("%lld\n", ans);
return 0;
}
P4597 序列 sequence
题目背景
原题 CF13C 数据加强版
题目描述
给定一个序列,每次操作可以把某个数 \(+1\) 或 \(-1\)。要求把序列变成非降数列。
输入格式
第一行,输入一个整数 \(n\)(\(n \leq 5 \times 10^5\)),表示有 \(n\) 个数字。
第二行输入 \(n\) 个整数,整数的绝对值不超过 \(10^9\)。
输出格式
输出一个数,表示最少的操作次数。
输入输出样例 #1
输入 #1
5
3 2 -1 2 11
输出 #1
4
输入输出样例 #2
输入 #2
5
2 1 1 1 1
输出 #2
1
说明/提示
对于 \(100 \%\) 的数据,\(1 \le n \le 5 \times {10}^5\)。
题解:
思维含量很高的一道题目,
设当前在处理 \(x\),此前的最大值为 \(y\)。
显然,当 \(x \geq y\) 时,不用做任何处理。
当 \(x < y\) 时,不难发现,无论怎么处理,代价一定是 \(y-x\)。
但所带来的影响是不唯一的,会存在若干个二元组 \((x,x),(x+1,x+1),\dots ,(y,y)\)。
数值越小,对后续的处理就越方便。
那么将 \((x,x)\) 作为处理结果似乎为最优。
但不要忘了,其有可能不合法,即在前面的数中存在比 \(x\) 大的数。
换而言之,如果 \(x\) 为当前的次大值,那么 \(x\) 即可作为最大值,即 \(y\) 可以被 \(x\) “同化”。
我们可以弄一个大根堆,从前往后遍历。每次处理时,先将当前节点加入堆。若当前节点比堆顶小,加上堆顶与当前节点的差,把堆顶拿掉,加入当前节点,视作 \(y\) 被 \(x\) 同化了。
Code:
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 5e5 + 7;
int n;
ll a[N];
priority_queue<ll>q;
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin >> n;
for(int i = 1; i <= n; i++){
cin >> a[i];
}
ll ans = 0;
for(int i = 1; i <= n; i++){
q.push(a[i]);
if(q.top() > a[i]){
ans += q.top() - a[i];
q.pop();
q.push(a[i]);
}
}
printf("%lld\n", ans);
return 0;
}
P8170 [eJOI 2021] Waterfront
题目描述
现有 \(N\) 丛初始高度为 \(\textit{height}_i\) 的灌木。每丛灌木每天都会生长 \(\textit{dailyGrowth}_i\) 的高度。
每天在灌木生长完毕后,园丁将对灌木剪枝 \(k\) 次。每次可以将任意一丛高度不小于 \(x\) 的灌木剪短 \(x\) 个单位。
求 \(M\) 天后最高的一丛灌木的高度的最小值。
输入格式
第一行四个正整数 \(N,M,k,x\)。
接下来的 \(N\) 行,每行两个非负整数 \(\textit{height}_i,\textit{dailyGrowth}_i\)。
输出格式
一个非负整数,表示 \(M\) 天后最高的一丛灌木的高度的最小值。
输入输出样例 #1
输入 #1
4 3 4 3
2 5
3 2
0 4
2 8
输出 #1
8
说明/提示
样例解释
| 天数 | 灌木编号 | 高度变化量 |
|---|---|---|
| \(1\) | \(1 \\ 2 \\ 3 \\ 4\) | \(2 \overset{+5}{\to} 7 \overset{-3}{\to} 4 \\ 3 \overset{+2}{\to} 5 \\ 0 \overset{+4}{\to} 4 \\ 2 \overset{+8}{\to} 10 \overset{-3}{\to} 7 \overset{-3}{\to} 4 \overset{-3}{\to} 1\) |
| \(2\) | \(1 \\ 2 \\ 3 \\ 4\) | \(4 \overset{+5}{\to} 9 \overset{-3}{\to} 6 \overset{-3}{\to} 3 \\ 5 \overset{+2}{\to} 7 \\ 4 \overset{+4}{\to} 8 \\ 1 \overset{+8}{\to} 9 \overset{-3}{\to} 6 \overset{-3}{\to} 3\) |
| \(3\) | \(1 \\ 2 \\ 3 \\ 4\) | \(3 \overset{+5}{\to} 8 \\ 7 \overset{+2}{\to} 9 \overset{-3}{\to} 6 \\ 8 \overset{+4}{\to} 12 \overset{-3}{\to} 9 \overset{-3}{\to} 6 \\ 3 \overset{+8}{\to} 11 \overset{-3}{\to} 8\) |
数据规模与约定
本题采用捆绑测试。
- Subtask 1(8 pts):\(1 \le N \le 100\),\(M=k=x=1\),\(\textit{height}_i \ge 1\),\(\textit{dailyGrowth}_i=0\)。
- Subtask 2(22 pts):\(1 \le N,M \le 500\)。
- Subtask 3(43 pts):\(1 \le N,M \le 5000\)。
- Subtask 4(27 pts):\(1 \le N,M \le 10^4\)。
对于 \(100\%\) 的数据,\(1 \le k \le 1000\),\(1 \le x \le 10^4\),\(0 \le \textit{height}_i,\textit{dailyGrowth}_i \le 10^4\)。
说明
本题译自 eJOI2021 Day 2 C Waterfront。
题解:
我们设 \(f_i=h_i+m \cdot d_i\),即一棵树不做修剪所能长到的高度。
很显然,我们应该通过修剪使得最终 \(f_i\) 的最大值最小。
但是在修剪时,树的高度必须不小于 \(x\) 这一点比较难处理。
我们考虑可不可以先将每棵树砍的次数算出来,在后续的操作中再处理顺序。
我们很容易发现可以维护一个大根堆,每次修剪 \(f_i\) 最大的数。
但由于总共有 \(k \times m\) 次修剪,这一步操作的时间复杂度就已达到 \(O(km \log n)\)。
考虑优化,我们若是模拟几个数据,便不难发现我们可以先按 \(f_i\) 降序排序,每次砍伐区间 \([1,r]\) 内的树。若 \(f_r<f_{r+1}\),便将这个区间扩展到 \([1,r+1]\)。
这样我们便得到了每棵树砍伐的次数。
接下来我们需要给每一次砍伐安排日期。
由于在修剪时树的高度必须不小于 \(x\),因此将每一次砍伐尽量往前提肯定是最优的。
对于每棵树,从第一天开始遍历,若是当前高度可行,便一直砍伐直到高度不够,按照先前求出的次数去处理。
这样我们便初步安排了日期
但是由于每天只能砍 \(k\) 次,而我们的处理并没有考虑这一点,因此会存在一些日期砍了超过 \(k\) 次,我们便可以将一些砍伐往后移至后面第一个有空位的日期,这里可以用并查集实现。
Code:
#include<bits/stdc++.h>
using namespace std;
const int N = 1e4 + 7, inf = 2e8;
int n, m, k, x;
int h[N], d[N], f[N];
int idx[N];
int cnt[N];
vector<int>cur, g[N];
int ans[N];
int fa[N], tim[N];
int get(int x){
return (fa[x] == x ? x : fa[x] = get(fa[x]));
}
void merge(int x, int y){
if(get(x)==get(y)) return;
fa[get(x)] = get(y);
}
bool cmp1(int x, int y){
return f[x] > f[y];
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin >> n >> m >> k >> x;
for(int i = 1; i <= n; i++){
cin >> h[i] >> d[i];
f[i] = h[i] + d[i] * m;
idx[i] = i;
}
for(int i = 1; i <= m + 1; i++) fa[i] = i;
sort(idx + 1, idx + n + 1, cmp1);
int r = 1;
while(cur.size() <= k * m){
bool flg = 0;
for(int i = 1; i <= r; i++){
if(f[idx[i]] >= x){
f[idx[i]] -= x;
cnt[idx[i]]++;
cur.emplace_back(idx[i]);
flg = 1;
}
}
if(!flg) break;
while(r < n && f[idx[r]] <= f[idx[r + 1]]){
r++;
for(int i = r; i > 1; i--){
if(f[idx[i]] > f[idx[i - 1]]) swap(idx[i], idx[i - 1]);
}
}
}
for(int i = 1; i <= n; i++){
int hh = h[i];
for(int j = 1; j <= m && g[i].size() < cnt[i]; j++){
hh += d[i];
while(hh >= x && g[i].size() < cnt[i]){
hh -= x;
g[i].emplace_back(j);
}
}
}
for(int i = 1; i <= m; i++) tim[i] = k;
for(int i : cur){
int t = get(g[i][ans[i]]);
if(t > m) break;
tim[t]--;
if(!tim[t]) merge(t, t + 1);
ans[i]++;
}
int c = 0;
for(int i = 1; i <= n; i++){
c = max(c, h[i] + m * d[i] - ans[i] * x);
}
printf("%d\n", c);
return 0;
}
浙公网安备 33010602011771号