• Day 1 下午


    POINT 1 贪心

    每一步都取当前的最优解的思想;一般来说符合直观思路,需要严格的证明;OI中使用多个错误的贪心策略进行加成有时会有良好的效

    例一
    给定N个农民,第i个农民有Ai单位的牛奶,单价Pi
    现在要求从每个农民手中购买不超过Ai单位,总共M单位的牛奶。求最小花费 
    题解:

    将所有农民按单价排序,能取就取即可

     例二
    给定n个物品,第i个物品有大小li,要求将这些物品装进容积为L的箱子里,每个箱子至多放两个物品,求最少所需箱子数。

    1 n 105

    题解:

    将物品从大到小排序考虑当前的最大物品,假设能与最小值凑成一对,就凑成一对
     
    否则必然不存在一个物品能与他凑成一对,因此单列用双指针维护这个过程即可 


    例三
    n个物品,每个物品有属性AiBi。你需要将他们排成一行 
    如果物品i被放在第j个位置,那么会产生代价A· (1) + B· (j)
    现在要求总代价的最小值
    1 n 105


    展开式子
    得到ans = ΣAi · j - Ai + Bi · n - Bi · j
    发现 Bi · n - Ai 是常数,会变化的只有Ai · j - Bi · j
    因此按Ai - Bi排序即可

    大的放前面,小的放后面

    例四
    给定n个水龙头,第i个水龙头有最大出水量Ai,且给定一个温度值ti

    定义一次出水得到的温度为Σ(Ai *ti)/Σ(Ai,给定一次出水得到的温度T,求最大总出水量。

    如果得不到该温度,输出0
    1 n 2 * 105, 0 Ai, ti 106

    题解:
    先把ti减去T,然后按照t排序
    把数组分成两块,一半小于等于0,一半大于0
    用贪心的思想,可以发现有一半必须全选,另一半选最靠近T的那些

    代码:

    #include<cstdio>
    #include<algorithm>
    #include<cstring>
    
    #define N 300005
    
    using namespace std;
    
    int a[N],t[N],i,j,m,n,p,k,id[N],ID[N],sum[N],T;
    
    double ans;
    
    int cmp(int x,int y)
    {
            return sum[x]<sum[y];
    }
    
    int main()
    {
            scanf("%d%d",&n,&T);
            for (i=1;i<=n;++i) 
            {
                    scanf("%d",&a[i]);
            }
            for (i=1;i<=n;++i) scanf("%d",&t[i]);
            for (i=1;i<=n;++i)
            {
                    if (t[i]==T) ans+=a[i];
                    else if (t[i]<T) id[++id[0]]=i,sum[i]=T-t[i];
                    else ID[++ID[0]]=i,sum[i]=t[i]-T;
            }
            sort(id+1,id+id[0]+1,cmp);
            sort(ID+1,ID+ID[0]+1,cmp);
            long long suma=0,sumb=0;
            for (i=1;i<=id[0];++i)
                suma+=1ll*sum[id[i]]*a[id[i]];
            for (i=1;i<=ID[0];++i)
                sumb+=1ll*sum[ID[i]]*a[ID[i]];
            if (suma<sumb)
            {
                    swap(suma,sumb);
                    for (i=0;i<=n;++i) swap(ID[i],id[i]);
            }
            for (i=1;i<=ID[0];++i) ans+=a[ID[i]];
            for (i=1;i<=id[0];++i)
                if (1ll*sum[id[i]]*a[id[i]]>=sumb)
                {
                        ans+=1.*sumb/sum[id[i]];
                        break;
                }
                else
                {
                        ans+=a[id[i]];
                        sumb-=1ll*sum[id[i]]*a[id[i]];
                }
            printf("%.10lf
    ",ans);
    }



    证明:
    假设负数集里面还有一些没选,正数集里还有数剩余
    那么我们就可以把他们凑出一个0出来,直到某一边用完为止.证毕.

    所以就可以直接贪心了



    例五
    n个闹钟,第i(1 i n)个闹钟将在第ai(1 ai 106)分钟鸣响,鸣响时间为一分钟。当在连续的m分钟内,有至少个闹钟鸣响,则会被叫醒。

    现要求关闭一些闹钟,使得在任意连续的m分钟内,鸣响的闹钟数量恒小于k。 

    题解:

    一个直观的想法是,我们按照重叠最多的顺序排序,但是这样是有问题的,毕竟如果这么一排,就会导致左右两边分开,反而会使情况更糟。

    但是如果我们从左往右扫就不一样了。只要碰到会吵醒的情况,我们就弹出,这样能够保证区间是连续的。


    并且,由于区间一定会出现问题,所以我们不能就此放置不管我们选择最右面的关闭,因为这样会让剩余的能关的更少 

    POINT 2 二分

    二分的思想
    给定一个单调的函数/数组,给定一个值,求这个值是否存在,或者找到这个值应当存在的位置 

    由于数组有序,不妨认为他单调递增
    假设Ai > x,则必然有j > i, Aj > x
    假设Aj < x,则必然有j < i, Aj < x
    二分的原理就是每次在待定区间中选择mid
    必然可以确定一边是没有意义的。每次问题的规模缩小 1/2
    因此复杂度为O(logN)

     

    寻找<=x的第一个位置

    如果两次二分找到的中点一样的话,就说明已经二分完了

    #include<cstdio>
    #include<algorithm>
    #include<cstring>
    
    #define N 300005
    
    using namespace std;
    
    int i,j,m,n,p,k,a[N],x;
    
    int check(int x)
    {
            int i,cnt=0;
            for (i=1;i<=n;++i) if (a[i]-a[i-1]>x) return 0;
            for (i=0;i<n;)
            {
                    for (j=i;j<=n&&a[j]-a[i]<=x;++j);
                    ++cnt;
                    i=j-1;
            }
            if (cnt<=m) return 1;
            return 0;
    }
    
    int main()
    {
            scanf("%d%d",&n,&m);
            for (i=1;i<=n;++i) scanf("%d",&a[i]);
            sort(a+1,a+n+1);
            int l=0,r=(int)1e9,mid=0;
            while ((l+r)>>1!=mid)
            {
                   mid=(l+r)>>1;
                   if (check(mid)) r=mid;
                   else l=mid;
            }
            printf("%d
    ",r);
    }

    最后是答案是存在变量L中

    注意一定要是单调序列

    二分答案

    顾名思义,就是对答案进行二分
    对于某些要求“满足某条件的最小值”类的问题,对答案进行二分,假设答案不超过mid,则问题变为“满足某条件且某值不超过mid”的判定性问题。
    常用于最大值最小化类问题。


    在二分答案之后往往需要一个贪心策略。

     

    例一
    一条河上有n个石子排成一条直线,第i个石子距离河岸xi。一只年长的青蛙想要从岸边(x=0)到达第n个石子上(其实是对岸)。这只青蛙实在是太年长了,所以最多只能

    m次,而且他希望他这些次跳跃中距离最远的那次距离尽可能的短。请你帮他求出这个最远距离最短能是多少。

    1 m n 105

    最小化:最大的跳跃距离
    二分答案:设答案为mid,则问题变为:
    n个石子,只能跳m次,每次跳远距离不能超过mid,问是否可行。

    或者n个石子,每次最远距离不超过mid,问最少跳多少次(然后和m比较即可)。

    贪心策略:每次跳的尽量远即可

    二分O(logN)*贪心O(N)=O(NlogN

    先检查是否能跳的过去

    再让他在不超过最远距离的情况下多跳

    如果他跳的步数不大于m就可行

    例二
    给定n个物品,每个物品有属性AiBi。要求在其中选择k个物品,使得选择的物品,的sum(A)/sum(B)尽可能大。 

    贪心:选Ai/Bi最高的k个物品?
    反例:
    3 2
    1000 10
    1000 100
    1 1
    除了最优的物品一定会选之外 可以考虑选择Bi非常小的物品, 减小对性价比的影响。此时物品3比物品2更优。 

    二分答案
    假设sum(Ai)/sum(Bi) >= mid
    则:sum(Ai) - mid * sum(Bi) >= 0
    即:sum(Ai-mid*Bi) >= 0
    Ai-mid*Bi作为第i个物品的权值,问题变为能否选k个物品使得权值和大于0.此时贪心选择权值最大的k个物品即可。
    二分O(logN)* 排序O(NlogN) = O(Nlog 2N)

     

    二分是对一个单调的函数进行的操作
    那么我们有没有办法对一个单峰的函数进行操作呢?
    求一个单峰函数的极值点

    三分函数

    三分

    发现共性:l,r中值较小的那一段一定会被舍去严格的实现每次都能缩小问题的 1/3

    事实上我们取两次mid会好写很多,只是常数问题

    #include<cstdio>
    #include<algorithm>
    #include<cstring>
    
    #define N 500005
    
    using namespace std;
    
    int i,j,m,n,p,k,a[N],ty,x;
    
    long long b[N];
    
    double check(int x)
    {
            return 1.*(b[x-1]+a[n])/x;
    }
    
    int main()
    {
            scanf("%d",&m);
            for (;m--;)
            {
                    scanf("%d",&ty);
                    if (ty==1) 
                    {
                            scanf("%d",&x);
                            a[++n]=x;
                            b[n]=b[n-1]+x;
                    }
                    else
                    {
                            int l=1,r=n;
                            while (r-l>10)
                            {
                                int len=(r-l+1)/3,mid1=l+len,mid2=mid1+len;
                                if (check(mid1)<check(mid2)) r=mid2;
                                else l=mid1;
                            }
                            double ans=0;
                            for (i=l;i<=r;++i) ans=max(ans,a[n]-check(i));
                            printf("%.10lf
    ",ans);
                    }
            }
    }

    例一
    初始有一个为空的集合,要求支持两种操作
    1.不断向集合中插入一个数,且这个数比集合中所有数都大
    2.在集合中找一个子集,使得找到的子集S中的最大值减去子集S中元素的平均值的差最大,并输出这个差

    操作数500000

    最大值肯定要选,可以自己证明一下

    然后使其他数尽可能小

    如何选取子集?
    最后插入的这个数是一定要选的,然后再选小的数,就是一个最大数加上几个用来拉低平均值的小数构成了所需子集

    小数一定是从最小值开始连续增加使平均值减小,直到达到一个临界点,再增加小数就会使平均值增大,易知这是一个单峰函数

    因此考虑三分选多少小数即可

     

    题解:

    让上线稍微大一些

    check:取前k个的和

    (前x-1小的数+a[n])/x

    main:

    其他的三分就是更改check函数

    分治的思想
    将一个问题划分成若干个(一般都是分成俩)子问题
    分别解决每个子问题后(也可能是前,还可能一前一后之类
    的)
    将各个子问题组合起来得到原问题的答案。

    快速幂
    如何快速计算X k

     

    我们将k进行二进制拆分。
    比如我们需要计算X 11即我们需要计算X 20+21+23
    因此我们只需要计算logk 次即可

     

    归并排序
    基本思想:先将整个数组分成两个部分,分别将两个部分排好序,然后将两个排好序的数组O(n)合并成一个数组。

    我们将问题分为两个阶段:分、治

     


    对于每个长度> 1的区间,拆成两个[l, mid]区间和[mid + 1r]区间

    直接递归下去



    我们认为在处理区间[l,r]时,已经有[l,mid][mid+1,r]内分别有序 

    这一次的操作就是合并两个有序序列,成为一个新的长有序序列

    用两个指针分别指向左右分别走到哪了即可

    比较两个指针指向的值

     复杂度O(nlogn)是一个严格的算法

     

    逆序对
    给定一个1 n的排列,求逆序对数量。
    1 n 105
    逆序对:对于1 x < y n, A[x] > A[y],则称(x,y)为一个逆序对。 

    题解
    首先显然我们枚举x,y可以做到O(N2)
    分治:
    假设当前问题 Work(l,r) 是求lr区间内的逆序对数量。
    讨论所有(x,y)可能在的位置:
    l x < y mid :子问题Work(l,mid)
    x mid < y : ???
    mid + 1 x < y r :子问题Work(mid+1,r)

     

    对于每个mid右边的数,我们要找到mid左边有多少比它大的数。

    1) 对左侧排序,右侧在左侧上二分即可。 总时间复杂度O(nlog2n)

    2) 归并排序:
    对于数组A和数组B的归并过程,每当我们将B中的元素取出时:
    说明A中最小的元素比该元素大:说明A中所有元素比该元素大:说明 答案+=A.size() 

     

    归并过程时间复杂度O(n),总时间复杂度O(nlogn)

     

    例二
    有一个序列,初始时只有一个数n
    对于序列中每一个> 1的数,拆分成三个数n/2,n%2,n/2并替换原数。
    直到序列中没有> 1的数为止
    查询最终序列中[l, r]中有多少1
    0 n < 250, 0 r - l 105

    平面最近点对
    给定二维平面上的N个点,求任意两点间的最近距离(欧几
    里得距离)。
    1 n 105

     

    题解
    不妨按照x坐标排序。对于区间[l,r],我们将其分成mid左右
    两个部分。
    两个点都在左侧:子问题Work(l,mid)
    两个点都在右侧:子问题Work(mid+1,r)
    两个点一个在左侧,一个在右侧 :

     不妨按照x坐标排序。对于区间[l,r],我们将其分成mid左右
    两个部分。
    两个点都在左侧:子问题Work(l,mid)
    两个点都在右侧:子问题Work(mid+1,r)
    两个点一个在左侧,一个在右侧:
    重点考虑第三种情况
    不妨假设左右两个子问题的答案为ans。则我们只需要考虑
    分界线两边距离不超过ans以内的点即可。

    不妨假设左右两个子问题的答案为ans。则我们只需要考虑
    分界线两边距离不超过ans以内的点即可。

    不妨假设左右两个子问题的答案为ans。则我们只需要考虑
    分界线两边距离不超过ans以内的点即可。

    对于每个点,可能和它距离不超过ans的点坐标范围
    横坐标:[mid-ans,mid+ans]
    纵坐标:[y-ans,y+ans]

    每个小正方形内点数不可能超过一个(因为任意两点距离不
    低于ans)。故总点数不超过6个。除去该点自身,该点至多
    需要和其他6个点求距离。
    故该部分复杂度不超过O(n)。实现时可以直接对所有点按
    y坐标排序,O(n log2 n),或者使用归并排序的技巧,直
    O(n log n)即可。

     

  • 相关阅读:
    编译JDK12
    Hexo Next背景动画Canvas_nest:true设置无效
    素数
    Linux 命令行样式
    四种排序算法实现
    Java中的简单容器
    一个关于2的次方问题
    LeetCode--20.有效的括号
    Java中的注释
    关于Metasploitable的测试
  • 原文地址:https://www.cnblogs.com/lcezych/p/10784996.html
Copyright © 2020-2023  润新知