Skip to content

贪心

为什么选择贪心?

~~除了数据结构和dp剩下的都是贪心模拟暴力~~

1) 状态不好设计,如P1016,每个加油站的加油量要精确到小数后两位,基本无法枚举,并且原题具有贪心选择性,即局部最优解能推出全局最优解,这样就用贪心。

2) 枚举量过大,如CF939E,如果dp就是\(O(n^2)\),但发现性质后尺取法就是\(O(n)\);再如CF47E也是尺取法,如果暴力就是\(O(n^2)\)

有些枚举量过大的问题还可以用数据结构维护。

P1080

$$假设已经排了几个人(包括国王),设他们左手上的数的乘积为S.\

现在要给2个人排序,记第一个人左手上的数为a_{1},右手上的数为b_{1};第二个人左手上的数为a_{2},右手上的数为b_{2}。\ 如果第一个人排在前面优于第二个人排在前面,那么\ max(S/b_1,S∗a_1/b_2)<max(S/b_2,S∗a_2/b_1)\

显然有S/b_2\leq S* a_1/b_2\

假设有S a_1/b_2\geq S a_2/b_1,则max(S/b_1,S∗a_1/b_2)\geq max(S/b_2,S∗a_2/b_1),矛盾。\

所以S a_1/b_2<S a_2/b_1,整理得a_1 b_1<a_2 b_2\

所以只需要按照a* b排序即可。 $$

  • 结论:对于贪心选择性的证明,可以假设一个中间状态,即已经做完\(i-1\)个人,考虑第\(i\)个位置可以放的人,为了证明方便可以只考虑2个或少量的人,像P3951一样。

CF939E

贪心尺取法。

有两条重要性质。

1) 对于一个加入的新数一定要选。

$$ 原式 ans=Max-\frac{sum}{n}\

考虑用新的最大替换掉原来的最大,设新的最大比原来最大大 \Delta(x)\ ans=\frac{(Max+\Delta(x))\times n-(sum+\Delta(x))}{n}\

因为 n\geq 1,ans一定变大(优秀) $$ 2) 对于集合 s 剩下的数,一定是选前面几个小的数,并且选的数的个数是单调不减的。

可以意会一下证明,一定是选比当前集合平均数小的数加进来,使平均数更小,答案才会变得更大。 $$ 原式 ans=Max-\frac{sum}{n}\

考虑新加一个数 \Delta(x)\ 新的式子 ans=Max-\frac{sum+\Delta(x)}{n+1}\

若新式减去原式>0,则新式更优秀\ 新式减原式得\ -\frac{sum+\Delta(x)}{n+1}+\frac{sum}{n}\

等于\ \frac{sum-n\times \Delta(x)}{n\times(n+1)}>0\

得出更优秀条件为:sum-n\times \Delta(x)>0,即 \Delta(x) <\frac{sum}{n}\ $$

证毕,可以用尺取法。

#include<iostream>
#include<cstdio>
#include<cstring>
#define db double
#define int long long 
using namespace std;
const int N=5e5+10;
int n,cnt,tot;
int vis[N],q[N];
db res[N],a[N],sum[N];
int read1(){
    int x=0,f=1;
    char ch=getchar();
    while(ch>'9' || ch<'0'){
        if(ch=='-')f=-1;
        ch=getchar();
    }   
    while(ch<='9' && ch>='0'){
        x=(x<<1)+(x<<3)+ch-'0';
        ch=getchar();
    }
    return x*f;
}
void work(){
    int l=1,r=0;
    db ans=-1.0;
    for(int i=1;i<=cnt;++i)sum[i]=sum[i-1]+a[i];
    for(int i=1;i<=cnt;++i){
        if(!vis[i])continue;
        ans=-1.0;
        while(l<=i){
            db tmp=(double)(a[i]-(double)(sum[l]+a[i])/(l+1));
            if(tmp>ans)ans=tmp,++l;
            else{
                --l;
                break;  
            }
        }
        if(l==i+1)--l;
        res[i]=ans;
    }
    for(int i=1;i<=tot;++i)printf("%.10lf\n",res[q[i]]);

}
signed main(){
    cnt=0,tot=0;
    n=read1();
    for(int i=1,op;i<=n;++i){
        op=read1();
        if(op==1){
            a[++cnt]=(double)read1();
        }else{
            vis[cnt]=1;
            q[++tot]=cnt;
        }
    }
    work();
    return 0;   
}

P1645

贪心的将序列按照右端点排序,那么每个区间内应该选的点应该分布在它的最右边,这样能够保证有的点最少,是最优解。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define int long long 
using namespace std;
const int N=2e3+10;
int n,ans;
int vis[N];
struct query{
    int l,r,m;
}q[N];

int w[N][N];
bool cmp(query a,query b){
    return a.r<b.r;
}
signed main(){
    memset(vis,0,sizeof vis);
    memset(w,0,sizeof w);
    scanf("%lld",&n);
    for(int i=1;i<=n;++i){
        scanf("%lld%lld%lld",&q[i].l,&q[i].r,&q[i].m);
    }
    sort(q+1,q+n+1,cmp);
    for(int i=1;i<=n;++i){
        for(int j=q[i].l;j<=q[i].r;++j){
            w[j][++w[j][0]]=i;
        }   
    }
    int ans=0;
    for(int i=1;i<=n;++i){
        int l=q[i].l,r=q[i].r,m=q[i].m;
        if(!q[i].m)continue;
        for(int j=r;j>=l;--j){
            if(!q[i].m)break;
            if(vis[j])continue;
            vis[j]=1;
            ans++;
            //q[i].m--;
            for(int k=1;k<=w[j][0];++k){
                if(q[w[j][k]].m)q[w[j][k]].m--;
            }
        }
    }
    printf("%lld",ans);
    return 0;
} 

/*

2
1 1000 1000
1 999 999
*/

P1191

将每一列前n-1行往上能数到的最多的点数记录下来,那么每个第n行的点的贡献就是从左往右扫并取最小值。

一个数扫描\(O(n)\),共\(n^2\)个数,所以总复杂度\(O(n^3)\)

P1016

1.只要存在一个加油站与下一个加油站或终点间的距离大于\(c* d_2\),那么无解.

2.对于每个加油站,有两种可能:

1)他能到达的所有点中有价格比他小的加油站,那就加油到刚好能到达那个加油站,此时油量会变为零;

2)他能到达的点里没有价格小于它的加油站,那就在这里充满油,再到后面那些加油站中价格相对最小的加油站,油量从$c \(减少\)(\Delta d)/d_2 $。

证明:

1.1)中会不会剩余的油量已经能满足能够到达下一加油站?

不会,因为它由上一个加油站转移过来,而上一个加油站最远能到的距离到不了目标加油站,因此一定会再加油,不会由不加油就走了的情况。

2.2)中为什么要选价格相对最少的加油站,并且要充满油?

因为如果再后面的加油站能够到达比它小的加油站,那么他会需要加油来到达那一加油站,这是就可以用之前价格低的油,达到最优;就算没有,为了到达终点也要加油,所以选择相对最小的补充油量。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=210,INF=1050;
double d1,c,p0,d2;
int n;
struct node{
    double d,p;
}q[N];
bool cmp(node a,node b){
    return a.d<b.d;
}
int main(){
    scanf("%lf%lf%lf%lf%d",&d1,&c,&d2,&p0,&n);  
    q[0].d=0,q[0].p=p0;
    for(int i=1;i<=n;++i){
        scanf("%lf%lf",&q[i].d,&q[i].p);
    }
    sort(q+1,q+n+1,cmp);
    q[n+1].d=d1,q[n+1].p=0;
    for(int i=1;i<=n+1;++i){
        if(q[i].d-q[i-1].d>c*d2){
            printf("No Solution");
            return 0;
        }
    }
    int pos=0;double cc=0;double ans=0;
    while(pos<n+1){
        int tmp=0,loc=n+1,col=n+1;
        double minn=INF;
        for(int i=pos+1;i<=n+1;++i){
            if(c<(q[i].d-q[pos].d)/d2){
                loc=i-1;
                break;
            }
        }
        for(int i=pos+1;i<=loc;++i){
            if(minn>q[i].p){
                minn=q[i].p;
                col=i;
            }
            if(q[i].p<=q[pos].p){
                tmp=i;
                break;
            }
        }
        if(tmp)ans+=(((q[tmp].d-q[pos].d)/d2)-cc)*q[pos].p,cc=0,pos=tmp;// delta must bigger than cc
        else ans+=(c-cc)*q[pos].p,cc=c-(q[col].d-q[pos].d)/d2,pos=col;
    }
    printf("%.2lf",ans);
    return 0;
}

P1658

假设当前手中的硬币能够组成1,2,...,sum这些面值,此时sum<x.

那么考虑添加一种硬币,使得sum变大。

当当前硬币的值为a时,能组成的最大面值由sum变成sum+a.

但是,如果a>sum+1,那么sum+1,sum+2,...,a-1这些数就不能组成,那么面值就不连续了.

所以我们要取a<=sum+1且a最大。

无解的情况就是没有1。

注意取a时要用upper_bound-1,不能用lower_bound-1. 考虑:

5 7 此时都求出了5正确

5 6 7 此时upper_bound求出6正确;lower_bound求出5错误.

code:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=110;
int n,x;
int a[N];
bool cmp(int a,int b){
    return a<b;
}
int main(){
    scanf("%d%d",&x,&n);
    for(int i=1;i<=n;++i)scanf("%d",&a[i]);

    sort(a+1,a+n+1,cmp);
    if(a[1]!=1){
        printf("-1");
        return 0;
    }
    int sum=0,cnt=0;
    while(sum<x){
        int tmp=upper_bound(a+1,a+n+1,sum+1)-a-1;
        sum+=a[tmp];
        cnt++;  
    }
    printf("%d",cnt);
    return 0;

}

P1248

这题的坑点就是\(B\)车间不能同时处理多个物件,要堆到后面逐个处理。

首先考虑每次的贡献:

\(fa\)\(a\)车间的处理时间,\(fb\)\(b\)车间的处理时间,则:

\[ fb=\max(fa+s[i].a,fb)+s[i].b;\\ fa+=s[i].a; \]

因为只有第\(i\)\(a\)物件和第\(i-1\)\(b\)物件都做完才能做第\(i\)\(b\)物件。

再考虑\(a\)车间已经做了\(P\)时间的工作,不考虑\(b\)车间剩余的物件,此时还剩两件。

那么\(1\)在前和\(2\)在前的贡献分别是:

\[ a_1+\max(a_2,b_1)+b_2\\和\\a_2+\max(a_1,b_2)+b_1 \]

如果1更优,就有

\[ a_1+\max(a_2,b_1)+b_2<a_2+\max(a_1,b_2)+b_1\\ a_1+b_2-\max(a_1,b_2)<b_1+a_2-\max(a_2,b_1)\\ 即\\ \min(a_1,b_2)<\min(a_2,b_1) \]

但这个式子不能直接做题,反例是能举出来的。

我们考虑两(三)种情况:

1) \(a_i<=b_i\),则有\(a_1<a_2\) 1) \(a_i>b_i\),则有\(b_1>b_2\)

又考虑\(a_1>b_1,a_2<b_2\)时一定不会满足上述式子,所以只需要保证\(a_1<b_1,a_2>b_2\),即\(a<b\)的放在前面,\(a>b\)的放在后面即可。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=1e3+10;
int n;
struct node{
    int a,b,d,id;
}s[N];

bool cmp(node a,node b){
    if(a.d==b.d){
        if(a.d<=0)return a.a<b.a;
        else return a.b>b.b;
    }
    return a.d<b.d;
}
int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;++i)scanf("%d",&s[i].a);
    for(int i=1;i<=n;++i)scanf("%d",&s[i].b);
    for(int i=1;i<=n;++i)s[i].d=s[i].a==s[i].b?0:(s[i].a>s[i].b?1:-1),s[i].id=i;
    sort(s+1,s+n+1,cmp);
    int fa=0,fb=0;
    for(int i=1;i<=n;++i){
        fb=max(fa+s[i].a,fb)+s[i].b;
        fa+=s[i].a;
    }
    printf("%d\n",fb);
    for(int i=1;i<=n;++i)printf("%d ",s[i].id);
    return 0;
} 

UVA11300

当面对这种看似每种操作都没有方向,~~很像模拟退火~~的题,就先强行规定一个方向。

注意到一个人给别人钱和别人给它钱的次数没有限制,所以设\(x_i\)表示后一个人给前一个人的钱数,负数表示逆方向给钱。

所以有:

\[A_1+x_1-x_2=M\\ A_2+x_2-x_3=M\\ ...\\ A_{n-1}+x_{n-1}-x_n=M \]

注意\(A_n\)那个式子就没有用了。

消去除\(x_1\)所有的未知量,整理可得:

\[ x_2=x_1-C_1\\ x_3=x_1-C_2\\ ...\\ x_{n-1}=x_1-C_{n-1}\\ 其中C_i=\sum_{k=1}^iA_k-i* M \]

所以这个问题就变成了坐标轴上\(n-1\)个点,选那个位置使得所有的点到这个位置的距离和最短。

此时输出中位数即可,贪心的证明很巧妙,~~但我懒,略~~

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define int long long 
using namespace std;
const int N=1e6+10;
int c[N];
int n,a,tot,ans;
bool cmp(int a,int b){
    return a<b;
}
int abs1(int a){
    return a>=0?a:-a;
}
signed main(){
    while(scanf("%lld",&n)!=EOF){
        tot=0,ans=0;
        for(int i=1;i<=n;++i)scanf("%lld",&a),c[i]=c[i-1]+a,tot+=a;
        tot/=n;
        for(int i=1;i<=n;++i)c[i]-=i*tot;
        sort(c+1,c+n+1,cmp);
        int mid=n/2+1;
        for(int i=1;i<=n;++i){
            ans+=abs1(c[i]-c[mid]);
        }
        printf("%lld\n",ans);
    }
    return 0;
}