概念
贪心法就是遵循某种规则,不断贪心地选取当前最优策略的算法设计方法。
一般步骤:
1.建立数学模型来描述问题;
2.把求解的问题分成若干个子问题;
3.对每一子问题求解,得到子问题的局部最优解;
4.把子问题的解局部最优解合成原来解问题的一个解。
算法的实现框架:
从问题的某一初始解出发;
while (能朝给定总目标前进一步)
{
利用可行的决策,求出可行解的一个解元素;
}
由所有解元素组合成问题的一个可行解;
典型例题
1.货币选择问题
问题描述: 分别有1,5,10,50,100元,分别有5,2,2,3,5张纸币。问若要支付k元,则最少需要多少张纸币?
问题分析: 优先使用面值大的货币
代码实现:
#include <iostream>
#include <algorithm>
using namespace std;
const int N=7;
int Money[N]={3,0,2,1,0,3,5};
int Value[N]={1,2,5,10,20,50,100};
int solve(int money){
int num=0;
for(int i=N-1;i>=0;i--){
int c=min(money/Value[i],Money[i]);
money=money-Value[i]*c;
num+=c;
}
if(money>0){
num=-1;
}
return num;
}
int main(){
int money;
cin>>money;
int res=solve(money);
if(res!=-1) cout<<res<<endl;
else cout<<"no"<<endl;
return 0;
}
2.区间调度问题
问题描述: 有n项工作,每项工作分别在Si开始,Ti结束。例如S={1,2,4,6,8},T={3,5,7,8,10}。对每项工作,你都可以选择与否,若选择参加,则必须至始至终参加全程参与,且参与工作的时间段不能有重叠。
问题分析: 我们把“在可选工作中,每次都选取结束时间最早的”策略作为贪心算法所遵循的规则。
代码实现:
#include<iostream>
#include<algorithm>
using namespace std;
//输入
const int n = 5;
int S[n]={1,2,4,6,8};
int T[n]={3,5,7,9,10};
struct actime{
int start,finish;
}act[1002];
bool cmp(actime a,actime b){
return a.finish<b.finish;
}
int solve()
{
//这里注意要按照工作的结束时间进行排序
for(int i = 0; i < n; i ++) {
act[i].start = S[i];
act[i].finish = T[i];
}
sort(act, act + n,cmp);
int count = 0;//选取的结果
int t = -1; //最后所选工作的结束时间
for(int i = 0; i < n; i ++) {
if(t < act[i].start) {
count ++;
t = act[i].finish;
}
}
return count;
}
int main() {
int k=solve();
cout << k<< endl;
return 0;
}
3.最小生成树问题
问题描述: 求一个连通无向图的最小生成树的代价(图边权值为正整数)。
第一行是一个整数N(1<=N<=20),表示有多少个图需要计算。以下有N个图,第i图的第一行是一个整数M(1<=M<=50),表示图的顶点数,第i图的第2行至1+M行为一个M*M的二维矩阵,其元素ai,j表示图的i顶点和j顶点的连接情况,如果ai,j=0,表示i顶点和j顶点不相连;如果ai,j>0,表示i顶点和j顶点的连接权值。
问题分析:
(1)对边升序排序
在此采用链式结构,通过插入排序完成。每一结点存放一条边的左右端点序号、权值及后继结点指针
(2)边的加入是否构成环
一开始假定各顶点分别为一组,其组号为端点序号。选择某边后,看其两个端点是否在同一组中,即所在组号是否相同,如果是,表示构成了环,则舍去。 如果两个端点所在的组不同,则表示可以加入,则将该边两端的组合并成同一组。
代码实现:
```cpp
#include<iostream>
using namespace std;
struct node
{
int l;
int r;
int len;
node *next;
};
void insert(node *&h,node *p) //指针插入排序
{
node *q=h;
while(q->next && q->next->len <= p->len)
{
q=q->next;
}
p->next=q->next;
q->next=p;
}
int main()
{
// freopen("001.in","r",stdin);
node *h,*p;
int n,m,x,temp;
int *a;
int i,j;
int sum;
cin>>n;
while(n--)
{
sum=0;
cin>>m;
a=new int[m+1];
for (i=1;i<=m;i++)
{
a[i]=i;
}
h=new node;
p=h;
p->next=NULL;
for (i=1;i<=m;i++)
for (j=1;j<=m;j++)
{
cin>>x;
if (i>j && x!=0)
{
p=new node;
p->l=i;
p->r=j;
p->len=x;
p->next=NULL;
insert(h,p); //调用插入排序
}
}
p=h->next;
while (p)
{
if (a[p->l]!=a[p->r])
{
sum+=p->len;
temp=a[p->l];
for(i=1;i<=m;i++)
if (a[i]==temp)
{
a[i]=a[p->r];
}
}
p=p->next;
}
cout<<sum<<endl;
}
return 0;
}
4.字典序最小问题
问题描述: 给定长度为N的字符串为S,要构造一个长度为N的字符串T。起初,T 是一个空串,随后反复进行下列任意操作。
(1)从S的头部删除一个字符,加到T的尾部;
(2)从S的尾部删除一个字符,加到T的尾部;
目标是要构造字典序尽可能小的字符串T。(字典序是指从前到后比较两个字符串大小的方法。首先比较第1个字符,如果不同则第1个字符较小的字符串更小,如果相同则继续比较第2个字符…如此继续,来比较整个字符串的大小)
问题分析: 从字典序的性质来看,不管字符串T的尾部有多大,只要前面够小就可以,所以我们可以采用贪心算法
(1)不断去D开头和尾部中较小的一个字符放到T的尾部
(2)针对开头和尾部字符相同的情况,我们希望能够尽早使用更小的字符,所以就要比较下一个字符的大小。下一个字符也有可能相同。
代码实现:
#include<iostream>
using namespace std;
int N ;
string S;
void solve(){
//剩余的字符串位S[left],S[left+1],.....,S[right]
int left = 0, right = N-1;
bool isLeft = false; //将从左起和从右起的字符串进行比较
while(left <= right){
isLeft = false;
for(int i = 0; left + i < right; ++i){
if(S[left+i] < S[right-i]){
isLeft = true;
break;
}
else if(S[left+i] > S[right-i]){
isLeft = false;
break;
}
}
if(isLeft) cout << S[left++];
else cout << S[right--];
}
cout <<endl;
}
int main()
{
cout <<"请输入N的值:";
cin>>N;
cout<<"请输入字符串S:";
cin>>S;
solve();
return 0;
}