摘要
本文主要介紹貪心算法。 貪心算法並不是一種特定的算法,而是一種策略,一種一招制敵的策略。每次都貪心選擇最好的,就是貪心算法。 所以貪心算法往往效率高,代碼短。常見的貪心問題:區間問題, Huffman樹等。
區間問題
最大不相交區間數
例題: HDU 2037 今年暑假不AC
題目大意就是給出n個節目,n個節目起止時間不同,要求選出最多的沒有時間衝突的節目。
這道題是一道經典的貪心問題。本質上就是求出最大不相交區間數。對於本題,區間端點可以相交。
做法:對所有節目按結束時間從小到大排序。然後枚舉每個區間是否和前一個區間有相交。如果沒有,節目數就加一。
C++代碼:
#include <iostream>
#include <algorithm>
using namespace std;
struct node{
int l, r;
}arr[100005];
int cmp(node a, node b){
return a.r < b.r;
}
int main(){
int n;
while(cin>>n, n){
int l, r;
for(int i = 0; i < n; i++){
cin>>l>>r;
arr[i].l = l;
arr[i].r = r;
}
sort(arr, arr + n, cmp);
int f = -2e9;
int res = 0;
for(int i = 0; i < n; i++){
if(arr[i].l >= f){
res ++;
f = arr[i].r;
}
}
cout<<res<<endl;
}
return 0;
}
區間覆蓋問題
題目大意:John有N頭牛,每頭牛都有不同的工作時間,John將一天分爲T個時間段,從1~T。他想讓每個時間段都有牛做靜潔工作,問最少需要幾頭牛。
也就說,找出最少的牛,這些牛的工作時間可以覆蓋1~T。
做法:將每頭牛按工作時間的起始時間從小到大進行排序,然後對於1~T時刻中未被覆蓋的時刻中的起始時刻,找出起始時間能覆蓋該時刻並且結束時間最晚的牛。
注意本題坑點:本題給出的是時刻而不是區間端點,所以除了第一頭牛的開始時間必須是1,此後的第i頭牛的起始時間最晚是第i-1頭牛的結束時間+1.
例如對於樣例:
4 10
1 3
3 5
4 7
8 10
答案是選擇[1 3] [4 7] [8 10]
首先,1~T時刻中 1時刻是未被覆蓋的時刻中的起始時刻,只有[1 3] 能夠覆蓋。
然後 4時刻變爲未被覆蓋的時刻中的起始時刻, 只有[4 7] 能夠覆蓋4時刻
最後 8時刻變爲未被覆蓋的時刻中的起始時刻, 只有[8 10] 能夠覆蓋8時刻
此時1~T時刻已經被覆蓋完了,結束。
所以最終答案是 3。
JAVA代碼:
import java.io.*;
import java.util.*;
import java.math.*;
public class Main{
static BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
static BufferedWriter out = new BufferedWriter(new OutputStreamWriter(System.out));
static class node implements Comparable<node>{ //自定義類,保存起止時間
int l, r; //需要按起始時間排序,所以實現Comparable接口,自定義排序。
node(int l, int r){
this.l = l;
this.r = r;
}
public int compareTo(node b) {
return this.l - b.l;
}
}
static node[] arr = new node[25005];
static int Int(String s){
return Integer.parseInt(s);
}
public static void main(String[] args) throws Exception{
int T, n;
String[] s = in.readLine().split(" ");
n = Integer.parseInt(s[0]);
T = Integer.parseInt(s[1]);
for(int i = 0; i < n; i++){
String[] s1 = in.readLine().split(" ");// 讀入
arr[i] = new node(Int(s1[0]), Int(s1[1]));
}
Arrays.sort(arr, 0, n - 1); // 排序
int flag = 0, res = 0, start = 1; // // 剛開始1時刻是未被覆蓋的區間的起始時刻
for(int i = 0; i < n; i++){
int j = i;
int r = -1000;
if(arr[0].l > 1){
break;
}
while(j < n &&(start != 1 &&arr[j].l <= start+1|| start == 1 && arr[j].l == 1)){
r = Math.max(r, arr[j].r);
j ++;
}
res ++;
if(r < start){ // 如果r < start說明沒有符合條件的區間。直接break輸出-1
break;
}
if(r >= T ){ // 如果 r >= T 說明區間已經被完全覆蓋,break輸出答案
flag = 1;
break;
}
start = r; // 更新起始時刻
i = j - 1; // 0 ~ j-1個區間已經遍歷過了,直接跳過。
}
if(flag != 1) res = -1;
out.write(Integer.toString(res));
out.flush();
}
}
Huffman樹
Huffman樹也是貪心思想的經典體現,給定n個葉結點,用這n個葉結點構造一個二叉樹,Huffman樹是帶權路徑長度最小的二叉樹。
Huffman樹詳解視頻:
https://www.bilibili.com/video/BV18t411U7bj/?spm_id_from=333.788.videocard.4
Huffman樹的特點:權值越大的葉結點一定距離根節點越近,權值越小的葉節點一定距離根節點越遠。
構造Huffman樹:
爲了保證得到的帶權路徑長度最小,我們每次都要合併兩個最小的葉節點。
例題: POJ 3253
題目大意: John想將一塊木板切割成n塊,沒切割一次,就會發費當前切割的木板總長度的代價,求最小代價。
轉化爲Huffman樹, John最終切成的木塊長度就是葉節點。 所以只需要建個Huffman樹然後求出帶權路徑長度就行了。
使用優先隊列每次合併兩個最小的節點。
代碼:
import java.io.*;
import java.util.*;
import java.math.*;
public class Main{
static BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
static BufferedWriter out = new BufferedWriter(new OutputStreamWriter(System.out));
static int Int(String s){
return Integer.valueOf(s);
}
public static void main(String[] args) throws Exception{
int n = Int(in.readLine());
PriorityQueue<Integer> heap = new PriorityQueue<Integer>(); // 小根堆
while(n --> 0){
heap.add(Int(in.readLine()));
}
long res = 0;// 答案很大,用long存
while(heap.size() != 1)
{
int min = heap.poll() + heap.poll();
res += min;
heap.add(min);
}
out.write(res + "");
out.flush();
}
}
還有很多其他類型的貪心問題,文本就不在一一列舉,做貪心主要就是看能不能貪對,想不出來怎麼貪心就做不對,想出來了代碼就很簡單,所以還是要多做題。