算法之动态规划 练习1

说明

动态规划作为算法设计技巧之一,难度较大,学习起来叫人头疼,故特此系列博客来学习一番。

先实践后理论。我们先看一个例子,以LeetCode上的338题.计算位数1为例,题目大意为:

给定一个非负整数num,对处于范围[0, m]的每个数i,计算其二进制中为1的个数,并将结果作为数列返回。

示例:如num = 5, 则应返回[0, 1, 1, 2, 1, 2].

方法一:暴力枚举

显然我们可以通过向右移位计数来实现,但是该方法的时间复杂度是O(n*m)(其中m为该非负整数的数位,一般为32),并不是最优解。代码如下(C实现)

int* countBits(int num, int* returnSize) {
    int size = 32;
    *returnSize = num+1;
    int *res = (int *)malloc((num+1)*sizeof(int));
    int count;
    int temp;
    for(int i=0; i<=num; i++){
        count = 0;
        temp = i;
        for(int j=0; j<size; j++){              
            if(temp != ((temp >> 1)<<1)) count++;         
            temp = temp >> 1;            
        }
        res[i] = count;
    }
    return res;

}

以下介绍两个不同的时间复杂度为O(n)即线性时间的的解法。

方法二:动态规划

动态规划,即Dynamic Programming亦简称DP,是典型的空间换时间的思想。
应用DP,一般有2要素。一是状态,二是状态转移关系2。解题过程一般是先用一个数组(一维或二维)将每个阶段计算得到的值保存起来,而在本阶段的状态(也就是值)是直接用上几个状态得到的,而不用从最初开始计算。这也是递归和动规的区别之一。

从本题入手,所求的是[0, num]中每个数二进制表示中1的个数,1的个数即是我们需要的状态,我们用一个一维数组dp保存。则对于示例我们可以得到:

dp[0] = 0

dp[1] = 1

dp[2] = 1

dp[3] = 2

dp[4] = 1

dp[5] = 2

再往下扩展一下可以得到:

dp[6] = 2

dp[7] = 3

dp[8] = 1

对比前后项我们会发现以下关系:

dp[0] = 0

dp[1] = dp[0] + 1

dp[2] = dp[0] + 1

dp[3] = dp1 + 1

dp[4] = dp[0] + 1

dp[5] = dp1 + 1

dp[6] = dp[2] + 1

dp[7] = dp[3] + 1

dp[8] = dp[0] + 1

再对比右侧式子的前后向,会得到

dp[0] = 0

dp[1] = dp[1-1] + 1

dp[2] = dp[2-2] + 1

dp[3] = dp[3-2] + 1

dp[4] = dp[4-4] + 1

dp[5] = dp[5-4] + 1

dp[6] = dp[6-4] + 1

dp[7] = dp[7-4] + 1

dp[8] = dp[8-8] + 1

看明白了没,能适用于全部状态的一般数学递推式呼之欲出:

dp[i] = dp[i - 偏移量],其中i即是[0, num]中的数i,而偏移量就是该数的二进制最高位所表示的数。该关系式即是上文提到的状态转移方程。

当得到了状态转移方程,再结合一个for循环,代码就不难写出(以C实现):

int* countBits(int num, int* returnSize) {
    int *ret = (int *)malloc((num+1) * sizeof(int));
    int offset = 1;
    *returnSize = num+1;
    ret[0] = 0;
    for(int i=1; i < num+1; ++i){
        if(offset * 2 == i) offset *= 2;
        ret[i] = ret[i - offset] + 1;
    }    
    return ret;
}

方法三:利用二进制数位关系
该方法比较取巧,利用的二进制数位的进位关系,代码如下(C++实现)

int* countBits(int num, int* returnSize) {
    int *ret = (int *)malloc((num+1) * sizeof(int));
    int offset = 1;
    *returnSize = num+1;
    ret[0] = 0;
    for(int i=1; i < num+1; ++i){
        if(offset * 2 == i) offset *= 2;
            ret[i] = ret[i & (i-1)] + 1; 
    }    
    return ret;
}

总结

任意一个数学递推公式都可以直接转换成递归算法,使得算法性能相对于简单暴力查找得到了重大的提升。但是潜在的问题是编译器不会公平对待递归算法,导致了程序的低效。当我们怀疑遇到这样的情况时,则需要通过将递归算法重写为非递归算法来系统地记录子问题的答案于一张表中,这样来给编译器提供帮助。而这样的方法我们称之为动态规划3


  1. LeetCode 338. Counting Bits
  2. 王道论坛计算机考研机试指南.
  3. Mark Allen Weiss, “Data Structures and Algorithm Analysis in C”, the 2nd edition, China Machine Press.

猜你喜欢

转载自blog.csdn.net/Shuang_Mo/article/details/80463693