基本算法之贪心算法

贪心算法

防嗮

题目链接

解题思路:

我们首先将奶牛可以承受的最小值,递减排序,也就是降序排列,然后将防晒霜固定的值,递减排序,还是降序排列.
对于每一个头奶牛而言,当然是要选择目前来说满足条件的最差的防晒霜,什么最差的定义,就是选择满足奶牛条件的SPF最大的那一瓶防晒霜.
注意:降序排序,保证对于每一头牛而言,它用的是,可以使用的最差的防晒霜,因为值越小的防晒霜,有可能让更多的牛使用.而升序排列,就恰好反了. 注意:如果一头牛没有合适的防嗮霜给它了就不用管他了。

难点:

  • 如何高效的找到小于等于一个值的最大值
  • 如何动态删除一个元素。(用完了要删除)
  • 区间排序

技巧:

  • 以上两个操作是平衡树的两个基本操作因此我们可以使用平衡树来解决。在STL中有map 和 set
  • 对于区间来说我们可以使用pair ,因为pair 内部安排了默认的排序方式:以first 为关键字 升序排列。
  • 设置哨兵是为了让查找不为空不返回空。

拓展知识点:

增广路径: 预先选一些匹配的边,从任一个不在匹配的点,先沿着非匹配边,再沿着匹配边走,。。。,交替,最后找到一个不在匹配里的点。(就找到了一个增广路径)。 如果一个匹配没有增广路径,则达到了最大匹配。

代码:

#include<stdio.h>
#include<map>
#include<algorithm>
using namespace std;

typedef pair<int ,int > pll;
const int N = 2510;

int n ,m;
pll cows[N];

int main(){
    scanf("%d%d",&n,&m);
    for(int i=0;i<n;++i)scanf("%d%d",&cows[i].first,&cows[i].second);
    sort(cows,cows+n);
    map<int,int >spfs;
    for(int i=0;i<m;++i){
        int x,cover;
        scanf("%d%d",&x,&cover);
        spfs[x] += cover;
    }
    int ans = 0;
    spfs[0] = spfs[1001] = n; // 哨兵,避免使用 查找函数时出错
    for(int i = n-1;i >= 0;--i){
        auto cow = cows[i];
        // 大于的最小的值
        auto it = spfs.upper_bound(cow.second); // 以区间的右边找 ,难点一
        it--; // 找到小于等于的最大值
        if(it->first >= cow.first && it->first <= cow.second){
            ans ++;
            if(-- it->second == 0)
                spfs.erase(it); // 难点二
        }
    }
    printf("%d\n",ans);
    return 0;    
}

畜栏预定

题目链接

解题思路:

  • 将所有牛按开始吃草的时间排序;
  • 用小根堆维护当前所有畜栏的最后一头牛的吃草结束时间;
  • 如果当前的牛可以安排在堆顶的畜栏中,则将其安排进去,否则就新建一个畜栏;

难点

  • 对于每头牛我们既要记录区间,还要记录编号。
  • 小根堆(即围栏)的更新操作。
  • 记录每头牛进入的围栏的编号。

技巧

  • 对于第一个难点我们可以用 pair<pair<int,int>,int>的方式来存储。
  • 对于第二个难点,我们小根堆里的元素类型也是 pair 这样我们记录两个元素一个了一头牛吃完草结束的时间,另一个是围栏的编号。所以在更新操作的时候我们把顶部的围栏拿出来修改得时候先把其删除,在更新结束时间的时候,把原来围栏的编号记录下。
  • 因为我们每头牛都有一个编号,而围栏也有,所以我们用一个数组来存储这两个对应的关系。其中牛的编号就是 0~n-1 ,围栏编号就是创建时小根堆中围栏元素个数。

代码:

#include<stdio.h>
#include<vector>
#include<algorithm>
#include<queue>

using namespace std;

typedef pair<int ,int> PII ;

pair <PII,int> cows[50010];
int n;
int id[50010];

int main (){
    scanf("%d",&n);
    for(int i = 0;i<n;++i){
        // 难点一就是如何存这么多元素
        scanf("%d%d",&cows[i].first.first,&cows[i].first.second);
        cows[i].second = i;
    }
    sort(cows,cows+n);
    priority_queue<PII,vector<PII>,greater<PII>>heap;
    for(int i=0;i<n;++i){
        auto cow = cows[i].first;
        // 新建一个围栏
        if(heap.empty() || heap.top().first >= cow.first){
            PII stall = {cow.second ,heap.size() + 1 };
            id[cows[i].second] = stall.second;
            heap.push(stall);
        }else{
            // 难点二,更新围栏
            PII stall = heap.top();
            heap.pop();
            stall.first = cow.second;
            id[cows[i].second] = stall.second;
            heap.push(stall);
        }
    }
    printf("%d\n",heap.size());
    for(int i = 0;i <n ;++i)
        printf("%d\n",id[i]);

    return 0;
}

雷达设备

题目链接

解题思路
如下图所示,对于任意一个小岛 (x,y)(x,y),我们都可以在海岸线上求出能覆盖该小岛的建造雷达的区间 [a,b]。
基本算法之贪心算法
由勾股定理,将所有小岛转化成区间后,问题转化为:给定 n 个区间,在 x轴上选择尽量少的点,使得所有区间至少包含一个点。

算法步骤:

  1. 将所有区间按右端点从小到大排序;
  2. 依次考虑每个区间:
    如果当前区间包含最后一个选择的点,则直接跳过;
    如果当前区间不包含最后一个选择的点,则在当前区间的右端点的位置选 一个新的点;

代码:

#include<stdio.h>
#include<algorithm>
#include<math.h>
using namespace std;

typedef pair<double,double> PDD;
const double eps = 1e-6 , INF = 1e10;
int n,R;
PDD segs[1010];
bool success ;
int main(){
    success = 1;
    scanf("%d%d",&n,&R);
    for(int i = 0;i< n;++i){
        int x,y;
        scanf("%d%d",&x,&y);
        if(y > R){
            success = 0;
            break;
        }
        double len = sqrt(R * R - y * y );
        segs[i] = {x + len,x - len};
    }
    if(!success) puts("-1");
    else{
        sort(segs,segs + n);
        int ans = 0;
        double pos = - INF;
        for(int i = 0;i<n;++i){
            auto seg = segs[i];
            if(seg.second - pos > eps){
                ans ++ ;
                pos = seg.first;
            }
        }
        printf("%d\n",ans);
    }
    return 0;
}

给树染色

题目链接

解题思路
每次找出当前权值最大的非根节点,将其染色顺序排在紧随父节点之后的位置,然后将该点合并进父节点中,更新父节点的权值。直到将所有点都合并进根节点为止。

如果直接按上述算法做的话,最终的分值不太容易计算,我们可以在将点合并的时候,实时更新当前的权值和:

  • 最初所有点各自为一组,总分值是 S = ∑ i = 1 n a i ∗ 1 S=\sum_{i=1}^n a_i∗1 S=∑i=1n​ai​∗1;
  • 接下来每次会将两组点合并,将其中一组点接在另一组点的后面。比如两组点分别是 x i x_i xi​和 y i y_i yi​,我们将 y i y_i yi​接在 x i x_i xi​ 之后,则 y i y_i yi​中每个点所乘的系数均会增加一个相同的偏移量,这个偏移量就是 x i x_i xi​中点的个数,假设是 k,则合并之后,总的权值直接加上 k ∗ ∑ y i k∗∑y_i k∗∑yi​即可;

难点:

  • 如何合并节点
  • 为什么可以这样简化

如何合并节点

基本算法之贪心算法
基本算法之贪心算法
因此可以看到只要将父节点的关系转化以下就好。

为什么可以这样简化、

我们看把 y 的一堆节点放到x 后边 ,x 的值是不会变得,而 y 每一个节点的值相当于 乘上 x 中 的节点个数。
基本算法之贪心算法

代码:

#include<stdio.h>
#include<algorithm>

using namespace  std;

const int N = 1010;
struct node{
    int father,size,sum;
    double avg;
}nodes[N];

int n ,root;
// 根据 平均值,找到最大的点
int find(){
    int pos = -1 ;
    double maxx = 0;
    for(int i=  1;i <= n ;++i){
        if( i != root && maxx < nodes[i].avg){
            maxx = nodes[i].avg;
            pos =  i;
        }
    }
    return pos;
}


int main(){
    int ans = 0;
    scanf("%d%d",&n,&root);
    for(int i = 1;i <=n; ++i)
    {
        node &nd =  nodes[i];
        scanf("%d",&nd.sum);
        nd.size = 1 ;
        nd.avg = nd.sum;
        ans += nd.sum; // 
    }
    // 建立关系
    for(int i = 0, a, b;i< n-1;++i ){
        scanf("%d%d",&a,&b);
        nodes[b].father = a;
    }
    // 遍历 n - 1 次
    for(int i = 0;i< n -1; ++ i){
        int ver = find();
        int fa = nodes[ver].father;
        // 当前节点的 总和 乘上 父节点 的大小
        ans += nodes[ver].sum * nodes[fa].size;
        // 用完这后将该节点删除,
        //因为 找节点是用find () 函数 依据avg 值找,所以置为 -1 就不会用到了
        nodes[ver].avg = -1;
        // 难点 :合并节点,遍历所有节点。
        // 将原本是其 子节点 的直接连上其父节点即可
        for(int  j = 1 ;  j <= n ;++j)
            if(nodes[j].father == ver)
                nodes[j].father = fa;
        // 难点三 : 更新父节点的信息
        nodes[fa].size += nodes[ver].size ;
        nodes[fa].sum  +=  nodes[ver].sum;
        nodes[fa].avg = (double)nodes[fa].sum / nodes[fa].size;
    }
    printf("%d\n",ans);


    return 0;
}

写在最后,感谢y总的讲解。

上一篇:【转】2018苹果开发者账号申请流程


下一篇:字符串的陷阱-记对字符串大小比较的错误理解