算法競賽進階指南隨筆: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;
}
相關文章
相關標籤/搜索