• 博客园logo
  • 会员
  • 周边
  • 新闻
  • 博问
  • 闪存
  • 赞助商
  • YouClaw
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录
The blog of EzSun
lyy && ?
博客园    首页    新随笔    联系   管理    订阅  订阅

若干贪心总结

对于贪心题,有一些通用的 \(trick\)。

  1. 分析题目中每个行为的贡献,我们要尽量使每一步的贡献最大/小。
  2. 当直接分析效果不好时,我们可以以小见大。如p11830,先分析每一个数的贡献,再去考虑优化方法。
  3. 有些题目是出自一些比较经典的模型,可以看这里
  4. 有很大一部分题目是关于最佳操作顺序的,我们可以任意找两个元素通过推式子的方式去分析,进而得出排序规则。
  5. 注意有些题目需要分类讨论。

特别的,对于反贪:
反贪主要有两种形式,一种是边做操作边反贪,一种是做完所有操作后再进行退款。实际做题时,要对于两种形式都分析一下,选取更合适的方式去做。

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\) 种单词。在这种方案下,编码以后的最短长度为:

\[1 × 2 + 1 × 2 + 2 × 2 + 2 × 2 = 12 \]

最长字符串 \(s_i\) 的长度为 \(2\) 。

一种非最优方案:令 \(000(2)\) 替换第 \(1\) 种单词,\(001(2)\) 替换第 \(2\) 种单词,\(01(2)\) 替换第 \(3\) 种单词,\(1(2)\) 替换第 \(4\) 种单词。在这种方案下,编码以后的最短长度为:

\[1 × 3 + 1 × 3 + 2 × 2 + 2 × 1 = 12 \]

最长字符串 \(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\) 操作的最晚时间点的最小值。

注:

  1. 机器在一次操作中干掉一个工件;

输入格式

第一行:三个用空格分开的整数:\(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; 
}
posted @ 2026-03-05 20:21  EzSun599  阅读(1)  评论(0)    收藏  举报
刷新页面返回顶部
博客园  ©  2004-2026
浙公网安备 33010602011771号 浙ICP备2021040463号-3