反悔貪心

目錄:

  • 我的理解
  • 反悔貪心的分類
    • 反悔自動機
    • 反悔堆
  • 例題簡析及代碼

1、我的理解:

貪心自己是沒有反悔操做的,貪心求的就是當前的最優解。但當前的最優解有多是局部最優解,而不是全局最優解,這時候就要進行反悔操做。php

反悔操做指的是這一步的貪心不是全局最優解,咱們就退回去一步(人工或自動判斷),換一種貪心策略。按照判斷方式的不一樣能夠分爲反悔自動機反悔堆兩種方法。node


2、反悔貪心的分類:

  1. 反悔自動機:c++

    即設計一種反悔策略,使得隨便一種貪心策略均可以獲得正解。算法

    基本的設計思路是:每次選擇直觀上最接近全局最優解的貪心策略,若發現最優解不對,就想辦法自動支持反悔策略。(這就是自動機的意思)數組

    具體題目具體分析。通常須要反悔自動機的題都是經過差值巧妙達到反悔的目的。spa

  2. 反悔堆:設計

    即經過(大根堆、小根堆)來維護當前貪心策略的最優解,若發現最優解不對,就退回上一步,更新最優解。code

    因爲堆的性質,使得堆的首數據必定是最優的,這就能夠實現快速更新最優解blog


3、例題簡析及代碼

  1. USACO09OPEN 工做調度Work Scheduling (反悔堆)排序

    Description:

    \(n\) 項工做,每 \(i\) 項工做有一個截止時間 \(D_i\) ,完成每項工做能夠獲得利潤 \(P_i\) ,求最大能夠獲得多少利潤。

    Method:

    作這道題的時候並無想到反悔貪心,只是想到一個錯誤的貪心算法。按照截止時間爲第一關鍵字,利潤爲第二關鍵字排序,統計一遍便可。

    顯然上面的貪心算法刻印被Hack掉。能夠先不選擇當前截止時間的利潤,等一下選擇下一個更大的利潤,這樣能夠獲得更大的最優解。

    但咱們發現這個貪心策略錯誤的緣由是當前的最優解可能不是全局最優解,顯然符合反悔貪心的思想。因而咱們用一個反悔堆維護最優解。

    假如知足題設條件(即沒有超出截止時間)就分紅兩種狀況:若當前的最優解比原來的最優解(堆頂)更優秀,咱們就更新全局最優解,把原來的最優解丟出去,再把當前的最優解放進去(即反悔策略);反之,就無論了。假如不知足特設條件,就把當前的最優解丟進堆裏,更新全局最優解便可。

    Code:

    #include<bits/stdc++.h>
    #define int long long 
    #define Maxn 100010
    inline void read(int &x)
    {
        int f=1;x=0;char s=getchar();
        while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
        while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
        x*=f;
    }
    using namespace std;
    int n;
    struct node
    {
     int D,P;
     bool operator <(const node &x)const
     {
         return D<x.D;
     }
    }job[Maxn];
    priority_queue<int,vector<int>,greater<int> >qu;
    signed main()
    {
    //   freopen("Job.in","r",stdin);
    //   freopen("Job.out","w",stdout);
     read(n);
     for(int i=1;i<=n;i++)
     {
         read(job[i].D),read(job[i].P);
     }
     sort(job+1,job+n+1);
     int ans=0;
     for(int i=1;i<=n;i++)
     {
         if(qu.size()>=job[i].D)//符合條件
         {
             if(qu.top()<job[i].P)//當前的最優解比原來的最優解(堆頂)更優秀
             {
                 ans-=qu.top();//更新全局最優解
                 qu.pop();//把原來的最優解丟出去
                 qu.push(job[i].P);//把當前的最優解放進去
                 ans+=job[i].P;//更新全局最優解
             }
         }else//不符合條件
         {
             qu.push(job[i].P);//把當前的最優解丟進堆裏
             ans+=job[i].P;//更新全局最優解
         }
     }
     printf("%lld",ans);
     return 0;
    }
  2. CF865D Buy Low Sell High(反悔自動機)

    Description:

    已知接下來 \(n\) 天的股票價格,天天能夠買入當天的股票,賣出已有的股票,或者什麼都不作,求 \(n\) 天以後最大的利潤。

    Method

    咱們能夠快速想出一種貪心策略:買入價格最小的股票,在能夠賺錢的當天賣出。

    顯然咱們能夠發現,上面的貪心策略是錯誤的,由於咱們買入的股票能夠等到能夠賺最多的當天在賣出。

    咱們考慮設計一種反悔策略,使全部的貪心狀況均可以獲得全局最優解。(即設計反悔自動機的反悔策略)

    定義 \(C_{buy}\) 爲全局最優解中買入當天的價格, \(C_{sell}\) 爲全局最優解中賣出當天的價格,則:
    \[ C_{sell}-C_{buy}=\left(C_{sell}-C_i\right)+\left(C_i-C_{buy}\right) \]
    \(C_i\) 爲任意一天的股票價格。

    即咱們買價格最小的股票去賣價格最大的股票,以期獲得最大的利潤。咱們先把當前的價格放入小根堆一次(此次是以上文的貪心策略貪心),判斷當前的價格是否比堆頂大,如果比其大,咱們就將差值計入全局最優解,再將當前的價格放入小根堆(此次是反悔操做)。至關於咱們把當前的股票價格若不是最優解,就沒有用,最後能夠獲得全局最優解。

    上面的等式即被稱爲反悔自動機的反悔策略,由於咱們並無反覆更新全局最優解,而是經過差值消去中間項的方法快速獲得的全局最優解。

    (假如尚未理解這道題,能夠看一看代碼,有詳細的註釋)

    Code:

    #include<bits/stdc++.h>
    #define int long long 
    using namespace std;
    inline void read(int &x)
    {
        int f=1;x=0;char s=getchar();
        while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
        while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
        x*=f;
    }
    priority_queue<int,vector<int>,greater<int> >qu;//開一個小根堆 
    int n;
    int ans=0;//全局最優解 
    signed main()
    {
     read(n);
     ans=0;
     for(int i=1,x;i<=n;i++)
     {
         read(x);//當前的股票價格 
         qu.push(x);//貪心策略:買價格最小的股票去買價格最大的股票 
         if(!qu.empty()&&qu.top()<x)//假如當前的股票價格不是最優解 
         {
             ans+=x-qu.top();//將差值計入全局最優解 
             qu.pop();//將已經統計的最小的股票價格丟出去 
             qu.push(x);//反悔策略:將當前的股票價格再放入堆中,即記錄中間變量(等式中間無用的Ci) 
         }
     }
     printf("%lld\n",ans);//輸出全局最優解 
     return 0;
    }
  3. BZOJ2151 種樹(反悔自動機)

    Description:

    \(n\) 個位置,每一個位置有一個價值。有 \(m\) 個樹苗,將這些樹苗種在這些位置上,相鄰位置不能都種。求能夠獲得的最大值或無解信息。

    Method

    先判斷無解的狀況,咱們顯然能夠發現,若 \(n<\frac{2}{m}\) ,則是不能在合法的條件下種上 \(m\) 棵樹的,故按題意輸出Error!便可。

    假若有解的話,咱們能夠很輕鬆的推出貪心策略:在合法的狀況下選擇最大的價值。

    顯然上面的策略是錯誤的,咱們選擇了最大價值的點,相鄰的兩個點就不能選,而選擇相鄰兩個點獲得的價值可能更大。

    考慮如何設計反悔策略。

    咱們一樣用差值來達到反悔的目的。假設有 \(A\)\(B\)\(C\)\(D\) 四個相鄰的點(如圖)。

    \(A\) 點的價值爲 \(a\) ,其餘點同理。若:
    \[ a+c>b+d \]
    則:
    \[ a+c-b>d \]
    假如咱們先選了 \(B\) 點,咱們就不能選 \(A\)\(C\) 兩點,這顯然是不對的,但咱們能夠新建一個節點 \(P\) , \(P\) 點的價值爲 \(a+c-b\) ,再刪去 \(B\) 點。(如圖,紅色的是刪去的點,橙色的新建的點)

    下一次選擇的點是 \(P\) 的話,說明咱們反悔了(即至關於 \(B\) 點沒有選),能夠保證最後的貪心最優解是全局最優解。

    如何快速插入 \(P\) 點和找出是否選擇 \(P\) 點呢?咱們可使用雙向鏈表和小根堆,使得最終在 \(O(n\log n)\) 的時間複雜度下快速求出全局最優解。

    Code:

    #include<bits/stdc++.h>
    #define int long long 
    #define Maxn 2000010 
    using namespace std;
    inline void read(int &x)
    {
        int f=1;x=0;char s=getchar();
        while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
        while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
        x*=f;
    }
    int n,m;
    int w[Maxn],lft[Maxn],rgh[Maxn];
    struct node
    {
     int val,id;
     bool operator <(const node &n) const 
     {
         return val<n.val;
     }
    };
    priority_queue<node>qu;
    int ind,ans=0;
    int vis[Maxn];
    signed main()
    {
     read(n),read(m);
     ind=n;
     if(n/2<m) 
     {
         puts("Error!");
         return 0;
     }
     for(int i=1;i<=n;i++)
     {
         read(w[i]);
         node tmp;
         tmp.id=i;
         tmp.val=w[i];
         qu.push(tmp);
         if(i==1)
         {
             lft[i]=n;
             rgh[i]=i+1;
         }else if(i==n)
         {
             lft[i]=i-1;
             rgh[i]=1;
         }else
         {
             lft[i]=i-1;
             rgh[i]=i+1;
         }
     }
     for(int i=1;i<=m;i++)
     {
         while(vis[qu.top().id]) qu.pop();
         int id=qu.top().id;
         int val=qu.top().val;
         qu.pop();
         ans+=val;
         ind++;
         vis[lft[id]]=vis[rgh[id]]=1;
         lft[rgh[rgh[id]]]=ind;rgh[lft[lft[id]]]=ind;
         lft[ind]=lft[lft[id]];rgh[ind]=rgh[rgh[id]];
         w[ind]=w[lft[id]]+w[rgh[id]]-val;
         int newid=ind;
         int newval=w[ind];
         node tmp;
         tmp.id=newid;
         tmp.val=newval;
         qu.push(tmp);
     }
     printf("%lld\n",ans);
     return 0;
    }

    Warning:

    • 必定要記錄這個點選沒有選過,假如已經選過了,就從堆中丟出去;
    • 1與 \(n\) 是相鄰的,必定要特判一下;
    • 雙向鏈表必定不要寫掛了;
    • 必定要先將新建的點的價值存入一開始的價值數組,再丟進堆裏;(卡在45卡了很久)
    • index是關鍵字,必定不要使用。(我成功CE了一次)
相關文章
相關標籤/搜索