貪婪演演算法篇——區間問題

2022-11-26 09:00:13

貪婪演演算法篇——區間問題

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

  • 區間選點
  • 區間分組
  • 區間覆蓋

區間選點

我們首先來介紹第一道題目:

/*題目名稱*/

區間選點

/*題目介紹*/

給定 N 個閉區間 [ai,bi],請你在數軸上選擇儘量少的點,使得每個區間內至少包含一個選出的點。

輸出選擇的點的最小數量。

位於區間端點上的點也算作區間內。

/*輸入格式*/
    
第一行包含整數 N,表示區間數。

接下來 N 行,每行包含兩個整數 ai,bi,表示一個區間的兩個端點。

/*輸出格式*/
    
輸出一個整數,表示所需的點的最小數量。

/*資料範圍*/
    
1 ≤ N ≤ 105,
−109 ≤ ai ≤ bi ≤ 109

/*輸入樣例*/
    
3
-1 1
2 4
3 5

/*輸出樣例*/
    
2

我們對題目採用貪婪演演算法來思考:

/*貪心思想*/

我們所使用的每一步都是目前該問題的最優解!
    
/*問題分析*/
    
我們需要在n個區間裡設定m個點,使每個區間中至少有一個點

那麼我們的每個點的取值必須是概括一個點,且最有可能概括多個點
    
那麼我們可以對區間進行排序:我們根據區間的右端點進行排序,然後如果該區間沒有對應的點,我們就將該區間的右端點設定為其中的點
    
由於我們該區間左側沒有不符合條件的點,所以不用顧及左側,而右側可能存在其他區間也概括這個點,我們可以進行判斷,若含該點,跳過即可

我們給出實際程式碼展示:

import java.util.*;

public class Main{
    
    static int N = 100010,INF = 0x3f3f3f3f,n;
    
    // 結構體建立陣列需要定義成全域性變數
    static Range[] range = new Range[N];
    
    public static void main(String[] args){
        
        Scanner scan = new Scanner(System.in);

        // 初始值輸入
        n = scan.nextInt();
        for(int i = 0 ; i < n ; i ++ ){
            int l = scan.nextInt();
            int r = scan.nextInt();
            range[i] = new Range(l,r);
        }
        
        //結構體排序
        Arrays.sort(range,0,n); 

        // 表示一共需要多少點
        int res = 0;
        
        // 上一個點的右端點(最開始為負無窮,為了使第一個區間必定賦值)
        int ed = -INF; 
        
        // 開始遍歷所有區間並挨個判斷
        for(int i = 0 ; i < n ; i ++ ){
            if(range[i].l > ed){
                res ++ ;
                ed = range[i].r;
            }
        }
        
        // 最後輸出結果即可
        System.out.println(res);
    }
}

// 區間class,因為我們需要重新設定排序條件,所以使用一個類,重塑compareTo方法
class Range implements Comparable<Range>{
    
    // l左端點,r右端點
    int l,r;
    
    // 構造方法
    public Range(int l,int r){
        this.l = l;
        this.r = r;
    }
    
    // 排序比較
    public int compareTo(Range o){
        return this.r - o.r;
    }
}

區間分組

我們首先來介紹一下題目:

/*題目名稱*/

區間分組
    
/*題目介紹*/
    
給定 N 個閉區間 [ai,bi],請你將這些區間分成若干組,使得每組內部的區間兩兩之間(包括端點)沒有交集,並使得組數儘可能小。

輸出最小組數。

/*輸入格式*/
    
第一行包含整數 N,表示區間數。

接下來 N 行,每行包含兩個整數 ai,bi,表示一個區間的兩個端點。

/*輸出格式*/
    
輸出一個整數,表示最小組數。

/*資料範圍*/
    
1 ≤ N ≤ 105,
−109 ≤ ai ≤ bi ≤ 109

/*輸入樣例*/
    
3
-1 1
2 4
3 5

/*輸出樣例*/
    
2

我們採用貪心思想來分析一下:

/*貪心思想*/

我們所使用的每一步都是目前該問題的最優解!
    
/*問題分析*/
 
該題目要求將n個區間劃分為m個組,使組中的區間不能接壤
    
該題和第一題不同之處在於:第一題在排序之後每個區間和後面的區間有關聯,不會越界;但該題後面的區間仍舊可以放在前面的組中使用
    
我們同樣採用最優解思考,我們依舊將區間排序:我們首先將區間按照左端點進行從小到大排序
    
我們從頭開始遍歷區間並做判斷:
    1.將該區間的左端點與之前每個組的右端點進行判斷(我們用p表示區間,用s表示組)
    2.若p[i].l > s[j].r:說明兩者不接壤,可以將該點放到該組中
    3.若所有組都不符合上述條件,就重新建立一個組即可

我們給出具體實現程式碼:

import java.util.*;

public class Main{
    
    static int N = 100010,n;
    
    // 存放區間
    static Range[] range = new Range[N];
    
    public static void main(String[] args){
        
        Scanner scan = new Scanner(System.in);
        
        // 初始化
        n = scan.nextInt();
        for(int i = 0 ; i < n ; i ++ ){
            int l = scan.nextInt();
            int r = scan.nextInt();
            range[i] = new Range(l,r); 
        }

        // 排序
        Arrays.sort(range,0,n);

        // 我們採用PriorityQueue讓其按從小到大的順序排列,方便我們後面遍歷從小到大遍歷
        Queue<Integer> minheap = new PriorityQueue<>(); 
        
        // 開始遍歷
        for(int i = 0 ; i < n ; i ++ ){
            
            Range r = range[i];
            
            // 小根堆的最小值要大於等於。因為相等也是有交點
            if(minheap.isEmpty() || minheap.peek() >= r.l){
                // 若不滿足條件,自己建立一個組
                minheap.add(r.r);
            }else{
                // 若滿足條件,將該組丟擲,重新加入一個組(因為無法更改資料,我們採用這種形式表示更換組的右端點資料)
                minheap.poll();
                minheap.add(r.r);
            }
        }
        
        // 輸出結果
        System.out.println(minheap.size());
    }
}

// 區間Class
class Range implements Comparable<Range>{
    int l,r;
    public Range(int l,int r){
        this.l = l;
        this.r = r;
    }
    public int compareTo(Range o){
        return Integer.compare(l,o.l);
    }
}

區間覆蓋

我們先來介紹一下題目:

/*題目名稱*/

區間覆蓋
    
/*題目介紹*/
    
給定 N 個閉區間 [ai,bi] 以及一個線段區間 [s,t],請你選擇儘量少的區間,將指定線段區間完全覆蓋。

輸出最少區間數,如果無法完全覆蓋則輸出 −1。

/*輸入格式*/
    
第一行包含兩個整數 s 和 t,表示給定線段區間的兩個端點。

第二行包含整數 N,表示給定區間數。

接下來 N 行,每行包含兩個整數 ai,bi,表示一個區間的兩個端點。

/*輸出格式*/
    
輸出一個整數,表示所需最少區間數。

如果無解,則輸出 −1。

/*資料範圍*/
    
1 ≤ N ≤ 105,
−109 ≤ ai ≤ bi ≤ 109,
−109 ≤ s ≤ t ≤ 109

/*輸入樣例*/
    
1 5
3
-1 3
2 4
3 5

/*輸出樣例*/
    
2

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

/*貪心思想*/

我們所使用的每一步都是目前該問題的最優解!
    
/*題目分析*/
    
我們希望用n個區間去覆蓋一塊[s,t]之間的區間
    
那麼我們每次使用的一個區間,自然是希望該區間所覆蓋的目的部分越大越好,而且我們依舊覆蓋過的區間可以直接丟擲
    
那麼我們只需要找到每次滿足覆蓋條件的區間組,並在組中找到一個最優解即可
    
我們將n個區間進行以左端點從小到大排序的操作
    
在排序結束之後,我們從頭開始遍歷,我們設st為目的起點,ed為目的終點
    
我們開始判斷,我們需要該區間的左端點小於等於st,且區間的右端點儘可能的大
    
那麼我們可以設定條件:p[i].l <= st 這時進入選擇區域
    
然後我們需要選擇一個右端點最大的區間,我們可以全部選擇,用max來判定即可:maxr = Math.max(maxr,p[i].r)
    
當最後該組內的選擇結束後,我們首先需要判斷是否符合條件(是否可以覆蓋起始點),然後我們再去更新起始點的位置進行下一輪判定

我們給出實際程式碼展示:

import java.util.*;

public class Main{
    
    static int N = 100010;
    
    static Range[] range = new Range[N];
    
    public static void main(String[] args){
        
        Scanner scan = new Scanner(System.in);
        
        int st = scan.nextInt();
        int ed = scan.nextInt();
        int n = scan.nextInt();
        
        for(int i = 0 ; i < n ; i ++ ){
            int l = scan.nextInt();
            int r = scan.nextInt();
            range[i] = new Range(l,r);
        }
        
        Arrays.sort(range,0,n);

        // 表示返回值,也就是最少多個區間可以覆蓋
        int res = 0;
        
        // 表示是否成功
        boolean success = false;
        
        // 使用雙指標演演算法,來查詢每個 小於等於 st的右端點最長的數
        for(int i = 0 ; i < n ; i ++ ){ 
            
            // 這裡j就是另一個指標,讓j移動判斷,最後更新i
            int j = i;
            
            // 我們第一個maxr需要負無窮以便於可以更新
            int maxr = (int)-(2e9);
            
            // 將所有左端點小於st的數的右端點進行比較,取出最大值
            while(j < n && range[j].l <= st){ 
                maxr = Math.max(maxr,range[j].r);
                j ++ ;
            }

            // 如果右端點最大的點小於st,就說明覆蓋失敗,success為false(預設)
            if(end < st)  break; 

            // 每進行一次就相當於加入了一個區間,我們的最小區間值需要++
            res ++; 

            // 如果進行到這一步完全覆蓋了,就標記一下,然後break
            if(end >= ed){ 
                success = true;
                break;
            }
            
            // 每選取一個區間,就將st賦值成這個區間的右端;
            st = maxr;
            
            // 然後更新i,將i更新到j的前一位,也就是大於之前的st的第一位,然後繼續判斷
            i = j - 1; 
        }
        
        // 如果沒有標記就是說明沒有完全覆蓋,將結果複製成-1
        if(!success) res = -1; 
        
        // 最後輸出res
        System.out.println(res);
    }
}

// Class類表示區間,更新了compareTo方法
class Range implements Comparable<Range>{
    int l,r;
    public Range(int l,int r){
        this.l = l;
        this.r = r;
    }
    public int compareTo(Range o){
        return Integer.compare(l,o.l);
    }
}

結束語

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