你聽過演算法也是可以貪心的嗎?
貪心演算法不是對所有問題都能得到整體最優解,關鍵是貪心策略的選擇,選擇的貪心策略必須具備無後效性,即某個狀態以前的過程不會影響以後的狀態,只與當前狀態有關。
基本思路
1、建立數學模型來描述問題;
2、把求解的問題分成若干個子問題;
3、對每一子問題求解,得到子問題的區域性最優解;
4、把子問題的解區域性最優解合成原來解問題的一個解。
演算法實現
1、從問題的某個初始解出發。
2、採用迴圈語句,當可以向求解目標前進一步時,就根據區域性最優策略,得到一個部分解,縮小問題的範圍或規模。
3、將所有部分解綜合起來,得到問題的最終解。
例項分析
例項1、揹包問題
問題描述
有一個揹包,揹包容量是M=150。有7個物品,物品可以分割成任意大小。要求儘可能讓裝入揹包中的物品總價值最大,但不能超過總容量。
問題分析
1、目標函式: ∑pi最大,使得裝入揹包中的所有物品pi的價值加起來最大。
2、約束條件:裝入的物品總重量不超過揹包容量:∑wi<=M( M=150)
3、貪心策略:
選擇價值最大的物品
選擇價值最大的物品
選擇單位重量價值最大的物品
有三個物品A,B,C,其重量分別為{30,10,20},價值分別為{60,30,80},揹包的容量為50,分別應用三種貪心策略裝入揹包的物品和獲得的價值如下圖所示:
三種策略
演算法設計
1、計算出每個物品單位重量的價值
2、按單位價值從大到小將物品排序
3、根據揹包當前所剩容量選取物品
4、如果揹包的容量大於當前物品的重量,那麼就將當前物品裝進去。否則,那麼就將當前物品捨去,然後跳出迴圈結束。
程式碼實現
#include<iostream> #include<algorithm> using namespace std; typedef struct{ int w; int v; double avg; }P; bool cmp(P a,P b){ return a.avg>b.avg; } int main(){ P *p; int n,i,m;//n 物品個數 m揹包容量 while(cin>>n>>m){ p=new P[n]; for(i=0;i<n;i++){ cin>>p[i].w>>p[i].v; p[i].avg=p[i].v/p[i].w*1.0; } sort(p,p+n,cmp); int maxvalue=0; for(i=0;i<n;i++){ if(p[i].w<=m){ m-=p[i].w; maxvalue+=p[i].v; }else{ break; } } cout<<maxvalue<<endl; } return 0; }
執行結果
例項2、活動安排問題
問題描述
設有n個活動的集合E={1,2,…,n},其中每個活動都要求使用同一資源,如演講會場等,而在同一時間內只有一個活動能使用這一資源。每個活動i都有一個要求使用該資源的起始時間si和一個結束時間fi,且si <fi 。要求設計程式,使得安排的活動最多。
(ps:活動結束時間按從小到大排序)
問題分析
活動安排問題要求安排一系列爭用某一公共資源的活動。用貪心演算法可提供一個簡單、漂亮的方法,使盡可能多的活動能相容的使用公共資源。設有n個活動的集合{0,1,2,…,n-1},其中每個活動都要求使用同一資源,如會場等,而在同一時間內只有一個活動能使用這一資源。每個活動i都有一個要求使用該資源的起始時間starti和一個結束時間endi,且starti<endi。如選擇了活動i,則它在半開時間區間[starti,endi)內佔用資源。若區間[starti,endi)與區間[startj,endj)不相交,稱活動i與活動j是相容的。也就是說,當startj≥endi或starti≥endj時,活動i與活動j相容。活動安排問題就是在所給的活動集合中選出最多的不相容活動。
活動安排問題就是要在所給的活動集合中選出最大的相容活動子集合,是可以用貪心演算法有效求解的很好例子。該問題要求高效地安排一系列爭用某一公共資源的活動。貪心演算法提供了一個簡單、漂亮的方法使得儘可能多的活動能相容地使用公共資源。
演算法設計
若被檢查的活動i的開始時間starti小於最近選擇的活動j的結束時間endj,則不選擇活動i,否則選擇活動i加入集合中。運用該演算法解決活動安排問題的效率極高。當輸入的活動已按結束時間的非減序排列,演算法只需O(n)的時間安排n個活動,使最多的活動能相容地使用公共資源。如果所給出的活動未按非減序排列,可以用O(nlogn)的時間重排。
程式碼實現
程式碼1
#include<iostream>
#include<algorithm>
using namespace std;
struct actime{
int start,finish;
}act[1002]; bool cmp(actime a,actime b)
{
return a.finish<b.finish;
}
int main(){ int i,n,t,total;
while(cin>>n){//活動的個數
for(i=0;i<n;i++){
cin>>act[i].start>>act[i].finish;
}
sort(act,act+n,cmp);//按活動結束時間從小到大排序
t=-1;
total=0;
for(i=0;i<n;i++){
if(t<=act[i].start){
total++;
t=act[i].finish;
}
}
cout<<total<<endl;
}
return 0;
}
執行結果1
程式碼2
#include <iostream> using namespace std;
template<class Type> void GreedySelector(int n, Type s[], Type f[], bool A[]);
const int N = 11; int main() { //下標從1開始,儲存活動開始時間
int s[] = {0,1,3,0,5,3,5,6,8,8,2,12}; //下標從1開始,儲存活動結束時間
int f[] = {0,4,5,6,7,8,9,10,11,12,13,14};
bool A[N+1];
cout<<"各活動的開始時間,結束時間分別為:"<<endl;
for(int i=1;i<=N;i++) {
cout<<"["<<i<<"]:"<<"("<<s[i]<<","<<f[i]<<")"<<endl; }
GreedySelector(N,s,f,A);
cout<<"最大相容活動子集為:"<<endl;
for(int i=1;i<=N;i++) {
if(A[i]){
cout<<"["<<i<<"]:"<<"("<<s[i]<<","<<f[i]<<")"<<endl; } }
return 0; }
template<class Type> void GreedySelector(int n, Type s[], Type f[], bool A[]) {
A[1]=true; int j=1;//記錄最近一次加入A中的活動
for (int i=2;i<=n;i++)//依次檢查活動i是否與當前已選擇的活動相容
{ if (s[i]>=f[j])
{ A[i]=true;
j=i; }
else {
A[i]=false;
} } }
執行結果2
例項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 6 0 6 1 5 0 0 6 0 5 0 3 0 1 5 0 5 6 4 5 0 5 0 0 2 0 3 6 0 0 6 0 0 4 2 6 0
樣例輸出
15
Kruskal演算法簡述
假設 WN=(V,{E}) 是一個含有 n 個頂點的連通網,則按照克魯斯卡爾演算法構造最小生成樹的過程為:先構造一個只含 n 個頂點,而邊集為空的子圖,若將該子圖中各個頂點看成是各棵樹上的根結點,則它是一個含有 n 棵樹的一個森林。之後,從網的邊集 E 中選取一條權值最小的邊,若該條邊的兩個頂點分屬不同的樹,則將其加入子圖,也就是說,將這兩個頂點分別所在的兩棵樹合成一棵樹;反之,若該條邊的兩個頂點已落在同一棵樹上,則不可取,而應該取下一條權值最小的邊再試之。依次類推,直至森林中只有一棵樹,也即子圖中含有 n-1條邊為止。
模擬過程:
模擬過程
演算法難點:
(1)邊的選擇要求從小到大選擇,則開始顯然要對邊進行升序排序。 (2)選擇的邊是否需要,則從判斷該邊加入後是否構成環入手。
演算法設計
(1)對邊升序排序 在此採用鏈式結構,通過插入排序完成。每一結點存放一條邊的左右端點序號、權值及後繼結點指標
(2)邊的加入是否構成環 一開始假定各頂點分別為一組,其組號為端點序號。選擇某邊後,看其兩個端點是否在同一組中,即所在組號是否相同,如果是,表示構成了環,則捨去。 如果兩個端點所在的組不同,則表示可以加入,則將該邊兩端的組合併成同一組。
程式碼實現
#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;
}
/* 可以測試程式工作是否正常
p=h->next;
while(p)
{
cout<<p->l<<':';cout<<p->r<<' ';
cout<<p->le
n<<" ";
p=p->next;
}
*/
cout<<sum<<endl;
}
return 0;
}
執行結果
例項4、hdu1050-Moving Tables
問題描述
在一個狹窄的走廊裡將桌子從一個房間移動到另一個房間,走廊的寬度只能允許一個桌子通過。給出t,表示有t組測試資料。再給出n,表示要移動n個桌子。n下面有n行,每行兩個數字,表示將桌子從a房間移到b房間。走廊的分佈圖如一圖所示,每移動一個桌子到達目的地房間需要花10分鐘,問移動n個桌子所需要的時間。
輸入
3 4 10 20 30 40 50 60 70 80 2 1 3 2 200 3 10 100 20 80 30 50
輸出
10 20 30
解題思路
若移動多個桌子時,所需要經過的走廊沒有重合處,即可以同時移動。若有一段走廊有m個桌子都要經過,一次只能經過一個桌子,則需要m*10的時間移動桌子。 設一個數組,下標值即為房間號。桌子經過房間時,該房間號為下標對應的陣列值即加10。最後找到最大的陣列值,即為移動完桌子需要的最短時間。(以上為程式碼2,程式碼3同這個思想)
注意:
1、可能出發位置比目的地房間大,無論大小,我們都可以看做從小的房間移動到大的房間
2、出發房間為偶數則減一,結束房間為奇數則加一
我們首先輸入每次移動的出發和結束房間,然後按每次移動的出發房間從小到大排序,然後直至所有的房間移動完畢。(程式碼1的解釋)
程式碼1(我自己感覺不是貪心演算法,屬於暴力破解吧,大家酌情考慮)
#include<iostream>
#include<algorithm>
using namespace std;
struct table{
int from,to;
bool flag;//記錄改房間是否被訪問過
}moving[205];
bool cmp(table a,table b){
return a.from<b.from;
}
int main(){
int i,T,n;//T代表測試例項個數,n代表每個測試下的table個數
cin>>T;
while(T--){
cin>>n;
for(i=0;i<n;i++){
cin>>moving[i].from>>moving[i].to;
if(moving[i].from > moving[i].to)
{
int temp = moving[i].from;
moving[i].from = moving[i].to;
moving[i].to = temp;
}//可能出發位置比目的地房間大,無論大小,我們都可以看做從小的房間移動到大的房間 if(moving[i].from%2==0){//考慮實際情況,出發房間為偶數是減一,可參照題中給出的圖一 moving[i].from--;
}
if(moving[i].to%2==1){//考慮實際情況,結束房間為奇數是加一,
moving[i].to++;
}
moving[i].flag=false;//初始化房間未被訪問過
}
sort(moving,moving+n,cmp);//將所有table按出發房間從小到大排序;
bool completion=false;//是否所有的table移動結束
int cnt=-1,priorTo;//priorTo 記錄前一個table移動結束的房間
while(!completion){
completion=true;
cnt++;
priorTo=0;
for(i=0;i<n;i++){//每一輪將可以同時移動的table全部移動完:比如2->5,6->10,因為他們沒有共用走廊
if(!moving[i].flag&&moving[i].from>priorTo){
moving[i].flag=true;//標記當前table已經移動完畢
priorTo=moving[i].to;
completion=false;
}
}
}
cout<<cnt*10<<endl;
}
return 0;
}
程式碼1執行結果
程式碼2
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int main()
{
int t,n,count[410],i,start,end,k;
scanf("%d",&t);
while(t--)
{
scanf("%d",&n);
memset(count,0,sizeof(count));
while(n--)
{
scanf("%d%d",&start,&end);
if(start>end)//可能出發位置比目的地房間大。
{ //無論大小,我們都可以看做從小的房間移動到大的房間
k=start;
start=end;
end=k;
}
if(start%2==0)//考慮實際情況,出發房間為偶數是減一,可參照題中給出的圖一
start-=1;
if(end%2==1)//目的地房間為奇數時加一
end+=1;
for(i=start;i<=end;++i)
count[i]+=10; }
printf("%dn",*max_element(count,count+400));//STL中尋找數列最大值函式
}
return 0;
}
程式碼3
#include <iostream>
#include <algorithm>
#include <cstring>
#define MAXN 500
using namespace std;
struct temp{
int be,en;
};
bool comp(temp a,temp b){
return a.be<b.be;
}
int main(){
temp my[MAXN];
int m,n,i;
cin>>m;
while(m--){
cin>>n;
i=0;
int a,b,j;
while(i<n){
cin>>a>>b;
if(a>b)a^=b^=a^=b;
my[i].be=(a+1)/2;
my[i++].en=(b+1)/2;
}
sort(my,my+n,comp);
int s=0,out=n,t=0;
int aa[203];
memset(aa,0,sizeof(aa));
for(i=0;i<n;++i){
for(j=my[i].be;j<=my[i].en;++j)aa[j]++;
}
sort(aa,aa+200);
cout<<aa[199]*10<<'12';
}
return 0;
}
參考
1、0021演算法筆記——【貪心演算法】貪心演算法與活動安排問題(http://blog.csdn.net/liufeng_king/article/details/8709005)
2、C++最小生成樹問題(http://blog.csdn.net/pukuimin1226/article/details/6440714)
3、C++c語言貪心演算法(https://wenku.baidu.com/view/8e5f335b77232f60ddcca144.html)
4、HDOJ 1050 Moving Tables(經典貪心)(https://www.2cto.com/kf/201508/434067.html)
5、貪心演算法——Hdu 1050 Moving Tables(http://blog.csdn.net/code_pang/article/details/8251240)
6、HDU Moving Tables (貪心)(http://blog.csdn.net/zp___waj/article/details/46494865)