動態(tài)規(guī)劃算法學習與思考

前言

動態(tài)規(guī)劃是筆試中經(jīng)常出現(xiàn)的一類題目瓢湃。掌握他很關(guān)鍵。

網(wǎng)易題目

小Q和牛博士合唱一首歌曲,這首歌曲由n個音調(diào)組成,每個音調(diào)由一個正整數(shù)表示业簿。
對于每個音調(diào)要么由小Q演唱要么由牛博士演唱,對于一系列音調(diào)演唱的難度等于所有相鄰音調(diào)變化幅度之和, 例如一個音調(diào)序列是8, 8, 13, 12, 那么它的難度等于|8 - 8| + |13 - 8| + |12 - 13| = 6(其中||表示絕對值)。
現(xiàn)在要對把這n個音調(diào)分配給小Q或牛博士,讓他們演唱的難度之和最小,請你算算最小的難度和是多少。
如樣例所示: 小Q選擇演唱{5, 6}難度為1, 牛博士選擇演唱{1, 2, 1}難度為2,難度之和為3,這一個是最小難度和的方案了撼唾。

貪心解法(錯誤)

將所有數(shù)字進行排序,取最大的兩個數(shù)字差作為間隔哥蔚,取其中一下半作為小Q的音符倒谷,取另一上半作為牛博士的音符,然后計算結(jié)果肺素。最終能過通過60%的用例恨锚。
如果時間不夠,或者不會的情況可以使用這種解法倍靡。

#include <vector>
#include <iostream>
#include <algorithm>
#include <cmath>
 
using namespace std;
 
int main(){
    int n;
    cin >> n;
    vector<int> nums;
    for(int i=0;i<n;i++)
    {
        int num;
        cin >> num;
        nums.push_back(num);
    }
    vector<int> nums_sort(nums.begin(), nums.end());
    sort(nums_sort.begin(), nums_sort.end());
    int max_gap = 0;
    auto max_gap_it = nums_sort.begin();
    for(auto it=nums_sort.begin(); it!=nums_sort.end()-1; it++){
        if(*(it+1) - *it > max_gap){
            max_gap = *(it+1) - *it;
            max_gap_it = it;
        }
    }
    int max_gap_num = *max_gap_it;
     
    int ret = 0;
    int last_1 = -1;
    int last_2 = -1;
    for(auto it = nums.begin(); it!=nums.end(); it++){
         
        if(*it <= max_gap_num){
            if(last_1 != -1){
                ret += abs(*it - last_1);
            }
            last_1 = *it;
        }
        else{
            if(last_2 != -1){
                ret += abs(*it - last_2);
            }
            last_2 = *it;
        }
    }
    cout << ret;
}

動態(tài)規(guī)劃小思

由于我對動態(tài)規(guī)劃一點也不了解猴伶,先從找零開始。

  • 找零問題

動態(tài)規(guī)劃的核心在于問題的分解和子問題遞歸組合變成原問題塌西。

舉個例子他挎,假設我們有面值為1元、3元和5元的硬幣若干枚捡需,如何用最少的硬幣湊夠11元办桨?
如果你是個人類的話,你會說站辉,我大眼一瞪呢撞,兩個五元和一個一元损姜,總共3個硬幣。我們換個例子殊霞,假設題目是237元呢摧阅?你可能會說,我先用5塊錢換出200元绷蹲,然后找到剩下37元的最優(yōu)組合棒卷。這種方法實際上是默認了將237元分成了兩份,并假設兩份的組合是最優(yōu)組合祝钢。如果真是這樣的話比规,那么就沒什么問題了±褂ⅲ可問題就出在分的200和37元蜒什,沒有方法能證明這是有效的分割,所以動態(tài)規(guī)劃就是建立在有效分割上的龄章。

分治算法與動態(tài)規(guī)劃也是建立在分割上的吃谣。只是動態(tài)規(guī)劃涉及到狀態(tài)轉(zhuǎn)移,而分治算法沒有狀態(tài)轉(zhuǎn)移做裙。分治算法可以由上向下分解岗憋,而動態(tài)規(guī)劃通常由下向上構(gòu)建。

動態(tài)規(guī)劃會涉及到兩個維度锚贱,第一個維度通常和問題的規(guī)模相關(guān)仔戈,第二個維度需要從題目中提取出來。在本題目中拧廊,第二個維度是允許使用的硬幣监徘,例如,允許使用1元吧碾、允許使用1元和3元凰盔、允許使用1元3元5元。我們把他記為下標1,2,3倦春,我們用j來代表這個變量户敬,如果用i來表示當前題目的規(guī)模,也就是237睁本,那么我們要求的是c[i][j] 為 c[237][3]的值尿庐。對于這個問題,我們可以分兩種假設呢堰,第一種假設抄瑟,這個最優(yōu)解中5元的數(shù)量>0,第二種假設枉疼,這個最優(yōu)值中5元的數(shù)量=0.對于第一種假設c[237][3]=c[237][2]皮假,因為沒有使用5元鞋拟,那么就不需要5元了,這個最優(yōu)解的解應該和只是用1元和3元的情況一致惹资。對于第二種情況严卖,由于這個解中必然有一個5元(因為你假設他大于0),那么布轿,除了這枚5元錢,剩下的錢數(shù)是232元錢来颤。由于原來c[237][3]是135元找237元的最優(yōu)值汰扭,又因為其中必然有一枚5元,在最優(yōu)值狀態(tài)下拿掉其中必然存在的一枚硬幣福铅,剩下的錢所組成的錢數(shù)必然也是最優(yōu)狀態(tài)萝毛,(假設子問題不是最優(yōu)的,即237元找135元的硬幣數(shù)中構(gòu)成232元的解有更優(yōu)解滑黔,則237找135元也有更有解笆包,和c[237][3]為最優(yōu)解矛盾),那么c[232][3]的值就是c[237][3]-1的值略荡。

這樣我們可以推導出
c[i][j] = c[i][j-1] 假設1
c[i][j] = c[i-j下標代表的錢數(shù)][j]+1 假設2
如果寫成min的形式庵佣,就是熟知的狀態(tài)轉(zhuǎn)移方程了。

最終求的是c[11][5]汛兜,使用表格從c[0][0]狀態(tài)開始向右下角推巴粪,最終可以推出答案。

我們要記住

題目的最優(yōu)解不一定是狀態(tài)矩陣的右下角的值

我們必須時刻記住粥谬,狀態(tài)矩陣的右下角方格不一定就是直接的最優(yōu)解肛根。如果這樣想的話,往往會陷入困境漏策。為此派哲,我們只能這樣設想,矩陣i掺喻,j一定和題目中的兩個維度有關(guān)芭届,但是dp[i][j]不一定代表了最優(yōu)解的值。

例如本題目巢寡,題目要求求出最優(yōu)難度系數(shù)喉脖。假如dp[i_max][j_max]就是最優(yōu)難度系數(shù),那i和j是什么的下標呢抑月?树叽?是無解的。反過來想谦絮,最優(yōu)難度系數(shù)到底是什么呢题诵?我們想想看洁仗,因為存在狀態(tài)轉(zhuǎn)移,最優(yōu)狀態(tài)必然是從多個狀態(tài)中獲取到的最小值性锭。i代表什么呢赠潦?i如果代表當前唱的音符,那么j代表什么呢草冈?j就只能代表另外一個人唱的音符了她奥。那么最優(yōu)狀態(tài)下,一共會有多少種情況怎棱,就從這些情況來推算最優(yōu)的值是哪個哩俭。在最優(yōu)狀態(tài)下,必然有一個人在唱最后一個音符拳恋,那么另一個人可能一個都沒有唱凡资,可能在唱第一個音符,可能在唱第二個音符谬运。隙赁。“鹋可能在唱倒數(shù)第二個音符伞访。然后我們計算所有情況的最小值,就知道了問題的最優(yōu)解式廷。

還有一個問題咐扭,i和j是不是要分人?如果i代表小Q唱的滑废,j代表牛博士唱的蝗肪,那么這個矩陣就是對稱的,我們多計算了一半的內(nèi)容蠕趁。其實我們不關(guān)心到底是誰唱的薛闪,因為都一樣。我們只能這樣想俺陋,其中一個i是當前人唱的豁延,j是上一個人唱到哪里了。

c[i][j]可能由哪些狀態(tài)轉(zhuǎn)移過來呢腊状?
例如某人唱到了第6個音符诱咏,另一個人最后一次唱到了第3個音符,此時如果當前的總的難度是最小的缴挖,那么有以下結(jié)論:
第6個音符袋狞、第5、第4個音符都是第一個人唱的。因為第二個人才唱到了第三個音符苟鸯,他不可能去唱后面的音符同蜻。
所以c[6][3] = c[5][3] + 56難度差 = c[4][3] + 45難度差。
但是c[4][3]是怎么轉(zhuǎn)移來的呢早处?如果此時第一個人唱到了第4個音符湾蔓,另一個人唱到了第3個音符,這說明第一個人唱第4個音符中斷了第二個人唱的音符砌梆。由于不知道上一次第一個人是從哪里跳到第4個音符的默责,所以我們分別假設是從可能的所有音符跳過來的,如果是從第二個音符跳過來的咸包,那么就是c[3][2]+ 34差傻丝。如果是從第一個音符跳過來的,那么就是c[3][1]+14差诉儒。如果是從沒有跳過來的,那么就是c[3][0]+04差亏掀。由于第一個人唱的時候忱反,第二個人上一次唱的總是比第一個人唱的要小,所以我們不需要考慮j>=i的時候滤愕。

基于此温算,有以下代碼

#include <vector>
#include <iostream>
#include <algorithm>
#include <cmath>
#include <array>

using namespace std;

vector<int> nums;
array<array<int, 2100>, 2100> dp;

/* 計算兩個音符之間的難度。注意第一個音符的下標在輸入數(shù)據(jù)里面是0间影,有個差1的關(guān)系注竿。如果起始音符是0,說明第一次唱魂贬,不增加難度 */
int diffcult(int s, int e)
{
    if(s == 0) return 0;
    // printf("diffcult %d %d to %d %d is %d \n", s,nums[s-1], e,nums[e-1], abs(nums[e-1] - nums[s-1]));
    return abs(nums[e-1] - nums[s-1]);
}

int main()
{
    int n;
    cin >> n;

    for (int i = 0; i < n; i++)
    {
        int num;
        cin >> num;
        nums.push_back(num);
    }

    /* 從0開始向后考慮巩割,直到考慮完所有音符,第一個人最差的情況是沒有唱付燥,此時j也沒有唱宣谈,直接continue了 */
    for (int i = 0; i <= n; i++)
    {
        /* 這里也是從0開始考慮,因為第一個人最差一個都不唱键科,就是處于0 */
        for (int j = 0; j <= n; j++)
        {
            /* 不考慮后面的情況闻丑。當然也可以直接寫到上面的范圍限制里面 */
            if (j >= i)
                continue;
            /* 如果是一般情況下,比如第一個人在唱6音符時勋颖,第二個人在唱3音符嗦嗡,顯然是從dp[5][3]轉(zhuǎn)移來的 */
            if (j + 1 < i)
            {
                dp[i][j] = dp[i - 1][j] + diffcult(i - 1, i);
            }
            /* 否則,如果第二個人剛唱完就被第一個人搶唱了饭玲,那么第一個人唱的音符就可能是前面跳過來的 */
            if (j + 1 == i)
            {
                /* k表示i是從第幾個跳過來的 */
                int min_cost = -1;
                for (int k = 0; k < j; k++)
                {
                    int cost = dp[j][k] + diffcult(k, j + 1);
                    /* 只用記錄最小跳唱難度 */
                    if(min_cost == -1 || min_cost > cost){
                        min_cost = cost;
                    }
                }
                if(min_cost == -1) min_cost = 0;
                dp[i][j] = min_cost;
            }
            // for(int a=0;a<=n;a++){
            //     for(int b=0;b<=n;b++){
            //         if(a==i && b == j){
            //             printf("\t【%d】", dp[a][b]);
            //         }
            //         else printf("\t%d", dp[a][b]);
            //     }
            //     // printf("\n");
            // }
            // printf("\n");
        }
    }
    int min_cost = -1;
    for(int j=0;j<n;j++){
        if(min_cost == -1 || min_cost > dp[n][j]){
            min_cost = dp[n][j];
        }
    }
    printf("%d", min_cost);
}

其實已經(jīng)明白了為什么動態(tài)規(guī)劃會以這種方式進行呈現(xiàn)了侥祭。二維動態(tài)規(guī)劃問題的最關(guān)鍵的點在于:

線性增長問題的平面展開

例如找零問題,展開第二個維度就是以當前硬幣種類找零。而合唱問題卑硫,則是以唱到當前位置時且上一個唱到某個位置時的難度最小值徒恋。最長回文子串,則變得是起始位置欢伏。

最長回文子串的i表示從第i個字符開始入挣,第j個字符結(jié)束時的最長子串問題。當i是起始字符硝拧,j是終止字符径筏,則是問題的答案。

對于最小的子問題來說障陶,就是單個字符了滋恬,他的長度就是1,對于長字符串來說抱究,如果他兩邊的字符不一樣恢氯,那么等于去掉右邊或者左邊的字符中小的那一個。如果兩邊的字符一樣鼓寺,那么就等于去掉兩邊的子串的值加2勋拟。


#include <vector>
#include <string>

using namespace std;

class Solution {
public:
    int longestPalindromeSubseq(string s) {
        vector<vector<int>> dp;
        dp.resize(s.size() + 1);
        for (int i = 0; i < s.size() + 1; i++)
        {
            dp[i].resize(s.size() + 1);
        }
        int len = s.size();
        for(int t=0;t<len;t++){
            for(int j=t; j<len;j++){
                int i = j-t;
                if(i==j){
                    dp[i][j] = 1;
                    
                }
                else if(s[i] == s[j]){
                    dp[i][j] = dp[i+1][j-1] + 2;
                    
                }
                else{
                    if(dp[i+1][j]> dp[i][j-1]){
                        
                        dp[i][j] = dp[i+1][j];
                    }
                    else{
                        
                        dp[i][j] = dp[i][j-1];
                    }
                }
            }
        }
        return dp[0][len-1];
    }
    
};
  • 最大子串問題

這個題目也很經(jīng)典,按照上一個題目的思路妈候,我們依次求出所有串的長度敢靡,就得到了最大的子串。

然而事情不是這么簡單苦银,這個題目可以用一維來做啸胧。(注意其中有負數(shù))。

我們的記得幔虏,最后一個方格內(nèi)存的不一定就是最優(yōu)解纺念。往往我們會認為dp[i]就是從開始到這個位置中,能夠找到的最大的子串的值想括。但是這個最大的子串與i又沒有什么關(guān)系柠辞,似乎i往左一位和往右移一位沒有什么區(qū)別。如果我們的下標與題目中的最優(yōu)解產(chǎn)生了“松弛”的關(guān)系主胧,我們一定要警惕起來叭首。我們需要緊緊把下標和最優(yōu)解綁在一起。最優(yōu)解必然和i產(chǎn)生直接關(guān)系踪栋。那么i是最優(yōu)的什么焙格。你會想到,i就是必須以第i個數(shù)字為結(jié)尾的串的最大值夷都。最大子串就是遍歷dp矩陣眷唉,找到那個最大的值。

狀態(tài)方程也容易寫出來了,每當我們多考慮一個數(shù)字冬阳,以這個數(shù)字為結(jié)尾的子串的最大值有兩種情況蛤虐,第一種就是以上一個數(shù)字為結(jié)尾的最大子串加上這個數(shù)字,第二種情況就是不以上一個數(shù)字 為結(jié)尾肝陪。顯然第二種情況就是以這個數(shù)字為開頭的情況驳庭,并且只有這一個數(shù)字。

input表示輸入矩陣氯窍,dp表示狀態(tài)矩陣
我們有 dp[i] = max (dp[i-1] + input[i] , input[i])

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末饲常,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子狼讨,更是在濱河造成了極大的恐慌贝淤,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,273評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件政供,死亡現(xiàn)場離奇詭異播聪,居然都是意外死亡,警方通過查閱死者的電腦和手機布隔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,349評論 3 398
  • 文/潘曉璐 我一進店門犬耻,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人执泰,你說我怎么就攤上這事《沈撸” “怎么了术吝?”我有些...
    開封第一講書人閱讀 167,709評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長茸苇。 經(jīng)常有香客問我排苍,道長,這世上最難降的妖魔是什么学密? 我笑而不...
    開封第一講書人閱讀 59,520評論 1 296
  • 正文 為了忘掉前任淘衙,我火速辦了婚禮,結(jié)果婚禮上腻暮,老公的妹妹穿的比我還像新娘彤守。我一直安慰自己,他們只是感情好哭靖,可當我...
    茶點故事閱讀 68,515評論 6 397
  • 文/花漫 我一把揭開白布具垫。 她就那樣靜靜地躺著,像睡著了一般试幽。 火紅的嫁衣襯著肌膚如雪筝蚕。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,158評論 1 308
  • 那天,我揣著相機與錄音起宽,去河邊找鬼洲胖。 笑死,一個胖子當著我的面吹牛坯沪,可吹牛的內(nèi)容都是我干的绿映。 我是一名探鬼主播,決...
    沈念sama閱讀 40,755評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼屏箍,長吁一口氣:“原來是場噩夢啊……” “哼绘梦!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起赴魁,我...
    開封第一講書人閱讀 39,660評論 0 276
  • 序言:老撾萬榮一對情侶失蹤卸奉,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后颖御,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體榄棵,經(jīng)...
    沈念sama閱讀 46,203評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,287評論 3 340
  • 正文 我和宋清朗相戀三年潘拱,在試婚紗的時候發(fā)現(xiàn)自己被綠了疹鳄。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,427評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡芦岂,死狀恐怖瘪弓,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情禽最,我是刑警寧澤腺怯,帶...
    沈念sama閱讀 36,122評論 5 349
  • 正文 年R本政府宣布,位于F島的核電站川无,受9級特大地震影響呛占,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜懦趋,卻給世界環(huán)境...
    茶點故事閱讀 41,801評論 3 333
  • 文/蒙蒙 一晾虑、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧仅叫,春花似錦帜篇、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,272評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至遂跟,卻和暖如春逃沿,著一層夾襖步出監(jiān)牢的瞬間婴渡,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,393評論 1 272
  • 我被黑心中介騙來泰國打工凯亮, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留边臼,地道東北人。 一個月前我還...
    沈念sama閱讀 48,808評論 3 376
  • 正文 我出身青樓假消,卻偏偏與公主長得像柠并,于是被迫代替她去往敵國和親茸习。 傳聞我的和親對象是個殘疾皇子跨晴,可洞房花燭夜當晚...
    茶點故事閱讀 45,440評論 2 359

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

  • 動態(tài)規(guī)劃(Dynamic Programming) 本文包括: 動態(tài)規(guī)劃定義 狀態(tài)轉(zhuǎn)移方程 動態(tài)規(guī)劃算法步驟 最長...
    廖少少閱讀 3,291評論 0 18
  • 回溯算法 回溯法:也稱為試探法倒得,它并不考慮問題規(guī)模的大小梅屉,而是從問題的最明顯的最小規(guī)模開始逐步求解出可能的答案,并...
    fredal閱讀 13,669評論 0 89
  • 背景 一年多以前我在知乎上答了有關(guān)LeetCode的問題, 分享了一些自己做題目的經(jīng)驗陌粹。 張土汪:刷leetcod...
    土汪閱讀 12,747評論 0 33
  • 1. Java基礎部分 基礎部分的順序:基本語法憔晒,類相關(guān)的語法偎箫,內(nèi)部類的語法创千,繼承相關(guān)的語法缰雇,異常的語法,線程的語...
    子非魚_t_閱讀 31,660評論 18 399
  • 我一直堅定的認為時間是最有說服力的資本。我們認識十年殿雪,相戀三年暇咆。遠比她長。 我一直堅定的抓著過去不放認為這一切都是...
    你好_我是鄧等等閱讀 602評論 0 1