算法之贪心&分治

12 篇文章 0 订阅

1. 贪心(Greedy)

贪心策略,也称贪婪策略。
每一步都采取当前状态下最优的选择(局部最优解),从而推导出全局最优解。
应用:

  1. 哈夫曼树;
  2. PrimKruskal
  3. Dijkstra

1.1 加勒比海盗

海盗们打劫了古董船,里面的古董都加载连城,打碎就没有加载。
设船的载重量为W,没件古董的重量为wi,如何尽可能的多装古董?
例如:W = 30wi分别是:3、5、4、10、7、14、2、11
贪心: 每一次都选择重量最小的古董:
在这里插入图片描述

/**
 * @Description 加勒比海盗
 * @date 2022/5/20 15:06
 */
public class Pirate {
    public static void main(String[] args) {
        int[] weights = {3, 5, 4, 10, 7, 14, 2, 11};
        Arrays.sort(weights);
        int capacity = 30;
        int weight = 0;
        int count = 0;
        for (int i = 0; i < weights.length; i++) {
            int newWeight = weight + weights[i];
            if (newWeight > capacity){
                break;
            }else {
                count ++;
                weight = newWeight;
                System.out.println(weights[i]);
            }
        }
        System.out.println(count);
    }
}

1.2 零钱兑换

假设有25分、10分、5分、1分的硬币(可以重复使用),现要找给客户41分零钱,如何用最少的硬币数量?
贪心: 每次都选择面值最大的硬币。

/**
 * @Description 零钱兑换
 * @date 2022/5/20 15:13
 */
public class CoinChange {

    public static void main(String[] args) {
        Integer[] faces = {25, 10, 5, 1};
        Arrays.sort(faces, (Integer f1, Integer f2) -> f2 - f1);
        int money = 41;
        int coins = 0;
        int i = 0;
        while (i < faces.length){
            if (money < faces[i]){ // 只有无法选定当前硬币是才切换下一个小的硬币,
                i++;
                continue;
            }
            money -= faces[i];
            coins++;
            System.out.println(faces[i]);
        }
    }
}

1.3零钱兑换Ⅱ

假设有25分、20分、5分、1分的硬币(可以重复使用),现要找给客户41分零钱,如何用最少的硬币数量?
贪心: 每次都选择面值最大的硬币。

使用贪心的解法:25、5、5、5、1;共五枚硬币。
但明显可以看出应该使用:40、40、1;共三枚硬币。

1.4 贪心的注意点

  • 特点并不一定能够达到最优解:没有测试所有可能,过早的做决定;贪图眼前的利益最大化。
  • 优点:简单、高效、不需要穷举所有可能,一般辅助其它算法。
  • 缺点:不从整体考虑,每次都是局部最优,很少可以得到最优解。

1.5 0-1 背包

n件物品和一个最大承重为W的背包,每件物品重量是wi,价值是vi
在保证重量不超过W的前提下,如果装使得背包价值最大。
每个物品只有一件,只能选择0或1,因此称为0 - 1问题。

  • 贪心方案
  1. 价值主导:优先选择价值最高的物品放入背包;
  2. 重量主导:优先选择重量最轻的物品放入背包;
  3. 价值密度主导:优先选择性价比最高的物品放入背包(价值密度 = 价值 ÷ 重量)。

假设背包总重量150,物品如下:
在这里插入图片描述

  1. 价值主导:4、2、6、5;总重量130,总价值165。
  2. 重量主导:6、7、2、1、5;总重量140,总价值155。
  3. 价值密度主导:6、2、7、4、1;总重量150,总价值170。
/**
 * @Description 0-1 背包
 * @date 2022/5/20 15:56
 */
public class Knapsack {
    public static void main(String[] args) {
        // 价值主导
        Article[] values = select((Article a1, Article a2) -> a2.value - a1.value);
        Info art = getArt(values, 150);
        System.out.println(art);
        // 重量主导
        Article[] weights = select((Article a1, Article a2) -> a1.weight - a2.weight);
        Info art1 = getArt(weights, 150);
        System.out.println(art1);

        // 重量主导
        Article[] density = select((Article a1, Article a2) -> Double.compare(a2.valDensity,a1.valDensity));
        Info art2 = getArt(density, 150); 
        System.out.println(art2);
    }

    /**
     * 传入比较器选择以什么为主导
     * @param cmp
     */
    static Article[] select(Comparator<Article> cmp){
        Article[] articles = new Article[]{
                new Article(35, 10), new Article(30, 40),
                new Article(60, 30), new Article(50, 50),
                new Article(40, 35), new Article(10, 40),
                new Article(25, 30)
        };
        Arrays.sort(articles,cmp);
        return articles;
    }

    static Info getArt(Article[] articles, int capacity){
        List<Article> selectArticles = new LinkedList<>();
        int weight = 0;
        int value = 0;
        for (int i = 0; i < articles.length && weight < capacity; i++) {
            int newWeight = weight + articles[i].weight;
            if (newWeight <= capacity){
                weight = newWeight;
                value += articles[i].value;
                selectArticles.add(articles[i]);
            }
        }
        return new Info(selectArticles,weight,value);
    }

    private static class Article{
        int weight;
        int value;
        double valDensity;

        public Article(int weight, int value) {
            this.weight = weight;
            this.value = value;
            this.valDensity = value * 1.0 / weight;
        }

        @Override
        public String toString() {
            return "Article{" +
                    "weight=" + weight +
                    ", value=" + value +
                    ", valDensity=" + valDensity +
                    '}';
        }
    }

    private static class Info{
        int weight = 0;
        int value = 0;
        List<Article> articles;


        public Info(List<Article> articles, int weight, int value) {
            this.articles = articles;
            this.weight = weight;
            this.value = value;
        }

        @Override
        public String toString() {
            return "Info{" +
                    "weight=" + weight +
                    ", value=" + value +
                    ", articles=" + articles +
                    '}';
        }
    }
}

2. 分治(Divide And Conquer)

即分而治之,子问题之间是相互独立的。

  1. 将原问题分解成若干个规模较小的子问题(子问题和原问题的结构一样,只是规模不同);
  2. 子问题不断分解成更小的子问题,直到不能分解(轻易算出子问题的解)。
  3. 利用子问题推导原问题的解。

在这里插入图片描述

  • 应用
  1. 归并排序;
  2. 快速排序;
  3. 大数乘法。

2.1 主定理(Master Theorem)

分治遵守的通用模式:
在这里插入图片描述

2.2 最大连续子序列和

  • 子序列:一个序列中的部分序列,不一定非连续。
    例如:-2、-1、-3、4、-1、2、1、-5、4子序列可以是:-2 、-1-2、-3等。
  • 子串、子数组、子区间:必须连续。

给定一个长度为n的子序列和,求它的最长子序列和。
例如:-2、-1、-3、4、-1、2、1、-5、4的最大连续子序列为:4、-1、2、1,和为:6

2.2.1 暴力出奇迹

穷举出所有可能的连续子序列,计算出和,取最大值。

  • 不断获取从[0,0][n,n]之间获取[begin,end]区间的值,并计算当前区间的和。
    /**
     * 暴力法最大连续子序列
     * @param nums
     * @return
     */
    static int maxSubarray(int[] nums){
        if (nums == null || nums.length == 0) return 0;
        int max = Integer.MIN_VALUE;
        for (int begin = 0; begin < nums.length; begin++) {
            for (int end = 0; end < nums.length; end++) {
                int sum = 0;
//               拿到 [begin,end] 区间的值
                // 计算子序列的和
                for (int i = begin; i < end; i++) {
                    sum += nums[i];
                }
                // 将大的值给max
                max = Math.max(max,sum);
            }
        }
        return max;
    }
  • 时间复杂度为:O(n3)
  • 空间复杂度为:O(1)
2.2.2 暴力 - 优化

上方代码中重复计算了很多次;这里使用上次计算得出的和与本次的值进行计算。

    /**
     * 暴力法最大连续子序列 - 优化
     * @param nums
     * @return
     */
    static int maxSubarray2(int[] nums){
        if (nums == null || nums.length == 0) return 0;
        int max = Integer.MIN_VALUE;
        for (int begin = 0; begin < nums.length; begin++) {
            int sum = 0;
            // 拿到 [begin,end] 区间的值
            for (int end = begin; end < nums.length; end++) {
                // 重复利用上次计算得到的和与本次的值进行相加。
                sum += nums[end];
                max = Math.max(max,sum);
            }
        }
        return max;
    }
  • 时间复杂度为:O(n2)
  • 空间复杂度为:O(1)
2.2.3 分治

将序列均与的分割成2个子序列。
[begin,end) = [begin,mid) + [mid,end);mid = (begin + end ) >> 1;

如果问题的解是[i, j),那么存在3种可能:

  1. [i, j)存在于[begin, mid)中;
    在这里插入图片描述

  2. [i, j)存在于[mid, end)中;
    在这里插入图片描述

  3. [i, j)部分存在于[begin, mid)中,另一部分存在于 [mid, end)中;也就是[i, j) = [i,mid) + [mid, j)
    在这里插入图片描述


    /**
     * 分治最大连续子序列
     * @param nums
     * @return
     */
    static int maxSubarray3(int[] nums){
        if (nums == null || nums.length == 0) return 0;
        return maxSubarray3(nums, 0, nums.length);
    }

    static int maxSubarray3(int[] nums, int begin, int end){
        // 只有一个元素,不需要分治,即最大和就是第一个元素
        if (end - begin < 2) return nums[begin];

        int mid = (begin + end) >> 1;

        // 左侧子序列最大连续子序列和
        int maxLeft = Integer.MIN_VALUE;
        int sumLeft = 0;

        // 计算[begin,mid) 最小序列和
        for (int i = mid - 1; i >= begin; i--) {
            sumLeft += nums[i];
            maxLeft = Math.max(maxLeft,sumLeft);
        }

        // 右侧子序列最大连续子序列和
        int maxRight = Integer.MIN_VALUE;
        int sunRight = 0;

        // 计算[mid,end) 最小序列和
        for (int i = mid; i < end; i++) {
            sunRight += nums[i];
            maxRight = Math.max(maxRight,sunRight);
        }

        // 获取横跨两部分的最大值
        int max = maxLeft + maxRight;


        return Math.max(max,
                Math.max(
                        maxSubarray3(nums, begin, mid),
                        maxSubarray3(nums, begin, mid)
                )
        );
    }
  • 空间复杂度O(logn)
  • 时间复杂度O(nlogn)

2.3 大数乘法

两个超大的数字进行乘法运算,如何才能进行计算?
依据小时候学习的乘法方式,每个位分别计算,大概需要计算**n2**次。

  • 比如36 x 54:每一位都分别进行计算。
    在这里插入图片描述

  • 采用分治的思想去求解:拆分分别求解,纵向求和。
    在这里插入图片描述
    时间复杂度为:O(n2)

  • Karatsuba:根据公式优化分治。
    根据上方的求积公式可以推导出:BC + AD = AD + BD - (A-B)(C-D)
    因此可以优化分治的策略:拆分成三次乘法。

在这里插入图片描述

时间复杂度为O(n1.585)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值