算法6:贪心


一、区间问题

1.1 例1:区间选点

给定 $N$ 个闭区间 $[a_i,b_i]$,请你在数轴上选择尽量少的点,使得每个区间内至少包含一个选出的点,输出选择的点的最小数量

注:位于区间端点上的点也算作区间内。

算法步骤

  1. 将每个区间按右端点从小到大排序
  2. 从前往后依次枚举每个区间
    • 如果当前区间中已经包含点,则直接pass
    • 如果当前区间中没有包含点,选择当前区间的右端点[选择当前的最好情况]

区间选点示意图

证明:

Case 1:$Ans\leq cnt$,其中$Ans$表示所有方案中的最小值,$cnt$表示一种可行解

Case 2:$Ans\geq cnt$,$Ans$至少要$cnt$个点覆盖掉$cnt$个区间

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 100010;

int n;
struct Range
{
    int l, r;
    bool operator< (const Range &W)const
    {
        return r < W.r;
    }
}range[N];

int main()
{
    scanf("%d", &n);
    for (int i = 0; i < n; i ++ ) scanf("%d%d", &range[i].l, &range[i].r);

    sort(range, range + n);

    int res = 0, ed = -2e9;
    for (int i = 0; i < n; i ++ )
        if (range[i].l > ed)
        {
            res ++ ;
            ed = range[i].r;
        }

    printf("%d\n", res);

    return 0;
}

注:C++语法知识:

[1] 运算符重载

返回类型 operator@ (参数表)const
{
// 函数体
}

const修饰函数,表明在该函数体内,不能修改对象的数据成员而且不能调用非const函数,用于保护数据。

有两种条件时

bool operator< (const location &W)const
{
    	if(path != W.path)
        	return path < W.path;
    	return id < W.id;			// 路径相等按照序号排列
}

[2] sort函数的两个参数first,last,将[first, last)区间内元素升序排列。【注意区间为左闭右开】

1.2 例2:区间分组

给定 $N$ 个闭区间 $[a_i,b_i]$,请你将这些区间分成若干组,使得每组内部的区间两两之间(包括端点)没有交集,并使得组数尽可能小。

输出最小组数。

算法步骤

  1. 将每个区间按左端点从小到大排序
  2. 从前往后处理每个区间,依次判断能否将其放到某个现有的组中。若$L[i]<=Max_r(组中区间的最右端)$,则有交集
    • 如果不存在这样的组,则开新组,然后再将其放进去
    • 如果存在这样的组,将其放入组中,并更新当前组的$Max_r$

证明:

Case 1:$Ans\leq cnt$,其中$Ans$表示所有方案中的最小值,$cnt$表示一种可行解

Case 2:$Ans\geq cnt$,$cnt$区间有公共点

证明图解

我们可以用一个小根堆来存储每个组的最大右端点值。

即若 $a[-1,1],b[2,3]$ 两个区间为一组,那么将该区间的最大右端点 3 放到小根堆内。

若目前查看的区间的左端点大于小根堆堆顶的值(range[i].l > heap.top()),那么至少说明,当前区间能放到这个组中,则将堆顶元素pop出来,堆中存入新的最大右端点代表这个组;

否则(heap.empty() || heap.top() >= r.l),需要新开一个组,直接将当前区间的右端点 push 进堆即可。

#include <iostream>
#include <algorithm>
#include <queue>

using namespace std;

const int N = 100010;

int n;
struct Range
{
    int l, r;
    bool operator< (const Range &W)const
    {
        return l < W.l;
    }
}range[N];

int main()
{
    scanf("%d", &n);
    for (int i = 0; i < n; i ++ )
    {
        int l, r;
        scanf("%d%d", &l, &r);
        range[i] = {l, r};
    }

    sort(range, range + n);

    priority_queue<int, vector<int>, greater<int>> heap;		// 小根堆
    for (int i = 0; i < n; i ++ )
    {
        auto r = range[i];
        if (heap.empty() || heap.top() >= r.l) heap.push(r.r);	// 新的组
        else
        {
            heap.pop();			// 堆顶删除
            heap.push(r.r);		// 新的右端点加到堆中
        }
    }

    printf("%d\n", heap.size());

    return 0;
}

1.3 例3:区间覆盖

给定 $N$ 个闭区间 $[a_i,b_i]$ 以及一个线段区间 $[s,t]$,请你选择尽量少的区间,将指定线段区间完全覆盖。

输出最少区间数,如果无法完全覆盖则输出 $−1$。

算法步骤

  1. 将每个区间按左端点从小到大排序
  2. 从前往后依次枚举每个区间,在所有能覆盖$start$的区间中,选择右端点最大的区间,然后将$start$更新成右端点的最大值

任何一最优解,都能通过替换变成算法得出来的解,每一步替换都是合法的,则$Ans = cnt$

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 100010;

int n;
struct Range
{
    int l, r;
    bool operator< (const Range &W)const
    {
        return l < W.l;
    }
}range[N];

int main()
{
    int st, ed;
    scanf("%d%d", &st, &ed);
    scanf("%d", &n);
    for (int i = 0; i < n; i ++ )
    {
        int l, r;
        scanf("%d%d", &l, &r);
        range[i] = {l, r};
    }

    sort(range, range + n);

    int res = 0;
    bool success = false;
    for (int i = 0; i < n; i ++ )
    {
        int j = i, r = -2e9;
        while (j < n && range[j].l <= st)	// 遍历所有左端点在start左边的区间,记录右端点的最大值
        {
            r = max(r, range[j].r);
            j ++ ;
        }

        if (r < st)			// 无解
        {
            res = -1;
            break;
        }

        res ++ ;
        if (r >= ed)
        {
            success = true;
            break;
        }

        st = r;
        i = j - 1;
    }

    if (!success) res = -1;
    printf("%d\n", res);

    return 0;
}

二、Huffman树

例:合并果子

#include <iostream>
#include <algorithm>
#include <queue>

using namespace std;

int main()
{
    int n;
    scanf("%d", &n);

    priority_queue<int, vector<int>, greater<int>> heap;
    while (n -- )
    {
        int x;
        scanf("%d", &x);
        heap.push(x);
    }

    int res = 0;
    while (heap.size() > 1)
    {
        int a = heap.top(); heap.pop();		// 取出最小的两个元素
        int b = heap.top(); heap.pop();
        res += a + b;
        heap.push(a + b);
    }

    printf("%d\n", res);
    return 0;
}

三、不等式

3.1 排序不等式

例:排队打水

有 $n$ 个人排队到 $1$ 个水龙头处打水,第 $i$ 个人装满水桶所需的时间是 $t_i$,请问如何安排他们的打水顺序才能使所有人的等待时间之和最小。

每个人耗的时间分别为$t_1,t_2,…,t_n$,则总时间=$t_1(n-1)+t_2(n-2)+…+0$

按照从小到大的顺序排队,总时间最小。

#include <iostream>
#include <algorithm>

using namespace std;

typedef long long LL;

const int N = 100010;

int n;
int t[N];

int main()
{
    scanf("%d", &n);
    for (int i = 0; i < n; i ++ ) scanf("%d", &t[i]);

    sort(t, t + n);
    reverse(t, t + n);

    LL res = 0;
    for (int i = 0; i < n; i ++ ) res += t[i] * i;		// 不reverse则每次乘n - i + 1

    printf("%lld\n", res);

    return 0;
}

3.2 绝对值不等式

例:货仓选址

在一条数轴上有 $N$ 家商店,它们的坐标分别为 $A_1\sim A_N$。

现在需要在数轴上建立一家货仓,每天清晨,从货仓到每家商店都要运送一车商品。

为了提高效率,求把货仓建在何处,可以使得货仓到每家商店的距离之和最小。

$f(x)=|x_1-x|+|x_2-x|+…+|x_n-x|$的最小值

算法示意图

分组思想

$f(x)=|x_1-x|+|x_2-x|+…+|x_n-x|\\=(|x_1-x|+|x_n-x|)+(|x_2-x|+|x_{n-1}-x|)+…\\\geq x_n-x_1+x_{n-1}-x_{2}+…$

当$x$取到中位数时(或中间两个数之间),可以取到等号

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 100010;

int n;
int q[N];

int main()
{
    scanf("%d", &n);

    for (int i = 0; i < n; i ++ ) scanf("%d", &q[i]);

    sort(q, q + n);

    int res = 0;
    for (int i = 0; i < n; i ++ ) res += abs(q[i] - q[n / 2]);

    printf("%d\n", res);

    return 0;
}

四、推公式

例:耍杂技的牛

农民约翰的 $N$ 头奶牛(编号为 $1…N$)计划逃跑并加入马戏团,为此它们决定练习表演杂技。

奶牛们不是非常有创意,只提出了一个杂技表演:

叠罗汉,表演时,奶牛们站在彼此的身上,形成一个高高的垂直堆叠。

奶牛们正在试图找到自己在这个堆叠中应该所处的位置顺序。

这 $N$ 头奶牛中的每一头都有着自己的重量 $W_i$ 以及自己的强壮程度 $S_i$。

一头牛支撑不住的可能性取决于它头上所有牛的总重量(不包括它自己)减去它的身体强壮程度的值,现在称该数值为风险值,风险值越大,这只牛撑不住的可能性越高。

任务是确定奶牛的排序,使得所有奶牛的风险值中的最大值尽可能的小。

结论:按照$W_i+S_i$从小到大的顺序排,最大的危险系数一定是最小的。

证明:

  • 贪心得到的答案 >= 最优解

  • 贪心得到的答案 <= 最优解

    • 假设不按照此顺序排序,则必然存在有$W_i+S_i>W_{i+1}+S_{i+1}$

    • | | 第i个位置上的牛 | 第i+1个位置上的牛 |
      | :——: | :—————————————-: | :———————————————-: |
      | 交换前 | $W_1+W_2+…+W_{i-1}-S_i$ | $W_1+W_2+…+W_i-S_{i+1}$ |
      | 交换后 | $W_1+W_2+…+W_{i-1}-S_{i+1}$ | $W_1+W_2+…+W_{i-1}+W_{i+1}-S_i$ |

      将$W_1+…+W_{i-1}$删去并同时增加$S_i+S_{i+1}$项,整理得到:

    • | | 第i个位置上的牛 | 第i+1个位置上的牛 |
      | :——: | :——————-: | :———————-: |
      | 交换前 | $S_{i+1}$ | $W_{i}+S_{i}$ |
      | 交换后 | $S_{i}$ | $W_{i+1}+S_{i+1}$ |

    • 最大值都不会变大

#include <iostream>
#include <algorithm>

using namespace std;

typedef pair<int, int> PII;

const int N = 50010;

int n;
PII cow[N];

int main()
{
    scanf("%d", &n);
    for (int i = 0; i < n; i ++ )
    {
        int s, w;
        scanf("%d%d", &w, &s);
        cow[i] = {w + s, w};
    }

    sort(cow, cow + n);

    int res = -2e9, sum = 0;
    for (int i = 0; i < n; i ++ )
    {
        int s = cow[i].first - cow[i].second, w = cow[i].second;
        res = max(res, sum - s);
        sum += w;
    }

    printf("%d\n", res);

    return 0;
}

文章作者: ShiQuLiZhi
版权声明: 本博客所有文章除特别声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 ShiQuLiZhi !
评论
  目录