貪婪演演算法篇——經典題型

2022-11-28 09:02:25

貪婪演演算法篇——經典題型

本次我們介紹貪婪演演算法篇的經典題型,我們會從下面幾個角度來介紹:

  • Huffman樹
  • 排序不等式
  • 絕對值不等式
  • 推公式

Huffman樹

我們直接給出對應題型:

/*題目名稱*/

合併果子
    
/*題目介紹*/
    
在一個果園裡,達達已經將所有的果子打了下來,而且按果子的不同種類分成了不同的堆。

達達決定把所有的果子合成一堆。

每一次合併,達達可以把兩堆果子合併到一起,消耗的體力等於兩堆果子的重量之和。

可以看出,所有的果子經過 n−1 次合併之後,就只剩下一堆了。

達達在合併果子時總共消耗的體力等於每次合併所耗體力之和。

因為還要花大力氣把這些果子搬回家,所以達達在合併果子時要儘可能地節省體力。

假定每個果子重量都為 1,並且已知果子的種類數和每種果子的數目,你的任務是設計出合併的次序方案,使達達耗費的體力最少,並輸出這個最小的體力耗費值。

例如有 3 種果子,數目依次為 1,2,9。

可以先將 1、2 堆合併,新堆數目為 3,耗費體力為 3。

接著,將新堆與原先的第三堆合併,又得到新的堆,數目為 12,耗費體力為 12。

所以達達總共耗費體力=3+12=15。

可以證明 15 為最小的體力耗費值。

/*輸入格式*/
    
輸入包括兩行,第一行是一個整數 n,表示果子的種類數。

第二行包含 n 個整數,用空格分隔,第 i 個整數 ai 是第 i 種果子的數目。

/*輸出格式*/
    
輸出包括一行,這一行只包含一個整數,也就是最小的體力耗費值。

輸入資料保證這個值小於 231。

/*資料範圍*/
    
1 ≤ n ≤ 10000,
1 ≤ ai ≤ 20000

/*輸入樣例*/
    
3 
1 2 9 

/*輸出樣例*/

15

我們採用貪心的思想來進行分析:

/*貪心思想*/

我們在當前情況以最優解的形式給出結果
    
/*題目分析*/
    
我們僅針對當前情況進行分析,如果我們想要得到當前情況的體力耗費最小值
    
那麼我們只需要找到該果子中最小的兩堆,進行合併即可

我們直接給出求解程式碼:

import java.util.*;
public class Main{
    public static void main(String[] args){
        
        Scanner scanner = new Scanner(System.in);
        
        int N = 10010;
        
        int n = scanner.nextInt();
        
        // 我們這裡直接採用一個優先佇列來儲存,直接將其變為從小到大排列的方式
        Queue<Integer> minheap = new PriorityQueue<>();
        
        // 輸入資料
        for(int i = 0 ; i < n ; i ++ ){
            int x = scanner.nextInt();
            minheap.add(x);
        }
        
        // 這是我們的最後返回結果
        int res = 0;
        
        // 我們迴圈n次,就相當於將n堆果子合併n次
        for(int i = 0 ; i < n ; i ++ ){
            // 這裡需要判斷大小,當大小大於1時,我們再合併,因為當大小為1時就是結果了
            if(minheap.size() > 1){ 
                // 將最小的數取出來後彈出
                int a = minheap.poll(); 
                //將第二小的數取出來後彈出
                int b = minheap.poll(); 
                // 將每一次兩堆最小值相加之後的加過累加
                res += a + b;   
                //然後將他放入到堆中
                minheap.add(a + b);
            }
        }
        
        // 最後輸出結果即可
        System.out.println(res);
    }
}

排序不等式

我們直接給出對應題型:

/*題目名稱*/

排隊打水
    
/*題目介紹*/
    
有 n 個人排隊到 1 個水龍頭處打水,第 i 個人裝滿水桶所需的時間是 ti,請問如何安排他們的打水順序才能使所有人的等待時間之和最小?

/*輸入格式*/
    
第一行包含整數 n。
    
第二行包含 n 個整數,其中第 i 個整數表示第 i 個人裝滿水桶所花費的時間 ti。

/*輸出格式*/
    
輸出一個整數,表示最小的等待時間之和。

/*資料範圍*/
    
1 ≤ n ≤ 105,
1 ≤ ti ≤ 104

/*輸入樣例*/
    
7
3 6 1 4 2 5 7

/*輸出樣例*/

56

我們採用貪心的思想來進行分析:

/*貪心思想*/

我們在當前情況以最優解的形式給出結果
    
/*題目分析*/
    
我們僅針對當前情況進行分析,如果我們想要得到當前狀況後續等待時間最少
    
那麼我們只需要找到耗時最少的人,那麼後面的人等待時間就是最少的

我們直接給出求解程式碼:

import java.util.*;

public class Main {
    public static void main(String[] agrs){
        
        Scanner scan = new Scanner(System.in);
        
        int N = 100010;
        int[] w = new int[N];
        
        int n = scan.nextInt();
        
        // 賦值
        for(int i = 0 ; i < n ; i ++ ){
            w[i] = scan.nextInt();
        }
        
        // 從小到大排序,後面直接遍歷即可
        Arrays.sort(w,0,n);

        // 返回值(最小等待時間)
        long res = 0;
        
        // 開始遍歷
        for(int i = 0 ; i < n ; i ++ ){
            // 我們直接計算該時間段後面的人所需要等待的總時間(n-1-i是指當前排隊人數,w[i]是當前耗費時間,+=即可統計)
            res += w[i] * (n - i - 1);
        }

        System.out.println(res);
    }
}

絕對值不等式

我們直接給出對應題型:

/*題目名稱*/

貨倉選址
    
/*題目介紹*/
    
在一條數軸上有 N 家商店,它們的座標分別為 A1∼AN。

現在需要在數軸上建立一家貨倉,每天清晨,從貨倉到每家商店都要運送一車商品。

為了提高效率,求把貨倉建在何處,可以使得貨倉到每家商店的距離之和最小。

/*輸入格式*/
    
第一行輸入整數 N。
    
第二行 N 個整數 A1∼AN。

/*輸出格式*/
    
輸出一個整數,表示距離之和的最小值。

/*資料範圍*/
    
1 ≤ N ≤ 100000,
0 ≤ Ai ≤ 40000

/*輸入樣例*/
    
4
6 2 9 1

/*輸出樣例*/

12

我們採用貪心的思想來進行分析:

/*貪心思想*/

我們在當前情況以最優解的形式給出結果
    
/*題目分析*/
    
我們僅針對當前情況進行分析
    
我們如果只有兩家商家,那麼只要在他倆之間的點都可以保證兩者到貨艙距離和的最小值
    
但是當出現三家商家時,我們首先無法使用前面的思想了
    
這時我們會發現如果貨艙在兩邊的商家中間時,兩邊商家到該貨艙的距離為最小且不改變,這時我們只需要保證貨艙到中間的商家距離最小即可
    
那麼該貨艙的位置肯定就是中間的商家位置了
    
那麼我們就直接擬設一個點,設為最中間商家的點並嘗試獲得答案(貪心就是不斷猜測並驗證的過程)

我們直接給出求解程式碼:

import java.util.*;
public class Main{
    public static void main(String[] agrs){
        
        Scanner scan = new Scanner(System.in);
        
        int N = 100010;
        int[] a = new int[N];
        
        int n = scan.nextInt();
        
        // 賦值
        for(int i = 0 ; i < n ; i ++ ){
            a[i] = scan.nextInt();
        }
        
        
        // 從小到大排序
        Arrays.sort(a,0,n);
        
        // 返回值
        int sum = 0;
        
        // 遍歷
        for(int i = 0 ; i < n ; i ++ ){
            // 我們用當前點來減去中間點獲得當前距離,並不斷累加即可
            sum += Math.abs(a[i] - a[n / 2]); 
        }
        System.out.println(sum);
    }
}

推公式

我們直接給出對應題型:

/*題目名稱*/

耍雜技的牛
    
/*題目介紹*/
    
農民約翰的 N 頭奶牛(編號為 1..N)計劃逃跑並加入馬戲團,為此它們決定練習表演雜技。

奶牛們不是非常有創意,只提出了一個雜技表演:

疊羅漢,表演時,奶牛們站在彼此的身上,形成一個高高的垂直堆疊。

奶牛們正在試圖找到自己在這個堆疊中應該所處的位置順序。

這 N 頭奶牛中的每一頭都有著自己的重量 Wi 以及自己的強壯程度 Si。

一頭牛支撐不住的可能性取決於它頭上所有牛的總重量(不包括它自己)減去它的身體強壯程度的值,現在稱該數值為風險值,風險值越大,這隻牛撐不住的可能性越高。

您的任務是確定奶牛的排序,使得所有奶牛的風險值中的最大值儘可能的小。

/*輸入格式*/
    
第一行輸入整數 N,表示奶牛數量。

接下來 N 行,每行輸入兩個整數,表示牛的重量和強壯程度,第 i 行表示第 i 頭牛的重量 Wi 以及它的強壯程度 Si。

/*輸出格式*/
    
輸出一個整數,表示最大風險值的最小可能值。

/*資料範圍*/
    
1 ≤ N ≤ 50000,
1 ≤ Wi ≤ 10,000,
1 ≤ Si ≤ 1,000,000,000

/*輸入樣例*/
    
3
10 3
2 5
3 3

/*輸出樣例*/

2

我們採用貪心的思想來進行分析:

/*貪心思想*/

我們在當前情況以最優解的形式給出結果
    
/*題目分析*/
    
我們如果直接按數值排序,因為存在兩個數值,我們是無法找到規律的
    
但是我們可以進行推算:(危險係數設計為a[i])
    
我們最開始按照順序進行排列,那麼他們的危險係數為:
	a[i]   = w[1]+w[2]+...+w[i-1]-s[i]
	a[i+1] = w[1]+w[2]+...+w[i]-s[i+1]
    
我們採用貪婪演演算法,我們如果每次將第i頭牛和第i+1頭牛進行位置轉換,那麼是否可以縮減危險係數:
	a[i]   = w[1]+w[2]+...+w[i-1]+w[i+1]-s[i]
    a[i+1] = w[1]+w[2]+...+w[i-1]-s[i+1]
    
我們如果丟擲掉多餘元素:
	a[i]  交換前:-s[i]
    a[i+1]交換前:w[i]-s[i+1]
    
    a[i]  交換後:w[i+1]-s[i]
    a[i+1]交換後:-s[i+1]
    
我們只需要比較MAX即可,同時由於w[i],s[i]均大於0,我們可以進行推算(推算步驟這裡不演示了):
    - 當s[i]+w[i] > s[i+1]+w[i+1]時,進行交換,我們的危險係數可以下降或者不變
    
那麼我們只需要將牛按照s[i]+w[i]的數值進行排序,並最後計算出對應值危險係數值即可

我們直接給出求解程式碼:

import java.util.*;

public class Main{
    
    static int N = 50010;
    
    static PII[] p = new PII[N];
    static int[] s = new int[N];
    
    public static void main(String[] args){
        
        Scanner scan = new Scanner(System.in);
        
        int n = scan.nextInt();
        
        for(int i = 0 ; i < n ; i ++ ){
            int w = scan.nextInt();
            int s = scan.nextInt();
            
            // 存入(體重+強壯值,體重)
            p[i] = new PII(w + s,w); 
        }

        // 按照體重+強壯值排序
        Arrays.sort(p,0,n);

        // 首先設定返回結果,將其設為負無窮以便於替換
       int res = (int) -1e9;
        
        // 這裡設定重量,我們會從上往下計算,因此每次加上一個重量
       int sum = 0;
        
        // 開始遍歷
       for(int i = 0 ; i < n ; i ++ ){
           int w = p[i].y; // 體重
           int s = p[i].x - w; // 強壯值
           res = Math.max(res,sum - s); // 減去的是最下面的一個人的強壯值
           sum += w; // 疊羅漢上去一個人就得疊加重量
       }
        
        // 最後返回結果
       System.out.println(res);
    }
}

// 模擬牛,主要是為了更換CompareTo比較條件
class PII implements Comparable<PII>{
    
    int x,y;
    
    public PII(int x,int y){
        this.x = x;
        this.y = y;
    }
    
    // 注意傳參, 我們這裡的x是指體重+強壯值,還因為我們需要從上往下計算,所以我們以體重+強壯值從小到大排序
    public int compareTo(PII o){
        return Integer.compare(x,o.x);
    }
}

結束語

好的,關於貪婪演演算法篇的經典題型就介紹到這裡,希望能為你帶來幫助~