算法·贪心

贪心

基本原理

  • 核心:局部最优到全局最优
  • 贪心策略:使用贪心时采取的策略

适用条件

  • 异常广泛,不需要特别注意

注意事项

  • 贪心策略:必须具备无后效性,即某个状态以前的状态不会影响以后的状态

个人总结

贪心的精髓在于贪心策略的选取,目前已有明显的贪心策略包含:

  • 局部最优
  • 只考虑一侧
  • 动态局部最优(哈夫曼树类)
  • 搭积木模型

以下为例题

【深基12.例1】部分背包问题

题目描述

阿里巴巴走进了装满宝藏的藏宝洞。藏宝洞里面有 N ( N ≤ 100 ) N(N \le 100) N(N100) 堆金币,第 i i i 堆金币的总重量和总价值分别是 m i , v i ( 1 ≤ m i , v i ≤ 100 ) m_i,v_i(1\le m_i,v_i \le 100) mi,vi(1mi,vi100)。阿里巴巴有一个承重量为 T ( T ≤ 1000 ) T(T \le 1000) T(T1000) 的背包,但并不一定有办法将全部的金币都装进去。他想装走尽可能多价值的金币。所有金币都可以随意分割,分割完的金币重量价值比(也就是单位价格)不变。请问阿里巴巴最多可以拿走多少价值的金币?

输入格式

第一行两个整数 N , T N,T N,T
接下来 N N N 行,每行两个整数 m i , v i m_i,v_i mi,vi

输出格式

一个实数表示答案,输出两位小数

解题思路

  • 贪心策略:局部最优,优先装"性价比最高“的金币
  • 关键操作:对性价比(v/m)进行排序
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
int n;
double t,m, v, ans = 0;
struct coin {
    double m, v,p;//p是比例
    coin(double a, double b) :m(a), v(b), p(b / a){}
};
vector<coin>vec;
bool cmp(const coin&a,const coin&b) {
    return a.p > b.p;
}
void solve() {
    cin >> n >> t;
    while (n--) {
        cin >> m >> v;
        vec.push_back(coin(m, v));
    }
    sort(vec.begin(), vec.end(), cmp);
    for (int i = 0; i < vec.size(); i++) {
        if (t - vec[i].m >= 0) {
            t -= vec[i].m;
            ans += vec[i].v;
        }
        else {
            ans += vec[i].v * t / vec[i].m;
            break;
        }
    }
    printf("%.2f", ans);
}
signed main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(0); std::cout.tie(0);
    solve();
    return 0;
}

凌乱的yyy / 线段覆盖

题目背景

快 noip 了,yyy 很紧张!

题目描述

现在各大 oj 上有 n n n 个比赛,每个比赛的开始、结束的时间点是知道的。

yyy 认为,参加越多的比赛,noip 就能考的越好(假的)。

所以,他想知道他最多能参加几个比赛。

由于 yyy 是蒟蒻,如果要参加一个比赛必须善始善终,而且不能同时参加 2 2 2 个及以上的比赛。

输入格式

第一行是一个整数 n n n,接下来 n n n 行每行是 2 2 2 个整数 a i , b i   ( a i < b i ) a_{i},b_{i}\ (a_{i}<b_{i}) ai,bi (ai<bi),表示比赛开始、结束的时间。

输出格式

一个整数最多参加的比赛数目。

解题思路

  • 贪心策略:只考虑一侧(控制变量);
  • 题目本质:区间的交集问题,保证如果有多个区间有交集,即A∩B∩C∩D∩...!=空集,当作一次比赛
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
int n,ct;
struct date {
    int start, end;
    date(){}
    date(int a,int b):start(a),end(b){}
};
bool cmp(const date&a,const date&b) {
    return a.end < b.end;
}
void solve() {
    cin >> n;
    if (n == 1) {
        cout << 1; return;//特判
    }
    vector<date>vec(n, date());
    for (int i = 0; i < n; i++) {
        cin >> vec[i].start >> vec[i].end;
    }
    sort(vec.begin(), vec.end(), cmp);
    int pre = vec[0].end;
    for (int i = 1; i < n; i++) {
        if (vec[i].start >= pre) {//[0,2]和[2,4]交集为空
            pre = vec[i].end;
            ct++;//有交集只能算一个
        }
    }
    cout << ++ct;//自己也算一个交集
}
signed main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(0); std::cout.tie(0);
    solve();
    return 0;
}

[NOIP2004 提高组] 合并果子 / [USACO06NOV] Fence Repair G

题目描述

在一个果园里,多多已经将所有的果子打了下来,而且按果子的不同种类分成了不同的堆。多多决定把所有的果子合成一堆。

每一次合并,多多可以把两堆果子合并到一起,消耗的体力等于两堆果子的重量之和。可以看出,所有的果子经过 n − 1 n-1 n1 次合并之后, 就只剩下一堆了。多多在合并果子时总共消耗的体力等于每次合并所耗体力之和。

因为还要花大力气把这些果子搬回家,所以多多在合并果子时要尽可能地节省体力。假定每个果子重量都为 1 1 1 ,并且已知果子的种类 数和每种果子的数目,你的任务是设计出合并的次序方案,使多多耗费的体力最少,并输出这个最小的体力耗费值。

例如有 3 3 3 种果子,数目依次为 1 1 1 2 2 2 9 9 9 。可以先将 1 1 1 2 2 2 堆合并,新堆数目为 3 3 3 ,耗费体力为 3 3 3 。接着,将新堆与原先的第三堆合并,又得到新的堆,数目为 12 12 12 ,耗费体力为 12 12 12 。所以多多总共耗费体力 = 3 + 12 = 15 =3+12=15 =3+12=15 。可以证明 15 15 15 为最小的体力耗费值。

输入格式

共两行。
第一行是一个整数 n ( 1 ≤ n ≤ 10000 ) n(1\leq n\leq 10000) n(1n10000) ,表示果子的种类数。

第二行包含 n n n 个整数,用空格分隔,第 i i i 个整数 a i ( 1 ≤ a i ≤ 20000 ) a_i(1\leq a_i\leq 20000) ai(1ai20000) 是第 i i i 种果子的数目。

输出格式

一个整数,也就是最小的体力耗费值。输入数据保证这个值小于 2 31 2^{31} 231

解题思路

  • 贪心策略:哈夫曼树—树的带权路径和最小(我称之为动态局部最优)
  • 解题技巧:使用优先级队列模拟哈夫曼树.
  • 经典错误:优先合并小的策略是错误的,没有考虑到合并后数组的排序也应该动态变化;
    用贪心的原理解释是:合并后新的元素影响了原数组的排序,具有后效性
using namespace std;
using ll = long long;
int n,num,sum;
class cmp {
public:
    bool operator()(int a,int b) {
        return a > b;
    }
};
priority_queue<int, vector<int>, cmp>q;
void solve() {
    cin >> n;
    while (n--) {
        cin >> num;
        q.push(num);
    }
    while (q.size() >= 2) {
        int pre = q.top(); q.pop();
        int next = q.top(); q.pop();
        sum += pre + next;
        q.push(pre + next);
    }
    cout << sum;
}
signed main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(0); std::cout.tie(0);
    solve();
    return 0;
}

以下为练习

小A的糖果

题目描述

小 A 有 n n n 个糖果盒,第 i i i 个盒中有 a i a_i ai 颗糖果。

小 A 每次可以从其中一盒糖果中吃掉一颗,他想知道,要让任意两个相邻的盒子中糖的个数之和都不大于 x x x,至少得吃掉几颗糖。

输入格式

输入的第一行是两个用空格隔开的整数,代表糖果盒的个数 n n n 和给定的参数 x x x

第二行有 n n n 个用空格隔开的整数,第 i i i 个整数代表第 i i i 盒糖的糖果个数 a i a_i ai

输出格式

输出一行一个整数,代表最少要吃掉的糖果的数量。

样例 #1

样例输入 #1

3 3
2 2 2

样例 #2

样例输入 #2

6 1
1 6 1 2 0 4

个人代码

  • 贪心策略:考虑一侧
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
int n;
ll x,ans=0;
void solve() {
    cin >> n >> x;
    vector<ll>vec(n, 0);
    for (int i = 0; i < n; i++) {
        cin >> vec[i];
    }
    int temp = 0;
    for (int i = 0; i < n - 1; i++) {
        if (vec[i] + vec[i + 1] > x) {
            temp = vec[i + 1];
            vec[i + 1] = vec[i + 1] >= vec[i + 1] + vec[i] - x ? x-vec[i] : 0;
            ans += temp - vec[i + 1];
        }
    }
    for (int i = 1; i < n; i++) {
        if (vec[i] + vec[i - 1] > x) {
            temp = vec[i - 1];
            vec[i-1] = vec[i - 1] >= vec[i] + vec[i - 1] - x ? x-vec[i] : 0;
            ans += temp - vec[i - 1];
        }
    }
    cout << ans;
}
signed main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(0); std::cout.tie(0);
    solve();
    return 0;
}

[NOIP2013 提高组] 积木大赛

题目背景

NOIP2013 提高组 D2T1

题目描述

春春幼儿园举办了一年一度的“积木大赛”。今年比赛的内容是搭建一座宽度为 n n n 的大厦,大厦可以看成由 n n n 块宽度为 1 1 1 的积木组成,第 i i i 块积木的最终高度需要是 h i h_i hi

在搭建开始之前,没有任何积木(可以看成 n n n 块高度为 0 0 0 的积木)。接下来每次操作,小朋友们可以选择一段连续区间 [ l , r ] [l, r] [l,r],然后将第 L L L 块到第 R R R 块之间(含第 L L L 块和第 R R R 块)所有积木的高度分别增加 1 1 1

小 M 是个聪明的小朋友,她很快想出了建造大厦的最佳策略,使得建造所需的操作次数最少。但她不是一个勤于动手的孩子,所以想请你帮忙实现这个策略,并求出最少的操作次数。

输入格式

包含两行,第一行包含一个整数 n n n,表示大厦的宽度。

第二行包含 n n n 个整数,第 i i i 个整数为 h i h_i hi

输出格式

建造所需的最少操作数。

样例 #1

样例输入 #1

5
2 3 4 1 2

样例输出 #1

5

解题思路

  • 经典的搭积木问题:见下一题
using namespace std;
using ll = long long;
int n,ans=0;
void solve() {
    cin >> n;
    vector<int>vec(100009, 0);
    for (int i = 1; i <= n; i++) {
        cin >> vec[i];
    }
    for (int i = 1; i <= n; i++) {
        if (vec[i] > vec[i - 1])ans += vec[i] - vec[i - 1];
    }
    cout << ans;
}
signed main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(0); std::cout.tie(0);
    solve();
    return 0;
}

[NOIP2018 提高组] 铺设道路

题目背景

NOIP2018 提高组 D1T1

题目描述

春春是一名道路工程师,负责铺设一条长度为 n n n 的道路。

铺设道路的主要工作是填平下陷的地表。整段道路可以看作是 n n n 块首尾相连的区域,一开始,第 i i i 块区域下陷的深度为 d i d_i di

春春每天可以选择一段连续区间 [ L , R ] [L,R] [L,R] ,填充这段区间中的每块区域,让其下陷深度减少 1 1 1。在选择区间时,需要保证,区间内的每块区域在填充前下陷深度均不为 0 0 0

春春希望你能帮他设计一种方案,可以在最短的时间内将整段道路的下陷深度都变为 0 0 0

输入格式

输入文件包含两行,第一行包含一个整数 n n n,表示道路的长度。 第二行包含 n n n 个整数,相邻两数间用一个空格隔开,第 i i i 个整数为 d i d_i di

输出格式

输出文件仅包含一个整数,即最少需要多少天才能完成任务。

解题思路

  • 将数组几何化,本质上等价于堆积木,如果ai-1<ai,则在堆ai时顺便也完成了ai-1的工作
  • 例如堆第4列时已经完成了第3列的工作,只需要额外完成第4列多出来的工作,所以有ai-ai-1
    以下图例不是我的,引自大佬
  • ans+vec[1]是因为默认先搭建第一列,剩下所有的积木都是相对于第一列搭建的
    在这里插入图片描述
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
int n;
ll ans = 0;
vector<ll>vec(100009, 0);
void solve() {
    cin >> n;    
    for (int i = 1; i <= n; i++) {
        cin >> vec[i];
    }
    for (int i = 2; i <= n; i++) {
        if (vec[i] > vec[i - 1]) {
            ans += vec[i] - vec[i - 1];
        }
    }
    cout << ans+vec[1];
}
signed main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(0); std::cout.tie(0);
    solve();
    return 0;
}

跳跳!

题目描述

你是一只小跳蛙,你特别擅长在各种地方跳来跳去。
这一天,你和朋友小 F 一起出去玩耍的时候,遇到了一堆高矮不同的石头,其中第 i i i 块的石头高度为 h i h_i hi,地面的高度是 h 0 = 0 h_0 = 0 h0=0。你估计着,从第 i i i 块石头跳到第 j j j 块石头上耗费的体力值为 ( h i − h j ) 2 (h_i - h_j) ^ 2 (hihj)2,从地面跳到第 i i i 块石头耗费的体力值是 ( h i ) 2 (h_i) ^ 2 (hi)2
为了给小 F 展现你超级跳的本领,你决定跳到每个石头上各一次,并最终停在任意一块石头上,并且小跳蛙想耗费尽可能多的体力值。
当然,你只是一只小跳蛙,你只会跳,不知道怎么跳才能让本领更充分地展现。
不过你有救啦!小 F 给你递来了一个写着 AK 的电脑,你可以使用计算机程序帮你解决这个问题,万能的计算机会告诉你怎么跳。
那就请你——会写代码的小跳蛙——写下这个程序,为你 NOIp AK 踏出坚实的一步吧!

输入格式

输入一行一个正整数 n n n,表示石头个数。
输入第二行 n n n 个正整数,表示第 i i i 块石头的高度 h i h_i hi

输出格式

输出一行一个正整数,表示你可以耗费的体力值的最大值。

解题思路

  • 十年oi一场空,不开__见祖宗
  • 贪心策略:局部最优,优先选高度差距最大的石头跳
using namespace std;
using ll = long long;
int n;ll ans=0;
void solve() {
    cin >> n;
    vector<ll>vec(n, 0);
    for (int i = 0; i < n; i++) {
        cin >> vec[i];
    }
    sort(vec.begin(), vec.end());
    int l = 0, r = n - 1, cur = r;
    ans += vec[r] * vec[r];
    while (l < r) {
        if (cur > l) {
            ans += (vec[cur] - vec[l]) * (vec[cur] - vec[l]);
            r--;
            cur = l;
        }
        else {
            ans += (vec[r] - vec[cur]) * (vec[r] - vec[cur]);
            l++;
            cur = r;
        }
        /*cout << ans << endl;*/
    }
    cout << ans;
}
signed main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(0); std::cout.tie(0);
    solve();
    return 0;
}
  • 12
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Prim算法是一种求解加权无向连通图的最小生成树的算法。其基本思想是从一个点开始,每次选择一个与当前生成树相邻且权值最小的边,直到生成一棵包含所有节点的树为止。具体步骤如下: 1. 任选一个起始点,将其加入生成树中。 2. 找到与生成树相邻的所有边中权值最小的那条边,将其加入生成树中。 3. 重复第二步,直到生成一棵包含所有节点的树为止。 下面是一个使用Prim算法求解最小生成树的Python代码示例: ```python def prim(graph): # 初始化 nodes = list(graph.keys()) visited = [nodes[0]] edges = [] # 循环直到所有节点都被访问 while len(visited) < len(nodes): min_edge = None # 找到与已访问节点相邻的所有边中权值最小的那条边 for node in visited: for neighbor, weight in graph[node].items(): if neighbor not in visited: if min_edge is None or weight < min_edge[2]: min_edge = (node, neighbor, weight) # 将该边加入生成树中 edges.append(min_edge) visited.append(min_edge[1]) return edges ``` 贪心算法是一种在求解最优解问题时采用贪心策略的算法。其基本思想是每次选择当前看来最优的解决方案,直到达到全局最优解。贪心算法通常适用于满足最优子结构性质的问题,即问题的最优解可以通过子问题的最优解推导得到。具体步骤如下: 1. 将问题分解为若干个子问题。 2. 对每个子问题求解,得到子问题的最优解。 3. 将所有子问题的最优解合并成原问题的解。 下面是一个使用贪心算法求解背包问题的Python代码示例: ```python def knapsack(items, capacity): # 按照单位价值排序 items = sorted(items, key=lambda x: x[1]/x[0], reverse=True) # 初始化 total_value = 0 knapsack = [] # 循环直到背包装满 for item in items: if capacity >= item[0]: knapsack.append(item) capacity -= item[0] total_value += item[1] else: knapsack.append((capacity, item[1]/item[0]*capacity)) total_value += item[1]/item[0]*capacity break return knapsack, total_value ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值