C++贪心算法学习&练习

C++贪心算法学习&练习

  • 写在前面
  • 例子引入
    • 什么是贪心算法?
  • 做题
    • 合并果子
    • 数列分段Section I
    • 混合牛奶 Mixing Milk
    • 排队接水

写在前面

这篇是按照link来写的,记录一下学习过程~

例子引入

找给黄焖鸡6元3角钱,我们只有2元5角、1元和1角的硬币,贪心算法:先找一个面值不超过6元3角的最大硬币,即2元5角,然后继续这么做,选出了另一个两元五角,选出了一个1元,选出了三个1角,就是每次要么一直找最小的,要么一直找最大的。

什么是贪心算法?

1.当一个问题的最优解包含其子问题的最优解时,它可能时动态规划问题或是贪心算法问题。(最优子结构性质)(我的书上写的是最优性原理)
2.若整体最优解可以用过一系列局部最优的选择来达到,则为贪心算法问题。(贪心选择性质)
3.增长型,不回溯。
4.个人认为最难的地方在于如何不暴力地找到下一个最值。

最优子结构
贪心选择
贪心选择能达到最优解
非贪心选择
贪心选择不能达到最优解
贪心算法问题
动态规划问题

常见的贪心算法:单源最短路径、最小生成树、哈夫曼编码
判断是否用贪心算法:找非最优解,如果用贪心算法找到一个不是最优解就使用动态规划算法。
如果写贪心算法写的特别复杂,就要考虑适不适合了哈哈哈,因为它比较简单。
查了一下,某乎上说绝大多数贪心算法的问题可以用动态规划做,不清楚,明天问一下老师。

做题

合并果子

题目:在一个果园里,按果子的不同种类分成了不同的堆。合并规则:每一次合并,可以把两堆果子合并到一起,消耗的体力等于两堆果子的重量之和。合并果子时总共消耗的体力等于每次合并所耗体力之和,要尽可能地节省体力,假定每个果子重量都为 1,输出最小体力耗费值。
例如有 3 种果子,数目依次为 1,2,9。可以先将 1、2 堆合并,新堆数目为 3,耗费体力为 3。接着,将新堆与原先的第三堆合并,又得到新的堆,数目为 12,耗费体力为 12。总共耗费体力=3+12=15。
输入格式:第一行整数 n,种类数。第二行包含 n 个整数,用空格分隔,第i个整数 a i a_i ai是第 i 种果子的数目。
输出格式:输出包括一行,最小的体力耗费值。输入数据保证这个值小于 2 31 2^{31} 231
数据范围:1≤n≤10000, 1≤ a i a_i ai≤20000
example:
input:
3
1 2 9
output:
15
这题是构造哈夫曼树,每次找一列数中最小的两个数相加后在找到相应的位置相加,最后的值为最小体力耗费值,哈夫曼树是经典的贪心算法。先将数目sort排序,选取最小a[i]和a[i+1]的两个相加后然后将和插入从a[i+2]到a[n-1]的数组,然后重复选取a[i+2]开始的数组,直到待选取数组只有一个值。后面的过程也可以sort排序.

#include <iostream>
#include <algorithm>
using namespace std;

int Merge_Fruit(int  a[],int n) {
     
	sort (a, a + n);
	int num=0;
	for (int i = 0; i <n-1; ++i) {
     
		a[i + 1] += a[i];
		num+=a[i+1];
		sort(a+i+1, a + n);
	}
	return num;
}

int main() {
     
	int n;
	cin >> n;
	int fruit[100] = {
      0 };
	for (int i = 0; i < n; ++i) {
     
		cin >> fruit[i];
	}
	cout<<Merge_Fruit(fruit, n);
}

后面的部分使用直接插入插入会降低它的时间消耗,可以将后面的部分从O(nlogn)减小到O(n)。

#include <iostream>
#include <algorithm>
using namespace std;

int Merge_Fruit(int  a[],int n) {
     
	sort (a, a + n);
	int num=0;
	for (int i = 0; i <n-1; ++i) {
     
		a[i + 1] += a[i];
		num+=a[i+1];
		int j=i+2;
		while(a[j]<=a[i+1]&&j<n){
     
            j++;
		}
		//a[i+1]插到a[j-1]和a[j]之间
		int temp=a[i+1],k;
		for(k=i+1;k<j-1;k++){
     
            a[k]=a[k+1];
		}
		a[k]=temp;
	}
	return num;
}

int main() {
     
	int n;
	cin >> n;
	int fruit[100] = {
      0 };
	for (int i = 0; i < n; ++i) {
     
		cin >> fruit[i];
	}
	cout<<Merge_Fruit(fruit, n);
}

这里视频里提到了优先队列# i n c l u d e < q u e u e > include include<queue>每次插入时,可以自动帮你排序插好,根据C++ queue使用方法详细介绍和STL里的priority_queue用法总结又写了一个版本。

#include <iostream>
#include <vector>
#include <stdlib.h>
#include <stdio.h>
#include <math.h>
#include <algorithm>
#include <queue>
using namespace std;

int Merge_Fruit(priority_queue<int, vector<int>, greater<int> >  a, int n) {
     
	int num = 0, first, second;
	while(a.size()>1) {
     
		first = a.top();
		a.pop();
		second = a.top();
		a.pop();
		num += first + second;
		a.push(first + second);
	}
	return num;
}

int main() {
     
	int n, temp;
	cin >> n;
	priority_queue<int, vector<int>, greater<int> >fruit;
	for (int i = 0; i < n; ++i) {
     
		cin >> temp;
		fruit.push(temp);
	}
	cout << Merge_Fruit(fruit, n);
}

数列分段Section I

题目:对于给定的一个长度为N的正整数数列 A i A_i Ai,现要将其分成练习的若干段,并且每段和不超过M(可以等于M),问最少能将其分成多少段使得满足要求。
输入格式:第1行包含两个正整数N,M,表示了数列 A i A_i Ai的长度与每段和的最大值,第2行包含N个空格隔开的非负整数 A i A_i Ai,如题目所述。
输出格式:一个正整数,输出最少划分的段数。
example:
input:
5 6
4 2 4 5 1
output:
3
由于每段需要连续,所以每次分段的和是不超过M的最大和,从输入的左边开始,一旦连续子段和超过M就进行分段,分段数加一。

#include <iostream>
using namespace std;

int main() {
     
	int n, m,num=0,k=0,x;
	cin >> n >> m;
	for (int i = 1; i <= n; i++) {
     
		cin >> x;
		num += x;
		if (num > m) {
     
			k++;
			num = x;
		}
	}
	if(num!=0) k++;
	cout << k;
}

使用while循环:

#include <iostream>
using namespace std;

int main() {
     
	int n, m,num=0,k=0,x;
	cin >> n >> m;
	while(cin>>x){
     
		num += x;
		if (num > m) {
     
			k++;
			num = x;
		}
	}//因为是正整数,所以只要输入num就不会为0
	//当num==0时,没有输入,k=0;
	if(num!=0)  k++;
	cout << k;
}

这里主要需要n没起到什么作用,因为没有索引,注意终止输入时可以Ctrl+Z/D/Y…我目前试了这三个都行,C/F不行,是特殊快捷键。

混合牛奶 Mixing Milk

降低乳制品原材料牛奶的价格,找到最优的牛奶采购方案。从奶农手中采购牛奶,每位奶农提供的牛奶价格不同,由于奶牛产奶量每天不是无限制的,所以每位奶农每天能提供的牛奶的数量是一定的,所以只能采购到小于等于奶农最大提供量的整数数量的牛奶。(假设每天所有奶农的总产量大于采购需求量)
输入:第一行两个整数n,m,表示需要的牛奶总量和奶农个数。接下来m行,没两行整数 p i p_i pi a i a_i ai,表示第i个农民牛奶的单价,和农民i一天最多能卖的牛奶量。
输出:单独一行包含一个整数,表示拿到满足(>=)所需的牛奶所要的最小费用。
example:
input:
100 5
5 20
9 10
3 10
8 80
6 30
output:
630
该问题属于背包问题,但不是0/1背包问题,因为可以只取一部分,不全部取走。每次都取单价最低的,就可使总费用最低了。
这里总结一下背包问题的解法:离散的都使用动态规划算法,因为很难设计一个贪心算法找到最优解,连续的可以使用贪心算法。什么叫做离散的呢,比如把十种物品放入有最大承受重量的背包,他们有着不同的重量和价值,0/1背包就是说每种物品只有一件,完全背包是指每种物品有无数件,这两种问题中,单个物品是不可拆分的,就是离散的,像是整块黄金。而连续的是都是黄金粉,你想拿多少拿多少,不够了再从"好"的里面抓一点就可以了,这样的问题用贪心法就可以解决。本题中牛奶可以一份一份的买,如果不够了,我们再补上缺少的份数即可。属于部分背包问题。
综上:0/1背包、完全背包使用动态规划,部分背包使用贪心算法。
这里用了vector存放结构体变量,参考vector容器中存放结构体变量当然直接用数组更简单,我是想多练习一下vector

#include <iostream>
#include <vector>
#include <stdlib.h>
#include <stdio.h>
#include <math.h>
#include <algorithm>
#include <queue>
using namespace std;

struct Dairy_farmer {
     
	int p;
	int a;
};

bool cmp(Dairy_farmer a, Dairy_farmer b) {
     
	return a.p < b.p;
}

int main() {
     
	vector<Dairy_farmer> farmers;
	int n, m;
	Dairy_farmer x;
	cin >> n >> m;
	for (int i = 0; i < m; i++) {
     
		cin >> x.p >> x.a;
		farmers.push_back(x);
	}
	sort(&farmers[0], &farmers[0] + m, cmp);
	int num = 0, sum = 0;
	for (int i = 0; i < m; i++) {
     
		num += farmers[i].a;
		sum += farmers[i].p * farmers[i].a;
		if (num > n) {
     
			sum -= farmers[i].p * (num - n);
			break;
		}
	}
	cout << sum;
}

当然,在C++中,这个结构体可以写一个class,也可以写一个pair可以参考C++ pair的用法

#include <iostream>
#include <algorithm>
using namespace std;

bool cmp(pair<int, int> a, pair<int, int> b) {
     
	return a.first < b.first;
}

int main() {
     
	int n, m;
	cin >> n >> m;
	pair<int, int> farmers[5000];
	pair<int, int> x;
	for (int i = 0; i < m; i++) {
     
		cin >> x.first >> x.second;
		farmers[i]=x;
	}
	sort(&farmers[0], &farmers[0] + m, cmp);
	int num = 0, sum = 0;
	for (int i = 0; i < m; i++) {
     
		num += farmers[i].second;
		sum += farmers[i].first * farmers[i].second;
		if (num > n) {
     
			sum -= farmers[i].first * (num - n);
			break;
		}
	}
	cout << sum;
	return 0;
}

排队接水

问题:有n个人在一个水龙头前排队接水,加入每个人接水的时间为 T i T_i Ti,找出这n个人排队的一种顺序,使得n个人的平均等待时间最小。
输入:第一行为一个整数n,第二行n个整数,第i个整数 T i T_i Ti表示第i个人的等待时间 T i T_i Ti
输出:两行,第一行为一种平均时间最短的排序顺序,第二行为这种排列方案下的平均等待时间(输出结果精确到效数点后两位)。
接水快的人先接水,通过局部最优解可以导出整体最优解。

#include <iostream>
#include <algorithm>
#include <iomanip>
using namespace std;

//这里需要加强健壮性~
//是true的时候,a在b的前面
bool cmp(pair<int, int>a, pair<int, int>b) {
     
	if (a.second < b.second) return true;
	if (a.second == b.second && a.first < b.first) return true;
	else return false;
}

int main() {
     
	int n;
	pair<int, int> T[1000];
	pair<int,int> x;
	cin >> n;
	for (int i = 1; i <= n; i++) {
     
		x.first = i;
		cin >> x.second;
		T[i] = x;
	}
	sort(T+1, T + n+1,cmp);
	long num = 0;
	//这里设置为int时,最大的数据量通过不了
	for (int i=1; i <= n; i++) {
     
		cout << T[i].first << " ";
		num += T[i].second * (n - i);
	}
	cout << endl;
	cout <<fixed<< setprecision(2)<< num/10.0;
	return 0;
}

1.小数输出问题,我还看了一下这个:cout输出怎么控制几位小数_C+±-输出:保留两位小数
2.如果数据量太大不能复制过来进行调试可以# i n c l u d e < f s t r e a m > include include<fstream>使用file读进来输入。
3.输入数据时打错了,可以F5清空这一行(不过这个好像跟自己电脑也有关系,不一定行)

你可能感兴趣的:(算法设计与分析,c++,贪心算法,算法,stl)