leetcode實(shí)戰(zhàn)——最大子序列的和(動態(tài)規(guī)劃,分治法铣鹏,Kadane算法)

這個題目需要使用到動態(tài)規(guī)劃敷扫,還不清楚什么是動態(tài)規(guī)劃的同學(xué),可以看我的另一篇文章的解釋

題目

給定一個整數(shù)數(shù)組 nums 诚卸,找到一個具有最大和的連續(xù)子數(shù)組(子數(shù)組最少包含一個元素)葵第,返回其最大和。

示例

輸入: [-2,1,-3,4,-1,2,1,-5,4],
輸出: 6
解釋: 連續(xù)子數(shù)組 [4,-1,2,1] 的和最大合溺,為 6卒密。

進(jìn)階:

如果你已經(jīng)實(shí)現(xiàn)復(fù)雜度為 O(n) 的解法,嘗試使用更為精妙的分治法求解辫愉。

解法

初看這道題栅受,思路基本就是用遍歷的方法,就這個例子來說,一共有9個數(shù)字屏镊,我們把所有的排列組合列出來依疼,然后求出最大值。按照排列組合的數(shù)學(xué)算法而芥,一共有45個組合律罢,如果有n個數(shù)字,時間復(fù)雜度是O(n^2)棍丐,這樣的時間復(fù)雜度是明顯不能接受的误辑。

解法1 動態(tài)規(guī)劃

于是我們把目光落到動態(tài)規(guī)劃上面來,首先需要把這個問題分解成最優(yōu)子問題來解歌逢。最主要的思路就是將上面的45個組合進(jìn)行分類巾钉,分解成數(shù)量較少的幾個子問題。在這里我們一共有9個數(shù)字秘案,順理成章的我們把組合分解成9個小組的組合砰苍。

  1. 第一個子組合是以第一個數(shù)字結(jié)尾的連續(xù)序列,也就是[-2]阱高,最大值-2

  2. 第二個子組合是以第二個數(shù)字結(jié)尾的連續(xù)序列赚导,也就是[-2,1], [1],最大值1

  3. 第三個子組合是以第三個數(shù)字結(jié)尾的連續(xù)序列赤惊,也就是[-2,1,3], [1,3], [3]吼旧,最大值4

  4. 以此類推。未舟。圈暗。

如果我們能夠得到每一個子組合的最優(yōu)解,也就是子序列的最大值处面,整體的最大值就可以通過比較這9個子組合的最大值來得到了〕е茫現(xiàn)在我們找到了最優(yōu)子問題,重疊子問題在哪呢魂角?那就得細(xì)心比較一下每個子問題昵济。

從第二個子組合和第三個子組合可以看到,組合3只是在組合2的基礎(chǔ)上每一個數(shù)組后面添加第3個數(shù)字野揪,也就是3访忿,然后增加一個只有第三個數(shù)字的數(shù)組[3]。這樣兩個組合之間的關(guān)系就出現(xiàn)了斯稳,可是我們不關(guān)心這個序列是怎么生成的海铆,只是關(guān)心最大值之間的關(guān)系。我們將子組合三分成兩種情況:

  1. 繼承子組合二得到的序列挣惰,也就是[-2,1,3], [1,3] (最大值 = 第二個組合的最大值 + 第三個數(shù)字)
  2. 單獨(dú)第三個數(shù)字的序列卧斟,也就是[3] (最大值 = 第三個數(shù)字)

如果第二個序列的最大值大于0殴边,那么最大值1就比最大值2要大,反之最大值2較大珍语。這樣锤岸,我們就通過第二個組合的最大值和第三個數(shù)字,就得到了第三個組合的最大值板乙。因?yàn)榈诙€組合的結(jié)果被重復(fù)用到了是偷,所以符合這個重疊子問題的定義。通俗來講這個問題就變成了募逞,第i個子組合可以通過第i-1個子組合的最大值和第i個數(shù)字獲得蛋铆,如果第i-1個子組合的最大值沒法給第i個數(shù)字帶來正增益,我們就拋棄掉前面的子組合放接,自己就是最大的了刺啦。
\begin{aligned} &如果Max(i-1) > 0, Max(i) = Max(i-1) + Nums(i) \\ &如果Max(i-1) <= 0, Max(i) = Nums(i) \end{aligned}

來看看代碼,我們只需要一個變量subMax保存前面子組合的最大值透乾,另外一個max保存全局最大值洪燥。

    public int maxSubArray(int[] nums) {
        if (nums == null) {
            return 0;
        }

        int max = nums[0];    // 全局最大值
        int subMax = nums[0];  // 前一個子組合的最大值
        for (int i = 1; i < nums.length; i++) {
            if (subMax > 0) {
                // 前一個子組合最大值大于0,正增益
                subMax = subMax + nums[i];
            } else {
                // 前一個子組合最大值小于0乳乌,拋棄前面的結(jié)果
                subMax = nums[i];
            }
            // 計算全局最大值
            max = Math.max(max, subMax);
        }

        return max;
    }

解法2 分治法

分治法是將整個數(shù)組切分成幾個小組,每個小組然后再切分成幾個更小的小組市咆,一直到不能繼續(xù)切分也就是只剩一個數(shù)字為止汉操。然后每個小組會計算出最優(yōu)值,匯報給上一級的小組蒙兰,一級一級匯報磷瘤,上級拿到下級的匯報找到最大值,得到最終的結(jié)果搜变。和合并排序的算法類似采缚,先切分,再合并結(jié)果挠他。

這個問題中的關(guān)鍵就是如何切分這些組合才能使每個小組之間不會有重復(fù)的組合(有重復(fù)的組合意味著有重復(fù)的計算量)扳抽,這個問題應(yīng)該困擾了不少的同學(xué),我在學(xué)習(xí)理解的時候也花了不少時間殖侵。

首先是切分分組方法贸呢,就這個案例中的例子來,我們有一個數(shù)組[-2,1,-3,4,-1,2,1,-5,4]拢军,一共有9個元素楞陷,我們center=(start + end) / 2這個原則,得到中間元素的索引為4茉唉,也就是-1固蛾,拆分成三個組合:

  • [-2,1,-3,4,-1]以及它的子序列(在-1左邊的并且包含它的為一組)
  • [2,1,-5,4] 以及它的子序列(在-1右邊不包含它的為一組)
  • 任何包含-1以及它右邊元素2的序列為一組(換言之就是包含左邊序列的最右邊元素以及右邊序列最左邊元素的序列结执,比如[4,-1,2,1],這樣就保證這個組合里面的任何序列都不會和上面兩個重復(fù))

以上的三個組合內(nèi)的序列沒有任何的重復(fù)的部分艾凯,而且一起構(gòu)成所有子序列的全集昌犹,計算出這個三個子集合的最大值,然后取其中的最大值览芳,就是這個問題的答案了斜姥。

然而前兩個子組合可以用遞歸來解決,一個函數(shù)就搞定沧竟,第三個跨中心的組合應(yīng)該怎么計算最大值呢铸敏?

答案就是先計算左邊序列里面的包含最右邊元素的的子序列的最大值,也就是從左邊序列的最右邊元素向左一個一個累加起來悟泵,找出累加過程中每次累加的最大值杈笔,就是左邊序列的最大值。同理找出右邊序列的最大值糕非,就得到了右邊子序列的最大值蒙具。左右兩邊的最大值相加,就是包含這兩個元素的子序列的最大值朽肥。

在計算過程中禁筏,累加和比較的過程是關(guān)鍵操作,一個長度為n的數(shù)組在遞歸的每一層都會進(jìn)行n次操作衡招,分治法的遞歸層級在logN級別篱昔,所以整體的時間復(fù)雜度是O(nlogn),在時間效率上不如動態(tài)規(guī)劃的O(n)復(fù)雜度始腾。

代碼如下

    public int maxSubArray(int[] nums) {
        return maxSubArrayDivideWithBorder(nums, 0, nums.length-1);
    }

    private int maxSubArrayDivideWithBorder(int[] nums, int start, int end) {
        if (start == end) {
            // 只有一個元素州刽,也就是遞歸的結(jié)束情況
            return nums[start];
        }

        // 計算中間值
        int center = (start + end) / 2;
        int leftMax = maxSubArrayDivideWithBorder(nums, start, center); // 計算左側(cè)子序列最大值
        int rightMax = maxSubArrayDivideWithBorder(nums, center + 1, end); // 計算右側(cè)子序列最大值

        // 下面計算橫跨兩個子序列的最大值

        // 計算包含左側(cè)子序列最后一個元素的子序列最大值
        int leftCrossMax = Integer.MIN_VALUE; // 初始化一個值
        int leftCrossSum = 0;
        for (int i = center ; i >= start ; i --) {
            leftCrossSum += nums[i];
            leftCrossMax = Math.max(leftCrossSum, leftCrossMax);
        }

        // 計算包含右側(cè)子序列最后一個元素的子序列最大值
        int rightCrossMax = nums[center+1];
        int rightCrossSum = 0;
        for (int i = center + 1; i <= end ; i ++) {
            rightCrossSum += nums[i];
            rightCrossMax = Math.max(rightCrossSum, rightCrossMax);
        }

        // 計算跨中心的子序列的最大值
        int crossMax = leftCrossMax + rightCrossMax;

        // 比較三者,返回最大值
        return Math.max(crossMax, Math.max(leftMax, rightMax));
    }

解法3 Kadane算法

Kadane全名叫Joseph "Jay" Born Kadane浪箭,是卡耐基梅隆大學(xué)的統(tǒng)計學(xué)方面的教授穗椅,于1984年提出提出了線性解決這個問題的辦法。

國內(nèi)網(wǎng)上有很多材料提到了Kadane算法奶栖,并且將Kadane算法和動態(tài)規(guī)劃并列到了一起匹表,表明是兩個不同的算法,但是當(dāng)搜索外文網(wǎng)站的時候驼抹,大家用的Kadane算法和動態(tài)規(guī)劃的思路是一模一樣的桑孩,參考我的第一個參考文章,這里貼一下Wikipedia的代碼框冀,感覺和我們的動態(tài)規(guī)劃的算法似乎不太一樣

def max_subarray(numbers):
    """Find a contiguous subarray with the largest sum."""
    best_sum = 0  # or: float('-inf')
    current_sum = 0
    for x in numbers:
        current_sum = max(0, current_sum + x)
        best_sum = max(best_sum, current_sum)
    return best_sum

我們把這個代碼放到網(wǎng)站跑一邊流椒,發(fā)現(xiàn)沒有通過,因?yàn)楫?dāng)這個數(shù)組全是負(fù)數(shù)的時候明也,它的結(jié)果是0宣虾,改進(jìn)方法是把max(0, current_sum + x)換成max(x, current_sum + x)惯裕,這樣就沒有問題了:

def max_subarray(numbers):
    """Find a contiguous subarray with the largest sum."""
    best_sum = 0  # or: float('-inf')
    current_sum = 0
    for x in numbers:
        current_sum = max(x, current_sum + x)
        best_sum = max(best_sum, current_sum)
    return best_sum

現(xiàn)在大家有沒有感覺這段代碼很眼熟,沒錯绣硝!這就是我們的動態(tài)規(guī)劃的解法蜻势。和原來的一樣,核心思路都是判斷以前一個元素結(jié)尾的子序列的最大值能不能給當(dāng)前元素結(jié)尾的序列提供增益鹉胖。當(dāng)初布朗大學(xué)的Ulf Grenander教授在1977年提出這個問題的時候是為了展示數(shù)字圖像中一個簡單的最大似然估計模型握玛,可能沒有過多考慮道負(fù)數(shù)的問題,所以后來在解決的時候就沒有考慮到全是負(fù)數(shù)的情況甫菠。

延伸——獲取最大序列的起始和結(jié)束位置

可以使用我們的第一種方法也就是動態(tài)規(guī)劃的方法來找到這個位置挠铲,將以這個元素結(jié)尾的的最大子序列的位置找出來,然后每次比較最大值的時候更新一下最大值的位置就行了

    public int maxSubArrayPosition(int[] nums) {
        if (nums == null) {
            return 0;
        }

        int start = 0;
        int end = 0;
        int subStart = 0;
        int subEnd = 0;
        int max = nums[0];    // 全局最大值
        int subMax = nums[0];  // 前一個子組合的最大值
        for (int i = 1; i < nums.length; i++) {
            if (subMax > 0) {
                // 前一個子組合最大值大于0寂诱,正增益拂苹,更新最后元素位置
                subMax = subMax + nums[i];
                subEnd++;
            } else {
                // 前一個子組合最大值小于0,拋棄前面的結(jié)果痰洒,更新當(dāng)前最大值位置
                subMax = nums[i];
                subStart = i;
                subEnd = i;
            }
            // 計算全局最大值瓢棒,更新位置,將全局最優(yōu)解的位置更新
            if (subMax > max) {
                max = subMax;
                start = subStart;
                end = subEnd;
            }
        }

        System.out.println("[" + start + ","+ end +"]");
        return max;
    }

參考

Kadane’s Algorithm Explained
Maximum subarray problem
求最大連續(xù)子序列的和丘喻,兩種解法:動態(tài)規(guī)劃 & Kadane算法
分治策略結(jié)合遞歸思想求最大子序列和
Kadane’s Algorithm — (Dynamic Programming) — How and Why does it Work?

更多內(nèi)容請看我的個人博客

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末脯宿,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子仓犬,更是在濱河造成了極大的恐慌嗅绰,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,657評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件搀继,死亡現(xiàn)場離奇詭異,居然都是意外死亡翠语,警方通過查閱死者的電腦和手機(jī)叽躯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來肌括,“玉大人点骑,你說我怎么就攤上這事〉玻” “怎么了黑滴?”我有些...
    開封第一講書人閱讀 164,057評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長紧索。 經(jīng)常有香客問我袁辈,道長,這世上最難降的妖魔是什么珠漂? 我笑而不...
    開封第一講書人閱讀 58,509評論 1 293
  • 正文 為了忘掉前任晚缩,我火速辦了婚禮尾膊,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘荞彼。我一直安慰自己冈敛,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,562評論 6 392
  • 文/花漫 我一把揭開白布鸣皂。 她就那樣靜靜地躺著抓谴,像睡著了一般。 火紅的嫁衣襯著肌膚如雪寞缝。 梳的紋絲不亂的頭發(fā)上癌压,一...
    開封第一講書人閱讀 51,443評論 1 302
  • 那天,我揣著相機(jī)與錄音第租,去河邊找鬼措拇。 笑死,一個胖子當(dāng)著我的面吹牛慎宾,可吹牛的內(nèi)容都是我干的丐吓。 我是一名探鬼主播,決...
    沈念sama閱讀 40,251評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼趟据,長吁一口氣:“原來是場噩夢啊……” “哼券犁!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起汹碱,我...
    開封第一講書人閱讀 39,129評論 0 276
  • 序言:老撾萬榮一對情侶失蹤粘衬,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后咳促,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體稚新,經(jīng)...
    沈念sama閱讀 45,561評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,779評論 3 335
  • 正文 我和宋清朗相戀三年跪腹,在試婚紗的時候發(fā)現(xiàn)自己被綠了褂删。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,902評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡冲茸,死狀恐怖屯阀,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情轴术,我是刑警寧澤难衰,帶...
    沈念sama閱讀 35,621評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站逗栽,受9級特大地震影響盖袭,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,220評論 3 328
  • 文/蒙蒙 一苍凛、第九天 我趴在偏房一處隱蔽的房頂上張望趣席。 院中可真熱鬧,春花似錦醇蝴、人聲如沸宣肚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽霉涨。三九已至,卻和暖如春惭适,著一層夾襖步出監(jiān)牢的瞬間笙瑟,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評論 1 269
  • 我被黑心中介騙來泰國打工癞志, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留往枷,地道東北人。 一個月前我還...
    沈念sama閱讀 48,025評論 2 370
  • 正文 我出身青樓凄杯,卻偏偏與公主長得像错洁,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子戒突,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,843評論 2 354

推薦閱讀更多精彩內(nèi)容