Jeanny
寂兮,寥兮,独立不改,周行而不殆

目录:

  • 个人理解
  • 反悔贪心的分类
    • 反悔自动机
    • 反悔堆
  • 例题简析及代码

一、个人理解:

贪心本身是没有反悔操作的,贪心求的就是当前的最优解。但当前的最优解有可能是局部最优解,而不是全局最优解,这时候就要进行反悔操作。

反悔操作指的是这一步的贪心不是全局最优解,我们就退回去一步(人工或自动判断),换一种贪心策略。按照判断方式的不同可以分为反悔自动机反悔堆两种方法。


二、反悔贪心的分类:

  1. 反悔自动机:

    即设计一种反悔策略,使得随便一种贪心策略都可以得到正解。

    基本的设计思路是:每次选择直观上最接近全局最优解的贪心策略,若发现最优解不对,就想办法自动支持反悔策略。(这就是自动机的意思)

    具体题目具体分析。一般需要反悔自动机的题都是通过差值巧妙达到反悔的目的。

  2. 反悔堆:

    即通过(大根堆、小根堆)来维护当前贪心策略的最优解,若发现最优解不对,就退回上一步,更新最优解。

    由于堆的性质,使得堆的首数据一定是最优的,这就可以实现快速更新最优解


三、例题简析及代码

  1. USACO09OPEN 工作调度Work Scheduling (反悔堆)

    Description:

    \(n\) 项工作,每 \(i\) 项工作有一个截止时间 \(D_i\) ,完成每项工作可以得到利润 \(P_i\) ,求最大可以得到多少利润。

    Method:

    做这道题的时候并没有想到反悔贪心,只是想到一个错误的贪心算法。按照截止时间为第一关键字,利润为第二关键字排序,统计一遍即可。

    比如时间1,价值3;时间2,价值8;时间2,价值9。 实际上的答案为17,但是错误贪心是10.

    显然上面的贪心算法刻印被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. 如果这道题范围小一点n<=10000,可以直接贪心,按照权值从大到小排序,当前位置从后往前判断如果可以就塞进去。
  3. #include<iostream>
    #include<cstdio>
    #include<algorithm>
    #include<cmath>
    #include<queue>
    #define ll long long
    using namespace std;
    int n, vis[10005]; ll ans;
    struct Node{
        int p, v;
        bool operator < (const Node x) const{
            return v > x.v;
        }
    }a[10005];
    int main(){
        scanf("%d",&n);
        for(int i = 1; i <= n; i++){
            scanf("%d%d",&a[i].p, &a[i].v);
        }
        sort(a+1, a+n+1);
        for(int i = 1; i <= n; i++){
            int t = a[i].p;
            while(vis[t] && t) --t;
            if(t){
                vis[t] = 1; ans += (ll)a[i].v;
            }
        }
        printf("%lld\n", ans);
        return 0;
    }

     

  4. CF865D Buy Low Sell High(反悔自动机)

    Description:

    已知接下来 \(n\) 天的股票价格,每天可以买入当天的股票,卖出已有的股票,或者什么都不做,求 \(n\) 天之后最大的利润。


    样例解释:  
        样例1:分别在价格为5,4,2的时候买入,分别在价格为9,12,10的时候卖出,总利润为$-5-4+9+12-2+10=20$ .

    对于20%的数据, $n、p_i$<=300,且$p_i$是单调不降序列

    对于另20%的数据, n<=15, $p_i$<=50

    对于另30%的数据, n<=3000, $p_i$<=10000

    对于100%的数据,n<=300000, $p_i$<=1000000;
  5. dp的写法
    // 8 10 6 12 15 20 4 7
    // 4 5 9
    // f[i][j]前i只股票目前手上有j只股票的最大价值
    #include<iostream>
    #include<cstdio>
    #include<map>
    #include<algorithm>
    #include<cstring>
    #include<cmath>
    using namespace std;
    int n, f[3005][3005], a[3005], ans;
    int main(){
        scanf("%d",&n);
        for(int i = 1; i <= n; i++){
            scanf("%d",&a[i]);
        }
        memset(f,0xA0,sizeof f);
        f[0][0] = 0;
        for(int i = 1; i <= n; i++){
            for(int j = 0; j <= i; j++){//这里可以是0
                if(j-1 >= 0) f[i][j] = max(f[i][j], f[i-1][j-1] - a[i]);
                if(j+1 <= n) f[i][j] = max(f[i][j], f[i-1][j+1] + a[i]);
                f[i][j] = max(f[i][j], f[i-1][j]);
                // ans = max(ans, f[i][j]);
            }
        }
        printf("%d\n", f[n][0]);
        return 0;
    }
  6.  

    Code:

    Method

    我们可以快速想出一种贪心策略:买入价格最小的股票,在可以赚钱的当天卖出。

    显然我们可以发现,上面的贪心策略是错误的,因为我们买入的股票可以等到可以赚最多的当天在卖出。

    我们考虑设计一种反悔策略,使所有的贪心情况都可以得到全局最优解。(即设计反悔自动机的反悔策略)

    定义 \(C_{buy}\) 为全局最优解中买入当天的价格, \(C_{sell}\) 为全局最优解中卖出当天的价格,则:

     

    \[C_{sell}-C_{buy}=\left(C_{sell}-C_i\right)+\left(C_i-C_{buy}\right) \]

     

    \(C_i\) 为任意一天的股票价格。

    即我们买价格最小的股票去卖价格最大的股票,以期得到最大的利润。我们先把当前的价格放入小根堆一次(这次是以上文的贪心策略贪心),判断当前的价格是否比堆顶大,若是比其大,我们就将差值计入全局最优解,再将当前的价格放入小根堆(这次是反悔操作)。相当于我们把当前的股票价格若不是最优解,就没有用,最后可以得到全局最优解。每一个价格作为中间变量和开始变量,都要进入一次堆。

    上面的等式即被称为反悔自动机的反悔策略,因为我们并没有反复更新全局最优解,而是通过差值消去中间项的方法快速得到的全局最优解。

    (假如还没有理解这道题,可以看一看代码,有详细的注释)



    /*
    8
    10 5 4 7 9 12 11 20
    q: 10 5 4
    7 - 4 = 3                     +7 -4
    q: 10 5  7 7
    9 - 5 = 4                     -5  +9
    q: 10  7 7 9  9
    12 - 7 = 5                    +12 -7
    q: 10 7 9 9  12 12
    11 - 7 = 4                    +11 -7
    q: 10  9 9  12 12 11 11
    20 - 9 = 11                   +20 -9
    q: 10  9  12 12 11 11 20 20
    先买,再卖
    当前i, 假设卖了x, 前面选择一个最小mn的买下来,就赚了x - mn。
    万一后悔的话,就不应该在i位置卖,因此在i的位置买x(放入堆中,实现-x的反悔),
    同时将x还要放入堆中, 实现真正买卖
    放到堆里(反悔操作)
    */
    #include<iostream>
    #include<cstdio>
    #include<map>
    #include<algorithm>
    #include<cstring>
    #include<cmath>
    #include<queue>
    #define int long long
    using namespace std;
    priority_queue<int, vector<int>, greater<int> > q;
    int x, n, ans;
    signed main(){
        scanf("%lld",&n);
        for(int i = 1; i <= n; i++){
            scanf("%lld",&x);
            if(!q.empty() && q.top() < x){
                ans += x - q.top();//假设可以卖,就执行卖掉操作+x
                q.pop();
                q.push(x);//反悔操作,让后面的买它-x
            }
            q.push(x);//贪心策略:待定的买操作
        }
        printf("%lld\n",ans);
        return 0;
    }

  7. P3049 [USACO12MAR]Landscaping S

  8. [Usaco 2012 Feb]Cow coupons牛券:反悔型贪心

  9. BZOJ2151 种树(反悔自动机)

    1. Description:

      \(n\) 个位置,每个位置有一个价值。有 \(m\) 个树苗,将这些树苗种在这些位置上,相邻位置不能都种。求可以得到的最大值或无解信息。

      // dp[i][j][1]  dp[i-1][j-1][0] + a[i]
      //前i个序列,第i个种树的最大获利,前i-1个树,第i-1个不种树,
      // dp[i][j][0]  dp[i-1][j][0] dp[i-1][j][1]
      //前i个序列,第i个不种树的最大获利,前i-1个树,第i-1个不种树,
      
      //50分么?MLE导致30分
      #include<iostream>
      #include<cstdio>
      #include<map>
      #include<algorithm>
      #include<cstring>
      #include<cmath>
      #include<queue>
      #define ll long long
      using namespace std;
      int n, k, a[60005], dp[5000][2500][2], ans;
      int main(){
          scanf("%d%d",&n,&k);
          for(int i = 1; i <= n; i++) scanf("%d",&a[i]);
          memset(dp, 0xA1, sizeof dp); ans = -0x7fffffff;
          // cout<<"ddd "<<dp[0][0][0]<<endl;
          for(int i = 0; i <= n; i++){
              dp[0][i][0] = dp[i][0][0] = 0;
          }
          for(int i = 1; i <= n; i++){
              for(int j = 1; j <= k; j++){
                  dp[i][j][1] = dp[i-1][j-1][0] + a[i];
                  dp[i][j][0] = max(dp[i-1][j][0], dp[i-1][j][1]);
                  // cout<<"dp "<<i<<" "<<j<<" "<<dp[i][j][1]<<" "<<dp[i][j][0]<<endl;
                  if(i == n){
                      ans = max(ans, max(dp[n][j][1], dp[n][j][0]));
                  }
              }
          }
          printf("%d\n",ans);
          return 0;
      }

       dp另外的写法:f[i][j]=max(f[i-1][j],f[i-2][j-1]+a[i])

      降维:
      //
      dp[i][j][1] dp[i-1][j-1][0] + a[i] //前i个序列,第i个种树的最大获利,前i-1个树,第i-1个不种树, // dp[i][j][0] dp[i-1][j][0] dp[i-1][j][1] //前i个序列,第i个不种树的最大获利,前i-1个树,第i-1个不种树, //50分么? #include<iostream> #include<cstdio> #include<map> #include<algorithm> #include<cstring> #include<cmath> #include<queue> #define ll long long using namespace std; int n, k, a[60005], dp[6000][2], ans; int main(){ scanf("%d%d",&n,&k); for(int i = 1; i <= n; i++) scanf("%d",&a[i]); memset(dp, 0xA1, sizeof dp); ans = -0x7fffffff; // cout<<"ddd "<<dp[0][0][0]<<endl; // for(int i = 0; i <= n; i++){ dp[0][0] = 0; // } // dp[1][1][1] = a[1]; // cout<<"ans: "<<ans<<endl; for(int i = 1; i <= n; i++){ for(int j = k; j >= 1; j--){ dp[j][0] = max(dp[j][0], dp[j][1]); dp[j][1] = dp[j-1][0] + a[i]; //err:这个写后面了 dp[j][0] = max(dp[j][0], dp[j][1]);      if(i == n){ ans = max(ans, max(dp[j][1], dp[j][0])); } } } printf("%d\n",ans); return 0; }

       

  10. Method

    先判断无解的情况,我们显然可以发现,若 \(n&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;lt;\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:

    // 栈 堆
    // 6 3
    // 100 1 -1 100 1 -1
    // 99 100 -3
    //
    // 2 100
    //
    // o o' o o o' o  o
    //     18   21   99   100  95  6
    //     18   21         94      6
    // +100
    // 99 + 95 - 100 = 94(这个位置是什么?21的右边,6的左边,100这个位置,相当于删除99,95)
    // +94
    //
    
    
    //双线程,一个维护位置信息,一个维护大根堆
    #include<iostream>
    #include<cstdio>
    #include<map>
    #include<algorithm>
    #include<cstring>
    #include<cmath>
    #include<queue>
    #define int long long
    using namespace std;
    int n, m, x, vis[500005], ans;
    struct One{
        int v, l, r;
    }a[500005];
    struct Node{
        int v, id;
        bool operator < (const Node &c) const{
            return v < c.v;
        }
    };
    priority_queue<Node> q;
    signed main(){
        scanf("%lld%lld",&n,&m);
        for(int i = 1; i <= n; i++){
            scanf("%lld",&x);
            a[i].l = i - 1;
            a[i].r = i + 1;
            a[i].v = x;
            q.push((Node){x, i});
        }
        for(int i = 1; i <= m; i++){
            while(vis[q.top().id]) q.pop();
            Node t = q.top();
            if(t.v <= 0) break;
            q.pop();
            ans += t.v;
            int p = t.id; vis[a[p].l] = vis[a[p].r] = 1;
            a[p].v = a[a[p].l].v + a[a[p].r].v - a[p].v;//err:tmp = ,双线程的原序列没有维护
            q.push((Node){a[p].v, p});
    
            a[p].l = a[a[p].l].l;
            a[p].r = a[a[p].r].r;
            a[a[p].l].r = p;
            a[a[p].r].l = p;
        }
        printf("%lld\n", ans);
        return 0;
    }

     

     

    Warning:

    • 一定要记录这个点选没有选过,假如已经选过了,就从堆中丢出去;
    • 1与 \(n\) 是相邻的,一定要特判一下;
    • 双向链表一定不要写挂了;
    • 一定要先将新建的点的价值存入一开始的价值数组,再丢进堆里;(卡在45卡了好久)
    • index是关键字,一定不要使用。(我成功CE了一次)
 
posted on 2022-04-20 09:40  Jeanny  阅读(425)  评论(0)    收藏  举报