演算法競賽進階指南隨筆:0x00基本演算法-0x07 貪心
0x07 貪心
貪心的證明手段:(主要是說給自己聽的)
1)微擾(臨項交換)
對區域性最優形成的解進行的任何調整都會讓整體結果變壞
通常要結合氣泡排序的知識:任何一個序列都可能通過臨項交換的方法達到有序序列
2)範圍縮放
3)決策包容性
在任何局面下,作出區域性最優決策後,之後可達的集合包含了其他決策之後可達的集合,簡而言之就是“不虧”。以奶牛晒太陽為例,x,y都能拿,在這種情況下我拿y更優,因為拿y未來的可行狀態包含了拿x的所有可行狀態。
4)反證法
5)數學歸納法
例題1:奶牛晒太陽
![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預設是大根堆,小根堆可以用負號實現。
小根堆維護最小值,大根堆維護最大值。
例題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 位大臣排成一排,國王站在隊伍的最前面。排好隊後,所有的大臣都會獲得國王獎賞的若干金幣,每位大臣獲得的金幣數分別是:排在該大臣前面的所有人的左手上的數的乘積除以他自己右手上的數,然後向下取整得到的結果。
國王不希望某一個大臣獲得特別多的獎賞,所以他想請你幫他重新安排一下隊伍的順序,
使得獲得獎賞最多的大臣,所獲獎賞儘可能的少。注意,國王的位置始終在隊伍的最前面。
這裡很重要的一點是“微擾”轉變為整個序列的有序:其實一個序列的排序規則就是由相鄰兩數的偏序關係決定的(且這個偏序關係必須有傳遞性)例如x<y,y<z可以推到x<z,那麼這個序列也就唯一確定了
以這題為例,要求相鄰兩數按照左手*右手小的排在前面,且就 “左手*右手“ 這個規則來說(此時不要求相鄰),是有傳遞性的:L1*R1, L2*R2, L3*R3
附:傳送門:皇后遊戲 https://www.cnblogs.com/Miracevin/p/9694887.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;
}