贪心算法之区间选点问题

目录

贪心算法之区间选点问题

1. 区间选点问题概述

2. 基本区间选点问题的贪心策略

(1)策略思路

(2)具体示例

3. 区间选点问题变体及处理

(1)变体描述

(2)贪心策略调整

(3)示例演示

4. Java 实现代码及解释

(1)定义区间类

(2)贪心算法实现

(3)代码解释

5. 性能优化

(1)当前实现的性能问题

(2)树状数组优化思路

(3)示例代码片段(树状数组相关操作)

(4)优化后的性能分析

6. 总结与展望

(1)区间选点问题贪心算法总结

(2)贪心算法在其他问题中的应用拓展

(3)进一步学习方向


贪心算法是一种在每一步选择中都采取当前状态下最优选择的算法,从而希望最终结果也是最优的。区间选点问题是贪心算法中的一个经典应用场景,通过合理地选择点来满足一定的区间覆盖要求。

1. 区间选点问题概述

区间选点问题的基本描述为:给定若干区间,要求选择最少的点,使得每个区间内至少有一个点被选中(命中)。例如,有区间 [1, 3][2, 5][4, 6] 等,我们需要确定最少选择几个点能满足所有区间的要求。

2. 基本区间选点问题的贪心策略

(1)策略思路

  • 对区间的结束点进行排序,先结束的区间排在前面。
  • 依次选取排序后的区间,总是选择当前区间的最右点(即结束点)作为选中的点,因为这样能最大程度地覆盖后续可能的区间。

(2)具体示例

例如有区间 [1, 3][2, 4][3, 5][4, 6]

  • 首先对结束点排序,得到 [1, 3][2, 4][3, 5][4, 6](已排序)。
  • 选择第一个区间 [1, 3] 的最右点 3
  • 对于第二个区间 [2, 4],由于其起点 2 小于等于已选点 3,所以该区间已被命中,无需新选点。
  • 对于第三个区间 [3, 5],其起点等于已选点 3,也已被命中。
  • 对于第四个区间 [4, 6],其起点 4 大于已选点 3,所以选择该区间的最右点 6
    最终,只需要选择两个点(3 和 6)就能命中所有区间。

3. 区间选点问题变体及处理

(1)变体描述

在一些变体中,如 POG 1201 题,除了区间的起点和终点外,每个区间还有一个命中数要求,即需要在该区间内选择特定数量的点。例如,区间 [1, 3] 要求命中一个点,区间 [3, 7] 要求命中三个点等。

(2)贪心策略调整

  • 仍然先对区间的结束点进行排序。
  • 从左到右遍历区间,对于每个区间:
    • 先计算该区间内已选点的数量。
    • 用区间的命中数要求减去已选点数量,得到还需要新增的点数量。
    • 从区间的终点开始,从右往左选点,每选一个点,将需要新增的点数量减 1,并标记该点已选(注意不能重复选点),直到新增点数量为 0 或遍历到区间起点。

(3)示例演示

假设有区间 [1, 3](要求命中 1 个点)、[3, 7](要求命中 3 个点)、[6, 8](要求命中 1 个点)、[8, 10](要求命中 3 个点)、[10, 11](要求命中 1 个点)。

  • 对结束点排序后得到:[1, 3][3, 7][6, 8][8, 10][10, 11]
  • 对于区间 [1, 3],选择最右点 3,此时该区间命中数满足要求。
  • 对于区间 [3, 7],已选点数量为 1(3 点已选),还需新增 2 个点。从右往左选点,选择 6 和 7,此时该区间命中数满足要求,同时区间 [6, 8] 也被命中。
  • 对于区间 [8, 10],已选点数量为 0,需要新增 3 个点,选择 8910
  • 对于区间 [10, 11],已选点数量为 1(10 点已选),满足命中要求。
    最终选择的点为 3678910

4. Java 实现代码及解释

(1)定义区间类

class Interval {
    int start;
    int end;
    int count;

    public Interval(int start, int end, int count) {
        this.start = start;
        this.end = end;
        this.count = count;
    }
}

(2)贪心算法实现

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class IntervalPointSelection {
    public static int minPoints(List intervals) {
        // 按照区间结束点进行排序
        Collections.sort(intervals, new Comparator() {
            @Override
            public int compare(Interval i1, Interval i2) {
                return i1.end - i2.end;
            }
        });

        int[] points = new int[100001]; // 假设区间范围在[0, 100000]内,用于标记点是否被选中
        int totalPoints = 0;

        for (Interval interval : intervals) {
            int existingPoints = 0;
            for (int i = interval.start; i <= interval.end; i++) {
                if (points[i] == 1) {
                    existingPoints++;
                }
            }

            int pointsToAdd = interval.count - existingPoints;
            for (int i = interval.end; i >= interval.start && pointsToAdd > 0; i--) {
                if (points[i] == 0) {
                    points[i] = 1;
                    pointsToAdd--;
                    totalPoints++;
                }
            }
        }

        return totalPoints;
    }

    public static void main(String[] args) {
        List intervals = new ArrayList<>();
        intervals.add(new Interval(1, 3, 1));
        intervals.add(new Interval(3, 7, 3));
        intervals.add(new Interval(6, 8, 1));
        intervals.add(new Interval(8, 10, 3));
        intervals.add(new Interval(10, 11, 1));

        int minPoints = minPoints(intervals);
        System.out.println("最少需要选择的点的数量为:" + minPoints);
    }
}

(3)代码解释

  • 首先定义了一个 Interval 类来表示区间,包含区间的起点、终点和命中数要求。
  • 在 minPoints 方法中,使用 Collections.sort 方法对区间列表按照结束点进行排序。
  • 然后遍历每个区间,计算区间内已选点数量,确定还需新增的点数量。
  • 从区间终点开始从右往左选点,标记选中的点并更新相关计数。
  • 最后返回总共选择的点的数量。

5. 性能优化

(1)当前实现的性能问题

在上述代码中,计算每个区间内已选点数量时,需要遍历区间内的每个点,这在区间数量较多或区间范围较大时效率较低,可能导致超时。

(2)树状数组优化思路

可以使用树状数组来优化计算区间内已选点数量的操作。树状数组可以高效地实现单点更新和区间查询操作,通过维护一个额外的树状结构,能够快速计算出区间内的点的数量,从而提高算法的整体效率。

(3)示例代码片段(树状数组相关操作)

class FenwickTree {
    private int[] tree;

    public FenwickTree(int n) {
        tree = new int[n + 1];
    }

    public int sum(int i) {
        int sum = 0;
        while (i > 0) {
            sum += tree[i];
            i -= i & (-i);
        }
        return sum;
    }

    public void update(int i, int val) {
        while (i < tree.length) {
            tree[i] += val;
            i += i & (-i);
        }
    }
}

// 在minPoints方法中使用树状数组优化
public static int minPointsWithFenwickTree(List intervals) {
    int maxEnd = 0;
    for (Interval interval : intervals) {
        maxEnd = Math.max(maxEnd, interval.end);
    }

    FenwickTree fenwickTree = new FenwickTree(maxEnd + 1);
    int totalPoints = 0;

    for (Interval interval : intervals) {
        int existingPoints = fenwickTree.sum(interval.end) - fenwickTree.sum(interval.start - 1);
        int pointsToAdd = interval.count - existingPoints;
        for (int i = interval.end; i >= interval.start && pointsToAdd > 0; i--) {
            if (fenwickTree.sum(i) - fenwickTree.sum(i - 1) == 0) {
                fenwickTree.update(i, 1);
                pointsToAdd--;
                totalPoints++;
            }
        }
    }

    return totalPoints;
}

(4)优化后的性能分析

使用树状数组优化后,计算区间内已选点数量的时间复杂度从原来的 O(n)( 为区间长度)降低到O(logn),大大提高了算法的效率,尤其是在处理大规模数据时效果更为显著。

6. 总结与展望

(1)区间选点问题贪心算法总结

区间选点问题的贪心算法通过合理选择区间的结束点或根据命中数要求从右往左选点等策略,能够有效地解决基本问题和变体问题。贪心算法的核心在于每一步都做出当前看似最优的选择,通过巧妙的排序和选择策略,在区间选点问题中能够找到满足条件的最少点集。

(2)贪心算法在其他问题中的应用拓展

贪心算法在很多其他领域和问题中也有广泛应用,如任务调度(选择具有最高优先级或最短执行时间的任务先执行)、背包问题(在有限容量下选择价值最高的物品)、找零问题(用最少的货币数量找零)等。在不同问题中,贪心算法的关键在于找到合适的贪心策略,即每一步的最优选择标准。

(3)进一步学习方向

对于对贪心算法感兴趣的读者,可以进一步研究贪心算法的证明方法,以深入理解为什么贪心策略在某些问题中能够得到最优解。同时,可以探索更多复杂的问题场景,尝试运用贪心算法解决问题,并思考如何优化贪心策略以提高算法性能。此外,学习其他相关算法和数据结构,如动态规划、树状数组、线段树等,能够帮助更好地理解和解决各种算法问题,在实际编程中能够根据问题特点选择最合适的算法和数据结构来提高程序效率。

希望通过本文的介绍,读者能够对贪心算法在区间选点问题中的应用有深入的理解,并能够运用贪心算法解决类似的问题,同时对算法的优化和拓展有一定的思考方向。

你可能感兴趣的:(java,贪心算法,算法)