貪心演算法篇——區間問題
本次我們介紹貪心演算法篇的區間問題,我們會從下面幾個角度來介紹:
- 區間選點
- 區間分組
- 區間覆蓋
區間選點
我們首先來介紹第一道題目:
/*題目名稱*/
區間選點
/*題目介紹*/
給定 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);
}
}
結束語
好的,關於貪心演算法篇的區間問題就介紹到這裡,希望能為你帶來幫助~