枚举、递归和动态规划

动态规划我一直感觉有点难,主要原因是感觉每次都需要具体问题具体分析。听了《算法导论》的课程,感觉自己从中学到了一点套路,于是就想写下来,和大家分享一下。

动态规划通常用来求解最优化问题,并且,求解的问题需要满足两个要素:最优子结构和子问题重叠。

动态规划的基本套路

课堂上老师对于动态规划的理解是:

  • 动态规划 = 小心的暴力枚举。(DP = “careful brute force”)
  • 动态规划 = 枚举 + 递归 + 记忆化。(DP = guessing + recursion + memorization)

解决动态规划问题主要有以下几个步骤:

  1. 定义子问题。
  2. 只考虑当前情况下,枚举所有的可能,然后选择一个最好的。
  3. 解决相关的子问题。
  4. 使用递归+记忆化的方式解决,或者使用动态规划表,自底向上的解决。这里需要检查子问题是不是有环的。
  5. 解决基本情况。

时间复杂度 = 子问题的个数 * 解决子问题花费的时间。

这里最难的一点是定义子问题。如果输入是字符串或者数组的时候,子问题通常为下面三种情况之一:

  • 后缀,从i开始到字符串结尾。
  • 前缀,从0开始到i。
  • 子串,从i到j。

对于子问题的定义会影响算法的复杂度。考虑下面这个问题。

给定一个整型数组arr,代表数值不同的纸牌排成一条线,玩家A和玩家B依次拿走每张纸牌,规定玩家A先拿,玩家B后拿,但是每个玩家每次只能拿走最左和最右的纸牌,玩家A和玩家B绝顶聪明。请返回最后的获胜者的分数。

上面这个题目时左程云书中的一个题目,他给出的解答是用了两个递归函数,也就是,子问题划分为两种情况,在(i,j)上先手最后获得分数,和在(i,j)上后手获得的分数。但后来我看到了另外一种解法,就是把子问题定义在(i,j)上先手比后手多获得的分数。这样,子问题的数量就减少了,但最后还需对结果进行处理,因为,这里求的是先手比后手多的值。

首先,在定义好子问题后,我们分析如何利用上面的套路来解决这个问题。

  1. 对于状态(i,j),都只有两种选择,选择前面的和后面的,如果选择前面的,状态变为了(i+1,j),如果选择后面的,状态变为了(i,j-1)。当前状态的值为两个值中较大的。
  2. 从上面的状态转移,可知,状态的转移是单向的,是无环的。
  3. 如果只有一张牌了,那么当前状态的值就为这张牌的值。

从上面的分析,写出的代码如下:

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

#define INF 0x07FFFFFFF

int guess(vector<int> &iterms, int left, int right, vector<vector<int> > &dp){
    if(left == right){ /* 只有一个,先手可以拿完 */
        dp[left][right] = iterms[left];
    }
    if(dp[left][right] == INF){
        /* 可以拿左边的,也可以拿右边的,取最大值 */
        dp[left][right] = max(iterms[left] - guess(iterms,left+1,right,dp),iterms[right] - guess(iterms,left,right-1,dp));
    }
    return dp[left][right];
}

int stoneGame(vector<int>& iterms) {
    if(iterms.size() == 0){
        return 0;
    }
    /* dp代表只考虑序列i,j的情况下,先手比后手多的值 */
    vector<vector<int> > dp(iterms.size(),vector<int>(iterms.size(),INF));
    guess(iterms,0,iterms.size()-1,dp);
    return dp[0][iterms.size()-1];
}

int main(int argc, char const *argv[])
{

    int n,num;
    cin>>n;
    vector<int> v;
    int sum = 0;
    for(int i=0;i<n;i++){
        cin>>num;
        v.push_back(num);
        sum += num;
    }
    int ret = stoneGame(v);
    if(ret < 0){ /* 先手可能会输 */
        ret = - ret; 
    }
    sum = (sum - ret) / 2 + ret;
    cout<<sum<<endl;
    system("pause");
    return 0;
}

在看下面一个问题。

给定两个字符串str1和str2,再给定三个整数ic,dc和rc,分别代表插入、删除和替换一个字符的代价,请输出将str1编辑成str2的最小代价。

这里就直接给出代码。

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

#define INF 0x07FFFFFFF

int minCostGuess(string &s1, string &s2, int l1, int l2, int ic,int dc,int rc,vector<vector<int> > &dp){
    if(l1 == s1.size()){
        //只能插入
        dp[l1][l2] = (s2.size() - l2)*ic;
    }else if(l2 == s2.size()){
        //只能删除
        dp[l1][l2] = (s1.size() - l1) * dc;
    }
    if(dp[l1][l2] == INF){ //没有算出来
        int res = INF;
        res = min(res,ic + minCostGuess(s1,s2,l1,l2+1,ic,dc,rc,dp)); // 插入
        res = min(res,dc + minCostGuess(s1,s2,l1+1,l2,ic,dc,rc,dp)); // 删除
        if(s1[l1] == s2[l2]){
            rc = 0; //如果相同,替换的代价为0
        }
        res = min(res,rc + minCostGuess(s1,s2,l1+1,l2+1,ic,dc,rc,dp)); // 替换
        dp[l1][l2] = res;
    }
    return dp[l1][l2];
}

int minCost(string &s1, string &s2, int ic,int dc,int rc){
    vector<vector<int> > dp(s1.size()+1,vector<int>(s2.size()+1,INF));
    minCostGuess(s1,s2,0,0,ic,dc,rc,dp);
    return dp[0][0];
}


int main(int argc, char const *argv[])
{
    string s1,s2;
    int ic,dc,rc;
    cin>>s1>>s2>>ic>>dc>>rc;
    cout<<minCost(s1,s2,ic,dc,rc)<<endl;    
    system("pause");
    return 0;
}

上面的代码去牛客网测试,发现时间超了(但我多试了几次,发现有时候能够过),所以,有时候写出这种记忆化递归版本并不能通过,还需要对其进行优化。

动态规划的优化——使用动态规划表、空间优化

int minCost(string s1,string s2,int ic,int dc,int rc){
    int n = s1.size();
    int m = s2.size();
    vector<vector<int>> dp(n +1 ,vector<int>(m+1,0));
    for(int i=1;i<=m;i++){
        dp[0][i] = ic * i;
    }
    for(int i=1;i<=n;i++){
        dp[i][0] = dc * i;
    } 
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            dp[i][j] = min(dp[i][j-1] + ic,dp[i-1][j] + dc);
            int tmp = s1[i-1] == s2[j-1]?dp[i-1][j-1]:dp[i-1][j-1] + rc;
            dp[i][j] = min(dp[i][j],tmp);
        }
    }
    return dp[n][m];
}

未完待续。

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

推荐阅读更多精彩内容