[oj.leetcode] #174 - Dungeon Game 一次特别的DP之旅

原题篇幅挺长,关于一个2D关卡游戏,这里以矩阵的方式简单陈述一下。

在一个二维数组M*N中, 有一个王子需要从起点[0][0]出发,移动到终点[m-1][n-1],每次移动一格,方向只能向右或者向下。出发前,王子的(健康)值至少为1。矩阵中每一格有一整型值,可正可负可0,对于经过的王子,会把这值加到他身上。规则是王子在移动过程中(包括到达终点),无论何时他身上的值都不能小于1。问王子在出发前的初始值,最小是多少?

---------------------------------------

先看几个简单例子:

 {{  1,  -2},

   {-1, -1}}

答案是2,路线是起点开始,往下,往右。

{{ -2,  -3,   3}

 { -5, -10,  1}

 {10,  30, -5}}

答案是7,路线是起点开始,右,右,下,下。

显然,这题的基本思路是使用动态规划DP。我们需要找到最优子结构。对于每一格,它可能来自左边,或者上边的格子。假设某一格[i][j], 它的既定(健康)值是dungeon[i][j]; 以这一格为终点,从起点出发走到这一格最小的初始值是P[i][j];以P[i][j]为初始值,沿着该路线(最优)经过[i][j]后的值为Sum[i][j]。

这里我们先讨论一个子问题,如果以格子[i][j]为终点,那么它的P和Sum该如何计算?

对于上图的右下格,如果进入它的选项是左边格,它的P = 3 (Sum = 1); 而如果从上边格进入,它的P = 4 (Sum = 2), 所以它应该选择从左边格子进入。

这里我们定义一个概念,叫依赖深度,即当前子结构依赖之前n步的前继。此时的依赖深度为1

如果这道题到这就结束了,也就是个普通DP。我们再来看一个例子:

这是个3*3矩阵,沿用上一例的最优子结构式,我们在终点右下角得到P = 5。但实际上,如果我们沿着右,右,下,下的方向移动到终点,我们可以得到P = 3。问题出在终点格的上方格[1][2]。对它来说,如果以此为终点,那么应该选择(2, 1)从左边进入它;而如果考虑它对于之后格子的贡献,那么应该选择(3, 4),从上方进入它。

实际上,这已经揭示了一个全新的最优子结构。对于每一个格子来说,到达它的所谓最优路线,必须要考虑两种需求:以它自己为终点的,和对于后续格子作贡献的。对于前者,我们希望在Sum相等时,P尽可能小;而对于后者,则是在P相等时,Sum尽可能大。这时,该子结构的依赖深度是2

修正过的处理中矩阵如下图:

这里我们可以很清楚的看到,对于终点格[2][2], 符合条件的最小初始值P 应该等于3。

C++代码实现一份:

class Solution{
public:
    int calculateMinimumHP(vector<vector<int> > &dungeon){
        int n = dungeon.size();
        if(n == 0)    return 0;
        int m = dungeon[0].size();
        vector<vector<int> >  horiRow;
        vector<vector<int> >  leftBound;
        vector<int> startRoom;
        int p0 = 0, sum0 = 0;
        nextStep(1, 1, 1, 1, dungeon[0][0], p0, sum0);
        startRoom.push_back(p0);
        startRoom.push_back(sum0);
        startRoom.push_back(p0);  // room[0][0] has no predecessor
        startRoom.push_back(sum0);
        horiRow.push_back(startRoom);
        leftBound.push_back(startRoom);

        for(int j=1; j<m; j++){  // for row[0]
            int p1   = horiRow[j-1][0];
            int sum1 = horiRow[j-1][1];
            int p2   = horiRow[j-1][2];
            int sum2 = horiRow[j-1][3];
            int np = 0, nsum = 0;
            nextStep(p1, sum1, p2, sum2, dungeon[0][j], np, nsum);
            vector<int> room;
            room.push_back(np);
            room.push_back(nsum);
            room.push_back(np);  // for row[0], every room has only one predecessor
            room.push_back(nsum);
            horiRow.push_back(room);
        }

        for(int i=1; i<n; i++){  // for column[0]
            int p1   = leftBound[i-1][0];
            int sum1 = leftBound[i-1][1];
            int p2   = leftBound[i-1][2];
            int sum2 = leftBound[i-1][3];
            int np = 0, nsum = 0;
            nextStep(p1, sum1, p2, sum2, dungeon[i][0], np, nsum);
            vector<int> room;
            room.push_back(np);
            room.push_back(nsum);
            room.push_back(np);  // for column[0], every room has only one predecessor
            room.push_back(nsum);
            leftBound.push_back(room);
        }

        for(int i=1; i<n; i++){
            horiRow[0].clear();
            for(int k = 0; k < 4; k++){
                horiRow[0].push_back(leftBound[i][k]);
            }
            for(int j=1; j<m; j++){
                int np1 = 0, nsum1 = 0, np2 = 0, nsum2 = 0;
                nextStep(horiRow[j][0],
                         horiRow[j][1],
                         horiRow[j][2],
                         horiRow[j][3],
                         dungeon[i][j],
                         np1,
                         nsum1);
                nextStep(horiRow[j-1][0],
                         horiRow[j-1][1],
                         horiRow[j-1][2],
                         horiRow[j-1][3],
                         dungeon[i][j],
                         np2,
                         nsum2);
                horiRow[j].clear();
                horiRow[j].push_back(np1);
                horiRow[j].push_back(nsum1);
                horiRow[j].push_back(np2);
                horiRow[j].push_back(nsum2);
            }
        }
        return min(horiRow[m-1][0], horiRow[m-1][2]);
    }

private:
    /*
     * p1:   for path1 from predecessor(e.g. up room), min init HP
     * sum2: for path1 from predecessor(e.g. up room), sum HP with init HP p1
     * p2:   for path2 from predecessor(e.g. left room), min init HP
     * sum2: for path2 from predecessor(e.g. left room), sum HP with init HP p2
     * val: HP value of current room
     * np: 
     * */
    void nextStep(int p1, int sum1, int p2, int sum2,
            int val, int& np, int& nsum){
        int np1 = 0, nsum1 = 0, np2 = 0, nsum2 = 0;
        directStep(p1, sum1, val, np1, nsum1);
        directStep(p2, sum2, val, np2, nsum2);
        if(nsum1 == nsum2){  // key block to determine next step choice
            nsum = nsum1;
            np = min(np1, np2);
        }else if(np1 == np2){
            np = np1;
            nsum = max(nsum1, nsum2);
        }else if((np1 < np2 && nsum1 > nsum2) || (np1 > np2 && nsum1 < nsum2)){
            np = min(np1, np2);
            nsum = max(nsum1, nsum2);
        }else{   // (np1, nsum1) > or < (np2, nsum2)
            np = min(np1, np2);
            nsum = min(nsum1, nsum2);
        }
        return;
    }

    void directStep(int p, int sum, int val, int& np, int& nsum){
        int delta = (val >= 0 ? 0 : max(0, 1 - val - sum));
        np = p + delta;
        nsum = sum + delta + val;
        return;
    }
};
以上代码已经经过一些优化,通过将中间数组从二维降到一维,空间上复杂度仅为O(n)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
core-oj.jar是一个用于在线评测系统的核心文件。在线评测系统是为了方便对编程作业进行自动化评测而开发的系统。它主要提供了编程语言的编译及执行环境,能够接收用户提交的代码,并执行测试用例进行输出结果的比对。 core-oj.jar是这个在线评测系统的核心库文件,它包含了评测系统运行所需的各种功能模块。 首先,core-oj.jar包含了编程语言的编译器。它能够将用户提交的代码进行编译,生成可执行文件或者字节码,以便后续的运行和评测。 其次,core-oj.jar提供了丰富的执行环境。它能够创建并管理多个运行环境,每个环境可以运行一个用户提交的代码。这些环境可以在独立的进程运行,避免了代码之间的相互影响。 此外,core-oj.jar还提供了用于输入输出重定向的功能。它能够将用户代码的输入输出连接到指定的输入输出流,以便比对用户代码的输出和预期输出是否一致。 最后,core-oj.jar还提供了一些用于评测和判题的辅助函数。它们可以根据定义的评测规则,对用户代码的输出结果进行比对,并给出相应的评测结果。 总而言之,core-oj.jar是一个在线评测系统的核心文件,通过它提供的各种功能,可以实现对编程作业的自动化评测。它起到了连接用户提交的代码和评判系统之间的桥梁作用,提供了安全、高效和准确的评测环境,帮助学生更好地完成编程任务。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值