(Week 6)贪心(C++)

[NOIP2010 普及组] 三国游戏(C++,贪心,博弈论)

题目描述

小涵很喜欢电脑游戏,这些天他正在玩一个叫做《三国》的游戏。

在游戏中,小涵和计算机各执一方,组建各自的军队进行对战。游戏中共有 N N N 位武将( N N N为偶数且不小于 4 4 4),任意两个武将之间有一个“默契值”,表示若此两位武将作为一对组合作战时,该组合的威力有多大。游戏开始前,所有武将都是自由的(称为自由武将,一旦某个自由武将被选中作为某方军队的一员,那么他就不再是自由武将了),换句话说,所谓的自由武将不属于任何一方。

游戏开始,小涵和计算机要从自由武将中挑选武将组成自己的军队,规则如下:小涵先从自由武将中选出一个加入自己的军队,然后计算机也从自由武将中选出一个加入计算机方的军队。接下来一直按照“小涵→计算机→小涵→……”的顺序选择武将,直到所有的武将被双方均分完。然后,程序自动从双方军队中各挑出一对默契值最高的武将组合代表自己的军队进行二对二比武,拥有更高默契值的一对武将组合获胜,表示两军交战,拥有获胜武将组合的一方获胜。

已知计算机一方选择武将的原则是尽量破坏对手下一步将形成的最强组合,它采取的具体策略如下:任何时刻,轮到计算机挑选时,它会尝试将对手军队中的每个武将与当前每个自由武将进行一一配对,找出所有配对中默契值最高的那对武将组合,并将该组合中的自由武将选入自己的军队。 下面举例说明计算机的选将策略,例如,游戏中一共有 6 6 6个武将,他们相互之间的默契值如下表所示:

双方选将过程如下所示:

小涵想知道,如果计算机在一局游戏中始终坚持上面这个策略,那么自己有没有可能必胜?如果有,在所有可能的胜利结局中,自己那对用于比武的武将组合的默契值最大是多少?

假设整个游戏过程中,对战双方任何时候均能看到自由武将队中的武将和对方军队的武将。为了简化问题,保证对于不同的武将组合,其默契值均不相同。

输入格式

共 N 行。

第一行为一个偶数 N N N,表示武将的个数。

第 $2 $行到第 $N 行 里 , 第 行里,第 i+1 行 有 行有 N_i 个 非 负 整 数 , 每 两 个 数 之 间 用 一 个 空 格 隔 开 , 表 示 个非负整数,每两个数之间用一个空格隔开,表示 i 号 武 将 和 号武将和 i+1,i+2,…,N $号武将之间的默契值( 0 ≤ 0≤ 0默契值 ≤ 1 , 000 , 000 , 000 ≤1,000,000,000 1,000,000,000)。

输出格式

1 1 1 或 $2 $行。

若对于给定的游戏输入,存在可以让小涵获胜的选将顺序,则输出$ 1$,并另起一行输出所有获胜的情况中,小涵最终选出的武将组合的最大默契值。如果不存在可以让小涵获胜的选将顺序,则输出 0 0 0

样例 #1

样例输入 #1

6 
5 28 16 29 27 
23 3 20 1 
8 32 26 
33 11 
12

样例输出 #1

1
32

样例 #2

样例输入 #2

8 
42 24 10 29 27 12 58 
31 8 16 26 80 6 
25 3 36 11 5 
33 20 17 13 
15 77 9 
4 50 
19

样例输出 #2

1
77

提示

【数据范围】

对于$ 40%$的数据有 N ≤ 10 N≤10 N10

对于$ 70% 的 数 据 有 的数据有 N≤18$。

对于 100 % 100\% 100%的数据有 N ≤ 500 N≤500 N500

解题思路:

博弈论题目的典型特征:只有两个玩家,游戏规则固定,游戏里面有可变的数据,询问你是否必胜

由此可以看出这是一道博弈论类型的题目

博弈论类型的题目,不要尝试去用代码模拟过程,而是要去分析获胜的条件、先手的影响,接下来对本题进行分析

我们先说出结论,然后对其进行证明

小涵是必胜的,证明如下:

小涵每拿到一名武将,与其最默契的那位就会被计算机取走,所以小涵不可能拿到最优组合

那么小涵能拿到的最大值一定是次优解中的最大值,拿到该值的过程如下

小涵先取走次优解中的一个,计算机会取走与之匹配的最默契的武将,然后小涵就能拿到次优解了

简单的分析一下就能知道,如果存在比次优解更大的默契值,那么一定是最优组合中的一种

但之后计算机无论选择哪一种最优组合,我们都可以去抢夺另一半

至此,证明完毕,以下是代码的实现

#include <iostream>
#include <algorithm>
using namespace std;

int generals[501][501] = { 0 };

int main() {
	int N, value, max_value;
	cin >> N;
	for (int i = 1; i < N; i++)
		for (int j = i + 1; j <= N; j++) {//读入武将数据
			cin >> value;
			generals[i][j] = value;
			generals[j][i] = value;
		}

	for (int i = 1; i <= N; i++)//排序,便于找出每一个武将对应的最优解和次优解
		sort(generals[i] + 1, generals[i] + N + 1);

	max_value = generals[1][N - 1];
	for (int i = 2; i <= N; i++) {
		max_value < generals[i][N - 1] ? max_value = generals[i][N - 1] : max_value = max_value;
	}
	cout << 1 << '\n' << max_value << endl;
	return 0;
}

独木桥(C++,贪心)

(ps:虽说这题是我在贪心类型里面做到的,但我完全没明白到底哪里是贪心)

题目背景

战争已经进入到紧要时间。你是运输小队长,正在率领运输部队向前线运送物资。运输任务像做题一样的无聊。你希望找些刺激,于是命令你的士兵们到前方的一座独木桥上欣赏风景,而你留在桥下欣赏士兵们。士兵们十分愤怒,因为这座独木桥十分狭窄,只能容纳 1 1 1 个人通过。假如有 2 2 2 个人相向而行在桥上相遇,那么他们 2 2 2 个人将无法绕过对方,只能有 1 1 1 个人回头下桥,让另一个人先通过。但是,可以有多个人同时呆在同一个位置。

题目描述

突然,你收到从指挥部发来的信息,敌军的轰炸机正朝着你所在的独木桥飞来!为了安全,你的部队必须撤下独木桥。独木桥的长度为 L L L,士兵们只能呆在坐标为整数的地方。所有士兵的速度都为 1 1 1,但一个士兵某一时刻来到了坐标为 0 0 0 L + 1 L+1 L+1 的位置,他就离开了独木桥。

每个士兵都有一个初始面对的方向,他们会以匀速朝着这个方向行走,中途不会自己改变方向。但是,如果两个士兵面对面相遇,他们无法彼此通过对方,于是就分别转身,继续行走。转身不需要任何的时间。

由于先前的愤怒,你已不能控制你的士兵。甚至,你连每个士兵初始面对的方向都不知道。因此,你想要知道你的部队最少需要多少时间就可能全部撤离独木桥。另外,总部也在安排阻拦敌人的进攻,因此你还需要知道你的部队最多需要多少时间才能全部撤离独木桥。

输入格式

第一行共一个整数 L L L,表示独木桥的长度。桥上的坐标为 1 , 2 , ⋯   , L 1, 2, \cdots, L 1,2,,L

第二行共一个整数 N N N,表示初始时留在桥上的士兵数目。

第三行共有 N N N 个整数,分别表示每个士兵的初始坐标。

输出格式

共一行,输出 2 2 2 个整数,分别表示部队撤离独木桥的最小时间和最大时间。 2 2 2 个整数由一个空格符分开。

样例 #1

样例输入 #1

4
2
1 3

样例输出 #1

2 4

提示

对于 100 % 100\% 100% 的数据,满足初始时,没有两个士兵同在一个坐标, 1 ≤ L ≤ 5 × 1 0 3 1\le L\le5\times 10^3 1L5×103 0 ≤ N ≤ 5 × 1 0 3 0\le N\le5\times10^3 0N5×103,且数据保证 N ≤ L N\le L NL

解题思路:

注意点1:士兵只能位于整数位置,可以有多个士兵位于同一位置,士兵相遇时会改变方向

也就是说,士兵在同一点时不一定改变自己的方向,只有方向不同时才会改变方向

接下来说明几种特殊情况,当士兵间距为1相向而行时,士兵会在1s后回到自己的原位,但是方向相反

当一名士兵遇到多名士兵时,多名士兵会同时反向,由此可见,其实在同一点的若干士兵可以看做一个士兵

注意点2:桥不应该被理解为线段,而应该被理解为点的集合,桥长代表点的数量,0 和 L+1点代表撤离点(区别于端点),相邻两点间距为1

接下来对解题思路进行说明

(1)最短时间:

最短时间 = max{士兵到撤离点距离的绝对值}

求最短时间时,只需要让士兵向着离自己最近的一端前进即可,很容易证明

(2)最长时间:

<1>如果只有一名士兵,最长时间是其到远端撤离点的距离

<2>有多名士兵,为了说明方便,我们把最左端士兵称为left,最右端士兵称为right,设 L 1 = r i g h t − l e f t L_1 = right - left L1=rightleft那么有最长时间 = L 1 L_1 L1 + max{left, L + 1 - right}

接下来进行证明

我们只需要考虑 L 1 L_1 L1范围内的情况,至于为什么emmm,因为来到这个范围以外的士兵行为是单一的,后续处理就是简单的“+ max{left, L + 1 - right}",这点不予证明

如果有两名士兵,显然让他们对撞才是最长时间 L 1 L_1 L1

为了之后便于理解,这里说明一下,会发生对撞的两名士兵,从出发到再次回到起点,所需时间即为二者间距

如果有三名士兵,现在证明最长时间为 L 1 L_1 L1

两侧的士兵一定要向中间走,这是显然的

假设第三名士兵(之后称为middle)在中点右侧,现在讨论他的起始方向

middle若起始向左走,那么一定在中点左侧与left对撞,之后再与right对撞

D 1 = m i d d l e − l e f t D_1 = middle - left D1=middleleft D 2 = r i g h t − m i d d l e D_2 = right - middle D2=rightmiddle

T r i g h t = D 1 = m i d d l e − l e f t < r i g h t − l e f t = L 1 T_{right} = D_1 = middle - left < right - left = L_1 Tright=D1=middleleft<rightleft=L1

T m i d d l e = D 1 2 + D 2 + D 1 2 = D 1 + D 2 = r i g h t − m i d d l e = L 1 T_{middle} = \frac{D_1}{2} + D_2 + \frac{D_1}{2} = D_1 + D_2 = right - middle = L_1 Tmiddle=2D1+D2+2D1=D1+D2=rightmiddle=L1

T l e f t = D 1 2 + D 2 + D 1 2 = D 1 + D 2 = r i g h t − m i d d l e = L 1 T_{left} = \frac{D_1}{2} + D_2 + \frac{D_1}{2} = D_1 + D_2 = right - middle = L_1 Tleft=2D1+D2+2D1=D1+D2=rightmiddle=L1

middle若起始向右走,则在middle与right对撞后,middle会再与left对撞

T r i g h t = D 2 2 + D 1 + D 2 2 = L 1 T_{right} = \frac{D_2}{2} + D_1 + \frac{D_2}{2} = L_1 Tright=2D2+D1+2D2=L1

T m i d d l e = D 2 2 + D 1 + D 2 2 = L 1 T_{middle} = \frac{D_2}{2} + D_1 + \frac{D_2}{2} = L_1 Tmiddle=2D2+D1+2D2=L1

T r i g h t = D 2 < L 1 T_{right} = D_2 < L_1 Tright=D2<L1

综上所述,最长时间为 L 1 L_1 L1

上面的没看懂也没有关系,现在我要说的才是最关键的
两名士兵对撞你可以看作他们穿过了对方
为什么呢?想一下你就明白了,无论是位置还是方向,能不能穿过对方都没有任何区别
是不是豁然开朗了?最长时间还需要证明吗?

实现代码如下:

#include <iostream>
#include <cmath>
using namespace std;

int main() {
	int L, N, soldier, min, max, min_middle;
	double middle;//中点不一定是整数
	cin >> L >> N;
	min = L + 1, middle = double(L + 1) / 2, min_middle = L + 1, max = 0;
	for (int i = 0; i < N; i++) {
		cin >> soldier;
		min > soldier ? min = soldier : min = min;
		max < soldier ? max = soldier : max = max;		
		abs(middle - min_middle) > abs(middle - soldier) ? min_middle = soldier : min_middle = min_middle;
	}
	if (min_middle < L + 1 - min_middle) cout << min_middle << ' ';
	else cout << L + 1 - min_middle << ' ';
	if (min < L + 1 - max) cout << L + 1 - min << endl;
	else cout << max << endl;
	return 0;
}

排队接水(C++,贪心)

题目描述

n n n 个人在一个水龙头前排队接水,假如每个人接水的时间为 T i T_i Ti,请编程找出这 n n n 个人排队的一种顺序,使得 n n n 个人的平均等待时间最小。

输入格式

第一行为一个整数 n n n

第二行 n n n 个整数,第 i i i 个整数 T i T_i Ti 表示第 i i i 个人的等待时间 T i T_i Ti

输出格式

输出文件有两行,第一行为一种平均时间最短的排队顺序;第二行为这种排列方案下的平均等待时间(输出结果精确到小数点后两位)。

样例 #1

样例输入 #1

10 
56 12 1 99 1000 234 33 55 99 812

样例输出 #1

3 2 7 8 1 4 9 6 10 5
291.90

提示

n ≤ 1000 , t i ≤ 1 0 6 n \leq 1000,t_i \leq 10^6 n1000,ti106,不保证 t i t_i ti 不重复。

t i t_i ti 重复时,按照输入顺序即可(sort 是可以的)

解题思路:

emmm很典型的一道题,解题思路就是让T较小的排在前面

因为排在最前面的人,所有人都要等他,对等待时间影响较大

实现代码如下:

#include <iostream>
#include <algorithm>
#include <vector>
#include <iomanip>
using namespace std;

class person {
public:
	int id;
	int t;

	bool operator>(const person p) const {
		return t > p.t;
	}

	bool operator<(const person p) const {
		return t < p.t;
	}
};

vector<person> person_queue;

int main() {
	int n;
	double sum_t = 0.0;
	person temp_p;
	cin >> n;
	for (int i = 1; i <= n; i++) {//读入数据
		cin >> temp_p.t;
		temp_p.id = i;
		person_queue.push_back(temp_p);
	}

	sort(person_queue.begin(), person_queue.end());//排序
	
	int head = 0, tail = int(person_queue.size());
	while (head != tail) {//输出顺序,统计sum_t
		cout << person_queue[head].id;
		if (head != tail - 1) putchar(' ');
		sum_t += (tail - head - 1) * person_queue[head].t;
		head++;
	}
	cout << '\n' << setiosflags(ios::fixed) << setprecision(2) << sum_t / n;//输出平均时间
	return 0;
}

这里解释一下为什么不用priority_queue而用vector + sort(),因为前者不能“当 t i t_i ti重复时,按照输入顺序输出”

[NOIP2004 提高组] 合并果子 / [USACO06NOV] Fence Repair G(C++,贪心,哈夫曼树)

题目描述

在一个果园里,多多已经将所有的果子打了下来,而且按果子的不同种类分成了不同的堆。多多决定把所有的果子合成一堆。

每一次合并,多多可以把两堆果子合并到一起,消耗的体力等于两堆果子的重量之和。可以看出,所有的果子经过 n − 1 n-1 n1 次合并之后, 就只剩下一堆了。多多在合并果子时总共消耗的体力等于每次合并所耗体力之和。

因为还要花大力气把这些果子搬回家,所以多多在合并果子时要尽可能地节省体力。假定每个果子重量都为 1 1 1 ,并且已知果子的种类 数和每种果子的数目,你的任务是设计出合并的次序方案,使多多耗费的体力最少,并输出这个最小的体力耗费值。

例如有 3 3 3 种果子,数目依次为 1 1 1 2 2 2 9 9 9 。可以先将 1 1 1 2 2 2 堆合并,新堆数目为 3 3 3 ,耗费体力为 3 3 3 。接着,将新堆与原先的第三堆合并,又得到新的堆,数目为 12 12 12 ,耗费体力为 12 12 12 。所以多多总共耗费体力 = 3 + 12 = 15 =3+12=15 =3+12=15 。可以证明 15 15 15 为最小的体力耗费值。

输入格式

共两行。
第一行是一个整数 n ( 1 ≤ n ≤ 10000 ) n(1\leq n\leq 10000) n(1n10000) ,表示果子的种类数。

第二行包含 n n n 个整数,用空格分隔,第 i i i 个整数 a i ( 1 ≤ a i ≤ 20000 ) a_i(1\leq a_i\leq 20000) ai(1ai20000) 是第 i i i 种果子的数目。

输出格式

一个整数,也就是最小的体力耗费值。输入数据保证这个值小于 2 31 2^{31} 231

样例 #1

样例输入 #1

3 
1 2 9

样例输出 #1

15

提示

对于 30 % 30\% 30% 的数据,保证有 n ≤ 1000 n \le 1000 n1000

对于 50 % 50\% 50% 的数据,保证有 n ≤ 5000 n \le 5000 n5000

对于全部的数据,保证有 n ≤ 10000 n \le 10000 n10000

解题思路:

采用贪心算法,每次都合并尽量小的两堆水果最节省体力,证明如下

先进行一下引入,不将合并后的两堆看成一堆水果,而是看作可以一次性移动两堆水果了

这样之后,我们以样例来说明,为了说明方便,分别称重量为1、2、9的水果堆为1、2、3

<1>合并1和2,再合并1、2和3,结果就是1、2均被移动了2次,3被移动1次

<2>合并1和3,再合并1、3和2,结果就是1、3均被移动了2次,2被移动1次

<3>合并2和3,再合并2、3和1,结果就是2、3均被移动了2次,1被移动1次

所以,节省体力的本质就是使越重的水果堆被移动次数越少,越轻的水果堆被移动的次数越多

更进一步,对于n堆水果,我们分配给每堆水果一个节点,然后每次取value最小的两个节点合并生成一个新的节点,这样,我们就构建了一棵树

现在这棵树上的n个叶子节点就是我们最初分配给n堆水果的n个节点

则我们消耗的体力为 ∑ i = 1 n a i ∗ d e p t h i \sum_{i=1}^{n}{a_i*{depth}_i} i=1naidepthi(其中 a i a_i ai代表第i个节点,也就是第i堆水果的重量, d e p t h i depth_i depthi代表其深度,也就是被移动的次数)

那么现在我们如何证明这就能实现“越重的水果堆被移动次数越少,越轻的水果堆被移动次数越多”呢?

直接证明是不太好证明的,但我们可以借助哈夫曼编码来证明(不了解哈夫曼编码的读者可以百度一下,这是一种压缩方法),我们构建的树其实就是哈夫曼树

实现代码如下

#include <iostream>
#include <queue>
#include <vector>
using namespace std;

priority_queue<int, vector<int>, greater<int>> weight_queue;

int main() {
	int n, temp_w, sum = 0, combine;
	cin >> n;
	for (int i = 1; i <= n; i++) {
		cin >> temp_w;
		weight_queue.push(temp_w);
	}

	while (int(weight_queue.size()) != 1) {
		combine = weight_queue.top();
		weight_queue.pop();
		combine += weight_queue.top();
		weight_queue.pop();
		weight_queue.push(combine);//合并
		sum += combine;//累计
	}
	cout << sum;
	return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

WitheredSakura_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值