1. 程式人生 > >一往直前!貪心法

一往直前!貪心法

integer 子節點 就是 行高 ide har char 字典 復雜度

2018-07-10 18:30:19

貪心法就是遵循某種規則,不斷貪心的選取當前最優策略的算法設計方法。一般來說,如果一個問題可以使用貪心法來解決的話,那麽它通常是非常高效的。

貪心法困難之處在於:

1)最優策略的選擇;

2)算法有效性的證明。

一、區間問題

問題描述:

技術分享圖片

問題求解:

這個問題其實是區間問題的變種題了,問題中需要求的是最少需要剔除多少區間,換言之就是求最大的相容區間數目。

區間問題是一個典型的可以使用貪心算法予以解決的問題,使用貪心算法,在未排序的情況下,時間復雜度為O(nlogn),若已經排序好,則時間復雜度為O(n)。

這裏貪心算法使用的策略是:

在可選的工作中,每次都選擇結束時間最早的工作。

證明:

結束時間越早,之後可選的工作就越多。這是該算法能夠正確處理問題的一個直觀的解釋。但是這不是一個嚴格的證明。我們可以通過一下的方法來證明。

(1)與其他選擇方案相比,該算法在選取相同數量的更早開始的工作時,其最終的結束時間不會比其他方案更晚。

(2)所以不存在選取更多工作的選擇方案。

可以使用數學歸納法或者反證法來進行證明。

    public int eraseOverlapIntervals(Interval[] intervals) {
        if (intervals.length == 0) return 0;
        Arrays.sort(intervals, new Comparator<Interval>() {
            @Override
            public int compare(Interval o1, Interval o2) {
                return o1.end - o2.end;
            }
        });
        int cnt = 1;
        int end = intervals[0].end;
        for (int i = 1; i < intervals.length; i++) {
            if (intervals[i].start >= end) {
                cnt++;
                end = intervals[i].end;
            }
        }
        return intervals.length - cnt;
    }

二、字典序最小問題

問題描述:

給定長度為N的字符串S,要構造一個長度為N的字符串T。起初,T是一個空串,隨後反復進行下列任意操作。

  • 從S的頭部刪除一個字符,加到T尾部
  • 從S的尾部刪除一個字符,加到T尾部

目標是要構造字典序盡可能小的字符。

問題求解:

字典序的大小和前面字符的大小相關,因此T中前面的字符越小則最終結果的字典序越小。

可以制定如下的策略:

不斷取S的開頭和末尾中較小的一個字符放到T的末尾;

如果出現開頭和末尾字符大小相同的情況,則需要按照字典序比較S和S的反轉序列S‘

如果S較小,則將頭部字符添加到T中;

如果S’較小,則將尾部字符添加到T中。

public class BestCowLine {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        StringBuffer sb = new StringBuffer();
        StringBuffer res = new StringBuffer();
        for (int i = 0; i < n; i++) sb.append(sc.next());
        int l = 0;
        int r = sb.length() - 1;
        while (l <= r) {
            if (sb.charAt(l) < sb.charAt(r)) res.append(sb.charAt(l++));
            else if (sb.charAt(r) < sb.charAt(l)) res.append(sb.charAt(r--));
            else {
                String s1 = sb.substring(l, r + 1);
                String s2 = new StringBuffer(s1).reverse().toString();
                if (s1.compareTo(s2) < 0) res.append(sb.charAt(l++));
                else if (s1.compareTo(s2) > 0) res.append(sb.charAt(r--));
                else {
                    res.append(s1);
                    break;
                }
            }
        }
        int idx = 0;
        while (idx < res.length()) {
            int i;
            for (i = 0; i < 80; i++) {
                System.out.print(res.charAt(idx++));
                if (idx >= res.length()) break;
            }
            if (i == 80) System.out.println();
        }
    }
}

三、Saruman‘s Army

問題描述:

直線上有N個點。點i的位置是Xi。從這N個點中選擇若幹個,給他們加上標記。對每個點,其距離為R以內的區域裏必須有標記的點。在滿足這個條件下,希望能給盡量少的點添加標記。請問至少有多少點被加上標記?

問題求解:

本題也是可以通過貪心法進行高效求解的,使用的策略為:

從最左邊開始考慮,對於這個點,到其距離為R的區域內必須包含帶有標記的點,由於此點位於最左邊,因此帶有標記的點必然在右側,顯然標記點應該是從最左邊的點開始,距離為R以內的最遠的點。因為更左的區域沒有覆蓋的意義,所以應該盡量找覆蓋更靠右的點。

下一步就是尋找不被這個標記點覆蓋的下一個最左點,重復上述的過程。

public class SarumanArmy {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int R, n;
        int[] nums;
        int cnt;
        while((R = sc.nextInt()) != -1) {
            n = sc.nextInt();
            nums = new int[n];
            for (int i = 0; i < n; i++) nums[i] = sc.nextInt();
            Arrays.sort(nums);
            cnt = 0;
            int idx = 0;
            while (idx < n) {
                int l = nums[idx];
                while (idx < n && nums[idx] <= l + R) idx++;
                cnt++;
                l = nums[idx - 1];
                while (idx < n && nums[idx] <= l + R) idx++;
            }
            System.out.println(cnt);
        }
    }
}

四、Fence Repair

問題描述:

農夫為了修理柵欄,需要將一塊很長的木板切割成N塊。準備切成的木板的長度為L1,L2,...,LN,未切割前木板的長度恰好為切割後木板長度的總和。每次切斷木板時,需要的開銷為這塊木板的長度。例如長度為21的木板切成5,8,8三塊。長度21的木板切成13和8的時候,開銷為21。再將長度為13的木板切割成5,8的時候,開銷為13。因此總的開銷為34。請求出按照目標要求將木板切割完最小的開銷是多少。

問題求解:

首先切割的方法可以是用二叉樹來形象的表示,二叉樹中每一個葉子節點就對應了切割出的一塊木板。葉子節點的深度就對應了為了得到該木板需要的切割次數,開銷的合計就是各個葉子節點的木板長度 * 節點深度。

有了以上的分析,最佳策略就是:

最短的板和次短的板的節點應該是兄弟節點。

最短的板應當是深度最深的節點之一。所以與這個節點同一深度的兄弟節點一定存在,並且由於同樣是深度最深的葉子節點,所以應當對應的是次短的板。

只需要遞歸的對上述的板進行拼接,就可以得到結果。

public class FencerRepair {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int[] nums = new int[n];
        for (int i = 0; i < n; i++) nums[i] = sc.nextInt();
        int min1, min2;
        long res = 0;
        while (n > 1) {
            min1 = 0;
            min2 = 1;
            if (nums[min1] > nums[min2]) {
                int temp = min1;
                min1 = min2;
                min2 = temp;
            }
            for (int i = 2; i < n; i++) {
                if (nums[i] <= nums[min1]) {
                    min2 = min1;
                    min1 = i;
                }
                else if (nums[i] < nums[min2]) min2 = i;
            }
            int t = nums[min1] + nums[min2];
            res += t;
            if (min1 == n - 1) {
                int temp = min1;
                min1 = min2;
                min2 = temp;
            }
            nums[min1] = t;
            nums[min2] = nums[n - 1];
            n--;
        }
        System.out.println(res);
    }
}

這個問題的解法作為計算霍夫曼編碼的算法而被熟知。在上述算法中將木板換成字符,長度換成頻度就可以了。

五、Cleaning Shifts

問題描述:

Farmer John is assigning some of his N (1 <= N <= 25,000) cows to do some cleaning chores around the barn. He always wants to have one cow working on cleaning things up and has divided the day into T shifts (1 <= T <= 1,000,000), the first being shift 1 and the last being shift T.
Each cow is only available at some interval of times during the day for work on cleaning. Any cow that is selected for cleaning duty will work for the entirety of her interval.
Your job is to help Farmer John assign some cows to shifts so that (i) every shift has at least one cow assigned to it, and (ii) as few cows as possible are involved in cleaning. If it is not possible to assign a cow to each shift, print -1.

Input

* Line 1: Two space-separated integers: N and T
* Lines 2..N+1: Each line contains the start and end times of the interval during which a cow can work. A cow starts work at the start time and finishes after the end time.

Output

* Line 1: The minimum number of cows Farmer John needs to hire or -1 if it is not possible to assign a cow to each shift.

Sample Input

3 10
1 7
3 6
6 10

Sample Output

2

問題求解:

問題實際上就是求能否使用最少的區間來完成覆蓋,如果可以則輸出最小的數量,如果不行,則輸出-1。

使用貪心法可以解決,策略為:

每次尋找可能的下一個最大覆蓋,如果不存在下一個更大的覆蓋,比如出現間斷等情況,則直接返回-1

編程實現的時候有幾點需要註意:

1)排序的時候在開始時間相同的情況下,需要按照結束時間降序排序;

2)如果排序後的第一個開始時間不為1,則直接輸出-1;

3)必須要對不存在下一個更大覆蓋進行判斷,如果出現了這種情況則,返回-1

public class CleaningShifts {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int N, T;
        N = sc.nextInt();
        T = sc.nextInt();
        int[][] intervals = new int[N][2];
        for (int i = 0; i < N; i++) {
            intervals[i][0] = sc.nextInt();
            intervals[i][1] = sc.nextInt();
        }
        Arrays.sort(intervals, new Comparator<int[]>() {
            @Override
            public int compare(int[] o1, int[] o2) {
                if (o1[0] == o2[0]) return o2[1] - o1[1];
                else return o1[0] - o2[0];
            }
        });
        int res = 1;
        int end = intervals[0][1];
        if (intervals[0][0] != 1) System.out.println(-1);
        else {
            int i = 0;
            while (i < N) {
                if (end >= T) break;
                int idx = i;
                while (i < N && intervals[i][0] <= end + 1) {
                    if (intervals[i][1] > intervals[idx][1]) idx = i;
                    i++;
                }
                if (intervals[idx][1] == end) break;
                i = idx;
                end = intervals[idx][1];
                res++;
            }
            if (end < T) System.out.println(-1);
            else System.out.println(res);
        }
    }
}

一往直前!貪心法