1. 程式人生 > 實用技巧 >貪心演算法-例題講解

貪心演算法-例題講解

前言:

此部落格在寫作過程中參考了大量資料和部落格,不能一一列舉,還請見諒。


概述

  • 貪心法:從問題的某一個初始狀態出發,逐步構造最優解從而向目標前進,並期望通過這種方法產生出一個全域性最優解的方法
  • 貪心是一種解題策略,也是一種解題思想,而不是演算法

貪心策略與其他演算法的區別

  • 貪心與遞推:貪心法推進每一步不依據某一固定的遞推式,而是當前看似最佳的貪心決策,不斷的將問題歸納為更加小的相似的子問題
  • 貪心與動態規劃:貪心是“鼠目寸光”;動態規劃是“統攬全域性”

貪心法的優缺點

  • 優點:思維複雜度低、程式碼量小、執行效率高、空間複雜度低等
  • 缺點:很難找到一個簡單可行並且保證正確的貪心思路

貪心演算法的應用

貪心演算法的常用範圍有

  • 明顯的貪心
  • 可證明貪心策略的貪心(最常見的)
  • 貪心資料結構:堆/Kruskal/Prim/Dijkstra
  • 博弈/遊戲策略,這些策略大多是貪心
  • 求較優解或多次逼近求最優解

幾個簡單的貪心例子

  • 最優裝載問題:給n個物體,第i個物體重量為wi,選擇儘量多的物體,使得總重量不超過C
    貪心策略:先拿輕的
  • 部分揹包問題:有n個物體,第i個物體的重量為wi,價值為vi,在總重量不超過C的情況下讓總價值儘量高。每一個物體可以只取走一部分,價值和重量按比例計算
    貪心策略:先拿價效比高的
  • 乘船問題:有n個人,第i個人重量為wi。每艘船的載重量均為C,最多乘兩個人。用最少的船裝載所有人
    貪心策略:最輕的人和最重的人配對

例題(基礎)

1.刪數問題

-【問題描述】鍵盤輸入一個高精度的正整數n(n<=240位),去掉其中任意s個數字後剩下的數字按照原來的次序將組成一個新的正整數。程式設計對給定的n和s,尋求一種方案,使得剩下組成的新數最小。

  • 貪心策略為:每一步總是選擇一個使剩下的數最小的數字刪去,即按高位到低位的順序搜尋,若各位數字遞增,則刪除最後一個數字,否則刪除第一個遞減區間的首字元。然後回到串首,按上述規則再刪除下一個數字。重複以上過程s次,剩下的數字串便是問題的解了。例如對n=178543,s=4,刪數的過程如下:
    n=178543 {刪掉8}
    n=17543 {刪掉7}
    n=1543 {刪掉5}
    n=143 {刪掉4}
    n=13 {解為13}

2.排隊打水問題(進階)

  • 【問題描述】有n個人排隊到r個水龍頭去打水,他們裝滿水桶的時間T1、T2…,Tn為整數且各不相等,應如何安排他們的打水順序才能使他們總共花費的時間最少?
  • 越靠前面的計算的次數越多,顯然越小的排在越前面得出的結果越小
  • 核心程式碼
cin>>n>>t;
   for(i=1;i<=n;i++)cin>>a[i];
   qsort(1,n); //按照打水時間從小到大快排
   j=0;k=0;
   for(i=1;i<=n;i++)//對n個人依次安排
   {  j++;
      if(j==t+1)j=1;//j依次列舉打水龍頭,k為累加總共打水時間
      b[j]+=a[i];  //記錄第j個水龍頭的花費時間
      k+=b[j];   //累加總時間
   }
   cout<<k<<endl;

3.取數遊戲

  • 【問題描述】給出2n(n<=100)個自然數(小於等於30000)。將這n個自然數排成一列,遊戲雙方A和B從中取數,只允許從兩端取數。A先取,然後雙方輪流取數。取完時,誰取得數字總和最大為取勝方;若雙方和相等,屬B勝。試問A方是否有必勝策略?
  • 我們發現一個有趣的事實:A方取走偶位置的數後,剩下兩端數都處於奇位置;反之,若A方取走奇位置的數後,剩下兩端數都處於偶位置。即無論B方如何取法,A方即可以取走奇位置的所有數,亦可以取走偶位置的所有數。由此一種有效貪心策略:若能夠讓A方取走“數和較大的奇(或偶)位置上的所有數”,則A方必勝。

4.打包

  • 【問題描述】某工廠生產出的產品都要被打包放入正四稜柱的盒子內,所有盒子的高度為h,但地面尺寸不同,可以為 1x1、2x2、3x3、4x4、5x5、6x6。如圖:

    這些盒子將被放入高度為h,地面尺寸為6x6的箱子中。為了降低運送成本,工廠希望儘量減少箱子的數量。如果有一個好演算法,能使箱子的數量降到最低,這將給工廠節省不少的資金。請你編寫一個程式。
  • 難點在於細節。A[i]表示i*i的正方形個數。
  • 核心程式碼
int a[7],i,k,tot;
 for(i=1;i<=6;i++)cin>>a[i];  //讀入數量
   tot=a[6]+a[5]+a[4];  //至少得盒子數量
   a[1]=a[1]-11*a[5];a[2]=a[2]-a[4]*5;
   if(a[2]<0){a[1]=a[1]+a[2]*4;a[2]=0;}
   tot=tot+a[3]/4;
   a[3]=a[3]%4;
   if(a[3]==3){tot++;if(a[2]!=0){a[2]--;a[1]-=5;}else a[1]-=9;}
   if(a[3]==2){tot++;k=min(a[2],3);a[2]-=k;a[1]-=18-k*4;}
   if(a[3]==1){tot++;k=min(a[2],5);a[2]-=k;a[1]-=27-k*4;}
   if(a[2]>0){tot=tot+a[2]/9; a[2]=a[2]%9;}
   if(a[2]>0){tot++;a[1]-=36-a[2]*4;}
   if(a[1]>0)tot+=a[1]/36+1;
   cout<<tot<<endl;

5.均分紙牌 題解

  • 【問題描述】有N堆紙牌,編號分別為 1,2,…,N。每堆上有若干張,但紙牌總數必為N的倍數。可以在任一堆上取若於張紙牌,然後移動。
    移牌規則:編號為1堆上取的紙牌,只能移到編號為2的堆上;編號為N的堆上取的紙牌,只能移到編號為N-1的堆上;其他堆上取的紙牌,可以移到相鄰左邊或右邊的堆上。
    現在要求找出一種移動方法,用最少的移動次數使每堆上紙牌數都一樣多。
  • 我們要使移動次數最少,就是要把浪費降至零。通過對具體情況的分析,可以看出在某相鄰的兩堆之間移動兩次或兩次以上,是一種浪費,因為我們可以把它們合併為一次或零次。
    採用“移動一次使得一堆牌數達到平均值”的貪心策略:先把每堆的牌數減去平均數,然後由左而右的順序移動紙牌。若第i堆紙牌的張數a[i]不為0,則將值移動到下一堆

6.【HAOI2008】糖果傳遞 題解

  • 【問題描述】有n個小朋友坐成一圈,每人有ai個糖果。每人只能給左右兩人傳遞糖果。每人每次傳遞一個糖果代價為1。求使所有人獲得均等糖果的最小代價。
  • 和均分紙牌類似,現在假設編號為i的人初始有Ai個糖果。對於1號來說,他給了n號x1個糖果,還剩A1-x1個;但是因為2號給了他x2個糖果,所以最後還剩A1-x1+x2個糖果。根據題設,該金幣數等於M。換句話說,我們得到了一個方程:A1-x1+x2=M。
    同理,對於第2個人,有A2-x2+x3=M。最終,我們可以得到n個方程,一共n個變數,是不是可以直接解方程組了呢?很可惜,還不行。因為從前n-1個方程可以推匯出最後一個方程。所以,實際上只有n-1個方程是有用的。
    儘管無法直接解出答案,我們還是可以嘗試著用x1表示出其他的xi,則本題就變成了單變數的極值問題。
    對於第1個人,A1-x1+x2=M→x2=M-A1+x1=x1-C1(令C1=A1-M,下面類似)
    對於第2個人,A2-x2+x3=M→x3=M-A2+x2=2M-A1-A2+x1=x1-C2(C2=C1+A2-M)
    對於第3個人,A3-x3+x4=M→x4=M-A3+x3=3M-A1-A2-A3+x1=x1-C3
    .....
    對於第1個人,An-xn+x1=M。這是一個多餘的等式,並不能給我們更多的資訊。
    我們希望所有的xi的絕對值之和儘量小,即|x1|+|x1-C1|+|x1-C2|+...+|x1-Cn-1|要最小。注意到|xi-Ci|的集合意思是數軸上點x1到Ci的距離,所以問題變成了:給定數軸上的n個點,找出一個到它們的距離之和儘量小的點。
    結論:給定數軸上的n個點,在數軸上的所有點中,中位數離所有頂點的距離之和最小。凡是能轉化為這個模型的題目都可以用中位數求解。
  • 核心程式碼
const int MaxN=1000005;
long long a[MaxN],C[MaxN],tot,M;
int main()
{  cin>>n;
   tot=0;
   for(i=1;i<=n;i++){cin>>a[i];tot+=a[i];}//讀入資料求出總和
   M=tot/n;
   for(i=1;i<=n;i++)c[i]=c[i-1]+a[i]-M;//遞推求c陣列的字首和
   sort(c+1,c+n+1);//從小到大快排
   long long x1=c[n/2],ans=0;//„計算一維中位數的和
   for(i=1;i<=n;i++)ans+=abs(x1-c[i]);
   cout<<ans<<endl; 
}

7.數列極差問題 題解

  • 【問題描述】佳佳的老師在黑板上寫了一個由n個正整陣列成的數列,要求佳佳進行如下操作:每次擦去其中的兩個數a和b,然後在數列中加入一個數a×b+1,如此下去直至黑板上剩下一個數為止,在所有按這種操作方式最後得到的數中,最大的為max,最小的為min, 則該數列的極差定義為M=max-min。由於佳佳忙於準備期末考試,現請你幫助他,對於給定的數列,計算出相應的極差d。
  • 所以若使第k(1<=k<=N-1)次變換後所得值最大,必使(k-1)次變換後所得值最大(符合貪心策略的特點2),在進行第k次變換時,只需取在進行(K-1)次變換後所得數列中的兩個最小數p,q進行合併操作:p←pq+1,q←∞即可(符合貪心策略特點1),因此此題可用貪心策略求解。討論完畢。在求min時,我們只需在每次變換的數列中找到兩個最大數p,q進行合併操作:p←pq+1,q←-∞即可,原理同上。
    綜上所述,先合併小數得到最大值;先合併大數得到最小值。

8.潛水比賽

  • 【問題描述】在馬其頓王國的ohide湖裡舉行了一次潛水比賽。其中一個專案是從高山上跳下水,再潛水到達終點。這是一個團體專案,一支隊伍由n個人組成。在潛水時必須使用氧氣瓶,但是每支隊伍只有一個氧氣瓶。最多兩個人同時使用一個氧氣瓶,但此時兩人必須同步遊戲,因此兩人達到終點的時間等於較慢的一個人單獨游到終點所需要的時間。好在大家都很友好,因此任何兩個人都願意一起游泳。安排一種潛水的策略,使得最後一名選手儘量早到終點。
  • 方法一N個人:每個人所需的時間:t1,t2,……tn。假設t1最小。每次由t1接送人和氧氣瓶,則總時間:s=t2+t3+...tn+(n-2)*t1
  • 方法二將n個人的時間從小到達排序,假設從小到大為:t1,t2,……tn
    t1和t2過:t2
    t1帶瓶返回:t1
    最大的兩個人:tn tn-1過:tn
    t2帶瓶返回:t2
    把以上看作一趟:把用時最長的兩個人tn,tn-1送過去 用時:2*t2+t1+tn
    重複上述過程:用t1和t2在把tn-2,tn-3送過去,用時2*t2+t1+tn-2,每趟都用t1和t2,每趟運送2人。
  • 方法三每一趟送用時間最長的兩個人時:根據情況選擇:用t1和t2兩個人還是隻用t1一個人。
    用t1和t2送一趟用時:x=2*t2+t1+tn
    用t1一個人送一趟(2人):y=2*t1+tn+tn-1
    每送一趟都要比較x和y的大小:If(x>y)用t1送;else 用t1和t2送
  • 核心程式碼
cin>>n;
for(int i=1;i<=n;i++)cin>>t[i];
sort(t+1,t+n+1,cmp);
f[1]=t[1];f[2]=t[2];
for(int i=3;i<=n;i++) f[i]=min(f[i-1]+t[1]+t[i],f[i-2]+t[1]+t[i]+2*t[2]);
cout<<f[n]<<endl;

後記:

貪心是一種思想,簡單貪心無腦題,複雜貪心難想難證明,和其他東西合在一起。。。蒟蒻只能說一句:多看題,看運氣