zoukankan      html  css  js  c++  java
  • [一本通学习笔记] 贪心问题

    #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;
    }
  • 相关阅读:
    java之redis篇(spring-data-redis整合) (转)
    web.xml
    点击类名方法名如何连接到相应的Android源代码
    eclipse能够自动提示变量名.
    Android自定义ActionBar
    Android 自定义View
    android 自定义titlebar
    Android SharedPreferences登录记住密码
    Android 正则表达式验证手机和邮箱格式是否正确
    Android 首次进入应用时加载引导界面
  • 原文地址:https://www.cnblogs.com/mollnn/p/11576463.html
Copyright © 2011-2022 走看看