JHHK

欢迎来到我的个人网站
行者常至 为者常成

【进阶】16、贪心、分治

目录

贪心(Greedy)

一、贪心

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

贪心的应用
哈夫曼树
最小生成树算法:Prim、Kruskal
最短路径算法:Dijkstra

二、最优装载问题(加勒比海盗)

在北美洲东南部,有一片神秘的海域,是海盗最活跃的加勒比海
有一天,海盗们截获了一艘装满各种各样古董的货船,每一件古董都价值连城,一旦打碎就失去了它的价值
海盗船的载重量为 W,每件古董的重量为 𝑤i,海盗们该如何把尽可能多数量的古董装上海盗船?
比如 W 为 30,𝑤i 分别为 3、5、4、10、7、14、2、11

贪心策略:每一次都优先选择重量最小的古董
① 选择重量为 2 的古董,剩重量 28
② 选择重量为 3 的古董,剩重量 25
③ 选择重量为 4 的古董,剩重量 21
④ 选择重量为 5 的古董,剩重量 16
⑤ 选择重量为 7 的古董,剩重量 9
最多能装载 5 个古董

+(void)pirate{
    NSArray * weights = @[@3,@5,@4,@10,@7,@14,@2,@11];
    
    weights = [weights sortedArrayUsingComparator:^NSComparisonResult(id  _Nonnull obj1, id  _Nonnull obj2) {
        return [obj1 compare:obj2];
    }];
    
    int capacity = 30,weight = 0,count = 0;
    
    for (int i  = 0; i<weights.count && weight<capacity; i++) {
        int newWeight = weight + [weights[i] intValue];
        
        if (newWeight<=capacity) {
            weight = newWeight;
            count++;
            NSLog(@"%@",weights[i]);
        }
    }
    
    NSLog(@"一共选了 %d 件古董",count);
}


+(void)pirate2{
    NSArray * weights = @[@3,@5,@4,@10,@7,@14,@2,@11];
    weights = [weights sortedArrayUsingComparator:^NSComparisonResult(id  _Nonnull obj1, id  _Nonnull obj2) {
        return [obj1 compare:obj2];
    }];
    
    int capacity = 30,count = 0;
    
    for (int i  = 0; i<weights.count; i++) {
        if ((capacity -= [weights[i] intValue])>0) {
            count++;
            NSLog(@"%@",weights[i]);
        }else{
            break;
        }
    }
    NSLog(@"一共选了 %d 件古董",count);
}

三、零钱兑换

1、例子1

假设有 25 分、10 分、5 分、1 分的硬币,现要找给客户 41 分的零钱,如何办到硬币个数最少?

贪心策略:每一次都优先选择面值最大的硬币
① 选择 25 分的硬币,剩 16 分
② 选择 10 分的硬币,剩 6 分
③ 选择 5 分的硬币,剩 1 分
④ 选择 1 分的硬币

最终的解是共 4 枚硬币
25 分、10 分、5 分、1 分硬币各一枚

/// 零钱兑换问题
+(void)coinChange{
    NSArray * coins = @[@5,@1,@10,@25];
    coins = [coins sortedArrayUsingComparator:^NSComparisonResult(id  _Nonnull obj1, id  _Nonnull obj2) {
        return [obj1 compare:obj2];
    }];
    
    int change = 41,index = (int)coins.count-1,count = 0;
    
    while (index>=0 && change > 0) {
        int newChange = change - [coins[index] intValue];
        if (newChange >= 0) {
            change = newChange;
            count++;
            NSLog(@"%@ 分",coins[index]);
        }else{
            index--;
        }
    }
    
    NSLog(@"共选择了 %d 枚硬币",count);
}

2、例子2

假设有 25 分、20 分、5 分、1 分的硬币,现要找给客户 41 分的零钱,如何办到硬币个数最少?
贪心策略:每一步都优先选择面值最大的硬币
① 选择 25 分的硬币,剩 16 分
② 选择 5 分的硬币,剩 11 分
③ 选择 5 分的硬币,剩 6 分
④ 选择 5 分的硬币,剩 1 分
⑤ 选择 1 分的硬币
最终的解是 1 枚 25 分、3 枚 5 分、1 枚 1 分的硬币,共 5 枚硬币
实际上本题的最优解是:2 枚 20 分、1 枚 1 分的硬币,共 3 枚硬币

注意
贪心策略并不一定能得到全局最优解
因为一般没有测试所有可能的解,容易过早做决定,所以没法达到最佳解
贪图眼前局部的利益最大化,看不到长远未来,走一步看一步
优点:简单、高效、不需要穷举所有可能,通常作为其他算法的辅助算法来使用
缺点:鼠目寸光,不从整体上考虑其他可能,每次采取局部最优解,不会再回溯,因此很少情况会得到最优解

四、0-1背包

有 n 件物品和一个最大承重为 W 的背包,每件物品的重量是 𝑤i、价值是 𝑣i
在保证总重量不超过 W 的前提下,将哪几件物品装入背包,可以使得背包的总价值最大?
注意:每个物品只有 1 件,也就是每个物品只能选择 0 件或者 1 件,因此称为 0-1背包问题

如果采取贪心策略,有3个方案
① 价值主导:优先选择价值最高的物品放进背包
② 重量主导:优先选择重量最轻的物品放进背包
③ 价值密度主导:优先选择价值密度最高的物品放进背包(价值密度 = 价值 ÷ 重量)

假设背包最大承重150,7个物品如表格所示

编号 1 2 3 4 5 6 7
重量 35 30 60 50 40 10 25
价值 10 40 30 50 35 40 30
价值密度 0.29 1.33 0.5 1.0 0.88 4.0 1.2

① 价值主导:放入背包的物品编号是 4、2、6、5,总重量 130,总价值 165
② 重量主导:放入背包的物品编号是 6、7、2、1、5,总重量 140,总价值 155
③ 价值密度主导:放入背包的物品编号是 6、2、7、4、1,总重量 150,总价值 170

代码实现

#import "LCKnapsack.h"

@interface LCArticle:NSObject
@property(nonatomic,assign)int weight;
@property(nonatomic,assign)int value;
@property(nonatomic,assign)double valueDensity;
+(instancetype)articleWeight:(int)weight value:(int)value;
@end

@implementation LCArticle

+(instancetype)articleWeight:(int)weight value:(int)value{
    LCArticle * article = [[LCArticle alloc] init];
    article.weight = weight;
    article.value = value;
    article.valueDensity = value * 1.0 / weight;
    return article;
}



-(NSString *)description{
    NSString * weight = [NSString stringWithFormat:@"weight=%d",_weight];
    NSString * value  = [NSString stringWithFormat:@"value=%d",_value];
    NSString * valueDensity = [NSString stringWithFormat:@"valueDensity=%.2lf",_valueDensity];
   return [NSString stringWithFormat:@"%@,%@,%@",weight,value,valueDensity];
}


@end

#pragma mark -
#pragma mark -


@implementation LCKnapsack

/// 0-1背包问题
+(void)knapsack{
    
    [self __knapsackTitle:@"重量主导" comparator:^NSComparisonResult(LCArticle* obj1, LCArticle* obj2) {
        
        //按照weight升序排列
        return obj1.weight - obj2.weight;
    }];
    
    
    [self __knapsackTitle:@"价值主导" comparator:^NSComparisonResult(LCArticle* obj1, LCArticle* obj2) {
        
        //按照value降序排列
        return obj2.value - obj1.value;
    }];
    
    
   
    
    [self __knapsackTitle:@"价值密度主导" comparator:^NSComparisonResult(LCArticle* obj1, LCArticle* obj2) {
        
        //由于浮点数的精度问题,不能通过相减来判断是否相等
        //浮点返回 0.3 会被强转为int认为是0 使结果不准
//        double result = obj2.valueDensity - obj1.valueDensity;
//        NSLog(@"result = %lf",result);
//        return result;

        
        //按照valueDensity降序排列
        if (obj1.valueDensity > obj2.valueDensity) {
            return -1;
        }else if (obj2.valueDensity > obj1.valueDensity){
            return 1;
        }else{
            return 0;
        }
        
    }];
}


+(void)__knapsackTitle:(NSString*)title comparator:(NSComparator)comparator{
    
    
    NSArray * articles = @[
        [LCArticle articleWeight:35 value:10],
        [LCArticle articleWeight:30 value:40],
        [LCArticle articleWeight:60 value:30],
        [LCArticle articleWeight:50 value:50],
        [LCArticle articleWeight:40 value:35],
        [LCArticle articleWeight:10 value:40],
        [LCArticle articleWeight:25 value:30],
    ];
    
    articles = [articles sortedArrayUsingComparator:comparator];
    
    NSMutableArray * selected = [NSMutableArray array];
    
    int capacity = 150, weight = 0, value = 0;
    
    for (LCArticle* article in articles) {
        if (weight>=capacity) break;
            
        int newWeight = weight + article.weight;
        if (newWeight <= capacity) {
            weight = newWeight;
            value += article.value;
            [selected addObject:article];
        }
    }
    
    NSLog(@"【 %@ 】",title);
    NSLog(@"总价值:%d,总重量:%d",value,weight);
    for (LCArticle * article in selected) {
        NSLog(@"%@",article);
    }
    NSLog(@"-------------------------------");
    
}
@end

【 重量主导 】
总价值:155,总重量:140
weight=10,value=40,valueDensity=4.00
weight=25,value=30,valueDensity=1.20
weight=30,value=40,valueDensity=1.33
weight=35,value=10,valueDensity=0.29
weight=40,value=35,valueDensity=0.88
——————————-
【 价值主导 】
总价值:165,总重量:130
weight=50,value=50,valueDensity=1.00
weight=30,value=40,valueDensity=1.33
weight=10,value=40,valueDensity=4.00
weight=40,value=35,valueDensity=0.88
——————————-
【 价值密度主导 】
总价值:170,总重量:150
weight=10,value=40,valueDensity=4.00
weight=30,value=40,valueDensity=1.33
weight=25,value=30,valueDensity=1.20
weight=50,value=50,valueDensity=1.00
weight=35,value=10,valueDensity=0.29
——————————-

分治(Divide And Conquer)

一、分支策略

分治,也就是分而治之。它的一般步骤是
① 将原问题分解成若干个规模较小的子问题(子问题和原问题的结构一样,只是规模不一样)
② 子问题又不断分解成规模更小的子问题,直到不能再分解(直到可以轻易计算出子问题的解)
③ 利用子问题的解推导出原问题的解
因此,分治策略非常适合用递归
需要注意的是:子问题之间是相互独立的

img

分治的应用
快速排序
归并排序
Karatsuba算法(大数乘法)

二、主定理

分治策略通常遵守一种通用模式

img

思考:为什么有些问题采取分治策略后,性能会有所提升?

比如:时间复杂度为log(n^2)的算法,分解为 2个 log((n/2)^) 的算法 时间复杂度变为 log(n^2/2)

三、最大连续子序列和

给定一个长度为 n 的整数序列,求它的最大连续子序列和
比如 –2、1、–3、4、–1、2、1、–5、4 的最大连续子序列和是 4 + (–1) + 2 + 1 = 6

这道题也属于最大切片问题(最大区段,Greatest Slice)

概念区分
子串、子数组、子区间必须是连续的,子序列是可以不连续的

1、解法一:暴力出奇迹

穷举出所有可能的连续子序列,并计算出它们的和,最后取它们中的最大值

/// 求最大连续子序列合
/// @param array 序列
+(int)maxSubArray:(NSArray*)array{
    
    if (array == nil || array.count == 0) return -1;
    
    int beginIdx = 0;
    int endIdx = 0;
    int maxSum = [array[0] intValue];
    for (int begin = 0; begin<array.count; begin++) {
        for (int end = begin; end<array.count; end++) {

            int sum = 0;
            for (int i = begin; i<=end; i++) {
                sum += [array[i] intValue];
            }
           
            
            if (sum > maxSum) {
                maxSum = sum;
                beginIdx = begin;
                endIdx = end;
            }
            
        }
    }
    
    NSLog(@"beginIdx = %d,endIdx = %d",beginIdx,endIdx);
    
    return maxSum;
}

空间复杂度:O(1),时间复杂度:O(n^3)

2、暴力出奇迹优化

重复利用前面计算过的结果

/// 求最大连续子序列合(优化)
/// @param array 序列
+(int)maxSubArray2:(NSArray*)array{
    
    if (array == nil || array.count == 0) return -1;
    
    int beginIdx = 0;
    int endIdx = 0;
    int maxSum = [array[0] intValue];
    for (int begin = 0; begin<array.count; begin++) {
        int sum = 0;
        for (int end = begin; end<array.count; end++) {
            sum += [array[end] intValue];
            if (sum > maxSum) {
                maxSum = sum;
                beginIdx = begin;
                endIdx = end;
            }
        }
    }
    
    NSLog(@"beginIdx = %d,endIdx = %d",beginIdx,endIdx);
    return maxSum;
}

空间复杂度:O(1),时间复杂度:O(n^2)

3、分治

将序列均匀地分割成 2 个子序列
[begin , end) = [begin , mid) + [mid , end),mid = (begin + end) » 1

假设 [begin , end) 的最大连续子序列和是 S[i , j) ,那么它有 3 种可能
S[i , j) 存在于 [begin , mid) 中,同时 S[i , j) 也是 [begin , mid) 的最大连续子序列和
S[i , j) 存在于 [mid , end) 中,同时 S[i , j) 也是 [mid , end) 的最大连续子序列和
S[i , j) 一部分存在于 [begin , mid) 中,另一部分存在于 [mid , end) 中
✓ S[i , j) = S[i , mid) + S[mid , j)
✓ S[i , mid) = max { S[k , mid) },begin ≤ k < mid
✓ S[mid , j) = max { S[mid , k) },mid < k ≤ end

img

/// 求最大连续子序列合(分治)
/// @param array 序列
+(int)maxSubArray3:(NSArray*)array{
    return [self __maxSubArray3:array begin:0 end:(int)array.count];
}



+(int)__maxSubArray3:(NSArray*)array begin:(int)begin end:(int)end{
    
    if (end - begin < 2) return [array[begin] intValue];
    int mid = (begin + end) >> 1;

    
    int leftMax = [array[mid-1] intValue];
    int leftSum = leftMax;
    for (int i = mid-2; i>=0; i--) {
        leftSum += [array[i] intValue];
        leftMax = MAX(leftSum, leftMax);
    }
    
    int rightMax = [array[mid] intValue];
    int rightSum = rightMax;
    for (int i = mid+1; i<=array.count-1; i++) {
        rightSum += [array[i] intValue];
        rightMax = MAX(rightSum, rightMax);
    }

    //跨越了左右两部分的最大值
    int lrMax = leftMax+rightMax;
    
    //左侧最大值
    int lMax = [self __maxSubArray3:array begin:begin end:mid];
    
    //右侧最大值
    int rMax = [self __maxSubArray3:array begin:mid+1 end:end];
    
    return MAX( lrMax,MAX(lMax,rMax));
}

空间复杂度:O(logn)
时间复杂度:O(nlogn)

跟归并排序、快速排序一样
T n = 2T(n^2) + O(n)

大数乘法

2个超大的数(比如2个100位的数),如何进行乘法?

拿64位的操作系统来说,能表示的最大整数是 64个bit为全部为1,那么两个100位的整数相乘必然超过了计算机能表示的最大范围。

用字符串来实现大数相乘。

img

按照小学时学习的乘法运算,在进行 n 位数之间的相乘时,需要大约进行 n^2 次个位数的相乘

比如计算 36 x 54

img

1960 年 Anatolii Alexeevitch Karatsuba 提出了 Karatsuba 算法,提高了大数乘法的效率

img


行者常至,为者常成!





R
Valine - A simple comment system based on Leancloud.