给初学者的——贪心算法(上)

引入

首先,我们先来看到一道例题,来引入贪心算法的学习。

最大乘积——例题引入

一个正整数可以表示为若干个互不相同的正整数之和,如:6 = 1 + 5 = 2 + 4 = 1 + 2 + 3 = 6有4种表示方法,这4种表示方法里,拆分出的数字的乘积分别为5 、8、6、6。

给出n,求n的所有表示方案中,拆分出的数字的乘积最大的方案,按升序依次输出拆分出的数字。

输入格式

从标准输入读入数据。

输入一个正整数n(n ≤ 10^6)。

输出格式

输出到标准输出。

按升序输出拆分出的数字。

样例输入

100

样例输出

2 3 5 6 7 8 9 10 11 12 13 14

运用贪心算法解决问题,就需要我们找到一种一以贯之的策略,这种策略就叫做贪心策略,这种策略能保证取得最优。也就是说,贪心算法就是在每一步操作中,都选择局部最优解(当然,你需要保证这样取能得到全局最优解)。

所以说,我们解决这一类问题,目标便是找到贪心策略。一般地,我们需要发现、挖掘性质得出最优解。

首先,非常显然地,如果n ≤ 4,那么直接输出n即可。对于n ≥ 5,首先为了乘积最大,是不可能拆出1来的(非常显然),又要为了乘积最大,所以拆出来的数就必须要多,所以说不难想出,最后的形式一定是2 + 3 + 4 + ... + (m - 1) + m + r,其中r ≤ m + 1。如果r = m + 1,直接输出即可,但是如果r < m + 1,就会出现重复,根据我们小学二年级就学过的“和一定,差小积大”,我们需要把r分配给m - r + 1到m的所有数,每个数分配1。(读者可以把1到10都试一遍,猜出策略)

这就是这道题的贪心策略(其实是非常简单的)。

下面,上代码!!!

AC代码

#include <iostream>
#include <algorithm> //为了用exit()函数 
using namespace std;
int n;
int main() {
	ios::sync_with_stdio(false), cin.tie(NULL);//给程序提提速 
    int tmp = 0, border;//临时变量,边界 
    cin >> n; 
    if (n <= 4) {//特判 
        cout << n;
        exit(0);//game over 
    }
    for (int i = 2;; i++) {
        tmp += i;
        if (tmp > n) {
            border = i - 1;//寻找边界,即什么时候这一串数的连续性被打断,出现重复 
            break;
        }
    }
    int tmp1 = n - ((border + 2) * (border - 1) / 2);
    int border1 = border - tmp1;//多少个数可以正常输出(即形如2 3 ... 2 + border1 - 1) 
    if (border1 == 0) {//特判 
        for (int i = 3; i <= border; i++)
			cout << i << ' ';
        cout << border + 2;
        exit(0);//game over 
    }
    for (int i = 2; i <= border1; i++) 
		cout << i << ' ';
    for (int i = border1 + 1; i <= border; i++)
		cout << i + 1 << ' ';
    return 0;//功德圆满 
}

小结

贪心算法的目标是找到某个“一以贯之的策略”;

从小到大分析,或逐个元素分析,往往会得到有用的结论;

把结论联合起来,能得到最终的策略。


几道有趣的例题

纸牌游戏

有n堆纸牌,编号从1至n,每堆纸牌有若干张,每次操作可以在任一堆上取若干张纸牌,然后移动到相邻的纸牌堆中去。

目标局面有两种,第一种是第i(1 ≤ i ≤ n)堆纸牌恰好有i张纸牌(即纸牌数量依次递增),第二种是第i(1 ≤ i ≤ n)堆纸牌恰好有n + 1 - i张纸牌(即纸牌数量依次递减)。

求将初始局面移动到任意一个目标局面的最小操作次数,如果目标局面不可能达成,输出-1。

从标准输入读入数据。

输入格式

从标准输入读入数据。

第一行输入一个正整数n(n ≤ 1000)。

第二行输入n个非负整数ai(ai ≤ 10^6),表示初始局面下每堆牌的张数。

输出格式

输出到标准输出。

输出一个整数,表示移动到任意一个目标局面的最小操作次数,如果目标局面不可能达成,输出-1。

样例输入

6
1 1 0 8 6 5

样例输出

3
样例解释

初始局面:(1,1,0,8,6,5)

第1次移动后:(1,1,0,8,5,6)

第2次移动后:(1,1,4,4,5,6)

第3次移动后:(1,2,3,4,5,6)

思路

首先,我们先要明确什么时候无法达成目标的两种局面。经过屏幕前的你两秒半的思考后,大概就会发现,当这n个数的和与1 + 2 + ... + n的和不相等时,那么无论怎么操作,都是达成不了的,所以直接输出-1即可。

那么,在去掉这种特殊情况后,我们就要去寻找这道题的贪心策略,而这道题的贪心策略也应该是比较好找的。请屏幕前的你自行思考两分半。。。

很好,相信在你的细心观察与思考下,不到两分半就找到了这道题的正确解法。

在样例解释的提醒下,真正的贪心策略如下:首先看升序情况,从后往前(其实从前往后也行,主要看你自己)依次判断第i个数是否等于i,如果是,就判断下一个数,如果不是,就把这个数变成i(就是加一个数tmp),那么下一个被判断的数就要减去这个数tmp,然后总操作数再加1,以此类推。。。这种情况模拟完之后,再同理模拟降序情况即可。

OK!!!这道题就基本做完了!!!

下面,上代码!!!(这份代码应该是最快的,不信你看)

AC代码

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e6 + 11;
int n, a[N], b[N], ans1, ans2;
int main(){
	ios::sync_with_stdio(false), cin.tie(NULL);//给程序提提速 
	cin >> n;
	int sum = 0;//标记总和
	for(int i = 1; i <= n; i++){
		cin >> a[i]; //读入,作为第一种升序情况
		sum += a[i];//总和加上a[i]
		b[i] = a[i];//额外记录第二种降序情况
	}
	if(n * (n + 1) != sum * 2){//用我们幼儿园就学过的知识判断总和
		cout << "-1" << '\n';
		exit(0);//game over
	}
	for(int i = n; i >= 2; i--){//升序
		if(a[i] != i){
			int ikun = a[i] - i;
			a[i - 1] += ikun;//模拟一次操作
			ans1++; 
		}
	}
	for(int i = n; i >= 2; i--){//降序
		if(b[i] != n + 1 - i){
			int ikun = b[i] - n - 1 + i;
			b[i - 1] += ikun;//模拟一次操作
			ans2++;
		}
	}
	int ans = (ans1 < ans2) ? ans1 : ans2;//得到两种情况的最小值
	cout << ans << '\n';//输出
	return 0;//功德圆满:)
}

上面这道题其实很简单,相信对你没有什么挑战性。

那么我们接着看这道题(相信普通的过河问题对你来说太简单了)

过河问题2.0

河边有一只小船,小船最多能容纳3人。有n个人想要过河,每个人都拥有一个属性:划船时间ti 。

  • 如果船上只有1人,那么只需要1人划船,小船从河的一岸到另一岸所需时间为划船者i的划船时间ti。
  • 如果船上有不少于2人,那么需要2人划船,小船从河的一岸到另一岸所需时间为2个划船者i,j的划船时间的较大值max{ti,tj}。

小船过河后,必须有人将它划回来,以载下一批人过河。不计上下船所需时间,求n人全部过河的最短时间。

输入格式

从标准输入读入数据。

第一行输入一个正整数n(n ≤ 2000)。

第二行输入n个正整数ti(ti ≤ 100000),代表每个人的划船时间。

输出格式

输出到标准输出。

输出一个正整数,代表过河所需最短时间。

样例1输入

5
10 1 4 2 8

样例1输出

7

样例1解释

一种最优的过河策略为:

  • 1号、2号、4号过河,时间为2
  • 2号回来,时间为1
  • 2号、3号、5号过河,时间为4

样例2输入

10
1 2 4 7 8 8 9 9 18 114514

样例2输出

27

样例2解释

一种最优的过河策略为:

  • 1号、2号、10号过河,时间为2
  • 1号、2号回来,时间为2
  • 1号、2号、9号过河,时间为2 
  • 1号、2号回来,时间为2
  • 1号、2号、 8号过河,时间为2 
  • 1号、2号回来,时间为2
  • 1号、2号、 7号过河,时间为2 
  • 1号回来,时间为1
  • 1号、3号、 5号过河,时间为4 
  • 1号回来,时间为1
  • 1号、4号、 6号过河,时间为7 

这道题还是很有难度的(不信你看↓)

(最左侧表示人数,下侧表示大于等于该分数的一共有多少人(366人惨烈牺牲)

但是

相信屏幕前聪明的你一定能做出来!!!

思考片刻,拿起纸和笔,算一算吧!(核善...和善的微笑)

思路

首先,我们需要对所有时间ti按照升序做排序处理(比较显然,不再赘述):t1 ≤ t1 ≤ ... ≤ tn

基本推论
推论一
以下均假设船最初在左岸。
从左岸到右岸的船一定是满载 3 人的(除非左岸不足3人)。这是因为如果只载 1 人,那么还需要人把船划回来,这一次划船就没有意义;如果载 2 人,不如再顺便捎带一个人,因为捎带的这个人不会影响过河时间。
推论二
从右岸到左岸的人数可能是 1 人或 2 人。
如果回来 1 个人,那么肯定是让划船最快的那个人回来;如果回来 2 个人,那么肯定
是让划船最快的两个人回来。这是因为,如果选取了任意划船更慢的人回来,那么他将来还必须得再划回去,不如直接让划船最快的人回来,这样答案肯定不会更差。
推论三
综合以上推论,本题的最优过河模式只有两种。
第一种是 t1 带两个人过河,t1 回来;
第二种是 t1, t2 带一个人过河,t1, t2 回来。
每一轮过河一定是这两个模式其中选一个。
这同时也说明,除了 t 1 , t 2,其余人一旦过河就不能再回来了。
详细分析
直观分析
两种过河模式有什么区别呢?
初步分析一下,如果 t 2 相对于剩下的人划船都比较快,那么采用双人摆渡模式( t 1 , t2带一个人过河,t1, t2回来),虽然摆渡次数会增加,但是单次摆渡的时间会节省很多;
如果 t 2 和剩下的人划船速度差不多,那么就没必要让 t 2 来参与摆渡,而是要选用单人摆渡模式(t1 带两个人过河,t1 回来),因为这时候摆渡次数增加量带来的时间增加已经抵消掉了单次摆渡时间的减少量,所以直接让 t1 单人摆渡就行。
本题的贪心思路基本可以确定了:先让 t 1 t2 双人摆渡,把那些划船最慢的人按顺
序送走,直到某个时候双人摆渡不再具有优势,这时候 t1 单人摆渡把剩下的人送走。
那么要如何确定这个界限呢?我们看下面:
数学分析
考虑 t 1 t 2 t i t j 4 个人中的 t 1 , t j 要过河的情况。
方案一:单人摆渡
1. t 1 , t i , t j 过河,时间为 t i;
2. t 1 回来,时间为 t 1。
总时间为 t1 + ti。
方案二:双人摆渡
1. t 1 , t 2 , t j 过河,时间为 t 2;
2. t 1 , t 2 回来,时间为 t 2。
3. t 1 , t 2 , t i 过河,时间为 t 2;
4. t 1 , t 2 回来,时间为 t 2。
总时间为 4t2。
比较与结论
采取单人摆渡而不采取双人摆渡的条件是:
4t2 > t1 + ti
也就是说,如果满足这个条件,单人摆渡就比双人摆渡要划算。这就是两个方案之间的分界线。
最终的方案是:
t2 到 ti 这 i − 1 个人是最终要参与双人摆渡和 t1 搭档划船的人,他们可以再捎带 i−1 个人。除了这 2i−1 个人以外的其他人,就得靠 t1, t2 双人摆渡先带过河。
当然,如果你满怀欣喜的以为你得到了正确的答案并提交。
你就会得到一个大大大大大大的WA!!!!(喜)
只因(bushi 还有一种特殊情况
特殊情况
  如果 2 i 1 n ,说明全部都要靠 t 1 单人摆渡送过去,如果 n 为奇数则每次过河都是 3 人满载,但 n 为偶数时,如果按这个方案执行会发现有一次过河只有 2 人上船,造成船不满载的效率损失
此时,要分析两种情况:
1. 保持方案不变,每次都是 t 1、左岸剩下的人中最快的一个、左岸剩下的人中最慢的一个过河,最后一次只有 t1 t(n/2+1) 二人过河,总共花费 t 1 + t 2 + · · · + t(n/2+1)。
2. t1、t2 先把 tn 运过河,之后一起回来,人数变为奇数,可以正常的每次都 3 人满载过河。
可以推出:
当 t1 + t(n/2) > 2t2 时采用后一种方案更优 。这是需要特判的一个情况。  
(擦汗        上代码!!!

AC代码

为了锻炼大家自己读代码的能力,作者本人就不写注释了。作者懒,不想写。

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 2011;
int n, t[N], ikun;
int main(){
	ios::sync_with_stdio(false), cin.tie(NULL);
	cin >> n;
	for(int i = 1; i <= n; i++)
		cin >> t[i];
	sort(t + 1, t + 1 + n);
	int border = 0;
	for(int i = n; i >= 1; i--){
		if(4 * t[2] > t[1] + t[i]){
			border = i;
			break;
		}
	}
	if(2 * border - 1 >= n){
		if(n % 2 == 0 && t[1] + t[n / 2 + 1] > 2 * t[2]){
			ikun += 2 * t[2];
			n--;
		}
		for(int i = 2; i <= n / 2 + 1; i++)
			ikun += t[1] + t[i];
		ikun -= t[1];
	} else {
		ikun += 2 * t[2] * (n - (2 * border - 1));
		for(int i = 2; i <= border; i++)
			ikun += t[1] + t[i];
		ikun -= t[1];
	}
	cout << ikun << '\n';
	return 0;
}

我们再来看到一道例题

金银岛

你来到了一座金银岛上,金银岛上有n块金属,重量分别为w1,w2,...,wn,价值分别为v1,v2,...,vn;金银岛上还有1块宝石,重量为a,价值为b。

每块金属可以被任意分割,分割后的价值与分割后的重量成正比关系;宝石不能分割。

你的背包最多能装下重量为y的东西,求背包能装下的最大价值。

输入格式

从标准输入读入数据。

第一行输入两个正整数n(n ≤ 1000)和y(y ≤ 10^6)。

第二行输入n个正整数wi(wi ≤ 1000)。

第三行输入n个正整数vi(vi ≤ 1000)。

第四行输入1个正整数a(a ≤ 10^4)。

第五行输入1个正整数b(b ≤ 10^4)。

输出格式

输出到标准输出。

输出背包能装下的最大价值,四舍五入保留3位小数。保证给出答案与真实答案的误差不超过10^-6。

样例输入

3 14
6 3 4
12 4 6
5
7

样例输出

23.500

样例解释

先选择装宝石(5,7) 、金属(6,12),此时背包空间剩余3,获得的价值为19。最后将金属(4,6)切割为原来的3/4大小,将背包装满,获得的总价值为23.500。

思路

考虑到宝石是一个非常烦人的东西,因为宝石不同于金属,它不能分割,只能整个一起拿走。但是正因为它的这个特点,就只会延伸出拿宝石与不拿宝石两种情况,第一种情况只需要把宝石的重量与价值分别计算即可。

因为题目给出的数据是混乱的,所以非常显然,我们需要排序。我们引入性价比的概念,也就是价值与重量之比,并按照性价比来进行升序排序(应该对屏幕前聪明的各位来说非常easy)。

那么接下来的过程,我们只需要依次模拟把金属装进背包的过程即可。比较这个金属的重量与背包剩余容量谁大谁小,然后依情况讨论即可。

总体来说,这道题的贪心策略比较简单,主要只需注意排序以及对于宝石这个特殊东西的处理就行了。

下面上代码!!!

AC代码

#include <iostream>
#include <algorithm>
#include <iomanip>//为了用fixed与setprecision()函数
using namespace std;
int k, w, s, aa, bb;
struct metal {
    int weight;
    int value;
} a[3005];
bool cmp(metal x, metal y){ 
	return x.value * y.weight > y.value * x.weight; //按照性价比升序排列(交叉相乘得到)
}
int main() {
    ios::sync_with_stdio(false), cin.tie(NULL);
    cin >> s >> w;
    for (int i = 1; i <= s; i++)
        cin >> a[i].weight;
    for (int i = 1; i <= s; i++)
        cin >> a[i].value;
    cin >> aa >> bb;
    int t = w - aa;
    sort(a + 1, a + s + 1, cmp);
    double ans = 0, ans1 = bb;
    for (int i = 1; i <= s; i++) {//不含宝石
        if (w >= a[i].weight) {
            ans += a[i].value;
            w -= a[i].weight;
        } else {
            ans += w * (1.0 * a[i].value / a[i].weight);
            break;
        }
    }
    for (int i = 1; i <= s; i++) {//含宝石(“算两次”)
        if (t >= a[i].weight) {
            ans1 += a[i].value;
            t -= a[i].weight;
        } else {
            ans1 += t * (1.0 * a[i].value / a[i].weight);
            break;
        }
    }
    cout << fixed << setprecision(3) << max(ans, ans1) << endl;//四舍五入保留三位
    return 0;//功德圆满
}

小结

通过上面三道例题,我们可以知道,对于贪心算法来解决的题目,关键点便是贪心策略的寻找,排序的选择,以及一些特殊情况的考虑。在解决这些问题的时候,着重注意以上3种情况,那么你大概率就能收获一个大大的 WA AC!!!


临项交换法

我们先来看一道例题。

堆积木
现在有 𝑁 1 ≤ 𝑛 ≤ 1,000 )块积木,每块积木都有自重 𝑤 𝑖 0 ≤ 𝑤 𝑖 ≤ 1,000)和正常状态下
的承重能力 𝑣 𝑖 0 ≤ 𝑣 𝑖 ≤ 1,000 ),现在要把这 𝑛 块积木垒在一起,但是有可能某块积木的负
重超过了它在正常状态下的承重能力,那么这块积木就有被压坏的危险,请问应该如何堆这
𝑛 块积木使得 𝑛 块积木中最大的压力指数 max{𝑝 1 , 𝑝 2 , … , 𝑝 𝑛 }最小。
这里定义压力指数𝑝 𝑖 为该积木的负重与其在正常状态下的承重能力的差值,即有: 𝑝 𝑖 = 𝑤 1 +
𝑤2 + ⋯ + 𝑤 𝑖−1 − 𝑣 𝑖 (从 1 开始编号)。
输入格式 (作者不想写了)
我们先来分析以下这道题,来引入这个临项交换法(所以这道题没有代码,有兴趣的话屏幕前的你可以自行打代码)。
思路如下:
首先,有两条非常显而易见的“公理”:
  1. 重量大的希望尽量放在下面,这样可以降低对别的积木的影响。
  2. 承重大的也希望尽量放在下面,因为他本身能承受更多的重量。

我们将1,2,3, … 𝑖 − 1, 𝑖, 𝑖 + 1, 𝑖 + 2, … , 𝑛变为1,2,3, 𝑖 − 1, 𝑖 + 1, 𝑖, 𝑖 + 2, … , 𝑛。交换前后,[1. . 𝑖 − 1] 和 [𝑖 + 1. . 𝑛] 这两个区间里面的所有编号的积木的压力指数不变。

也就是说,我们交换𝑖和𝑖 + 1两块积木,只影响交换的这两项,此时我们可以考虑使用临项交换法。

考虑交换前𝑖𝑖 + 1的压力指数:

  • 𝑝𝑖 = 𝑤1 + 𝑤2 + ⋯ + 𝑤𝑖−1 − 𝑣𝑖
  • 𝑝𝑖+1 = 𝑤1 + 𝑤2 + ⋯ + 𝑤𝑖 − 𝑣𝑖+1

再考虑交换后𝑖和𝑖 + 1的压力指数:

  • 𝑞𝑖+1 = 𝑤1 + 𝑤2 + ⋯ + 𝑤𝑖−1 − 𝑣𝑖+1
  • 𝑞𝑖 = 𝑤1 + 𝑤2 + ⋯ + 𝑤𝑖−1 + 𝑤𝑖+1 − 𝑣𝑖

这一坨 𝑐 = 𝑤1 + 𝑤2 + ⋯ + 𝑤𝑖−1 是所有项共有的,那么我们有:

  • 𝑝𝑖 = 𝑐 − 𝑣𝑖 .
  • 𝑝𝑖+1 = 𝑐 + 𝑤𝑖 − 𝑣𝑖+1.
  • 𝑞𝑖+1 = 𝑐 − 𝑣𝑖+1.
  • 𝑞𝑖 = 𝑐 + 𝑤𝑖+1 − 𝑣𝑖

经过分析,当max{𝑞𝑖 , 𝑞𝑖+1} < max{𝑝𝑖 , 𝑝𝑖+1} ,即max{−𝑣𝑖+1, 𝑤𝑖+1 − 𝑣𝑖}< max{−𝑣𝑖 , 𝑤𝑖 − 𝑣𝑖+1}时,交换之后可能变优(仅仅是这两项变优,整体可能不变)。

那么我们便可以得到我们的初始算法:

只要有相邻两项满足这个条件,就一直交换,直到没有相邻两项满足这个条件。

虑如下式子:

max{−𝑣𝑖+1, 𝑤𝑖+1 − 𝑣𝑖} < max{−𝑣𝑖 , 𝑤𝑖 − 𝑣𝑖+1}

考虑等号右侧的最大值,如果是 −𝑣 𝑖 则一定比左边小,所以右侧最大值一定是 𝑤 𝑖 − 𝑣 𝑖+1
再看左侧, −𝑣 𝑖+1 一定小于右边,所以等价于 𝑤 𝑖+1 − 𝑣 𝑖 < 𝑤 𝑖 − 𝑣 𝑖+1
移项得 𝑤 𝑖+1 + 𝑣 𝑖+1 < 𝑤 𝑖 + 𝑣 𝑖。
所以实际上,我们只需要按照 𝑤𝑖 + 𝑣𝑖从小到大排序即可。
经过上面一大堆的分析过程,我们最终得到了只需要按照𝑤𝑖 + 𝑣𝑖从小到大排序即可这样一个结果,而下面的过程就简单了。我们只需要模拟搭积木的过程即可得到最终答案。
而上面那一堆的分析过程,就是临项分析法的常见分析过程。
在需要给出一个排列时,如果相邻两项交换后对其他项的代价没有影响,则可以尝试使用临项交换法。
注意事项:
1. 排序的依据只能和比较的两个元素相关。
2. 能够排出“序”。
不确定时,在得出排序方式后,多尝试一些数据,难以举出反例时,一般来说是对的。
很好,那我们再看最后一道例题:

狂暴的老师

有n个同学(从0开始编号)在学习启发式的信奥课,同学们排队向老师提问。每个同学问的问题不同,因此答疑时长不同,设第i个同学的答疑时长为ti;每个同学的耐心值也不同,设第i个同学的耐心为pi;排在队伍里没有得到答疑的同学会发出吵闹声,第i个同学的吵闹值为ci。

通过细致的观察,你发现了一个重要的事实:对于每个i(0 ≤ i<n),均成立ti ≤ ci。

如果一个同学等待太久,他会暴躁;如果老师答疑时后面的同学太吵闹,他也会暴躁。每个同学的暴躁程度gi等于排在他前面的同学的答疑时长之和、减去自身耐心、再加上排在他后面的同学的吵闹值之和,即:gi = t0 + t1 + ... + ti-1 - pi + ci+1 + ci+2 + ... + cn-1(作者实在是不会用LateX,求谅解) 。

如果同学们很暴躁,老师会狂暴。老师的狂暴程度r等于所有同学暴躁程度gi的最大值,即 r = max{g1, g2, ..., gn-1}。

改变n个同学的排队顺序,老师的狂暴程度可能会发生变化。求所有的排队顺序中,老师的狂暴程度的最小值 min r。

输入格式

从标准输入读入数据。

第一行为一个正整数 n(1 ≤ n ≤ 3,000),表示有 n 位同学。

第二行到第 n+1 行,每行两个整数,分别是 ti(0 ≤ ti ≤ 300)、 pi(0 ≤ pi ≤300)、ci(0 ≤ ci ≤ 300)。

输出格式

输出到标准输出。

输出共一行,表示老师狂暴程度 r 的最小值。

样例输入

5
1 100 100
10 99 101
44 97 103
68 96 102
97 90 111

样例输出

316

子任务

对于20%的数据,n ≤ 5。

另外有20%的数据,ti = ci = 0 对所有 i 均成立。

此外还有20%的数据,ti = pi = 0 对所有 i 均成立。

思路

我们将1,2,3, … 𝑖 − 1, 𝑖, 𝑖 + 1, 𝑖 + 2, … , 𝑛变为1,2,3, 𝑖 − 1, 𝑖 + 1, 𝑖, 𝑖 + 2, … , 𝑛。交换前后,[1. . 𝑖 − 1] 和 [𝑖 + 1. . 𝑛] 这两个区间里面的所有编号的同学的暴躁程度不变。

也就是说,我们交换𝑖和𝑖 + 1两位同学,只影响交换的这两个,此时我们可以考虑使用临项交换法(这道题你可以认为是搭积木,排队接水这一类题的一个变式)。

首先,我们还是需要找到排序的参数。于是,我们又得经过一大堆繁琐的数学推导(其实也没有太繁琐)。仿照上面那一道堆积木的题,我们可以得到过程基本如下:

考虑交换前𝑖𝑖 + 1的暴躁程度:

  • g𝑖 = t0 + t1 + ... + ti-1 - pi + ci+1 + ci+2 + ... + cn-1
  • g𝑖+1 = t0 + t1 + ... + ti-1 + ti - pi+1 + ci+2 + ... + cn-1

再考虑交换后𝑖和𝑖 + 1的暴躁程度:

  • h𝑖+1 = t0 + t1 + ... + ti-1 - pi+1 + ci + ci+2 + ... + cn-1
  • h𝑖 = t0 + t1 + ... + ti-1 + ti+1 - pi  + ci+2 + ... + cn-1

这一坨k = t0 + t1 + ... + ti-1 + ci+2 + ... + cn-1是所有项共有的,那么我们有:

  • g𝑖 = k − pi + ci+1.
  • g𝑖+1 = k + t𝑖 − p𝑖+1.
  • h𝑖+1 = k − p𝑖+1 + ci.
  • h𝑖 = k + t𝑖+1 − p𝑖

接下来,我们考虑  max{h𝑖 , h𝑖+1} < max{g𝑖 , g𝑖+1},即max{ti+1 - pi, -pi+1 + ci} < max{-pi + ci+1, ti - pi+1}(这里取不取等其实是无所谓的,只要这样操作不变差就行)。

我们先来看右边的最大值。如果是ti - pi+1,因为题目中已经说过ti ≤ ci,所以-pi+1 + ci ≥ ti - pi+1,那么左边的最大值只能是ti+1 - pi。那么我们可以推出ti+1 - pi < ti - pi+1,也就是ti+1 + pi+1 < ti + pi。在此基础上,又因为我们假设ti - pi+1是右边的最大值,即ti - pi+1 > -pi + ci+1,且-pi + ci+1 ≥ -pi + ti+1,那么ti - pi+1 > -pi + ti+1,ti + pi > ti+1 + pi+1,与前面矛盾!

所以说,右边的最大值只能是-pi + ci+1,也就是说,-pi + ci+1 > ti - pi+1,如果左边的最大值是ti+1 - pi的话,是显然成立的,它一定小于(等于)左边,那么我们再考虑-pi+1 + ci。于是,我们便得到了-pi+1 + ci < -pi + ci+1,移项得pi + ci < pi+1 + ci+1。

终于,在历经千辛万苦之后,我们得到了这么一串式子。于是乎,我们便可以知道这道题需要我们按照pi + ci升序排序,然后再从0号学生开始依次进行操作。

OK,最主要的部分已经讲完了,相信下面的部分一定难不倒大家。

上代码!!!

AC代码
#include <iostream>
using namespace std;
struct stu{
	int t, p, c;//定义结构体
}ikun[3011];
int n;
void qswap(int x, int y){
	stu akun = ikun[x];
	ikun[x] = ikun[y];
	ikun[y] = akun;
}//交换结构体函数
int main(){
	ios::sync_with_stdio(false), cin.tie(NULL);//给程序提提速
	cin >> n;
	for(int i = 1; i <= n; i++)//作者本人还是习惯从1开始读入
		cin >> ikun[i].t >> ikun[i].p >> ikun[i].c;//读入
	bool flag1 = true;//特判,如果全是0那么跳过排序
	for(int i = 1; i <= n; i++){
		if(ikun[i].t != 0 || ikun[i].c != 0)
			flag1 = false;
	}
	if(flag1)
		goto FLAG;//跳过排序
	for(int i = 1; i <= n; i++)
		for(int j = i + 1; j <= n; j++){
			if(ikun[i].c + ikun[i].p < ikun[j].c + ikun[j].p)//对pi + ci进行升序排序
				qswap(i, j);
		}
	FLAG: int ckun = 0;
	for(int i = 3; i <= n; i++)
		ckun += ikun[i].c;
	int ans = -ikun[1].p + ckun + ikun[2].c, tkun = ikun[1].t;
	for(int i = 2; i <= n; i++){
		ans = max(ans, tkun - ikun[i].p + ckun);
		tkun += ikun[i].t;
		ckun -= ikun[i + 1].c;
	}//操作部分,不再赘述
	cout << ans;
	return 0;//功德圆满
}

总结

那么,以上就是本篇文章的全部内容。每一道例题的方法都不唯一,作者的方法也不一定是最快的,如果读者有兴趣,可以自行探索其它方法!这是作者本人在CSDN博客上的第一篇文章,希望大家能够多多点赞,收藏,支持一下!!!谢谢大家!!!

(作者本人还是萌新,有没有大佬教一教LateX的使用,感激不尽!!!)

完结撒花!!!

特别鸣谢

jx.7fa4.cn:8888

  • 15
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值