JHHK

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

【进阶】17、动态规划

目录

动态规划(Dynamic Programming)

动态规划,简称DP
是求解最优化问题的一种常用策略
通常的使用套路(一步一步优化)
① 暴力递归(自顶向下,出现了重叠子问题)
② 记忆化搜索(自顶向下)
③ 递推(自底向上)

找零钱

假设有25分、20分、5分、1分的硬币,现要找给客户41分的零钱,如何办到硬币个数最少?
此前用贪心策略得到的并非是最优解(贪心得到的解是 5 枚硬币)

假设 dp(n) 是凑到 n 分需要的最少硬币个数
如果第 1 次选择了 25 分的硬币,那么 dp(n) = dp(n – 25) + 1
如果第 1 次选择了 20 分的硬币,那么 dp(n) = dp(n – 20) + 1
如果第 1 次选择了 5 分的硬币,那么 dp(n) = dp(n – 5) + 1
如果第 1 次选择了 1 分的硬币,那么 dp(n) = dp(n – 1) + 1
所以 dp(n) = min { dp(n – 25), dp(n – 20), dp(n – 5), dp(n – 1) } + 1

一、暴力递归

暴力递归(自顶向下的调用,出现了重叠子问题)

+(int)coins:(int)n{
    
    if (n < 1) return INT_MAX;//
    
    //可选的硬币大小为 25 20 5 1
    if (n == 25 || n == 20 ||n == 5 ||n == 1 ) return 1;
    
    /**
     凑够零钱n,最后一次拿的肯定是 1 5 20 25中的一个
     所以求出 凑够 n-1 n-5 n-20 n-25 需要的最少硬币个数 加1,就是凑够n需要的硬币个数
     在其中选出最小的,就是凑够n的最小的硬币数。
     */
    int min1 = MIN([self coins:n-25], [self coins:n-20]);
    int min2 = MIN([self coins:n- 5], [self coins:n- 1]);
    return MIN(min1, min2)+1;
}

类似于斐波那契数列的递归版,会有大量的重复计算,时间复杂度较高

二、记忆化搜索

记忆化搜索(自顶向下的调用),将已经计算出的值进行存储,后续不再重复计算。

+(int)coins2:(int)n{
    if (n < 1) return -1;
    
    //dp[i]是凑够i分时的最少硬币个数,初始化为0
    NSMutableArray * dp = [NSMutableArray arrayWithCapacity:n+1];
    for (int i=0; i<n+1; i++) {
        dp[i] = @(0);
    }
    
    NSArray * faces = @[@1,@5,@20,@25];
    for (NSNumber* face in faces) {
        if (n < face.intValue) break;
        dp[face.intValue] = @(1);
    }
    
    return [self __coins2:n dp:dp];
}


+(int)__coins2:(int)n dp:(NSMutableArray*)dp{
    
    if (n < 1) return INT_MAX;
    
    //如果没有计算过,计算
    if ([dp[n] intValue] == 0) {
        int min1 = MIN([self __coins2:n-25 dp:dp], [self __coins2:n-20 dp:dp]);
        int min2 = MIN([self __coins2:n- 5 dp:dp], [self __coins2:n- 1 dp:dp]);
        dp[n] = @(MIN(min1, min2)+1);
    }
    
    //如果之前计算过,直接取出
    return [dp[n] intValue];
}

三、递推(自底向上)

+(int)coins3:(int)n {
    if (n < 1) return -1;
    //dp[i]是凑够i分时的最少硬币个数
    NSMutableArray * dp = [NSMutableArray arrayWithCapacity:n+1];
    for (int i=0; i<n+1; i++) {
        dp[i] = @(0);
    }
    
    
    /**
     凑够零钱n,最后一次拿的肯定是 1 5 20 25中的一个
     所以求出 凑够 n-1 n-5 n-20 n-25 需要的最少硬币个数 加1,就是凑够n需要的硬币个数
     在其中选出最小的,就是凑够n的最小的硬币数。
     */
    
    /**
     由于是自底向上,求i的时候 i-1,i-5,i-20,i-25肯定已经有值了
     */
    for (int i = 1; i <= n; i++) {
        //        int min = INT_MAX;
        //        if (i >=  1) min = MIN([dp[i -  1] intValue], min);
        //上边两句可用下面一句代替
        int min = min = [dp[i -  1] intValue];
        if (i >=  5) min = MIN([dp[i -  5] intValue], min);
        if (i >= 20) min = MIN([dp[i - 20] intValue], min);
        if (i >= 25) min = MIN([dp[i - 25] intValue], min);
        dp[i] = @(min + 1);
    }
    
    return [dp[n] intValue];
}

时间复杂度、空间复杂度:O(n)

通过上面的例子初步认识下动态规划,其实上面三步已经演示了动态规划是怎么来的。

四、输出具体方案

+(int)coins4:(int)n {
    
    if (n < 1) return -1;
    //dp[i]是凑够i分时的最少硬币个数
    NSMutableArray * dp = [NSMutableArray arrayWithCapacity:n+1];
    for (int i=0; i<n+1; i++) {
        dp[i] = @(0);
    }
    
    //faces[i]是凑够i分时最后那枚硬币的面值
    NSMutableArray * faces = [NSMutableArray arrayWithCapacity:n+1];
    for (int i=0; i<n+1; i++) {
        faces[i] = @(0);
    }
    
    
    for (int i = 1; i <= n; i++) {
        int min = INT_MAX;
        if (i >=  1 && [dp[i - 1] intValue]<min){
            min = [dp[i -  1] intValue];
            faces[i] = @(1);
        }
        
        
        if (i >=  5 && [dp[i - 5] intValue]<min){
            min = [dp[i -  5] intValue];
            faces[i] = @(5);
        }
        
        if (i >= 20 && [dp[i - 20] intValue]<min) {
            min = [dp[i -  20] intValue];
            faces[i] = @(20);
        }
        
        if (i >= 25 && [dp[i - 25] intValue]<min) {
            min = [dp[i -  25] intValue];
            faces[i] = @(25);
        }
        
        dp[i] = @(min + 1);
        
        [self printN:i faces:faces];
    }
    return [dp[n] intValue];
}


//打印凑够n分,选择的硬币
+(void)printN:(int)n faces:(NSArray*)faces{
    printf("[%d] = ",n);
    while (n>0) {
        int face = [faces[n] intValue];
        printf("%d ",face);
        n-=face;
    }
    printf("\n");
}

五、通用方案

/// 零钱兑换,返回最少硬币个数
/// @param n 找零
/// @param faces 硬币
+(int)coins5:(int)n faces:(nonnull NSArray *)faces{
    if (n < 1 || faces == nil || faces.count == 0) return -1;
    
    //dp[i]是凑够i分时的最少硬币个数
    NSMutableArray * dp = [NSMutableArray arrayWithCapacity:n+1];
    for (int i=0; i<n+1; i++) {
        dp[i] = @(0);
    }
    
    for (int i = 1; i <= n; i++) {
        int min = INT_MAX;
        
        // 2 5 20 25
        for (NSNumber* face in faces) {
            int faceInt = face.intValue;
            
            //i 比该枚硬币小不可选
            if ( i < faceInt) continue;
            
            int v = [dp[i-faceInt] intValue];
            //v<0 dp[i-faceInt] 不可凑 不选
            //v >= min 不是最小解 不选
            if (v < 0 || v >= min) continue;
            
            //最小解
            min = v;
        }
        
        
        //如果最小值还是INT_MAX,证明无法凑够
        if (min == INT_MAX) {
            dp[i] = @(-1);
        }else{
            dp[i] = @(min + 1);
        }
    }
    return [dp[n] intValue];
}

行者常至,为者常成!





R
Valine - A simple comment system based on Leancloud.