[一本通学习笔记] 贪心问题

#LOJ10000. 「一本通 1.1 例 1」活动安排

【题意】

数轴上若干线段,选出最多的线段且它们互不相交。

【思路】

考虑对线段排序。由于要尽可能节约总体右边界,我们以右端点为关键字进行排序,顺序扫描所有区间,如果能将区间加入集合就加入。

也可以考虑对端点离散化后,处理出从每个点开始续一个线段能“跳”到的最近位置,然后模拟跳一下即可。

这里给出后一种思路的代码。

#include <bits/stdc++.h>
using namespace std;

int s[1005],t[1005],f[2005],n;

int main() {
    cin>>n;
    for(int i=1;i<=n;i++) cin>>s[i]>>t[i];
    map<int,int> mp;
    for(int i=1;i<=n;i++) {
        mp[s[i]]++;
        mp[t[i]]++;
    }
    int idx = 0;
    for(map<int,int>::iterator it=mp.begin();it!=mp.end();it++)
        it->second = ++idx;
    for(int i=1;i<=n;i++) {
        s[i]=mp[s[i]];
        t[i]=mp[t[i]];
    }
    for(int i=1;i<=idx;i++)
        f[i]=idx+1;
    for(int i=1;i<=n;i++) {
        f[s[i]]=min(f[s[i]],t[i]);
    }
    for(int i=idx-1;i;--i)
        f[i]=min(f[i],f[i+1]);
    int pin = 1, cnt = 0;
    while(pin <= idx) {
        pin=f[pin];
        ++cnt;
    }
    cout<<cnt-1<<endl;
}

#LOJ10001. 「一本通 1.1 例 2」种树

【题意】

填充一个长度为n的01序列,有h个要求,每个要求描述为(b,e,t),即下标区间[b,e]中至少有t个1.

【思路】

显然可以差分约束,但可以利用一些性质贪心。

按照右端点排序后依次处理所有区间,同时维护目标01序列,如果当前扫描到的区间不符合要求,则从当前区间的右端点开始向左填充。这样可以保证当前填充进去的新1可以得到最大限度的利用。

本题数据范围较小,直接暴力维护,复杂度O(n^2)。

如果要加速,可以考虑用线段树维护01序列,节点记录区间和与覆盖标记。每次修改时通过在线段树上二分找到一个覆盖边界k,将区间[k,t]覆盖。查询很容易。复杂度O(nlogn).

或者用bitset维护,复杂度O(n^2 / 64)。

#include <bits/stdc++.h>
using namespace std;

struct range {
    int l,r,t;
    bool operator < (const range &b) {
        return r < b.r;
    }
} a[100005];

int n,h,b[100005],ans=0;

int main() {
    cin>>n>>h;
    for(int i=1;i<=h;i++) cin>>a[i].l>>a[i].r>>a[i].t;
    sort(a+1,a+h+1);
    for(int i=1;i<=h;i++) {
        int cnt = 0;
        for(int j=a[i].l;j<=a[i].r;j++) cnt+=b[j];
        if(cnt < a[i].t) {
            cnt = a[i].t - cnt;
            for(int j=a[i].r;j>=a[i].l && cnt>0; --j)
                if(b[j]==0)
                    b[j]=1, --cnt, ++ans;
        }
    }
    cout<<ans<<endl;
}

#LOJ10002. 「一本通 1.1 例 3」喷水装置

【题意】

L*W的矩形中有n个圆,每个圆心都在宽度方向的中心线上,并知道每个圆的位置与半径。求最少多少个圆可以覆盖整个矩形。

【思路】

考虑对线段排序。由于要尽可能节约总体右边界,我们以右端点为关键字进行排序,顺序扫描所有区间,如果能将区间加入集合就加入。

在这里离散化以后“跳”的思路比较容易出现精度问题而不推荐使用。

#include <bits/stdc++.h>
using namespace std;

int T,n;
double l,w;
double x[20005],r[20005];
double pl[20005],pr[20005];

struct Item {
    double l,r;
    bool operator < (const Item &x) {
        return l < x.l;
    }
} item[20005];

int main() {
    ios::sync_with_stdio(false);
    cin>>T;
    while(T--) {
        cin>>n;
        cin>>l>>w;
        memset(x,0,sizeof x);
        memset(r,0,sizeof r);
        memset(pl,0,sizeof pl);
        memset(pr,0,sizeof pr);
        for(int i=1;i<=n;i++) cin>>x[i]>>r[i];
        for(int i=1;i<=n;i++) {
            double tmp = pow(r[i],2)-pow(w/2.0,2);
            double del = (tmp>=0? sqrt(tmp) : 0);
            pl[i] = x[i] - del;
            pr[i] = x[i] + del;
            //cout<<pl[i]<<" ,"<<pr[i]<<endl;
            item[i].l = pl[i];
            item[i].r = pr[i];
        }
        sort(item+1,item+n+1);
        //for(int i=1;i<=n;i++) cout<<item[i].l<<" "<<item[i].r<<endl;
        double now = 0.0001, tmp = 0.00001;
        int cnt = 0;
        for(int i=1;i<=n;i++) {
            if(now < item[i].l && now < l) {
                now = tmp;
                ++cnt;//cout<<"now "<<now<<" "<<cnt<<endl;
            }
            tmp = max(tmp, item[i].r);
        }
        if(now < l) now=tmp, ++cnt;
        if(now >= l) cout<<cnt<<endl;
        else cout<<-1<<endl;
    }
}

#10003. 「一本通 1.1 例 4」加工生产调度

【题意】

有n个产品,每个需要先在A机器加工a[i]时间,再到B机器加工b[i]时间。求怎样使得总时间最短。

【思路】

Johnson双机调度模型。我们可以给出一个简化的proof.

假设只有两个产品。假设先加工1优于先加工2,那么有

a1 + max(b1,a2) + b2 < a2 + max(b2,a1) + b1

化简得到

max(b1,a2) - a2 - b1 < max(b2,a1) - b2 - a1

拆得

-min(b1,a2) < -min(b2,a1)

min(a1,b2) < min(a2,b1)

这个结果不可以推广,因为缺乏传递性。我们需要进一步处理

对a,b得大小进行讨论我们发现

1)  a1<b1 and a2<b2, 那么我们需要a1<a2排序

2)  a1=b1 and a2=b2, 随意

3)  a1>b1 and a2>b2, 那么我们需要b2>b1排序

4)  a1>b1 and a2<b2, 则不等式一定成立

5)  a1<b1 and a2>b2, 则不等式一定不成立

我们很容易发现,一个ai<bi的元素最终会出现在序列的靠前部分,而一个ai>bi的元素最终会出现在序列的靠后部分。即,所有ai>bi的元素一定位于ai<bi的元素之后。

而对于所有ai<bi的元素,我们会按照ai升序排序,对于所有ai>bi的元素,我们会按照bi降序排序。

于是我们得到了Johnson双机调度算法。

我们维护两个栈,将所有元素按min(ai,bi)升序排序后按顺序插入。如果ai<bi就插入第一个,否则插入第二个。最后将第二个栈倒置接在第一个栈后面,得到的就是操作顺序序列。

至于计算序列的时间花费,模拟即可。

#include <bits/stdc++.h>
using namespace std;

int n;
struct Item {
    int a,b,id;
    bool operator < (const Item &x) {
        return min(a,b) < min(x.a,x.b);
    }
} item[100005];
vector <int> ans1,ans2;
vector <int> seq;
int ta[100005],tb[100005];

int main() {
    ios::sync_with_stdio(false);
    cin>>n;
    for(int i=1;i<=n;i++) cin>>item[i].a, item[i].id=i;
    for(int i=1;i<=n;i++) cin>>item[i].b;
    sort(item+1,item+n+1);
    for(int i=1;i<=n;i++) {
        if(item[i].a<=item[i].b) ans1.push_back(i);
        else ans2.push_back(i);
    }
    seq.push_back(0);
    for(int i=0;i<ans1.size();i++) seq.push_back(ans1[i]);
    for(int i=ans2.size()-1;i>=0;--i) seq.push_back(ans2[i]);
    for(int i=1;i<=n;i++) ta[seq[i]]=ta[seq[i-1]]+item[seq[i]].a;
    for(int i=1;i<=n;i++) tb[seq[i]]=max(ta[seq[i]],tb[seq[i-1]])+item[seq[i]].b;
    cout<<tb[seq[n]]<<endl;
    for(int i=0;i<ans1.size();i++) cout<<item[ans1[i]].id<<" ";
    for(int i=ans2.size()-1;i>=0;--i) cout<<item[ans2[i]].id<<(i?" ":"");
    cout<<endl;
}

 【拓展】 一类邻项交换排序问题

例1 国王游戏

【题意】

N个人站成一排,每个人有a[i]和b[i]。每个人得到的分数=左边所有人a[i]的乘积 ÷ 自己的b[i]

要求最小化最高得分。

【思路】

考虑相邻的两个人i,j,有j=i+1。我们设i左边所有a[j]的乘积为k。

考虑到调整这两人的顺序不会对前面和后面人的得分造成影响,我们只需要让这两人中得分较高的人的得分尽量小即可。

如果i在前,j在后,则max(k/b[i], (ka[i])/b[j])

如果i在后,j在前,则max(k/b[j], (ka[j])/b[i])

化简得需要交换i,j的充要条件为a[i]b[i] > a[j]b[j]

例2 皇后游戏

【题意】

背景条件同上。设i获得的分数为c[i],则

i=1时,c[i]=a[i]+b[i]

i>1时,c[i] = max(c[i-1], a[1]+...+a[i]) + b[i]

同样我们要最小化最高得分。

【思路1】

模仿上例化简容易得到min(a[i],b[j]) > min(a[j],b[i])为交换条件。

【严格弱序】

对于一个运算符<,用<表示满足,用!<表示不满足。如果符合以下条件,则为严格弱序。

1)  非自反性  即x !< x

2)  非对称性  即若x<y 则y !< x

3)  传递性  即若x<y,y<z 则x<z

4)  不可比性的传递性  若x!<y y!<x y!<z z!<y 则x!<z z!<y

【对思路1的反思】

很显然对任意i,j,k,若min(ai,bj)<min(aj,bi)且min(aj,bk)<min(ak,bj)那么min(ai,bk)<min(ak,bi)

但不具有不可比性的传递性。由于这里的不可比性可以被看作等号,所以我们很容易找到反例。

这意味着,将若干相邻元素互换后可能会出现前面元素大于后面元素的情况,从而不是最优。

总的来说,一个排序方式如果时正确的,那么它需要满足严格弱序,并且对任意i<j,有min(ai,bj)<=min(aj,bi)

【思路2】

观察到思路1的错误主要出在不可比性上。那么我们可否为不可比性设置一种处理方式来使得取到最优?于是,在min(ai,bj)==min(bi,aj)时,我们将a小的往前放。

【小结】

对于一类可以通过交换相邻两项来使得解非严格变优的问题,我们可以通过邻项交换排序来处理问题。但是需要注意对严格弱序的满足。

#10004. 「一本通 1.1 例 5」智力大冲浪

【题意】

有n个任务,每个任务完成需要1单位时间,每个任务有它的ddl,完成不了则会失去对应的分数。最小化损失。

【思路】

考虑到对于任意的时间段0~t,如果一定要放弃一些,我们一定希望放弃那些损失较小的。

因此将任务按照ddl排序,维护小根堆,枚举当前时间,并加入所有这个时间为ddl的任务。如果小根堆中的元素个数大于当前可以容纳的个数(数值上等于时间),则不断将堆顶弹出。

#include <bits/stdc++.h>
using namespace std;

struct Item {
    int s,t;
    bool operator < (const Item &x) {
        return t < x.t;
    }
} item[100005];

int n,m;

int main() {
    ios::sync_with_stdio(false);
    cin>>m>>n;
    for(int i=1;i<=n;i++) cin>>item[i].t;
    for(int i=1;i<=n;i++) cin>>item[i].s;
    sort(item+1,item+n+1);

    priority_queue <int,vector<int>,greater<int> > q;

    for(int i=1;i<=n;i++) {
        q.push(item[i].s);
        while(q.size() > item[i].t) {
            m-=q.top();
            q.pop();
        }
    }

    cout<<m<<endl;
}

#10005. 「一本通 1.1 练习 1」数列极差

【题意】

给定n个数,每次操作可以删除其中两个数a,b,并插入一个数ab+1。直到只剩下一个数。最后能得到的数中最大的为max最小的为min,求max-min。

【思路】

考虑三个数的情况,如果a<b<c,那么

先操作a,b:{a,b,c} -> {ab+1,c} -> {abc+c+1}

先操作b,c:{a,b,c} -> ... -> {abc+a+1}

归纳可知先处理最小的两个数,最终得到的值最大,反之最小。

于是用一个大根堆和一个小根堆分别维护即可。

#include <bits/stdc++.h>
using namespace std;

int n,a[100005];
priority_queue <int,vector<int>,greater<int> > q1;
priority_queue <int,vector<int>,less<int> > q2;

int main() {
    cin>>n;
    for(int i=1;i<=n;i++) cin>>a[i];
    for(int i=1;i<=n;i++) q1.push(a[i]), q2.push(a[i]);
    for(int i=1;i<n;i++) {
        int t=q1.top(); q1.pop();
        t*=q1.top(); q1.pop();
        q1.push(t+1);
        t=q2.top(); q2.pop();
        t*=q2.top(); q2.pop();
        q2.push(t+1);
    }
    cout<<q1.top()-q2.top()<<endl;
}

#10006. 「一本通 1.1 练习 2」数列分段

【题意】

长为N的正数列需要分成至少多少段使得每段和不超过M。

【思路】

考虑到第一段一定要从开头开始划。从头开始扫描,必要时候切出一段即可。

#include <bits/stdc++.h>
using namespace std;

int n,m,a[100005];

int main() {
    cin>>n>>m;
    for(int i=1;i<=n;i++) cin>>a[i];
    int tmp = 0, ans = 1;
    for(int i=1;i<=n;i++) {
        if(tmp+a[i]<=m) {
            tmp+=a[i];
        }
        else {
            tmp=a[i];
            ans++;
        }
    }
    cout<<ans<<endl;
}

#10007. 「一本通 1.1 练习 3」线段

【题意】

数轴上有n线段,选出k个使得没有重合。求k最大值。

【思路】

同10002. 考虑对线段排序。由于要尽可能节约总体右边界,我们以右端点为关键字进行排序,顺序扫描所有区间,如果能将区间加入集合就加入。

#include <bits/stdc++.h>
using namespace std;

struct Item {
    int l,r;
    bool operator < (const Item &x) {
        return r < x.r;
    }
} item[1000005];

int n;

int main() {
    cin>>n;
    for(int i=1;i<=n;i++) cin>>item[i].l>>item[i].r;
    sort(item+1,item+n+1);
    int pos = 0, ans = 0;
    for(int i=1;i<=n;i++) {
        if(item[i].l >= pos)
            pos = item[i].r,
            ++ans;
    }
    cout<<ans<<endl;
}

#10008. 「一本通 1.1 练习 4」家庭作业

同10004.

#include <bits/stdc++.h>
using namespace std;

int n;
struct Item {
    int a,b;
    bool operator < (const Item &x) {
        return a<x.a;
    }
} item[1000005];

int main() {
    cin>>n;
    for(int i=1;i<=n;i++) cin>>item[i].a>>item[i].b;
    sort(item+1,item+n+1);
    priority_queue <int,vector<int>,greater<int> > q;
    for(int i=1;i<=n;i++) {
        q.push(item[i].b);
        while(q.size() > item[i].a) q.pop();
    }
    int ans = 0;
    while(q.size()) ans+=q.top(), q.pop();
    cout<<ans<<endl;
}

#10009. 「一本通 1.1 练习 5」钓鱼

【题意】

有n个湖,从i到i+1花时间t[i]。再每个湖花费1个时间可以得分f[i],再花费一个时间得分f[i]-d[i],再花费一个f[i]-2d[i],以此类推。最后可以再任意一个湖结束。求给定总时间内得最大得分。

【题解】

时间范围不是很大,维护一个小根堆为当前所有的选择,从左向右枚举最终在哪个湖结束。由于越向右花在路上的时间越长,能用来钓鱼的时间越少,故每次右移动时弹出堆中的多余元素。每到一个新的湖,将这个湖里所有的选择加入小根堆,同时维护总个数即可。

#include <bits/stdc++.h>
using namespace std;

int n,h,f[105],d[105],t[105],tmp,ans;
priority_queue <int,vector<int>,greater<int> > q;

int main() {
    cin>>n>>h;
    for(int i=1;i<=n;i++) cin>>f[i];
    for(int i=1;i<=n;i++) cin>>d[i];
    for(int i=1;i<n;i++) cin>>t[i];
    h*=12;
    for(int i=1;i<=n&&h>0;i++) {
        for(int j=1;j<=240&&f[i]>0;++j)
            q.push(f[i]), tmp+=f[i], f[i]-=d[i];
        while(q.size()>h) tmp-=q.top(), q.pop();
        ans=max(ans,tmp);
        h-=t[i];
    }
    cout<<ans<<endl;
}

#10010. 「一本通 1.1 练习 6」糖果传递

【题意】

环上有n个人,每个人有a[i]个物品。每个人只能向着两侧相邻的人传递,一次代价为1.求所有人的物品个数调整为相同的总代价。

【思路】

记avg = sum(ai)/n,则每个人需要补充的个数为ai - avg

由于引入负数,我们不妨设i像i+1传递的个数为xi,那么

avg = ai - x[i] + x[i-1]

于是x[i]-x[i-1] = ai - avg (i>2)

特别地 i=1时,又avg = a1 - x1 + xn

即x[1]-x[n] = a1 - avg

很容易发现这个关于x[]的方程组又无穷多组解。但是我们可以考虑求解其中的相对关系

不妨将x[1]视作变量,去表示其它

x[2] = ai - avg + x[1] = x[1] - (a[1]-avg)

x[3] = ai - avg + x[2] = a[2] - avg + x[1] + a[1] - avg = x[1] - (a[1]+a[2]-avg-avg)

......

我们现在需要找一个点到所有(sigma(a[1->i])-i*avg)距离和最小。显然是中位数。

#include <bits/stdc++.h>
using namespace std;

#define ll long long
ll n,a[1000005],avg,c[1000005],mid,dis;

int main() {
    cin>>n;
    for(int i=1;i<=n;i++) cin>>a[i];
    for(int i=1;i<=n;i++) avg+=a[i];
    avg/=n;
    for(int i=1;i<=n;i++) c[i]=c[i-1]+a[i]-avg;
    sort(c+1,c+n+1);
    if(n&1) mid = c[n/2+1] * 2;
    else mid = (c[n/2] + c[n/2+1]);
    for(int i=1;i<=n;i++) dis+=abs(c[i]*2-mid);
    dis/=2;
    cout<<dis<<endl;
}
原文地址:https://www.cnblogs.com/mollnn/p/11576463.html