动态规划算法(一)

概述

本篇回忆了动态规划问题的基本特征和求解的基本套路。
简单分析了问题走台阶问题最大连续子数组问题

什么是动态规划?(dynamic programing)

动态规划是一种算法思想,其命名的两个单词,dynamic和programing揭示了这种算法的思想特征,即为了求解问题需要不断在局部的子问题中求解最优值。
例如,一个比较简单的经典例子:
求解斐波那契数列的第n项。

我们知道, 斐波那契数列的定义:

image.png

除了开头的两项,每一项的值都依赖前两项的值。子问题f(n-2)和f(n-1)的解又依赖其它子问题的解, 这个递推过程不断膨胀,会得到一颗增长非常庞大的子问题树。如果递归求解,效率是非常慢的。为了优化速度,可以建立一个表来记忆已经计算过的中间结果,可以使用O(n)的时间复杂度和O(1)的空间复杂度求得问题的解。

子问题树,f(8)、f(7)... 被反复求解

动态规划 当我们要求解一个问题,可以将问题拆解成若干个子问题,并且可以根据子问题的结果计算出最终问题的解,那么该问题就具备了动态规划求解的可能。我们知道分治的策略与此过程类似,二者的主要区别是:

  • 分治策略中,子问题之间是被分割的,彼此之间不依赖;而动态规划问题中,子问题一般相互重叠
  • 动态规划问题的子问题具有最优子结构,即,最终解蕴含在子问题的最优解中。

以上斐波那契数列的例子中, 为了求解 f(n), 我们根据递推式,得到了它的两个子问题 f(n-1) 和 f(n-2), 子问题的解求出之后,可以直接得到最终问题的解。它就具备了动态规划求解的特征。

实际问题中,我们通常没有那么直观的递推式。

走台阶问题

比如斐波那契数列求解有一个不那么直观的登台阶的问题:
假设有一个台阶,共有n级,小明每次可以向上走1步或2步,问:小明共有多少种走法登上顶。
T(n) 是登上第n级台阶的走法
当台阶只有 1 级时,显然小明只能走一步上去 T(1) = 1;
当台阶只有 2 级时,小明可以走两次1步登上去,也可以一次登2步, 因此 T(2) = 2
当 n = 3时, 小明可能的走法 1 + 1 + 1 = 3, 或1 + 2 = 3 或 2 + 1 = 3 因此 T(3) = 3

我们可以运用动态规划的思路
考察T(n)的子问题 T(n-1)
假设我们已经求得T(n-1), 那么小明抵达n - 1级后,只需要再向前迈一步就到达第n级。
另外到达第n级并不是必须要经过第n-1级,因为可以一次走2步,所以不经过第n-1级到达n级的走法必须经过第n-2级。
此外我们还可以稍微论证一下,所有到达第n级的走法要么经过第n-1级,要么不经过第 n-1 级,不经过第n-1级的走法必须经过第n-2级
因此关于 T(n)我们得到一个基本的子问题的拆分:
一种是必须经过第n-1级的走法,它的数量是 T(n-1)
另一种是不经过第n-1级的走法,此时必须经过第n-2级,然后再迈一次两步可到达第n级。写成递推式则是

T(n) = T(n-1) + T(n-2), n ≥ 3
T(1) = 1, n = 1
T(2) = 2, n = 2

伪代码

calc_step_cnt(n):
  if n == 1
     return 1
  if n == 2
     return 2
  let a = 1, b = 2
  for i in range[3, n]
       a, b = b, a + b
  return b
最大连续子数组问题

假设一个整数数组 An,它包含 下标为0,1, ... , n - 1的n个整数,其中有正数和负数,计算它最大的连续子数组的和是多少?
朴素的想法是,将数组的所有连续子数组的和计算出来,由于连续子数组的起始下标分别可以是从0 到 n - 1, 最后求最大值需要一次复杂度为O(n)的计算,暴力解法的时间复杂度将会达到O(n³).
经过一些技巧优化,可以将复杂度降到 O(n²)

另外一种分治解法可以将时间复杂度优化到O(n logn)。即我们考虑将数组从中间分成两半 A[0, n/2], A[n/2, n],
那么最大连续子数组有三种情况:

  • 1 在左半子数组上
  • 2 在右半子数组上
  • 3 跨越分界点

对于1和2的情形, 处理起来相对简单。重点关注第3种情形。由于子数组被固定,并需要穿过数组的中点,那么我们可以计算所有跨过中点的连续子数组的和,取最大的那个,这个过程只需要O(n)的时间。分治拆分时间复杂度是log(n),整个算法的复杂度为O(nlogn)

int  max_subarray(int nums[], int l, int r)
{
        if (l >= r) {
            return nums[l];
        }
        int mid = l + (r - l) / 2;
        int max_crossmid = nums[mid];  //max_crossmid记录跨越中点的最大连续子数组的和
        int sum = 0;
        for (int i = mid; i >= l; --i) {
            sum += nums[i];
            max_crossmid = max(max_crossmid, sum);
        }
        sum = max_crossmid; 
        for (int i = mid + 1; i <= r; ++i) {
            sum += nums[i];
            max_crossmid = max(max_crossmid, sum);
        }
        return max(max(max_subarray(nums, l, mid), max_subarray(nums, mid + 1, r)), max_crossmid);
    }

分治算法对子问题的拆分:左半数组和右半数组互不关联,问题不重叠,可以通过递归逐渐求解。

考虑另一个子问题划分策略:我们用 S(n)表示数组A[0,n]中以第n项A[n]结尾的最大连续子数组的和
那么考察其子问题 S(n-1)

如果 S(n - 1) < 0, 那么为了求得最大和,对于S(n)而言,可以抛弃前面的负值只留下 A[n]项,因此S(n) = A[n];
如果S(n - 1) ≥ 0, 那么很显然, S(n) = A[n] + S(n-1)。
数组的最大连续子数组的和最终是 max{S(i)} ,i = 1, 2, ..., n
写成表达式:


image.png

算法实现:

int maxSubArray(vector<int>& nums) {
        if (nums.size() == 0) {
            return 0;
        }
        vector<int> s(nums.size());
        s[0] = nums[0];
        for (int i = 1; i < nums.size(); ++i) {
            if (s[i - 1] < 0) {
                s[i] = nums[i];
            } else {
                s[i] = s[i - 1] + nums[i];
            }
        }
        return *max_element(s.begin(), s.end());
    }

以上算法可以进一步优化,在循环体内顺带维护S(n)序列的最大值, 只需要维护当前以A[i]为右界的连续数组的最大和,以及所有右界产生的这个和的最大值就可以,因此无须用一个数组的额外空间,简化为两个变量。
以下是这种简化之后的代码,惊奇的是,这个算法只需要一次循环就实现了,因而时间复杂度是O(n)。
如果你甫一看这段代码,很难将上面的动态规划的分析思路联系起来。

int maxSubArray(vector<int>& nums) {
        if (nums.size() == 0) {
            return 0;
        }
        int max_rightbound_sum = 0; // 表示以A[i]为右界的连续子数组的最大和
        int max_sum = nums[0]; //表示每个元素对应的sum中最大的数
        for (int i = 0; i < nums.size(); ++i) {
            if (max_rightbound_sum < 0) {
                max_rightbound_sum = nums[i];
            } else {
                max_rightbound_sum += nums[i];
            }
            if (max_sum < max_rightbound_sum)
                max_sum = max_rightbound_sum;
        }
        return max_sum;
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,193评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,306评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,130评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,110评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,118评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,085评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,007评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,844评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,283评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,508评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,395评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,985评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,630评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,797评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,653评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,553评论 2 352

推荐阅读更多精彩内容