算法竞赛进阶指南随笔:0x00基本算法-0x07 贪心

0x07 贪心

贪心的证实手段:(主要是说给本身听的)html

1)微扰(临项交换)node

对局部最优造成的解进行的任何调整都会让总体结果变坏ios

一般要结合冒泡排序的知识:任何一个序列均可能经过临项交换的方法达到有序序列spa

2)范围缩放code

3)决策包容性htm

在任何局面下,做出局部最优决策后,以后可达的集合包含了其余决策以后可达的集合,简而言之就是“不亏”。以奶牛晒太阳为例,x,y都能拿,在这种状况下我拿y更优,由于拿y将来的可行状态包含了拿x的全部可行状态。blog

4)反证法排序

5)数学概括法游戏

例题1:奶牛晒太阳ci

![image-20210807234550748](/Users/josh/Library/Application Support/typora-user-images/image-20210807234550748.png)

能够看到本题简化后就是:给定了一堆区间[Li,Ri],和N个点,求最多的知足要求的区间数目

思路一:按照L进行降序,每次选择当前奶牛能用的最大的SPF防晒霜。正确性证实:若是有两瓶不一样的能够选择的SPF[x]<SPF[y],下一头奶牛只会出现3种状况:

1)x、y均能用

2)x、y均不能用

3)x能用,y不能用

可见当前奶牛选用y是更好的

此外,若是当前奶牛放弃日光浴,这瓶防晒霜给别的另外一头奶牛用,那么对于(除了这两头奶牛外的)其余奶牛来讲,效果是等价的。因此当前防晒霜给这头奶牛用不会让结果更差。

小知识STL:priority_queue默认是大根堆,小根堆能够用负号实现。

img

小根堆维护最小值,大根堆维护最大值。

例题2:雷达安装

![image-20210808003417619](/Users/josh/Library/Application Support/typora-user-images/image-20210808003417619.png)

这里从建筑出发,对于每个建筑来讲,给定了一个监控在数轴上的区间范围。将这些区间按照L排序,每次维护当前监控的在数轴上的最右侧的可能位置pos:

​ 若是Li大于pos,那就新建一个监控并令pos=Ri

​ 不然pos=min(pos,Ri)

一样使用“决策包容性”证实:

​ 对于每一个区间[Li, Ri],有两种选择:1)使用已有的监控;2)新建一个监控

​ 若是选择使用已有的监控,那么将来能够在任意位置新建一个监控;反之若是直接选择新建一个监控,那么这个监控就不能在任意位置。显然前者“不亏”(包含了后者的将来可行状态)

例题3:国王游戏(经典的“微扰法”贪心例题)

恰逢 H 国国庆,国王邀请 n 位大臣来玩一个有奖游戏。首先,他让每一个大臣在左、右手上面分别写下一个整数,国王本身也在左、右手上各写一个整数。而后,让这 n 位大臣排成一排,国王站在队伍的最前面。排好队后,全部的大臣都会得到国王奖赏的若干金币,每位大臣得到的金币数分别是:排在该大臣前面的全部人的左手上的数的乘积除以他本身右手上的数,而后向下取整获得的结果。
国王不但愿某一个大臣得到特别多的奖赏,因此他想请你帮他从新安排一下队伍的顺序,
使得得到奖赏最多的大臣,所获奖赏尽量的少。注意,国王的位置始终在队伍的最前面。

image-20210808005050191

这里很重要的一点是“微扰”转变为整个序列的有序:其实一个序列的排序规则就是由相邻两数的偏序关系决定的(且这个偏序关系必须有传递性)例如x<y,y<z能够推到x<z,那么这个序列也就惟一肯定了

以这题为例,要求相邻两数按照左手*右手小的排在前面,且就 “左手*右手“ 这个规则来讲(此时不要求相邻),是有传递性的:L1*R1, L2*R2, L3*R3

附:传送门:皇后游戏 http://www.javashuo.com/article/p-ttkwgmkg-wu.html

例题4:给树染色

![image-20210808115443847](/Users/josh/Library/Application Support/typora-user-images/image-20210808115443847.png)

错误的贪心:每次选择权值最大的染色。(反例:若是有一个父节点的A[i]很小,它有许多权值巨大的儿子,那么它的儿子们会安排在最后选)

可是能够发现,当前状态下权值最大的节点必定会在它的父节点染色后被马上染色。若是有三个节点x,y,z,x,y是连续染色的,那么就有如下两种状况:

  • x,y,z:x+2y+3z
  • z,x,y:z+2x+3y

要比较这两种状况作差能够获得

\[(1)-(2):-x-y+2z \]

若是(x+y)/2>z,那么选方案一;反之选方案二

也就是至关于两个节点:(x+y)/2 与 z 谁大先选谁

那么咱们就能够将这两个点合并,而且儿子的孩子也归为父亲,3个点就变成了2个点,最后全部的点就会合并为一个点。

如何计算最后的结果呢?

有两种方法:

1)合并后的节点记录它内部点的顺序

2)在每次合并后,ans+=被合并的点的值,被合并的点的父亲值=(值+被合并的点的值)/集合点数。

原理是父亲的值要算1次,而这个点的值要算两次。

#include <iostream>
#include <algorithm>
#include <set>
#include <vector>
using  namespace std;
struct node{
	int c,num,id;
	double p;
	node (int id=0,int c=0,int num=1,double p=0):id(id),c(c),num(num),p(p){}
}a[2010];
bool operator<(node x,node y){return x.p<y.p;}
multiset<node> S;
vector<int> son[2010];
int fa[2010];
void init(int n){
	for (int i=1;i<=n;i++) son[i].clear();
	S.clear();
}
int main(){
	ios::sync_with_stdio(false);
//	freopen("1","r",stdin);
	while (true){
		long long ans=0;
		int n,r;cin>>n>>r;
		if (n==0) break;
		init(n);
		for (int i=1;i<=n;i++){
			cin>>a[i].c;
			a[i].id=i;
			a[i].num=1;
			a[i].p=a[i].c;
			S.insert(a[i]);
		}
		for (int i=1;i<=n-1;i++){
			int father,son1;cin>>father>>son1;
			fa[son1]=father;
			son[father].push_back(son1);
		}
		
		for (int i=1;i<=n-1;i++){
			typedef multiset<node>::iterator it;
			it p=--S.end();
			if (p->id==r) p--;
			node father,current;
			current=*p;
			S.erase(p);//删除儿子
			for (it si=S.begin();si!=S.end();si++){
				if (si->id==fa[p->id]){
					father=*si;
					S.erase(si);//删除父亲
					break;
				}
			}
			for (vector<int>::iterator j=son[father.id].begin();j!=son[father.id].end();j++)
				if (*j==current.id){
					son[father.id].erase(j);
					break;
				}
			for (int j=0;j<son[current.id].size();j++){
				int y=son[current.id][j];
				fa[y]=father.id;//current的儿子的父亲是father
				son[father.id].push_back(y);//current的儿子加入父亲
			}
			
			S.insert(node(father.id,father.c+current.c,father.num+current.num,1.0*(father.c+current.c)/(father.num+current.num)));
			ans+=current.c*father.num;
		}
		cout<<ans+S.begin()->c<<endl;
	}
	return 0;
}
相关文章
相关标签/搜索